@visns-studio/visns-components 5.9.8 → 5.9.9
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.
|
@@ -0,0 +1,4083 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { toast } from 'react-toastify';
|
|
4
|
+
import moment from 'moment';
|
|
5
|
+
import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
|
|
6
|
+
import {
|
|
7
|
+
Question,
|
|
8
|
+
LinkChain,
|
|
9
|
+
Filter,
|
|
10
|
+
Download as DownloadIcon,
|
|
11
|
+
Save,
|
|
12
|
+
EyeOpen,
|
|
13
|
+
Info,
|
|
14
|
+
CircleCheck,
|
|
15
|
+
CirclePlus,
|
|
16
|
+
BookOpen,
|
|
17
|
+
Data,
|
|
18
|
+
File,
|
|
19
|
+
SettingsVertical,
|
|
20
|
+
Person,
|
|
21
|
+
Calendar,
|
|
22
|
+
Map,
|
|
23
|
+
Tag,
|
|
24
|
+
Money,
|
|
25
|
+
ShippingBoxV1,
|
|
26
|
+
Cart,
|
|
27
|
+
Newspaper,
|
|
28
|
+
} from 'akar-icons';
|
|
29
|
+
import CustomFetch from '../Fetch';
|
|
30
|
+
import Download from '../Download';
|
|
31
|
+
import { saveAs } from 'file-saver';
|
|
32
|
+
import Swal from 'sweetalert2';
|
|
33
|
+
import GenericReport from './GenericReport';
|
|
34
|
+
import styles from './styles/GenericReportImproved.module.scss';
|
|
35
|
+
import './styles/SweetAlert.module.css';
|
|
36
|
+
|
|
37
|
+
// Icon mapping for common table types
|
|
38
|
+
const tableIcons = {
|
|
39
|
+
// People/User related
|
|
40
|
+
clients: Person,
|
|
41
|
+
users: Person,
|
|
42
|
+
contacts: Person,
|
|
43
|
+
employees: Person,
|
|
44
|
+
customers: Person,
|
|
45
|
+
staff: Person,
|
|
46
|
+
|
|
47
|
+
// Product/Service related
|
|
48
|
+
products: ShippingBoxV1,
|
|
49
|
+
services: Newspaper,
|
|
50
|
+
items: ShippingBoxV1,
|
|
51
|
+
inventory: ShippingBoxV1,
|
|
52
|
+
|
|
53
|
+
// Financial
|
|
54
|
+
invoices: Money,
|
|
55
|
+
payments: Money,
|
|
56
|
+
orders: Cart,
|
|
57
|
+
transactions: Money,
|
|
58
|
+
quotes: File,
|
|
59
|
+
|
|
60
|
+
// Location
|
|
61
|
+
sites: Map,
|
|
62
|
+
locations: Map,
|
|
63
|
+
addresses: Map,
|
|
64
|
+
|
|
65
|
+
// Time
|
|
66
|
+
appointments: Calendar,
|
|
67
|
+
events: Calendar,
|
|
68
|
+
schedules: Calendar,
|
|
69
|
+
|
|
70
|
+
// Other
|
|
71
|
+
categories: Tag,
|
|
72
|
+
tags: Tag,
|
|
73
|
+
notes: File,
|
|
74
|
+
documents: File,
|
|
75
|
+
settings: SettingsVertical,
|
|
76
|
+
|
|
77
|
+
// Default
|
|
78
|
+
default: Data,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Get icon for a table name
|
|
82
|
+
const getTableIcon = (tableName) => {
|
|
83
|
+
const lowerName = tableName.toLowerCase();
|
|
84
|
+
|
|
85
|
+
// Check for exact matches first
|
|
86
|
+
if (tableIcons[lowerName]) {
|
|
87
|
+
return tableIcons[lowerName];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check for partial matches
|
|
91
|
+
for (const [key, icon] of Object.entries(tableIcons)) {
|
|
92
|
+
if (lowerName.includes(key) || key.includes(lowerName)) {
|
|
93
|
+
return icon;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return tableIcons.default;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Get table display name using definition or formatted name
|
|
101
|
+
const getTableDisplayName = (tableName, definition) => {
|
|
102
|
+
if (definition && definition.tables) {
|
|
103
|
+
const tableDefinition = definition.tables.find(
|
|
104
|
+
(t) => t.id === tableName
|
|
105
|
+
);
|
|
106
|
+
if (tableDefinition) {
|
|
107
|
+
return tableDefinition.label;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return formatName(tableName);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Get table description from definition
|
|
114
|
+
const getTableDescription = (tableName, definition) => {
|
|
115
|
+
if (definition && definition.tables) {
|
|
116
|
+
const tableDefinition = definition.tables.find(
|
|
117
|
+
(t) => t.id === tableName
|
|
118
|
+
);
|
|
119
|
+
if (tableDefinition && tableDefinition.description) {
|
|
120
|
+
return tableDefinition.description;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return `Table: ${tableName}`;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Utility function to format names from camelCase or snake_case to user-friendly format
|
|
127
|
+
const formatName = (name) => {
|
|
128
|
+
if (!name) return '';
|
|
129
|
+
|
|
130
|
+
// Replace underscores and hyphens with spaces
|
|
131
|
+
let formatted = name.replace(/[_-]/g, ' ');
|
|
132
|
+
|
|
133
|
+
// Insert space before capital letters (for camelCase)
|
|
134
|
+
formatted = formatted.replace(/([A-Z])/g, ' $1');
|
|
135
|
+
|
|
136
|
+
// Trim extra spaces, capitalize first letter of each word
|
|
137
|
+
return formatted
|
|
138
|
+
.trim()
|
|
139
|
+
.split(' ')
|
|
140
|
+
.map(
|
|
141
|
+
(word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
|
142
|
+
)
|
|
143
|
+
.join(' ');
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// User-friendly terminology mapping
|
|
147
|
+
const friendlyTerms = {
|
|
148
|
+
// Database terms
|
|
149
|
+
join: 'Connect Related Data',
|
|
150
|
+
joined: 'Connected',
|
|
151
|
+
relationship: 'Connection',
|
|
152
|
+
relationships: 'Connections',
|
|
153
|
+
'foreign key': 'LinkChain',
|
|
154
|
+
'primary key': 'Unique ID',
|
|
155
|
+
operators: 'How to Compare',
|
|
156
|
+
'execute query': 'Preview Report',
|
|
157
|
+
sql: 'Database Query',
|
|
158
|
+
|
|
159
|
+
// Actions
|
|
160
|
+
execute: 'Preview',
|
|
161
|
+
query: 'Report',
|
|
162
|
+
|
|
163
|
+
// Field types
|
|
164
|
+
varchar: 'Text',
|
|
165
|
+
int: 'Number',
|
|
166
|
+
datetime: 'Date & Time',
|
|
167
|
+
boolean: 'Yes/No',
|
|
168
|
+
text: 'Long Text',
|
|
169
|
+
decimal: 'Decimal Number',
|
|
170
|
+
|
|
171
|
+
// Operators
|
|
172
|
+
'=': 'is exactly',
|
|
173
|
+
'!=': 'is not',
|
|
174
|
+
LIKE: 'contains',
|
|
175
|
+
'NOT LIKE': 'does not contain',
|
|
176
|
+
'>': 'is greater than',
|
|
177
|
+
'<': 'is less than',
|
|
178
|
+
'>=': 'is at least',
|
|
179
|
+
'<=': 'is at most',
|
|
180
|
+
'IS NULL': 'is empty',
|
|
181
|
+
'IS NOT NULL': 'has a value',
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Get user-friendly term
|
|
185
|
+
const getFriendlyTerm = (term) => {
|
|
186
|
+
if (!term) return '';
|
|
187
|
+
const lower = term.toLowerCase();
|
|
188
|
+
return friendlyTerms[lower] || friendlyTerms[term] || term;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Format JSON key to be more human-readable (from original GenericReport)
|
|
192
|
+
const formatJsonKey = (key) => {
|
|
193
|
+
if (!key) return key;
|
|
194
|
+
|
|
195
|
+
// Convert snake_case to Title Case
|
|
196
|
+
return key
|
|
197
|
+
.split('_')
|
|
198
|
+
.map(
|
|
199
|
+
(word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
|
200
|
+
)
|
|
201
|
+
.join(' ');
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Check if a value is JSON (from original GenericReport)
|
|
205
|
+
const isJsonValue = (value) => {
|
|
206
|
+
if (!value) return false;
|
|
207
|
+
|
|
208
|
+
// Check if it's a string that looks like JSON
|
|
209
|
+
if (typeof value === 'string') {
|
|
210
|
+
const trimmed = value.trim();
|
|
211
|
+
return (
|
|
212
|
+
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
213
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check if it's already an object (parsed JSON)
|
|
218
|
+
return typeof value === 'object' && value !== null;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Transform JSON object to have human-readable keys (from original GenericReport)
|
|
222
|
+
const transformJsonForDisplay = (json) => {
|
|
223
|
+
if (!json || typeof json !== 'object') return json;
|
|
224
|
+
|
|
225
|
+
// For arrays, transform each item
|
|
226
|
+
if (Array.isArray(json)) {
|
|
227
|
+
return json.map((item) => transformJsonForDisplay(item));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// For objects, transform keys
|
|
231
|
+
const result = {};
|
|
232
|
+
|
|
233
|
+
for (const key in json) {
|
|
234
|
+
if (Object.prototype.hasOwnProperty.call(json, key)) {
|
|
235
|
+
const value = json[key];
|
|
236
|
+
const formattedKey = formatJsonKey(key);
|
|
237
|
+
|
|
238
|
+
// Recursively transform nested objects
|
|
239
|
+
result[formattedKey] =
|
|
240
|
+
typeof value === 'object' && value !== null
|
|
241
|
+
? transformJsonForDisplay(value)
|
|
242
|
+
: value;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return result;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Render JSON data in a human-friendly format (from original GenericReport)
|
|
250
|
+
const renderJsonData = (jsonData) => {
|
|
251
|
+
if (!jsonData || typeof jsonData !== 'object') return String(jsonData);
|
|
252
|
+
|
|
253
|
+
// Human-friendly format
|
|
254
|
+
// For simple objects with just a few key-value pairs, render as a list
|
|
255
|
+
const entries = Object.entries(jsonData);
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div className={styles.humanJsonContainer}>
|
|
259
|
+
{entries.map(([key, value], index) => {
|
|
260
|
+
// Format the key to be more readable
|
|
261
|
+
const formattedKey = formatJsonKey(key);
|
|
262
|
+
|
|
263
|
+
// Handle different value types
|
|
264
|
+
let displayValue;
|
|
265
|
+
if (value === null || value === undefined) {
|
|
266
|
+
displayValue = 'N/A';
|
|
267
|
+
} else if (typeof value === 'boolean') {
|
|
268
|
+
displayValue = value ? 'Yes' : 'No';
|
|
269
|
+
} else if (typeof value === 'number') {
|
|
270
|
+
// Format numbers nicely
|
|
271
|
+
displayValue = value.toLocaleString();
|
|
272
|
+
} else if (Array.isArray(value)) {
|
|
273
|
+
// For arrays, join the values with commas
|
|
274
|
+
displayValue = value
|
|
275
|
+
.map((item) =>
|
|
276
|
+
typeof item === 'object' && item !== null
|
|
277
|
+
? '[Object]'
|
|
278
|
+
: String(item)
|
|
279
|
+
)
|
|
280
|
+
.join(', ');
|
|
281
|
+
} else if (typeof value === 'object') {
|
|
282
|
+
// For nested objects, show a placeholder
|
|
283
|
+
displayValue = '[Object]';
|
|
284
|
+
} else {
|
|
285
|
+
displayValue = String(value);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<div key={index} className={styles.humanJsonRow}>
|
|
290
|
+
<span className={styles.humanJsonKey}>
|
|
291
|
+
{formattedKey + ': '}
|
|
292
|
+
</span>
|
|
293
|
+
<span className={styles.humanJsonValue}>
|
|
294
|
+
{displayValue}
|
|
295
|
+
</span>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
})}
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Smart format for boolean and status values (from original GenericReport)
|
|
304
|
+
const formatSmartValue = (value, columnName, columnType) => {
|
|
305
|
+
// Skip null/undefined values
|
|
306
|
+
if (value === null || value === undefined) {
|
|
307
|
+
return '';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Handle JSON values first
|
|
311
|
+
if (isJsonValue(value)) {
|
|
312
|
+
try {
|
|
313
|
+
// For JSON objects/arrays, format them nicely
|
|
314
|
+
let jsonValue;
|
|
315
|
+
|
|
316
|
+
// Parse the JSON if it's a string
|
|
317
|
+
if (typeof value === 'string') {
|
|
318
|
+
try {
|
|
319
|
+
jsonValue = JSON.parse(value);
|
|
320
|
+
} catch (e) {
|
|
321
|
+
return String(value);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
jsonValue = value;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Transform JSON to have human-readable keys
|
|
328
|
+
const transformedJson = transformJsonForDisplay(jsonValue);
|
|
329
|
+
|
|
330
|
+
// Just return the human-friendly format
|
|
331
|
+
return renderJsonData(transformedJson);
|
|
332
|
+
} catch (e) {
|
|
333
|
+
return String(value);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Handle date/datetime formatting first
|
|
338
|
+
if (
|
|
339
|
+
columnType &&
|
|
340
|
+
(columnType.includes('date') || columnType.includes('time'))
|
|
341
|
+
) {
|
|
342
|
+
try {
|
|
343
|
+
const date = new Date(value);
|
|
344
|
+
if (!isNaN(date.getTime())) {
|
|
345
|
+
// Check if it's a datetime or just date
|
|
346
|
+
const hasTime =
|
|
347
|
+
columnType.includes('datetime') ||
|
|
348
|
+
columnType.includes('timestamp') ||
|
|
349
|
+
(typeof value === 'string' && value.includes(':'));
|
|
350
|
+
|
|
351
|
+
if (hasTime) {
|
|
352
|
+
// Format as d-m-Y H:i A (Australian datetime format)
|
|
353
|
+
return date
|
|
354
|
+
.toLocaleString('en-AU', {
|
|
355
|
+
day: '2-digit',
|
|
356
|
+
month: '2-digit',
|
|
357
|
+
year: 'numeric',
|
|
358
|
+
hour: '2-digit',
|
|
359
|
+
minute: '2-digit',
|
|
360
|
+
hour12: true,
|
|
361
|
+
})
|
|
362
|
+
.replace(/\//g, '-');
|
|
363
|
+
} else {
|
|
364
|
+
// Format as d-m-Y (Australian date format)
|
|
365
|
+
return date
|
|
366
|
+
.toLocaleDateString('en-AU', {
|
|
367
|
+
day: '2-digit',
|
|
368
|
+
month: '2-digit',
|
|
369
|
+
year: 'numeric',
|
|
370
|
+
})
|
|
371
|
+
.replace(/\//g, '-');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} catch (e) {
|
|
375
|
+
// If date parsing fails, continue with other formatting
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Convert to string for comparison
|
|
380
|
+
const strValue = String(value).toLowerCase();
|
|
381
|
+
const lowerColumnName = columnName.toLowerCase();
|
|
382
|
+
|
|
383
|
+
// Handle boolean values (0/1) in status columns
|
|
384
|
+
if (
|
|
385
|
+
(columnType === 'tinyint' ||
|
|
386
|
+
columnType === 'boolean' ||
|
|
387
|
+
columnType === 'bit') &&
|
|
388
|
+
(strValue === '0' ||
|
|
389
|
+
strValue === '1' ||
|
|
390
|
+
strValue === 'true' ||
|
|
391
|
+
strValue === 'false')
|
|
392
|
+
) {
|
|
393
|
+
const boolValue = strValue === '1' || strValue === 'true';
|
|
394
|
+
|
|
395
|
+
// Status-specific formatting
|
|
396
|
+
if (lowerColumnName.includes('status')) {
|
|
397
|
+
return (
|
|
398
|
+
<span
|
|
399
|
+
className={boolValue ? 'status-active' : 'status-inactive'}
|
|
400
|
+
style={{
|
|
401
|
+
color: boolValue ? '#16a34a' : '#dc2626',
|
|
402
|
+
fontWeight: '500',
|
|
403
|
+
padding: '2px 8px',
|
|
404
|
+
borderRadius: '12px',
|
|
405
|
+
backgroundColor: boolValue ? '#dcfce7' : '#fef2f2',
|
|
406
|
+
fontSize: '0.75rem',
|
|
407
|
+
}}
|
|
408
|
+
>
|
|
409
|
+
{boolValue ? 'Active' : 'Inactive'}
|
|
410
|
+
</span>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Completion-specific formatting
|
|
415
|
+
if (
|
|
416
|
+
lowerColumnName.includes('complete') ||
|
|
417
|
+
lowerColumnName.includes('completed') ||
|
|
418
|
+
lowerColumnName.includes('done') ||
|
|
419
|
+
lowerColumnName.includes('finished')
|
|
420
|
+
) {
|
|
421
|
+
return (
|
|
422
|
+
<span
|
|
423
|
+
style={{
|
|
424
|
+
color: boolValue ? '#16a34a' : '#dc2626',
|
|
425
|
+
fontWeight: '500',
|
|
426
|
+
}}
|
|
427
|
+
>
|
|
428
|
+
{boolValue ? 'Yes' : 'No'}
|
|
429
|
+
</span>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Enabled/disabled formatting
|
|
434
|
+
if (
|
|
435
|
+
lowerColumnName.includes('enable') ||
|
|
436
|
+
lowerColumnName.includes('enabled') ||
|
|
437
|
+
lowerColumnName.includes('disable') ||
|
|
438
|
+
lowerColumnName.includes('disabled')
|
|
439
|
+
) {
|
|
440
|
+
return (
|
|
441
|
+
<span
|
|
442
|
+
style={{
|
|
443
|
+
color: boolValue ? '#16a34a' : '#dc2626',
|
|
444
|
+
fontWeight: '500',
|
|
445
|
+
padding: '2px 8px',
|
|
446
|
+
borderRadius: '12px',
|
|
447
|
+
backgroundColor: boolValue ? '#dcfce7' : '#fef2f2',
|
|
448
|
+
fontSize: '0.75rem',
|
|
449
|
+
}}
|
|
450
|
+
>
|
|
451
|
+
{boolValue ? 'Enabled' : 'Disabled'}
|
|
452
|
+
</span>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Approved/rejected formatting
|
|
457
|
+
if (
|
|
458
|
+
lowerColumnName.includes('approve') ||
|
|
459
|
+
lowerColumnName.includes('approved') ||
|
|
460
|
+
lowerColumnName.includes('reject') ||
|
|
461
|
+
lowerColumnName.includes('rejected')
|
|
462
|
+
) {
|
|
463
|
+
return (
|
|
464
|
+
<span
|
|
465
|
+
style={{
|
|
466
|
+
color: boolValue ? '#16a34a' : '#dc2626',
|
|
467
|
+
fontWeight: '500',
|
|
468
|
+
padding: '2px 8px',
|
|
469
|
+
borderRadius: '12px',
|
|
470
|
+
backgroundColor: boolValue ? '#dcfce7' : '#fef2f2',
|
|
471
|
+
fontSize: '0.75rem',
|
|
472
|
+
}}
|
|
473
|
+
>
|
|
474
|
+
{boolValue ? 'Approved' : 'Rejected'}
|
|
475
|
+
</span>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Verified formatting
|
|
480
|
+
if (
|
|
481
|
+
lowerColumnName.includes('verify') ||
|
|
482
|
+
lowerColumnName.includes('verified')
|
|
483
|
+
) {
|
|
484
|
+
return (
|
|
485
|
+
<span
|
|
486
|
+
style={{
|
|
487
|
+
color: boolValue ? '#16a34a' : '#dc2626',
|
|
488
|
+
fontWeight: '500',
|
|
489
|
+
padding: '2px 8px',
|
|
490
|
+
borderRadius: '12px',
|
|
491
|
+
backgroundColor: boolValue ? '#dcfce7' : '#fef2f2',
|
|
492
|
+
fontSize: '0.75rem',
|
|
493
|
+
}}
|
|
494
|
+
>
|
|
495
|
+
{boolValue ? 'Verified' : 'Not Verified'}
|
|
496
|
+
</span>
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Default boolean formatting
|
|
501
|
+
return (
|
|
502
|
+
<span
|
|
503
|
+
style={{
|
|
504
|
+
color: boolValue ? '#16a34a' : '#dc2626',
|
|
505
|
+
fontWeight: '500',
|
|
506
|
+
}}
|
|
507
|
+
>
|
|
508
|
+
{boolValue ? 'Yes' : 'No'}
|
|
509
|
+
</span>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Return the original value if no smart formatting applies
|
|
514
|
+
return value;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// Smart field filtering - hide technical fields by default
|
|
518
|
+
const shouldHideField = (fieldName, fieldType) => {
|
|
519
|
+
const name = fieldName.toLowerCase();
|
|
520
|
+
const type = fieldType.toLowerCase();
|
|
521
|
+
|
|
522
|
+
// Hide foreign keys (except main 'id')
|
|
523
|
+
if (name.endsWith('_id') && name !== 'id') return true;
|
|
524
|
+
|
|
525
|
+
// Hide UUIDs
|
|
526
|
+
if (name.includes('uuid') || type.includes('uuid')) return true;
|
|
527
|
+
|
|
528
|
+
// Hide technical timestamps
|
|
529
|
+
if (['updated_at', 'deleted_at'].includes(name)) return true;
|
|
530
|
+
|
|
531
|
+
// Hide Laravel/system fields
|
|
532
|
+
if (
|
|
533
|
+
[
|
|
534
|
+
'remember_token',
|
|
535
|
+
'email_verified_at',
|
|
536
|
+
'password',
|
|
537
|
+
'password_hash',
|
|
538
|
+
].includes(name)
|
|
539
|
+
)
|
|
540
|
+
return true;
|
|
541
|
+
|
|
542
|
+
// Hide pivot table fields
|
|
543
|
+
if (name.includes('pivot_')) return true;
|
|
544
|
+
|
|
545
|
+
// Hide technical fields
|
|
546
|
+
if (name.startsWith('_') || name.includes('hash') || name.includes('token'))
|
|
547
|
+
return true;
|
|
548
|
+
|
|
549
|
+
return false;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// Flatten filter criteria to match GenericReport format
|
|
553
|
+
const flattenFilterCriteria = (filterCriteria) => {
|
|
554
|
+
if (
|
|
555
|
+
!filterCriteria ||
|
|
556
|
+
!filterCriteria.groups ||
|
|
557
|
+
filterCriteria.groups.length === 0
|
|
558
|
+
) {
|
|
559
|
+
return [];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const flatFilters = [];
|
|
563
|
+
|
|
564
|
+
filterCriteria.groups.forEach((group) => {
|
|
565
|
+
if (group.filters && group.filters.length > 0) {
|
|
566
|
+
group.filters.forEach((filter) => {
|
|
567
|
+
if (
|
|
568
|
+
filter.table &&
|
|
569
|
+
filter.column &&
|
|
570
|
+
filter.operator &&
|
|
571
|
+
filter.value !== undefined
|
|
572
|
+
) {
|
|
573
|
+
flatFilters.push({
|
|
574
|
+
table: filter.table,
|
|
575
|
+
column: filter.column,
|
|
576
|
+
operator: filter.operator,
|
|
577
|
+
value: filter.value,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
return flatFilters;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// Enhanced field categorization with better grouping
|
|
588
|
+
const categorizeFields = (columns, showHiddenFields = false) => {
|
|
589
|
+
const categories = {
|
|
590
|
+
essential: [],
|
|
591
|
+
descriptive: [],
|
|
592
|
+
contact: [],
|
|
593
|
+
financial: [],
|
|
594
|
+
dates: [],
|
|
595
|
+
status: [],
|
|
596
|
+
hidden: [],
|
|
597
|
+
other: [],
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
columns.forEach((column) => {
|
|
601
|
+
const name = column.name.toLowerCase();
|
|
602
|
+
const type = column.type.toLowerCase();
|
|
603
|
+
|
|
604
|
+
// Check if field should be hidden
|
|
605
|
+
if (shouldHideField(column.name, column.type)) {
|
|
606
|
+
if (showHiddenFields) {
|
|
607
|
+
categories.hidden.push(column);
|
|
608
|
+
}
|
|
609
|
+
return; // Skip hidden fields
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Categorize visible fields
|
|
613
|
+
if (name === 'id' || name.includes('name') || name.includes('title')) {
|
|
614
|
+
categories.essential.push(column);
|
|
615
|
+
} else if (
|
|
616
|
+
name.includes('email') ||
|
|
617
|
+
name.includes('phone') ||
|
|
618
|
+
name.includes('contact') ||
|
|
619
|
+
name.includes('address')
|
|
620
|
+
) {
|
|
621
|
+
categories.contact.push(column);
|
|
622
|
+
} else if (
|
|
623
|
+
name.includes('description') ||
|
|
624
|
+
name.includes('note') ||
|
|
625
|
+
name.includes('comment') ||
|
|
626
|
+
name.includes('detail')
|
|
627
|
+
) {
|
|
628
|
+
categories.descriptive.push(column);
|
|
629
|
+
} else if (type.includes('date') || type.includes('time')) {
|
|
630
|
+
categories.dates.push(column);
|
|
631
|
+
} else if (
|
|
632
|
+
name.includes('price') ||
|
|
633
|
+
name.includes('cost') ||
|
|
634
|
+
name.includes('amount') ||
|
|
635
|
+
name.includes('total') ||
|
|
636
|
+
name.includes('fee')
|
|
637
|
+
) {
|
|
638
|
+
categories.financial.push(column);
|
|
639
|
+
} else if (
|
|
640
|
+
name.includes('status') ||
|
|
641
|
+
name.includes('active') ||
|
|
642
|
+
name.includes('enabled') ||
|
|
643
|
+
name.startsWith('is_') ||
|
|
644
|
+
type.includes('boolean') ||
|
|
645
|
+
type.includes('enum')
|
|
646
|
+
) {
|
|
647
|
+
categories.status.push(column);
|
|
648
|
+
} else {
|
|
649
|
+
categories.other.push(column);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
return categories;
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// Wizard steps with detailed guidance - reordered for better UX
|
|
657
|
+
const wizardSteps = [
|
|
658
|
+
{
|
|
659
|
+
id: 'table',
|
|
660
|
+
title: 'Select Your Data',
|
|
661
|
+
icon: Data,
|
|
662
|
+
description: 'Choose what type of information you want to report on',
|
|
663
|
+
guidance: {
|
|
664
|
+
summary: 'Pick the main data source for your report',
|
|
665
|
+
howTo: [
|
|
666
|
+
'Review the list of available data tables below',
|
|
667
|
+
'Each table represents a different type of information in your system',
|
|
668
|
+
'Click on the table that contains the main data you want to analyze',
|
|
669
|
+
'The table name shows both a user-friendly name and the technical name',
|
|
670
|
+
],
|
|
671
|
+
tip: 'Choose the table with the primary information you want to report on. You can connect related data in the next step.',
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
id: 'relationships',
|
|
676
|
+
title: 'Add Related Data',
|
|
677
|
+
icon: LinkChain,
|
|
678
|
+
description: 'Include information from related areas (optional)',
|
|
679
|
+
guidance: {
|
|
680
|
+
summary: 'Connect additional data to enrich your report',
|
|
681
|
+
howTo: [
|
|
682
|
+
'Review suggested connections we found automatically',
|
|
683
|
+
'Click "Connect" on relationships that add value to your report',
|
|
684
|
+
'Connected tables will provide additional fields in the next step',
|
|
685
|
+
'Skip this step if you only need data from your main table',
|
|
686
|
+
],
|
|
687
|
+
tip: 'Relationships help you get more complete information, like adding customer names to invoice data.',
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
id: 'columns',
|
|
692
|
+
title: 'Choose Information',
|
|
693
|
+
icon: File,
|
|
694
|
+
description: 'Select which details you want to see',
|
|
695
|
+
guidance: {
|
|
696
|
+
summary: 'Select the specific fields to include in your report',
|
|
697
|
+
howTo: [
|
|
698
|
+
'Review fields from your main table and any connected tables',
|
|
699
|
+
'Check the boxes next to fields you want to include',
|
|
700
|
+
'Use "Select All" to include everything, or start fresh with "Clear All"',
|
|
701
|
+
'Fields are organized by categories to make selection easier',
|
|
702
|
+
],
|
|
703
|
+
tip: 'Include key identifiers (like names or IDs) and the specific data you want to analyze.',
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
id: 'filters',
|
|
708
|
+
title: 'Filter Results',
|
|
709
|
+
icon: Filter,
|
|
710
|
+
description: 'Narrow down to specific records (optional)',
|
|
711
|
+
guidance: {
|
|
712
|
+
summary: 'Apply filters to show only the records you need',
|
|
713
|
+
howTo: [
|
|
714
|
+
'Create custom filters using any field from your selected tables',
|
|
715
|
+
'Use Quick Filters for common scenarios like "Last 30 Days"',
|
|
716
|
+
'Click "Add Filter" to create detailed filtering conditions',
|
|
717
|
+
'Skip this step to include all available records',
|
|
718
|
+
],
|
|
719
|
+
tip: 'Filters help you focus on specific data, like only active customers or recent transactions.',
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
id: 'preview',
|
|
724
|
+
title: 'Preview & Save',
|
|
725
|
+
icon: EyeOpen,
|
|
726
|
+
description: 'Review your report and save for future use',
|
|
727
|
+
guidance: {
|
|
728
|
+
summary: 'Generate, review, and save your custom report',
|
|
729
|
+
howTo: [
|
|
730
|
+
'Click "Generate Report" to see your data',
|
|
731
|
+
'Review the results in the table below',
|
|
732
|
+
'Save your report configuration for future use',
|
|
733
|
+
'Export your data in Excel, CSV, or PDF format',
|
|
734
|
+
],
|
|
735
|
+
tip: 'Saving your report lets you run it again later with updated data, or share it with others.',
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
];
|
|
739
|
+
|
|
740
|
+
// Legacy template reports (no longer used in UI but kept for backward compatibility)
|
|
741
|
+
const reportTemplates = [];
|
|
742
|
+
|
|
743
|
+
const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
744
|
+
// Extract settings with defaults
|
|
745
|
+
const { tableUrl, columnUrl } = setting;
|
|
746
|
+
|
|
747
|
+
// Ref for tooltip element
|
|
748
|
+
const tooltipRef = React.useRef(null);
|
|
749
|
+
|
|
750
|
+
// UI State
|
|
751
|
+
const [beginnerMode, setBeginnerMode] = useState(true);
|
|
752
|
+
const [currentWizardStep, setCurrentWizardStep] = useState(0);
|
|
753
|
+
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
|
754
|
+
// Removed selectedTemplate state as we now use saved reports instead of templates
|
|
755
|
+
const [showTemplates, setShowTemplates] = useState(true);
|
|
756
|
+
|
|
757
|
+
// State for database schema
|
|
758
|
+
const [tables, setTables] = useState([]);
|
|
759
|
+
const [selectedTable, setSelectedTable] = useState(null);
|
|
760
|
+
const [tableColumns, setTableColumns] = useState([]);
|
|
761
|
+
const [isLoadingTables, setIsLoadingTables] = useState(false);
|
|
762
|
+
const [isLoadingJoinColumns, setIsLoadingJoinColumns] = useState(false);
|
|
763
|
+
|
|
764
|
+
// State for report configuration
|
|
765
|
+
const [selectedColumns, setSelectedColumns] = useState([]);
|
|
766
|
+
const [availableTables, setAvailableTables] = useState([]);
|
|
767
|
+
const [joins, setJoins] = useState([]);
|
|
768
|
+
const [reportName, setReportName] = useState('');
|
|
769
|
+
const [isPublic, setIsPublic] = useState(false);
|
|
770
|
+
|
|
771
|
+
// State for manual relationship creation
|
|
772
|
+
const [showManualJoinForm, setShowManualJoinForm] = useState(false);
|
|
773
|
+
const [manualJoin, setManualJoin] = useState({
|
|
774
|
+
sourceTable: '',
|
|
775
|
+
targetTable: '',
|
|
776
|
+
sourceColumn: '',
|
|
777
|
+
targetColumn: '',
|
|
778
|
+
joinType: 'INNER JOIN',
|
|
779
|
+
});
|
|
780
|
+
const [manualJoinColumns, setManualJoinColumns] = useState({
|
|
781
|
+
source: [],
|
|
782
|
+
target: [],
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
// State for saved reports
|
|
786
|
+
const [savedReports, setSavedReports] = useState([]);
|
|
787
|
+
const [selectedReport, setSelectedReport] = useState(null);
|
|
788
|
+
const [loadedReportId, setLoadedReportId] = useState(null); // Track loaded report for overwrite functionality
|
|
789
|
+
const [isLoadingReports, setIsLoadingReports] = useState(false);
|
|
790
|
+
|
|
791
|
+
// State for suggested joins
|
|
792
|
+
const [suggestedJoins, setSuggestedJoins] = useState({});
|
|
793
|
+
const [isLoadingSuggestedJoins, setIsLoadingSuggestedJoins] = useState({});
|
|
794
|
+
|
|
795
|
+
// State for toggling visibility of sections
|
|
796
|
+
const [showSuggestedRelationships, setShowSuggestedRelationships] =
|
|
797
|
+
useState({});
|
|
798
|
+
const [showJoinTables, setShowJoinTables] = useState(false);
|
|
799
|
+
const [showSortingSection, setShowSortingSection] = useState(false);
|
|
800
|
+
const [showFilteringSection, setShowFilteringSection] = useState(false);
|
|
801
|
+
|
|
802
|
+
// State for sorting and filtering
|
|
803
|
+
const [sortCriteria, setSortCriteria] = useState([]);
|
|
804
|
+
|
|
805
|
+
// Enhanced filter criteria with visual builder support
|
|
806
|
+
const [filterCriteria, setFilterCriteria] = useState({
|
|
807
|
+
operator: 'AND',
|
|
808
|
+
groups: [
|
|
809
|
+
{
|
|
810
|
+
operator: 'AND',
|
|
811
|
+
filters: [],
|
|
812
|
+
},
|
|
813
|
+
],
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// State for report execution
|
|
817
|
+
const [totalResults, setTotalResults] = useState(0);
|
|
818
|
+
const [executedSql, setExecutedSql] = useState('');
|
|
819
|
+
const [showSqlPreview, setShowSqlPreview] = useState(false);
|
|
820
|
+
|
|
821
|
+
// State for report preview
|
|
822
|
+
const [previewData, setPreviewData] = useState([]);
|
|
823
|
+
const [gridColumns, setGridColumns] = useState([]);
|
|
824
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
825
|
+
|
|
826
|
+
// State for pagination - now using local pagination
|
|
827
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
828
|
+
const [pageSize, setPageSize] = useState(25); // Default to 25 for better UX
|
|
829
|
+
const [totalPages, setTotalPages] = useState(0);
|
|
830
|
+
const [allData, setAllData] = useState([]); // Store all fetched data for local pagination
|
|
831
|
+
|
|
832
|
+
// State for JSON field keys
|
|
833
|
+
const [jsonFieldKeys, setJsonFieldKeys] = useState({});
|
|
834
|
+
const [loadingJsonKeys, setLoadingJsonKeys] = useState({});
|
|
835
|
+
const [showJsonKeySelector, setShowJsonKeySelector] = useState({});
|
|
836
|
+
|
|
837
|
+
// State for suggested filters
|
|
838
|
+
const [suggestedFilters, setSuggestedFilters] = useState([]);
|
|
839
|
+
const [isGeneratingSuggestions, setIsGeneratingSuggestions] =
|
|
840
|
+
useState(false);
|
|
841
|
+
const [showSuggestedFilters, setShowSuggestedFilters] = useState(false);
|
|
842
|
+
|
|
843
|
+
// State for showing hidden fields
|
|
844
|
+
const [showHiddenFields, setShowHiddenFields] = useState(false);
|
|
845
|
+
|
|
846
|
+
// Note: Removed table search functionality to simplify user experience
|
|
847
|
+
|
|
848
|
+
// Extract additional settings with defaults
|
|
849
|
+
const {
|
|
850
|
+
reportsUrl = '/ajax/reportBuilder/reports',
|
|
851
|
+
executeUrl = '/ajax/reportBuilder/execute',
|
|
852
|
+
suggestedJoinsUrl = '/ajax/reportBuilder/getSuggestedJoins',
|
|
853
|
+
jsonFieldKeysUrl = '/ajax/reportBuilder/getJsonFieldKeys',
|
|
854
|
+
exportUrl = '/ajax/reportBuilder/export',
|
|
855
|
+
} = setting;
|
|
856
|
+
|
|
857
|
+
// Show guided help popup
|
|
858
|
+
const showGuidedHelp = () => {
|
|
859
|
+
Swal.fire({
|
|
860
|
+
title: '🎯 Quick Start Guide',
|
|
861
|
+
html: `
|
|
862
|
+
<div style="text-align: left; font-size: 14px; color: #4b5563;">
|
|
863
|
+
<div style="margin-bottom: 20px;">
|
|
864
|
+
<h4 style="color: #1f2937; margin-bottom: 10px;">Choose Your Path:</h4>
|
|
865
|
+
|
|
866
|
+
<div style="background: #f0f9ff; border: 1px solid #3b82f6; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
867
|
+
<h5 style="color: #1e40af; margin: 0 0 8px 0;">🚀 Quick Start (Recommended)</h5>
|
|
868
|
+
<p style="margin: 0;">Use a pre-built template to get started immediately. Perfect for common reports like client lists or recent invoices.</p>
|
|
869
|
+
</div>
|
|
870
|
+
|
|
871
|
+
<div style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px;">
|
|
872
|
+
<h5 style="color: #374151; margin: 0 0 8px 0;">✨ Custom Report</h5>
|
|
873
|
+
<p style="margin: 0;">Build your own report from scratch with our step-by-step wizard. We'll guide you through each step!</p>
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
|
|
877
|
+
<div style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 12px;">
|
|
878
|
+
<p style="margin: 0; color: #92400e;">
|
|
879
|
+
<strong>💡 Tip:</strong> You can switch between Beginner and Advanced mode at any time using the toggle in the top right.
|
|
880
|
+
</p>
|
|
881
|
+
</div>
|
|
882
|
+
</div>
|
|
883
|
+
`,
|
|
884
|
+
width: 500,
|
|
885
|
+
confirmButtonText: "Let's Get Started!",
|
|
886
|
+
confirmButtonColor: '#2563eb',
|
|
887
|
+
showCancelButton: true,
|
|
888
|
+
cancelButtonText: 'Skip Tour',
|
|
889
|
+
cancelButtonColor: '#6b7280',
|
|
890
|
+
}).then((result) => {
|
|
891
|
+
if (result.isConfirmed) {
|
|
892
|
+
setShowTemplates(true);
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// Initialize component
|
|
898
|
+
useEffect(() => {
|
|
899
|
+
fetchDatabaseTables();
|
|
900
|
+
fetchSavedReports();
|
|
901
|
+
|
|
902
|
+
// Show help on first load
|
|
903
|
+
const hasSeenHelp = localStorage.getItem('reportBuilder_hasSeenHelp');
|
|
904
|
+
if (!hasSeenHelp && beginnerMode) {
|
|
905
|
+
setTimeout(() => {
|
|
906
|
+
showGuidedHelp();
|
|
907
|
+
localStorage.setItem('reportBuilder_hasSeenHelp', 'true');
|
|
908
|
+
}, 500);
|
|
909
|
+
}
|
|
910
|
+
}, [tableUrl, reportsUrl]); // Add dependencies to ensure re-fetch when URLs change
|
|
911
|
+
|
|
912
|
+
// Fetch columns when table is selected (like advanced mode)
|
|
913
|
+
useEffect(() => {
|
|
914
|
+
if (selectedTable) {
|
|
915
|
+
fetchTableColumns(selectedTable);
|
|
916
|
+
fetchSuggestedJoins(selectedTable);
|
|
917
|
+
}
|
|
918
|
+
}, [selectedTable, columnUrl, suggestedJoinsUrl]);
|
|
919
|
+
|
|
920
|
+
// Auto-select common columns when user reaches the columns step
|
|
921
|
+
useEffect(() => {
|
|
922
|
+
if (
|
|
923
|
+
beginnerMode &&
|
|
924
|
+
selectedColumns.length === 0 &&
|
|
925
|
+
tableColumns.length > 0 &&
|
|
926
|
+
currentWizardStep === 2 // Step 3 is index 2 (columns step)
|
|
927
|
+
) {
|
|
928
|
+
const commonColumns = [
|
|
929
|
+
'id',
|
|
930
|
+
'name',
|
|
931
|
+
'title',
|
|
932
|
+
'email',
|
|
933
|
+
'status',
|
|
934
|
+
'created_at',
|
|
935
|
+
];
|
|
936
|
+
const autoSelect = tableColumns
|
|
937
|
+
.filter(
|
|
938
|
+
(col) =>
|
|
939
|
+
commonColumns.includes(col.name.toLowerCase()) &&
|
|
940
|
+
!shouldHideField(col.name, col.type)
|
|
941
|
+
)
|
|
942
|
+
.map((col) => ({
|
|
943
|
+
table: selectedTable,
|
|
944
|
+
column: col.name,
|
|
945
|
+
displayName: formatName(col.name),
|
|
946
|
+
}));
|
|
947
|
+
|
|
948
|
+
if (autoSelect.length > 0) {
|
|
949
|
+
setSelectedColumns(autoSelect);
|
|
950
|
+
toast.info(
|
|
951
|
+
`We've pre-selected some common fields for you. You can add or remove fields as needed.`,
|
|
952
|
+
{
|
|
953
|
+
autoClose: 5000,
|
|
954
|
+
}
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}, [
|
|
959
|
+
tableColumns,
|
|
960
|
+
selectedColumns.length,
|
|
961
|
+
beginnerMode,
|
|
962
|
+
selectedTable,
|
|
963
|
+
currentWizardStep,
|
|
964
|
+
]);
|
|
965
|
+
|
|
966
|
+
// Auto-execute report when user reaches step 5 (preview step)
|
|
967
|
+
useEffect(() => {
|
|
968
|
+
if (
|
|
969
|
+
beginnerMode &&
|
|
970
|
+
currentWizardStep === 4 && // Step 5 is index 4 (preview step)
|
|
971
|
+
selectedTable &&
|
|
972
|
+
selectedColumns.length > 0 &&
|
|
973
|
+
previewData.length === 0 && // Only if no data loaded yet
|
|
974
|
+
!isLoading
|
|
975
|
+
) {
|
|
976
|
+
executeReport();
|
|
977
|
+
}
|
|
978
|
+
}, [
|
|
979
|
+
currentWizardStep,
|
|
980
|
+
beginnerMode,
|
|
981
|
+
selectedTable,
|
|
982
|
+
selectedColumns.length,
|
|
983
|
+
previewData.length,
|
|
984
|
+
isLoading,
|
|
985
|
+
]);
|
|
986
|
+
|
|
987
|
+
// Delete saved report
|
|
988
|
+
const deleteReport = async (reportId, reportLabel) => {
|
|
989
|
+
const result = await Swal.fire({
|
|
990
|
+
title: 'Delete Report',
|
|
991
|
+
html: `Are you sure you want to delete "<strong>${reportLabel}</strong>"?<br><br>This action cannot be undone.`,
|
|
992
|
+
icon: 'warning',
|
|
993
|
+
showCancelButton: true,
|
|
994
|
+
confirmButtonText: 'Yes, Delete',
|
|
995
|
+
cancelButtonText: 'Cancel',
|
|
996
|
+
confirmButtonColor: '#ef4444',
|
|
997
|
+
cancelButtonColor: '#6b7280',
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
if (result.isConfirmed) {
|
|
1001
|
+
try {
|
|
1002
|
+
const deleteResult = await CustomFetch(
|
|
1003
|
+
`${reportsUrl}/${reportId}`,
|
|
1004
|
+
'DELETE'
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
if (deleteResult.error) {
|
|
1008
|
+
toast.error(deleteResult.error);
|
|
1009
|
+
} else if (deleteResult.data?.success || deleteResult.success) {
|
|
1010
|
+
toast.success(
|
|
1011
|
+
`Report "${reportLabel}" deleted successfully`
|
|
1012
|
+
);
|
|
1013
|
+
fetchSavedReports(); // Refresh the list
|
|
1014
|
+
} else {
|
|
1015
|
+
toast.error(
|
|
1016
|
+
deleteResult.data?.message ||
|
|
1017
|
+
deleteResult.message ||
|
|
1018
|
+
'Failed to delete report'
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
toast.error('Failed to delete report');
|
|
1023
|
+
console.error('Delete error:', error);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// Start again function to reset wizard to home
|
|
1029
|
+
const startAgain = () => {
|
|
1030
|
+
// Reset all state
|
|
1031
|
+
setSelectedTable(null);
|
|
1032
|
+
setSelectedColumns([]);
|
|
1033
|
+
setJoins([]);
|
|
1034
|
+
setFilterCriteria({
|
|
1035
|
+
operator: 'AND',
|
|
1036
|
+
groups: [
|
|
1037
|
+
{
|
|
1038
|
+
operator: 'AND',
|
|
1039
|
+
filters: [],
|
|
1040
|
+
},
|
|
1041
|
+
],
|
|
1042
|
+
});
|
|
1043
|
+
setSortCriteria([]);
|
|
1044
|
+
setPreviewData([]);
|
|
1045
|
+
setGridColumns([]);
|
|
1046
|
+
setReportName('');
|
|
1047
|
+
setIsPublic(false);
|
|
1048
|
+
setLoadedReportId(null); // Reset loaded report ID
|
|
1049
|
+
setCurrentWizardStep(0);
|
|
1050
|
+
setShowTemplates(true);
|
|
1051
|
+
setTableColumns([]);
|
|
1052
|
+
setAvailableTables([]);
|
|
1053
|
+
|
|
1054
|
+
toast.info(
|
|
1055
|
+
'Report builder reset. You can start creating a new report.'
|
|
1056
|
+
);
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
// Wizard navigation
|
|
1060
|
+
const goToNextStep = () => {
|
|
1061
|
+
if (currentWizardStep < wizardSteps.length - 1) {
|
|
1062
|
+
setCurrentWizardStep(currentWizardStep + 1);
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
const goToPreviousStep = () => {
|
|
1067
|
+
if (currentWizardStep > 0) {
|
|
1068
|
+
setCurrentWizardStep(currentWizardStep - 1);
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// Check if current step is complete
|
|
1073
|
+
const isStepComplete = (stepId) => {
|
|
1074
|
+
switch (stepId) {
|
|
1075
|
+
case 'table':
|
|
1076
|
+
return !!selectedTable;
|
|
1077
|
+
case 'relationships':
|
|
1078
|
+
return true; // Optional step
|
|
1079
|
+
case 'columns':
|
|
1080
|
+
return selectedColumns.length > 0;
|
|
1081
|
+
case 'filters':
|
|
1082
|
+
return true; // Optional step
|
|
1083
|
+
case 'preview':
|
|
1084
|
+
return previewData.length > 0;
|
|
1085
|
+
default:
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
// Fetch database tables from the API
|
|
1091
|
+
const fetchDatabaseTables = async () => {
|
|
1092
|
+
if (!tableUrl) {
|
|
1093
|
+
console.warn('No tableUrl provided to fetch database tables');
|
|
1094
|
+
setIsLoadingTables(false);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
setIsLoadingTables(true);
|
|
1099
|
+
|
|
1100
|
+
// List of tables to hide from the report builder
|
|
1101
|
+
const hiddenTables = [
|
|
1102
|
+
'audits',
|
|
1103
|
+
'crm_settings',
|
|
1104
|
+
'two_factor_remember_tokens',
|
|
1105
|
+
'system_requests',
|
|
1106
|
+
'migrations',
|
|
1107
|
+
'password_resets',
|
|
1108
|
+
'personal_access_tokens',
|
|
1109
|
+
'sessions',
|
|
1110
|
+
'cache',
|
|
1111
|
+
'jobs',
|
|
1112
|
+
'failed_jobs',
|
|
1113
|
+
];
|
|
1114
|
+
|
|
1115
|
+
try {
|
|
1116
|
+
// Use POST method to match the working advanced mode
|
|
1117
|
+
const result = await CustomFetch(tableUrl, 'POST', {});
|
|
1118
|
+
|
|
1119
|
+
if (result.error) {
|
|
1120
|
+
toast.error(result.error);
|
|
1121
|
+
} else {
|
|
1122
|
+
// Handle response format like the working advanced mode
|
|
1123
|
+
let tableData =
|
|
1124
|
+
result.data?.data || result.data || result.tables || result;
|
|
1125
|
+
|
|
1126
|
+
// Ensure tableData is an array
|
|
1127
|
+
if (!Array.isArray(tableData)) {
|
|
1128
|
+
console.error('Unexpected table data format:', tableData);
|
|
1129
|
+
toast.error('Unexpected response format from table API');
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Filter out hidden tables and tables with underscores (like advanced mode)
|
|
1134
|
+
let filteredTables = tableData
|
|
1135
|
+
.filter((table) => {
|
|
1136
|
+
const tableName =
|
|
1137
|
+
typeof table === 'string' ? table : table.name;
|
|
1138
|
+
return (
|
|
1139
|
+
!hiddenTables.includes(tableName) &&
|
|
1140
|
+
!tableName.includes('_')
|
|
1141
|
+
);
|
|
1142
|
+
})
|
|
1143
|
+
.map((table) => {
|
|
1144
|
+
// Normalize table format
|
|
1145
|
+
if (typeof table === 'string') {
|
|
1146
|
+
return { name: table };
|
|
1147
|
+
}
|
|
1148
|
+
return table;
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
// Apply definition-based filtering and renaming (like GenericReport)
|
|
1152
|
+
if (
|
|
1153
|
+
definition &&
|
|
1154
|
+
definition.tables &&
|
|
1155
|
+
definition.tables.length > 0
|
|
1156
|
+
) {
|
|
1157
|
+
const definitionTableIds = definition.tables.map(
|
|
1158
|
+
(t) => t.id
|
|
1159
|
+
);
|
|
1160
|
+
filteredTables = filteredTables.filter((table) =>
|
|
1161
|
+
definitionTableIds.includes(table.name)
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Sort alphabetically
|
|
1166
|
+
filteredTables.sort((a, b) => a.name.localeCompare(b.name));
|
|
1167
|
+
|
|
1168
|
+
console.log('Loaded tables:', filteredTables);
|
|
1169
|
+
setTables(filteredTables);
|
|
1170
|
+
|
|
1171
|
+
if (filteredTables.length === 0) {
|
|
1172
|
+
console.warn('No tables available after filtering');
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
toast.error('Failed to load database tables');
|
|
1177
|
+
console.error('Error fetching tables:', error);
|
|
1178
|
+
} finally {
|
|
1179
|
+
setIsLoadingTables(false);
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
// Fetch table columns when a table is selected
|
|
1184
|
+
const fetchTableColumns = async (tableName) => {
|
|
1185
|
+
if (!tableName || !columnUrl) return;
|
|
1186
|
+
|
|
1187
|
+
setIsLoadingTables(true);
|
|
1188
|
+
|
|
1189
|
+
try {
|
|
1190
|
+
const result = await CustomFetch(columnUrl, 'POST', {
|
|
1191
|
+
table: tableName,
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
console.log('Column API Response:', result); // Debug log
|
|
1195
|
+
|
|
1196
|
+
if (result.error) {
|
|
1197
|
+
toast.error(result.error);
|
|
1198
|
+
} else {
|
|
1199
|
+
// Handle the specific response format from getTableColumns (like GenericReport)
|
|
1200
|
+
let columnsData = null;
|
|
1201
|
+
|
|
1202
|
+
if (result.data?.success && result.data?.data?.columns) {
|
|
1203
|
+
// Primary format: {"success":true,"data":{"table":"contacts_tags","columns":[...]}}
|
|
1204
|
+
columnsData = result.data.data.columns;
|
|
1205
|
+
} else if (result.columns) {
|
|
1206
|
+
// Direct format: {"columns": [...]}
|
|
1207
|
+
columnsData = result.columns;
|
|
1208
|
+
} else if (result.data?.columns) {
|
|
1209
|
+
// Alternative format: {"data": {"columns": [...]}}
|
|
1210
|
+
columnsData = result.data.columns;
|
|
1211
|
+
} else {
|
|
1212
|
+
// Fallback formats
|
|
1213
|
+
const fallback = result.data?.data || result.data;
|
|
1214
|
+
if (Array.isArray(fallback)) {
|
|
1215
|
+
columnsData = fallback;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
if (Array.isArray(columnsData) && columnsData.length > 0) {
|
|
1220
|
+
setTableColumns(columnsData);
|
|
1221
|
+
console.log('Loaded columns:', columnsData);
|
|
1222
|
+
|
|
1223
|
+
// Note: Auto-selection of columns removed since user should choose relationships first,
|
|
1224
|
+
// then select columns in step 3
|
|
1225
|
+
} else {
|
|
1226
|
+
console.error('Invalid columns data format:', result);
|
|
1227
|
+
setTableColumns([]);
|
|
1228
|
+
toast.error(
|
|
1229
|
+
'Failed to load columns. Please check the table configuration.'
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
toast.error('Failed to load table columns');
|
|
1235
|
+
console.error('Error fetching columns:', error);
|
|
1236
|
+
} finally {
|
|
1237
|
+
setIsLoadingTables(false);
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1241
|
+
// Fetch saved reports
|
|
1242
|
+
const fetchSavedReports = async () => {
|
|
1243
|
+
if (!reportsUrl) return;
|
|
1244
|
+
|
|
1245
|
+
setIsLoadingReports(true);
|
|
1246
|
+
|
|
1247
|
+
try {
|
|
1248
|
+
const result = await CustomFetch(reportsUrl, 'GET');
|
|
1249
|
+
|
|
1250
|
+
if (result.error) {
|
|
1251
|
+
toast.error(result.error);
|
|
1252
|
+
} else if (result.data?.success && result.data?.data) {
|
|
1253
|
+
setSavedReports(result.data.data);
|
|
1254
|
+
} else if (result.reports) {
|
|
1255
|
+
setSavedReports(result.reports);
|
|
1256
|
+
} else if (result.data) {
|
|
1257
|
+
setSavedReports(result.data);
|
|
1258
|
+
}
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
toast.error('Failed to load saved reports');
|
|
1261
|
+
console.error('Error fetching reports:', error);
|
|
1262
|
+
} finally {
|
|
1263
|
+
setIsLoadingReports(false);
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
// Fetch suggested joins for a table
|
|
1268
|
+
const fetchSuggestedJoins = async (tableName) => {
|
|
1269
|
+
if (!tableName || !suggestedJoinsUrl) {
|
|
1270
|
+
console.warn(
|
|
1271
|
+
'Missing tableName or suggestedJoinsUrl for fetchSuggestedJoins'
|
|
1272
|
+
);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
setIsLoadingSuggestedJoins({
|
|
1277
|
+
...isLoadingSuggestedJoins,
|
|
1278
|
+
[tableName]: true,
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
try {
|
|
1282
|
+
const result = await CustomFetch(suggestedJoinsUrl, 'POST', {
|
|
1283
|
+
table: tableName,
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
console.log('Suggested joins API response:', result); // Debug log
|
|
1287
|
+
|
|
1288
|
+
if (result.error) {
|
|
1289
|
+
toast.error(result.error);
|
|
1290
|
+
} else {
|
|
1291
|
+
// Handle response format like GenericReport (robust parsing)
|
|
1292
|
+
let joinsData = null;
|
|
1293
|
+
|
|
1294
|
+
if (result.data?.success && result.data?.data?.suggestedJoins) {
|
|
1295
|
+
// Primary format: {"success":true,"data":{"suggestedJoins":[...]}}
|
|
1296
|
+
joinsData = result.data.data.suggestedJoins;
|
|
1297
|
+
} else if (result.suggestedJoins) {
|
|
1298
|
+
// Direct format: {"suggestedJoins": [...]}
|
|
1299
|
+
joinsData = result.suggestedJoins;
|
|
1300
|
+
} else if (result.data?.suggestedJoins) {
|
|
1301
|
+
// Alternative format: {"data": {"suggestedJoins": [...]}}
|
|
1302
|
+
joinsData = result.data.suggestedJoins;
|
|
1303
|
+
} else {
|
|
1304
|
+
// Fallback formats
|
|
1305
|
+
const fallback = result.data?.data || result.data;
|
|
1306
|
+
if (Array.isArray(fallback)) {
|
|
1307
|
+
joinsData = fallback;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if (Array.isArray(joinsData)) {
|
|
1312
|
+
// Filter out unwanted joins (like safebook)
|
|
1313
|
+
const filteredJoins = joinsData.filter((join) => {
|
|
1314
|
+
return !(
|
|
1315
|
+
(join.sourceTable &&
|
|
1316
|
+
join.sourceTable
|
|
1317
|
+
.toLowerCase()
|
|
1318
|
+
.includes('safebook')) ||
|
|
1319
|
+
(join.targetTable &&
|
|
1320
|
+
join.targetTable
|
|
1321
|
+
.toLowerCase()
|
|
1322
|
+
.includes('safebook'))
|
|
1323
|
+
);
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
console.log(
|
|
1327
|
+
`Found ${filteredJoins.length} suggested joins for ${tableName}:`,
|
|
1328
|
+
filteredJoins
|
|
1329
|
+
);
|
|
1330
|
+
|
|
1331
|
+
setSuggestedJoins({
|
|
1332
|
+
...suggestedJoins,
|
|
1333
|
+
[tableName]: filteredJoins,
|
|
1334
|
+
});
|
|
1335
|
+
} else {
|
|
1336
|
+
console.warn(
|
|
1337
|
+
'No valid suggested joins found or invalid response format:',
|
|
1338
|
+
result
|
|
1339
|
+
);
|
|
1340
|
+
setSuggestedJoins({
|
|
1341
|
+
...suggestedJoins,
|
|
1342
|
+
[tableName]: [],
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
console.error('Error fetching suggested joins:', error);
|
|
1348
|
+
setSuggestedJoins({
|
|
1349
|
+
...suggestedJoins,
|
|
1350
|
+
[tableName]: [],
|
|
1351
|
+
});
|
|
1352
|
+
} finally {
|
|
1353
|
+
setIsLoadingSuggestedJoins({
|
|
1354
|
+
...isLoadingSuggestedJoins,
|
|
1355
|
+
[tableName]: false,
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
// Add join relationship
|
|
1361
|
+
const addJoin = (joinData) => {
|
|
1362
|
+
const newJoin = { ...joinData, availableColumns: [] };
|
|
1363
|
+
setJoins([...joins, newJoin]);
|
|
1364
|
+
|
|
1365
|
+
// Fetch columns for the joined table if not already available
|
|
1366
|
+
if (!availableTables.includes(joinData.targetTable)) {
|
|
1367
|
+
setAvailableTables([...availableTables, joinData.targetTable]);
|
|
1368
|
+
fetchJoinColumns(joinData.targetTable, joins.length); // Use current joins.length as index
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
// Fetch columns for a joined table (similar to GenericReport)
|
|
1373
|
+
const fetchJoinColumns = async (tableName, joinIndex) => {
|
|
1374
|
+
if (!tableName || !columnUrl) {
|
|
1375
|
+
console.warn('Missing tableName or columnUrl for fetchJoinColumns');
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
setIsLoadingJoinColumns(true);
|
|
1380
|
+
|
|
1381
|
+
try {
|
|
1382
|
+
const result = await CustomFetch(columnUrl, 'POST', {
|
|
1383
|
+
table: tableName,
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
if (result.error) {
|
|
1387
|
+
toast.error(result.error);
|
|
1388
|
+
} else {
|
|
1389
|
+
// Handle the specific response format from getTableColumns
|
|
1390
|
+
let columnsData = null;
|
|
1391
|
+
|
|
1392
|
+
if (result.data?.success && result.data?.data?.columns) {
|
|
1393
|
+
columnsData = result.data.data.columns;
|
|
1394
|
+
} else if (result.columns) {
|
|
1395
|
+
columnsData = result.columns;
|
|
1396
|
+
} else if (result.data?.columns) {
|
|
1397
|
+
columnsData = result.data.columns;
|
|
1398
|
+
} else {
|
|
1399
|
+
const fallback = result.data?.data || result.data;
|
|
1400
|
+
if (Array.isArray(fallback)) {
|
|
1401
|
+
columnsData = fallback;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
if (Array.isArray(columnsData) && columnsData.length > 0) {
|
|
1406
|
+
// Update the join with available columns
|
|
1407
|
+
setJoins((prevJoins) => {
|
|
1408
|
+
const updatedJoins = [...prevJoins];
|
|
1409
|
+
if (updatedJoins[joinIndex]) {
|
|
1410
|
+
updatedJoins[joinIndex].availableColumns =
|
|
1411
|
+
columnsData;
|
|
1412
|
+
}
|
|
1413
|
+
return updatedJoins;
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
console.log(
|
|
1417
|
+
`Loaded ${columnsData.length} columns for joined table ${tableName}`
|
|
1418
|
+
);
|
|
1419
|
+
} else {
|
|
1420
|
+
console.error(
|
|
1421
|
+
'Invalid columns data format for joined table:',
|
|
1422
|
+
result
|
|
1423
|
+
);
|
|
1424
|
+
toast.error(
|
|
1425
|
+
`Failed to load columns for ${tableName}. Please check the table configuration.`
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
toast.error(`Failed to load columns for joined table ${tableName}`);
|
|
1431
|
+
console.error('Error fetching join columns:', error);
|
|
1432
|
+
} finally {
|
|
1433
|
+
setIsLoadingJoinColumns(false);
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
// Remove join relationship
|
|
1438
|
+
const removeJoin = (joinIndex) => {
|
|
1439
|
+
const updatedJoins = joins.filter((_, index) => index !== joinIndex);
|
|
1440
|
+
setJoins(updatedJoins);
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
// Manual relationship creation functions
|
|
1444
|
+
const handleManualJoinChange = (field, value) => {
|
|
1445
|
+
setManualJoin((prev) => ({
|
|
1446
|
+
...prev,
|
|
1447
|
+
[field]: value,
|
|
1448
|
+
}));
|
|
1449
|
+
|
|
1450
|
+
// Fetch columns when table selection changes
|
|
1451
|
+
if (field === 'sourceTable' && value) {
|
|
1452
|
+
fetchManualJoinColumns(value, 'source');
|
|
1453
|
+
} else if (field === 'targetTable' && value) {
|
|
1454
|
+
fetchManualJoinColumns(value, 'target');
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
|
|
1458
|
+
const fetchManualJoinColumns = async (tableName, type) => {
|
|
1459
|
+
if (!tableName || !columnUrl) return;
|
|
1460
|
+
|
|
1461
|
+
try {
|
|
1462
|
+
const result = await CustomFetch(
|
|
1463
|
+
`${columnUrl}/${tableName}`,
|
|
1464
|
+
'GET'
|
|
1465
|
+
);
|
|
1466
|
+
if (result.data?.success && result.data.data) {
|
|
1467
|
+
setManualJoinColumns((prev) => ({
|
|
1468
|
+
...prev,
|
|
1469
|
+
[type]: result.data.data,
|
|
1470
|
+
}));
|
|
1471
|
+
}
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
console.error(`Error fetching ${type} columns:`, error);
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
|
|
1477
|
+
const addManualJoin = () => {
|
|
1478
|
+
if (
|
|
1479
|
+
!manualJoin.sourceTable ||
|
|
1480
|
+
!manualJoin.targetTable ||
|
|
1481
|
+
!manualJoin.sourceColumn ||
|
|
1482
|
+
!manualJoin.targetColumn
|
|
1483
|
+
) {
|
|
1484
|
+
toast.warning(
|
|
1485
|
+
'Please fill in all fields for the manual relationship.'
|
|
1486
|
+
);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
const newJoin = {
|
|
1491
|
+
sourceTable: manualJoin.sourceTable,
|
|
1492
|
+
targetTable: manualJoin.targetTable,
|
|
1493
|
+
sourceColumn: manualJoin.sourceColumn,
|
|
1494
|
+
targetColumn: manualJoin.targetColumn,
|
|
1495
|
+
joinType: manualJoin.joinType,
|
|
1496
|
+
isManual: true,
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1499
|
+
setJoins([...joins, newJoin]);
|
|
1500
|
+
|
|
1501
|
+
// Reset form and hide it
|
|
1502
|
+
setManualJoin({
|
|
1503
|
+
sourceTable: '',
|
|
1504
|
+
targetTable: '',
|
|
1505
|
+
sourceColumn: '',
|
|
1506
|
+
targetColumn: '',
|
|
1507
|
+
joinType: 'INNER JOIN',
|
|
1508
|
+
});
|
|
1509
|
+
setManualJoinColumns({ source: [], target: [] });
|
|
1510
|
+
setShowManualJoinForm(false);
|
|
1511
|
+
|
|
1512
|
+
toast.success('Manual relationship added successfully!');
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
// Table selection handler
|
|
1516
|
+
const handleTableSelect = (tableName) => {
|
|
1517
|
+
setSelectedTable(tableName);
|
|
1518
|
+
setSelectedColumns([]);
|
|
1519
|
+
setJoins([]);
|
|
1520
|
+
setAvailableTables([tableName]);
|
|
1521
|
+
fetchTableColumns(tableName);
|
|
1522
|
+
fetchSuggestedJoins(tableName);
|
|
1523
|
+
|
|
1524
|
+
if (beginnerMode) {
|
|
1525
|
+
goToNextStep(); // Now goes to relationships step
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1529
|
+
// Column selection handler
|
|
1530
|
+
const handleColumnToggle = (column, tableName = selectedTable) => {
|
|
1531
|
+
const columnId = `${tableName}.${column.name}`;
|
|
1532
|
+
const isSelected = selectedColumns.some(
|
|
1533
|
+
(col) => `${col.table}.${col.column}` === columnId
|
|
1534
|
+
);
|
|
1535
|
+
|
|
1536
|
+
if (isSelected) {
|
|
1537
|
+
setSelectedColumns(
|
|
1538
|
+
selectedColumns.filter(
|
|
1539
|
+
(col) => `${col.table}.${col.column}` !== columnId
|
|
1540
|
+
)
|
|
1541
|
+
);
|
|
1542
|
+
} else {
|
|
1543
|
+
setSelectedColumns([
|
|
1544
|
+
...selectedColumns,
|
|
1545
|
+
{
|
|
1546
|
+
table: tableName,
|
|
1547
|
+
column: column.name,
|
|
1548
|
+
displayName: formatName(column.name),
|
|
1549
|
+
},
|
|
1550
|
+
]);
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
|
|
1554
|
+
// Execute report with local pagination - fetch all data at once
|
|
1555
|
+
const executeReport = async () => {
|
|
1556
|
+
if (!selectedTable || selectedColumns.length === 0) {
|
|
1557
|
+
toast.warning(
|
|
1558
|
+
beginnerMode
|
|
1559
|
+
? 'Please select at least one field to include in your report.'
|
|
1560
|
+
: 'Please select a table and at least one column.'
|
|
1561
|
+
);
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
setIsLoading(true);
|
|
1566
|
+
|
|
1567
|
+
try {
|
|
1568
|
+
// Use the same payload format as GenericReport (with query wrapper)
|
|
1569
|
+
const queryConfig = {
|
|
1570
|
+
mainTable: selectedTable,
|
|
1571
|
+
columns: selectedColumns.map((col) => ({
|
|
1572
|
+
table: col.table,
|
|
1573
|
+
column: col.column,
|
|
1574
|
+
alias: col.alias || col.displayName,
|
|
1575
|
+
})),
|
|
1576
|
+
joins: joins.map((join) => ({
|
|
1577
|
+
joinType: join.joinType,
|
|
1578
|
+
targetTable: join.targetTable,
|
|
1579
|
+
sourceColumn: join.sourceColumn,
|
|
1580
|
+
targetColumn: join.targetColumn,
|
|
1581
|
+
})),
|
|
1582
|
+
filters: flattenFilterCriteria(filterCriteria),
|
|
1583
|
+
sorting: sortCriteria,
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
const reportConfig = {
|
|
1587
|
+
query: queryConfig,
|
|
1588
|
+
limit: 1000, // Fetch up to 1000 records for local pagination
|
|
1589
|
+
offset: 0, // Always start from beginning
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
const result = await CustomFetch(executeUrl, 'POST', reportConfig);
|
|
1593
|
+
|
|
1594
|
+
if (result.error) {
|
|
1595
|
+
toast.error(result.error);
|
|
1596
|
+
} else if (result.data?.success) {
|
|
1597
|
+
// Store all fetched data for local pagination
|
|
1598
|
+
const allFetchedData = result.data.data || [];
|
|
1599
|
+
setAllData(allFetchedData);
|
|
1600
|
+
console.info('Fetched data:', allFetchedData.length);
|
|
1601
|
+
|
|
1602
|
+
setTotalResults(allFetchedData.length);
|
|
1603
|
+
setExecutedSql(result.data.sql || '');
|
|
1604
|
+
|
|
1605
|
+
// Calculate total pages for local pagination
|
|
1606
|
+
const calculatedTotalPages = Math.ceil(
|
|
1607
|
+
allFetchedData.length / pageSize
|
|
1608
|
+
);
|
|
1609
|
+
|
|
1610
|
+
setTotalPages(calculatedTotalPages);
|
|
1611
|
+
console.info('Total pages:', calculatedTotalPages);
|
|
1612
|
+
|
|
1613
|
+
setCurrentPage(1); // Reset to first page
|
|
1614
|
+
|
|
1615
|
+
// Set initial page data (first page)
|
|
1616
|
+
const startIndex = 0;
|
|
1617
|
+
const endIndex = Math.min(pageSize, allFetchedData.length);
|
|
1618
|
+
setPreviewData(allFetchedData.slice(startIndex, endIndex));
|
|
1619
|
+
|
|
1620
|
+
// Create grid columns dynamically from actual data with smart formatting
|
|
1621
|
+
if (result.data.data && result.data.data.length > 0) {
|
|
1622
|
+
const firstRow = result.data.data[0];
|
|
1623
|
+
const dynamicColumns = Object.keys(firstRow).map((key) => {
|
|
1624
|
+
// Find the column metadata from selected columns to get type information
|
|
1625
|
+
const columnMetadata = selectedColumns.find((col) => {
|
|
1626
|
+
// Handle both direct column names and aliased names
|
|
1627
|
+
return (
|
|
1628
|
+
col.column === key ||
|
|
1629
|
+
col.displayName === key ||
|
|
1630
|
+
key.includes(col.column) ||
|
|
1631
|
+
col.column.includes(key)
|
|
1632
|
+
);
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
// Check if any value in this column is JSON
|
|
1636
|
+
const hasJsonValues = result.data.data.some((row) => {
|
|
1637
|
+
return isJsonValue(row[key]);
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
// Try to find column type from table columns
|
|
1641
|
+
let columnType = 'varchar'; // default
|
|
1642
|
+
if (columnMetadata) {
|
|
1643
|
+
const tableColumn = tableColumns.find(
|
|
1644
|
+
(tc) => tc.name === columnMetadata.column
|
|
1645
|
+
);
|
|
1646
|
+
if (tableColumn) {
|
|
1647
|
+
columnType = tableColumn.type;
|
|
1648
|
+
} else {
|
|
1649
|
+
// Check in joined table columns
|
|
1650
|
+
joins.forEach((join) => {
|
|
1651
|
+
if (join.availableColumns) {
|
|
1652
|
+
const joinedColumn =
|
|
1653
|
+
join.availableColumns.find(
|
|
1654
|
+
(jc) =>
|
|
1655
|
+
jc.name ===
|
|
1656
|
+
columnMetadata.column
|
|
1657
|
+
);
|
|
1658
|
+
if (joinedColumn) {
|
|
1659
|
+
columnType = joinedColumn.type;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
return {
|
|
1667
|
+
name: key,
|
|
1668
|
+
header: formatName(key), // Use formatted name as header
|
|
1669
|
+
defaultFlex: 1,
|
|
1670
|
+
minWidth: hasJsonValues ? 250 : 100, // Wider columns for JSON data
|
|
1671
|
+
maxWidth: hasJsonValues ? 400 : undefined, // Max width for JSON columns
|
|
1672
|
+
render: ({ value }) => {
|
|
1673
|
+
// Use smart formatting for the cell value
|
|
1674
|
+
const formattedValue = formatSmartValue(
|
|
1675
|
+
value,
|
|
1676
|
+
key,
|
|
1677
|
+
columnType
|
|
1678
|
+
);
|
|
1679
|
+
|
|
1680
|
+
// For JSON content, wrap in a container that allows full height expansion
|
|
1681
|
+
if (isJsonValue(value)) {
|
|
1682
|
+
return (
|
|
1683
|
+
<div
|
|
1684
|
+
style={{
|
|
1685
|
+
whiteSpace: 'normal',
|
|
1686
|
+
wordWrap: 'break-word',
|
|
1687
|
+
lineHeight: '1.4',
|
|
1688
|
+
padding: '4px 0',
|
|
1689
|
+
width: '100%',
|
|
1690
|
+
}}
|
|
1691
|
+
>
|
|
1692
|
+
{formattedValue}
|
|
1693
|
+
</div>
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
return formattedValue;
|
|
1698
|
+
},
|
|
1699
|
+
};
|
|
1700
|
+
});
|
|
1701
|
+
setGridColumns(dynamicColumns);
|
|
1702
|
+
} else {
|
|
1703
|
+
// Fallback to selected columns if no data
|
|
1704
|
+
const columns = selectedColumns.map((col) => {
|
|
1705
|
+
// Find column type from table columns
|
|
1706
|
+
let columnType = 'varchar';
|
|
1707
|
+
const tableColumn = tableColumns.find(
|
|
1708
|
+
(tc) => tc.name === col.column
|
|
1709
|
+
);
|
|
1710
|
+
if (tableColumn) {
|
|
1711
|
+
columnType = tableColumn.type;
|
|
1712
|
+
} else {
|
|
1713
|
+
// Check in joined table columns
|
|
1714
|
+
joins.forEach((join) => {
|
|
1715
|
+
if (join.availableColumns) {
|
|
1716
|
+
const joinedColumn =
|
|
1717
|
+
join.availableColumns.find(
|
|
1718
|
+
(jc) => jc.name === col.column
|
|
1719
|
+
);
|
|
1720
|
+
if (joinedColumn) {
|
|
1721
|
+
columnType = joinedColumn.type;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
return {
|
|
1728
|
+
name: col.displayName || formatName(col.column),
|
|
1729
|
+
header: col.displayName || formatName(col.column),
|
|
1730
|
+
defaultFlex: 1,
|
|
1731
|
+
minWidth: 100,
|
|
1732
|
+
render: ({ value }) => {
|
|
1733
|
+
const formattedValue = formatSmartValue(
|
|
1734
|
+
value,
|
|
1735
|
+
col.column,
|
|
1736
|
+
columnType
|
|
1737
|
+
);
|
|
1738
|
+
|
|
1739
|
+
// For JSON content, wrap in a container that allows full height expansion
|
|
1740
|
+
if (isJsonValue(value)) {
|
|
1741
|
+
return (
|
|
1742
|
+
<div
|
|
1743
|
+
style={{
|
|
1744
|
+
whiteSpace: 'normal',
|
|
1745
|
+
wordWrap: 'break-word',
|
|
1746
|
+
lineHeight: '1.4',
|
|
1747
|
+
padding: '4px 0',
|
|
1748
|
+
width: '100%',
|
|
1749
|
+
}}
|
|
1750
|
+
>
|
|
1751
|
+
{formattedValue}
|
|
1752
|
+
</div>
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
return formattedValue;
|
|
1757
|
+
},
|
|
1758
|
+
};
|
|
1759
|
+
});
|
|
1760
|
+
setGridColumns(columns);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
if (
|
|
1764
|
+
beginnerMode &&
|
|
1765
|
+
currentWizardStep < wizardSteps.length - 1
|
|
1766
|
+
) {
|
|
1767
|
+
setCurrentWizardStep(wizardSteps.length - 1);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// Show success message with total records found
|
|
1771
|
+
toast.success(`Found ${allFetchedData.length} records`);
|
|
1772
|
+
|
|
1773
|
+
// Show warning if we hit the 1000 record limit
|
|
1774
|
+
if (allFetchedData.length === 1000) {
|
|
1775
|
+
toast.warning(
|
|
1776
|
+
'Showing first 1000 records. Consider adding filters to narrow your results.'
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
} else {
|
|
1780
|
+
toast.error(
|
|
1781
|
+
'Failed to execute report: ' +
|
|
1782
|
+
(result.data?.message ||
|
|
1783
|
+
result.message ||
|
|
1784
|
+
'Unknown error')
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
toast.error('Failed to execute report');
|
|
1789
|
+
console.error('Error executing report:', error);
|
|
1790
|
+
} finally {
|
|
1791
|
+
setIsLoading(false);
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
|
|
1795
|
+
// Local pagination handlers
|
|
1796
|
+
const handlePageChange = (newPage) => {
|
|
1797
|
+
if (
|
|
1798
|
+
newPage >= 1 &&
|
|
1799
|
+
newPage <= totalPages &&
|
|
1800
|
+
newPage !== currentPage &&
|
|
1801
|
+
allData.length > 0
|
|
1802
|
+
) {
|
|
1803
|
+
setCurrentPage(newPage);
|
|
1804
|
+
|
|
1805
|
+
// Calculate slice indices for local pagination
|
|
1806
|
+
const startIndex = (newPage - 1) * pageSize;
|
|
1807
|
+
const endIndex = Math.min(startIndex + pageSize, allData.length);
|
|
1808
|
+
setPreviewData(allData.slice(startIndex, endIndex));
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
|
|
1812
|
+
const handlePageSizeChange = (newPageSize) => {
|
|
1813
|
+
if (newPageSize !== pageSize && allData.length > 0) {
|
|
1814
|
+
setPageSize(newPageSize);
|
|
1815
|
+
setCurrentPage(1); // Reset to first page
|
|
1816
|
+
|
|
1817
|
+
// Recalculate total pages with new page size
|
|
1818
|
+
const calculatedTotalPages = Math.ceil(
|
|
1819
|
+
allData.length / newPageSize
|
|
1820
|
+
);
|
|
1821
|
+
setTotalPages(calculatedTotalPages);
|
|
1822
|
+
|
|
1823
|
+
// Update preview data for first page with new page size
|
|
1824
|
+
const endIndex = Math.min(newPageSize, allData.length);
|
|
1825
|
+
setPreviewData(allData.slice(0, endIndex));
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
|
|
1829
|
+
// Reset pagination when starting a new report
|
|
1830
|
+
const resetPagination = () => {
|
|
1831
|
+
setCurrentPage(1);
|
|
1832
|
+
setTotalPages(0);
|
|
1833
|
+
setPreviewData([]);
|
|
1834
|
+
setTotalResults(0);
|
|
1835
|
+
setAllData([]);
|
|
1836
|
+
};
|
|
1837
|
+
|
|
1838
|
+
// Render the appropriate content based on mode
|
|
1839
|
+
const renderContent = () => {
|
|
1840
|
+
if (beginnerMode && showTemplates) {
|
|
1841
|
+
return renderTemplateSelection();
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
if (beginnerMode) {
|
|
1845
|
+
return renderWizardMode();
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
return renderAdvancedMode();
|
|
1849
|
+
};
|
|
1850
|
+
|
|
1851
|
+
// Load saved report configuration
|
|
1852
|
+
const loadSavedReport = (report) => {
|
|
1853
|
+
try {
|
|
1854
|
+
const config =
|
|
1855
|
+
typeof report.detail === 'string'
|
|
1856
|
+
? JSON.parse(report.detail)
|
|
1857
|
+
: report.detail;
|
|
1858
|
+
|
|
1859
|
+
if (config.mainTable) {
|
|
1860
|
+
setSelectedTable(config.mainTable);
|
|
1861
|
+
setAvailableTables([config.mainTable]);
|
|
1862
|
+
fetchTableColumns(config.mainTable);
|
|
1863
|
+
fetchSuggestedJoins(config.mainTable);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
if (config.columns) {
|
|
1867
|
+
setSelectedColumns(config.columns);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
if (config.joins) {
|
|
1871
|
+
setJoins(config.joins);
|
|
1872
|
+
config.joins.forEach((join) => {
|
|
1873
|
+
if (!availableTables.includes(join.targetTable)) {
|
|
1874
|
+
setAvailableTables((prev) => [
|
|
1875
|
+
...prev,
|
|
1876
|
+
join.targetTable,
|
|
1877
|
+
]);
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (config.filters) {
|
|
1883
|
+
setFilterCriteria(config.filters);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
if (config.sorting) {
|
|
1887
|
+
setSortCriteria(config.sorting);
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
setReportName(report.label);
|
|
1891
|
+
setIsPublic(report.is_public || false);
|
|
1892
|
+
setLoadedReportId(report.id); // Track the loaded report ID for overwrite functionality
|
|
1893
|
+
setShowTemplates(false);
|
|
1894
|
+
setCurrentWizardStep(2); // Start from columns step since table and relationships are already selected
|
|
1895
|
+
|
|
1896
|
+
toast.success(`Loaded report: ${report.label}`);
|
|
1897
|
+
} catch (error) {
|
|
1898
|
+
toast.error('Failed to load report configuration');
|
|
1899
|
+
console.error('Error loading report:', error);
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
|
|
1903
|
+
// Render saved reports dashboard
|
|
1904
|
+
const renderTemplateSelection = () => {
|
|
1905
|
+
return (
|
|
1906
|
+
<div className={styles.templateSelection}>
|
|
1907
|
+
<div className={styles.templateHeader}>
|
|
1908
|
+
<h2>Choose a Starting Point</h2>
|
|
1909
|
+
<p>
|
|
1910
|
+
Select a saved report to modify, or create a new one
|
|
1911
|
+
from scratch
|
|
1912
|
+
</p>
|
|
1913
|
+
</div>
|
|
1914
|
+
|
|
1915
|
+
<div className={styles.reportsDashboard}>
|
|
1916
|
+
{/* Create New Report Option */}
|
|
1917
|
+
<div className={styles.newReportSection}>
|
|
1918
|
+
<div
|
|
1919
|
+
className={styles.newReportCard}
|
|
1920
|
+
onClick={() => setShowTemplates(false)}
|
|
1921
|
+
>
|
|
1922
|
+
<CirclePlus size={32} />
|
|
1923
|
+
<h4>Create New Report</h4>
|
|
1924
|
+
<p>
|
|
1925
|
+
Build a custom report from scratch with our
|
|
1926
|
+
guided wizard
|
|
1927
|
+
</p>
|
|
1928
|
+
</div>
|
|
1929
|
+
</div>
|
|
1930
|
+
|
|
1931
|
+
{/* Saved Reports List */}
|
|
1932
|
+
{isLoadingReports ? (
|
|
1933
|
+
<div className={styles.loading}>
|
|
1934
|
+
Loading your saved reports...
|
|
1935
|
+
</div>
|
|
1936
|
+
) : savedReports.length > 0 ? (
|
|
1937
|
+
<div className={styles.savedReportsSection}>
|
|
1938
|
+
<h3>Your Saved Reports ({savedReports.length})</h3>
|
|
1939
|
+
<div className={styles.reportsGrid}>
|
|
1940
|
+
{savedReports.map((report) => (
|
|
1941
|
+
<div
|
|
1942
|
+
key={report.id}
|
|
1943
|
+
className={styles.reportCard}
|
|
1944
|
+
>
|
|
1945
|
+
<div
|
|
1946
|
+
className={styles.reportCardContent}
|
|
1947
|
+
onClick={() =>
|
|
1948
|
+
loadSavedReport(report)
|
|
1949
|
+
}
|
|
1950
|
+
>
|
|
1951
|
+
<div
|
|
1952
|
+
className={styles.reportHeader}
|
|
1953
|
+
>
|
|
1954
|
+
<h4 title={report.label}>
|
|
1955
|
+
{report.label}
|
|
1956
|
+
</h4>
|
|
1957
|
+
<div
|
|
1958
|
+
className={
|
|
1959
|
+
styles.reportMeta
|
|
1960
|
+
}
|
|
1961
|
+
>
|
|
1962
|
+
<span
|
|
1963
|
+
className={
|
|
1964
|
+
styles.reportVisibility
|
|
1965
|
+
}
|
|
1966
|
+
>
|
|
1967
|
+
{report.is_public
|
|
1968
|
+
? '🌐'
|
|
1969
|
+
: '🔒'}
|
|
1970
|
+
</span>
|
|
1971
|
+
<span
|
|
1972
|
+
className={
|
|
1973
|
+
styles.reportDate
|
|
1974
|
+
}
|
|
1975
|
+
>
|
|
1976
|
+
{moment(
|
|
1977
|
+
report.created_at
|
|
1978
|
+
).fromNow()}
|
|
1979
|
+
</span>
|
|
1980
|
+
</div>
|
|
1981
|
+
</div>
|
|
1982
|
+
{report.detail && (
|
|
1983
|
+
<div
|
|
1984
|
+
className={
|
|
1985
|
+
styles.reportTableInfo
|
|
1986
|
+
}
|
|
1987
|
+
>
|
|
1988
|
+
<span
|
|
1989
|
+
className={
|
|
1990
|
+
styles.reportTable
|
|
1991
|
+
}
|
|
1992
|
+
>
|
|
1993
|
+
📊{' '}
|
|
1994
|
+
{formatName(
|
|
1995
|
+
typeof report.detail ===
|
|
1996
|
+
'string'
|
|
1997
|
+
? JSON.parse(
|
|
1998
|
+
report.detail
|
|
1999
|
+
).mainTable
|
|
2000
|
+
: report.detail
|
|
2001
|
+
.mainTable
|
|
2002
|
+
)}
|
|
2003
|
+
</span>
|
|
2004
|
+
</div>
|
|
2005
|
+
)}
|
|
2006
|
+
<div
|
|
2007
|
+
className={
|
|
2008
|
+
styles.reportClickHint
|
|
2009
|
+
}
|
|
2010
|
+
>
|
|
2011
|
+
Click to load and edit
|
|
2012
|
+
</div>
|
|
2013
|
+
</div>
|
|
2014
|
+
<div className={styles.reportActions}>
|
|
2015
|
+
<button
|
|
2016
|
+
className={
|
|
2017
|
+
styles.deleteReportBtn
|
|
2018
|
+
}
|
|
2019
|
+
onClick={(e) => {
|
|
2020
|
+
e.stopPropagation();
|
|
2021
|
+
deleteReport(
|
|
2022
|
+
report.id,
|
|
2023
|
+
report.label
|
|
2024
|
+
);
|
|
2025
|
+
}}
|
|
2026
|
+
title="Delete this report"
|
|
2027
|
+
>
|
|
2028
|
+
×
|
|
2029
|
+
</button>
|
|
2030
|
+
</div>
|
|
2031
|
+
</div>
|
|
2032
|
+
))}
|
|
2033
|
+
</div>
|
|
2034
|
+
</div>
|
|
2035
|
+
) : (
|
|
2036
|
+
<div className={styles.noReports}>
|
|
2037
|
+
<Data size={48} />
|
|
2038
|
+
<h3>No Saved Reports</h3>
|
|
2039
|
+
<p>
|
|
2040
|
+
You haven't created any reports yet. Click
|
|
2041
|
+
"Create New Report" to get started!
|
|
2042
|
+
</p>
|
|
2043
|
+
</div>
|
|
2044
|
+
)}
|
|
2045
|
+
</div>
|
|
2046
|
+
</div>
|
|
2047
|
+
);
|
|
2048
|
+
};
|
|
2049
|
+
|
|
2050
|
+
// Render wizard mode
|
|
2051
|
+
const renderWizardMode = () => {
|
|
2052
|
+
const currentStep = wizardSteps[currentWizardStep];
|
|
2053
|
+
const StepIcon = currentStep.icon;
|
|
2054
|
+
|
|
2055
|
+
return (
|
|
2056
|
+
<div className={styles.wizardMode}>
|
|
2057
|
+
{/* Progress bar */}
|
|
2058
|
+
<div className={styles.wizardProgress}>
|
|
2059
|
+
{wizardSteps.map((step, index) => {
|
|
2060
|
+
const StepIcon = step.icon;
|
|
2061
|
+
const isActive = index === currentWizardStep;
|
|
2062
|
+
const isComplete =
|
|
2063
|
+
index < currentWizardStep ||
|
|
2064
|
+
isStepComplete(step.id);
|
|
2065
|
+
|
|
2066
|
+
return (
|
|
2067
|
+
<div
|
|
2068
|
+
key={step.id}
|
|
2069
|
+
className={`${styles.wizardStep} ${
|
|
2070
|
+
isActive ? styles.active : ''
|
|
2071
|
+
} ${isComplete ? styles.complete : ''}`}
|
|
2072
|
+
onClick={() =>
|
|
2073
|
+
index <= currentWizardStep &&
|
|
2074
|
+
setCurrentWizardStep(index)
|
|
2075
|
+
}
|
|
2076
|
+
>
|
|
2077
|
+
<div className={styles.stepIcon}>
|
|
2078
|
+
{isComplete && !isActive ? (
|
|
2079
|
+
<CircleCheck size={20} />
|
|
2080
|
+
) : (
|
|
2081
|
+
<StepIcon size={20} />
|
|
2082
|
+
)}
|
|
2083
|
+
</div>
|
|
2084
|
+
<div className={styles.stepInfo}>
|
|
2085
|
+
<span className={styles.stepTitle}>
|
|
2086
|
+
{step.title}
|
|
2087
|
+
</span>
|
|
2088
|
+
<span className={styles.stepDescription}>
|
|
2089
|
+
{step.description}
|
|
2090
|
+
</span>
|
|
2091
|
+
</div>
|
|
2092
|
+
</div>
|
|
2093
|
+
);
|
|
2094
|
+
})}
|
|
2095
|
+
</div>
|
|
2096
|
+
|
|
2097
|
+
{/* Step content */}
|
|
2098
|
+
<div className={styles.wizardContent}>
|
|
2099
|
+
<div className={styles.stepHeader}>
|
|
2100
|
+
<div className={styles.stepNumber}>
|
|
2101
|
+
{currentWizardStep + 1}
|
|
2102
|
+
</div>
|
|
2103
|
+
<div className={styles.stepHeaderContent}>
|
|
2104
|
+
<div className={styles.stepIcon}>
|
|
2105
|
+
<StepIcon size={24} />
|
|
2106
|
+
</div>
|
|
2107
|
+
<div className={styles.stepHeaderText}>
|
|
2108
|
+
<h2>{currentStep.title}</h2>
|
|
2109
|
+
<p>{currentStep.description}</p>
|
|
2110
|
+
</div>
|
|
2111
|
+
</div>
|
|
2112
|
+
</div>
|
|
2113
|
+
|
|
2114
|
+
{/* Step Guidance */}
|
|
2115
|
+
{currentStep.guidance && (
|
|
2116
|
+
<div className={styles.stepGuidance}>
|
|
2117
|
+
<div className={styles.guidanceContent}>
|
|
2118
|
+
<h3>{currentStep.guidance.summary}</h3>
|
|
2119
|
+
<div className={styles.howToSteps}>
|
|
2120
|
+
<h4>How to complete this step:</h4>
|
|
2121
|
+
<ol>
|
|
2122
|
+
{currentStep.guidance.howTo.map(
|
|
2123
|
+
(step, index) => (
|
|
2124
|
+
<li key={index}>{step}</li>
|
|
2125
|
+
)
|
|
2126
|
+
)}
|
|
2127
|
+
</ol>
|
|
2128
|
+
</div>
|
|
2129
|
+
{currentStep.guidance.tip && (
|
|
2130
|
+
<div className={styles.guidanceTip}>
|
|
2131
|
+
<Info size={16} />
|
|
2132
|
+
<span>{currentStep.guidance.tip}</span>
|
|
2133
|
+
</div>
|
|
2134
|
+
)}
|
|
2135
|
+
</div>
|
|
2136
|
+
</div>
|
|
2137
|
+
)}
|
|
2138
|
+
|
|
2139
|
+
<div className={styles.stepBody}>
|
|
2140
|
+
{renderWizardStepContent(currentStep.id)}
|
|
2141
|
+
</div>
|
|
2142
|
+
|
|
2143
|
+
{/* Navigation buttons */}
|
|
2144
|
+
<div className={styles.wizardNavigation}>
|
|
2145
|
+
<button
|
|
2146
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
2147
|
+
onClick={goToPreviousStep}
|
|
2148
|
+
disabled={currentWizardStep === 0}
|
|
2149
|
+
>
|
|
2150
|
+
Previous
|
|
2151
|
+
</button>
|
|
2152
|
+
|
|
2153
|
+
{currentWizardStep === wizardSteps.length - 1 ? (
|
|
2154
|
+
<button
|
|
2155
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
2156
|
+
onClick={startAgain}
|
|
2157
|
+
>
|
|
2158
|
+
Start Again
|
|
2159
|
+
</button>
|
|
2160
|
+
) : (
|
|
2161
|
+
<button
|
|
2162
|
+
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
2163
|
+
onClick={goToNextStep}
|
|
2164
|
+
disabled={!isStepComplete(currentStep.id)}
|
|
2165
|
+
>
|
|
2166
|
+
Next
|
|
2167
|
+
</button>
|
|
2168
|
+
)}
|
|
2169
|
+
</div>
|
|
2170
|
+
</div>
|
|
2171
|
+
</div>
|
|
2172
|
+
);
|
|
2173
|
+
};
|
|
2174
|
+
|
|
2175
|
+
// Render wizard step content
|
|
2176
|
+
const renderWizardStepContent = (stepId) => {
|
|
2177
|
+
switch (stepId) {
|
|
2178
|
+
case 'table':
|
|
2179
|
+
return renderTableSelection();
|
|
2180
|
+
case 'relationships':
|
|
2181
|
+
return renderRelationships();
|
|
2182
|
+
case 'columns':
|
|
2183
|
+
return renderColumnSelection();
|
|
2184
|
+
case 'filters':
|
|
2185
|
+
return renderFilters();
|
|
2186
|
+
case 'preview':
|
|
2187
|
+
return renderPreview();
|
|
2188
|
+
default:
|
|
2189
|
+
return null;
|
|
2190
|
+
}
|
|
2191
|
+
};
|
|
2192
|
+
|
|
2193
|
+
// Render table selection - simple list format
|
|
2194
|
+
const renderTableSelection = () => {
|
|
2195
|
+
return (
|
|
2196
|
+
<div className={styles.tableSelection}>
|
|
2197
|
+
{isLoadingTables ? (
|
|
2198
|
+
<div className={styles.loading}>
|
|
2199
|
+
Loading available tables...
|
|
2200
|
+
</div>
|
|
2201
|
+
) : tables.length === 0 ? (
|
|
2202
|
+
<div className={styles.noTables}>
|
|
2203
|
+
<Data size={48} />
|
|
2204
|
+
<h3>No Data Available</h3>
|
|
2205
|
+
<p>
|
|
2206
|
+
No database tables are available. Please check your
|
|
2207
|
+
configuration or contact your administrator.
|
|
2208
|
+
</p>
|
|
2209
|
+
</div>
|
|
2210
|
+
) : (
|
|
2211
|
+
<div className={styles.simpleTableList}>
|
|
2212
|
+
<div className={styles.tableGrid}>
|
|
2213
|
+
{tables.map((table) => {
|
|
2214
|
+
const TableIcon = getTableIcon(table.name);
|
|
2215
|
+
const isSelected = selectedTable === table.name;
|
|
2216
|
+
const displayName = getTableDisplayName(
|
|
2217
|
+
table.name,
|
|
2218
|
+
definition
|
|
2219
|
+
);
|
|
2220
|
+
const description = getTableDescription(
|
|
2221
|
+
table.name,
|
|
2222
|
+
definition
|
|
2223
|
+
);
|
|
2224
|
+
|
|
2225
|
+
return (
|
|
2226
|
+
<div
|
|
2227
|
+
key={table.name}
|
|
2228
|
+
className={`${styles.tableCard} ${
|
|
2229
|
+
isSelected ? styles.selected : ''
|
|
2230
|
+
}`}
|
|
2231
|
+
onClick={() =>
|
|
2232
|
+
handleTableSelect(table.name)
|
|
2233
|
+
}
|
|
2234
|
+
>
|
|
2235
|
+
<div className={styles.tableCardIcon}>
|
|
2236
|
+
<TableIcon size={24} />
|
|
2237
|
+
</div>
|
|
2238
|
+
<div className={styles.tableCardInfo}>
|
|
2239
|
+
<h4>{displayName}</h4>
|
|
2240
|
+
<p>{description}</p>
|
|
2241
|
+
</div>
|
|
2242
|
+
{isSelected && (
|
|
2243
|
+
<div
|
|
2244
|
+
className={
|
|
2245
|
+
styles.tableCardCheck
|
|
2246
|
+
}
|
|
2247
|
+
>
|
|
2248
|
+
<CircleCheck size={20} />
|
|
2249
|
+
</div>
|
|
2250
|
+
)}
|
|
2251
|
+
</div>
|
|
2252
|
+
);
|
|
2253
|
+
})}
|
|
2254
|
+
</div>
|
|
2255
|
+
|
|
2256
|
+
{selectedTable && (
|
|
2257
|
+
<div className={styles.selectedTableInfo}>
|
|
2258
|
+
<CircleCheck size={16} />
|
|
2259
|
+
<span>
|
|
2260
|
+
Selected:{' '}
|
|
2261
|
+
{getTableDisplayName(
|
|
2262
|
+
selectedTable,
|
|
2263
|
+
definition
|
|
2264
|
+
)}
|
|
2265
|
+
</span>
|
|
2266
|
+
</div>
|
|
2267
|
+
)}
|
|
2268
|
+
</div>
|
|
2269
|
+
)}
|
|
2270
|
+
</div>
|
|
2271
|
+
);
|
|
2272
|
+
};
|
|
2273
|
+
|
|
2274
|
+
// Render column selection
|
|
2275
|
+
const renderColumnSelection = () => {
|
|
2276
|
+
// Add loading check at the beginning
|
|
2277
|
+
if (isLoadingTables || isLoadingJoinColumns) {
|
|
2278
|
+
return (
|
|
2279
|
+
<div className={styles.loading}>
|
|
2280
|
+
Loading table columns
|
|
2281
|
+
{isLoadingJoinColumns ? ' from joined tables' : ''}...
|
|
2282
|
+
</div>
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
if (tableColumns.length === 0) {
|
|
2287
|
+
return (
|
|
2288
|
+
<div className={styles.noColumns}>
|
|
2289
|
+
<Data size={48} />
|
|
2290
|
+
<h3>No Columns Available</h3>
|
|
2291
|
+
<p>
|
|
2292
|
+
No columns found for the selected table. Please try
|
|
2293
|
+
selecting a different table or check your configuration.
|
|
2294
|
+
</p>
|
|
2295
|
+
</div>
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// Get all available columns (main table + joined tables)
|
|
2300
|
+
const allAvailableColumns = [
|
|
2301
|
+
{
|
|
2302
|
+
tableName: selectedTable,
|
|
2303
|
+
displayName: getTableDisplayName(selectedTable, definition),
|
|
2304
|
+
columns: tableColumns,
|
|
2305
|
+
isMainTable: true,
|
|
2306
|
+
},
|
|
2307
|
+
...joins.map((join) => ({
|
|
2308
|
+
tableName: join.targetTable,
|
|
2309
|
+
displayName: getTableDisplayName(join.targetTable, definition),
|
|
2310
|
+
columns: join.availableColumns || [],
|
|
2311
|
+
isMainTable: false,
|
|
2312
|
+
})),
|
|
2313
|
+
];
|
|
2314
|
+
|
|
2315
|
+
// Use enhanced categorization with smart filtering for main table
|
|
2316
|
+
const mainTableCategories = categorizeFields(
|
|
2317
|
+
tableColumns,
|
|
2318
|
+
showHiddenFields
|
|
2319
|
+
);
|
|
2320
|
+
const mainHiddenCount = mainTableCategories.hidden.length;
|
|
2321
|
+
|
|
2322
|
+
const categoryInfo = {
|
|
2323
|
+
essential: {
|
|
2324
|
+
name: 'Essential Info',
|
|
2325
|
+
icon: Tag,
|
|
2326
|
+
description: 'Key identifiers and names',
|
|
2327
|
+
},
|
|
2328
|
+
contact: {
|
|
2329
|
+
name: 'Contact Details',
|
|
2330
|
+
icon: Person,
|
|
2331
|
+
description: 'Email, phone, and addresses',
|
|
2332
|
+
},
|
|
2333
|
+
descriptive: {
|
|
2334
|
+
name: 'Descriptions',
|
|
2335
|
+
icon: File,
|
|
2336
|
+
description: 'Notes and detailed information',
|
|
2337
|
+
},
|
|
2338
|
+
financial: {
|
|
2339
|
+
name: 'Financial',
|
|
2340
|
+
icon: Money,
|
|
2341
|
+
description: 'Prices, costs, and amounts',
|
|
2342
|
+
},
|
|
2343
|
+
dates: {
|
|
2344
|
+
name: 'Dates',
|
|
2345
|
+
icon: Calendar,
|
|
2346
|
+
description: 'Important dates and times',
|
|
2347
|
+
},
|
|
2348
|
+
status: {
|
|
2349
|
+
name: 'Status & Settings',
|
|
2350
|
+
icon: Newspaper,
|
|
2351
|
+
description: 'Current state and flags',
|
|
2352
|
+
},
|
|
2353
|
+
hidden: {
|
|
2354
|
+
name: 'Technical Fields',
|
|
2355
|
+
icon: SettingsVertical,
|
|
2356
|
+
description: 'System and technical fields',
|
|
2357
|
+
},
|
|
2358
|
+
other: {
|
|
2359
|
+
name: 'Other Fields',
|
|
2360
|
+
icon: Data,
|
|
2361
|
+
description: 'Additional information',
|
|
2362
|
+
},
|
|
2363
|
+
};
|
|
2364
|
+
|
|
2365
|
+
return (
|
|
2366
|
+
<div className={styles.columnSelection}>
|
|
2367
|
+
<div className={styles.columnSelectionHeader}>
|
|
2368
|
+
<p>
|
|
2369
|
+
Select information from your chosen tables.
|
|
2370
|
+
{allAvailableColumns.length > 1 && (
|
|
2371
|
+
<>
|
|
2372
|
+
{' '}
|
|
2373
|
+
You can select from {
|
|
2374
|
+
allAvailableColumns.length
|
|
2375
|
+
}{' '}
|
|
2376
|
+
tables.
|
|
2377
|
+
</>
|
|
2378
|
+
)}
|
|
2379
|
+
</p>
|
|
2380
|
+
<div className={styles.quickActions}>
|
|
2381
|
+
<button
|
|
2382
|
+
className={styles.linkButton}
|
|
2383
|
+
onClick={() => {
|
|
2384
|
+
const allVisibleColumns = [];
|
|
2385
|
+
allAvailableColumns.forEach((tableData) => {
|
|
2386
|
+
const visibleColumns = tableData.columns
|
|
2387
|
+
.filter(
|
|
2388
|
+
(col) =>
|
|
2389
|
+
!shouldHideField(
|
|
2390
|
+
col.name,
|
|
2391
|
+
col.type
|
|
2392
|
+
)
|
|
2393
|
+
)
|
|
2394
|
+
.map((col) => ({
|
|
2395
|
+
table: tableData.tableName,
|
|
2396
|
+
column: col.name,
|
|
2397
|
+
displayName: formatName(col.name),
|
|
2398
|
+
}));
|
|
2399
|
+
allVisibleColumns.push(...visibleColumns);
|
|
2400
|
+
});
|
|
2401
|
+
setSelectedColumns(allVisibleColumns);
|
|
2402
|
+
}}
|
|
2403
|
+
>
|
|
2404
|
+
Select All Visible
|
|
2405
|
+
</button>
|
|
2406
|
+
<button
|
|
2407
|
+
className={styles.linkButton}
|
|
2408
|
+
onClick={() => setSelectedColumns([])}
|
|
2409
|
+
>
|
|
2410
|
+
Clear All
|
|
2411
|
+
</button>
|
|
2412
|
+
{mainHiddenCount > 0 && (
|
|
2413
|
+
<button
|
|
2414
|
+
className={styles.linkButton}
|
|
2415
|
+
onClick={() =>
|
|
2416
|
+
setShowHiddenFields(!showHiddenFields)
|
|
2417
|
+
}
|
|
2418
|
+
>
|
|
2419
|
+
{showHiddenFields ? 'Hide' : 'Show'} Technical
|
|
2420
|
+
Fields ({mainHiddenCount})
|
|
2421
|
+
</button>
|
|
2422
|
+
)}
|
|
2423
|
+
</div>
|
|
2424
|
+
</div>
|
|
2425
|
+
|
|
2426
|
+
<div className={styles.tableColumnSections}>
|
|
2427
|
+
{allAvailableColumns.map((tableData) => (
|
|
2428
|
+
<div
|
|
2429
|
+
key={tableData.tableName}
|
|
2430
|
+
className={styles.tableSection}
|
|
2431
|
+
>
|
|
2432
|
+
<div className={styles.tableSectionHeader}>
|
|
2433
|
+
<h3>
|
|
2434
|
+
{tableData.displayName}
|
|
2435
|
+
{!tableData.isMainTable && ' (Connected)'}
|
|
2436
|
+
</h3>
|
|
2437
|
+
<span className={styles.tableColumnCount}>
|
|
2438
|
+
{
|
|
2439
|
+
tableData.columns.filter((col) =>
|
|
2440
|
+
tableData.isMainTable
|
|
2441
|
+
? !shouldHideField(
|
|
2442
|
+
col.name,
|
|
2443
|
+
col.type
|
|
2444
|
+
) || showHiddenFields
|
|
2445
|
+
: !shouldHideField(
|
|
2446
|
+
col.name,
|
|
2447
|
+
col.type
|
|
2448
|
+
)
|
|
2449
|
+
).length
|
|
2450
|
+
}{' '}
|
|
2451
|
+
fields
|
|
2452
|
+
</span>
|
|
2453
|
+
</div>
|
|
2454
|
+
|
|
2455
|
+
{tableData.isMainTable ? (
|
|
2456
|
+
// Main table: use categorization
|
|
2457
|
+
<div className={styles.columnCategories}>
|
|
2458
|
+
{Object.entries(mainTableCategories).map(
|
|
2459
|
+
([category, columns]) => {
|
|
2460
|
+
if (columns.length === 0)
|
|
2461
|
+
return null;
|
|
2462
|
+
|
|
2463
|
+
const info = categoryInfo[category];
|
|
2464
|
+
const CategoryIcon = info.icon;
|
|
2465
|
+
|
|
2466
|
+
return (
|
|
2467
|
+
<div
|
|
2468
|
+
key={category}
|
|
2469
|
+
className={
|
|
2470
|
+
styles.columnCategory
|
|
2471
|
+
}
|
|
2472
|
+
>
|
|
2473
|
+
<div
|
|
2474
|
+
className={
|
|
2475
|
+
styles.categoryHeader
|
|
2476
|
+
}
|
|
2477
|
+
>
|
|
2478
|
+
<CategoryIcon
|
|
2479
|
+
size={20}
|
|
2480
|
+
/>
|
|
2481
|
+
<div>
|
|
2482
|
+
<h4>{info.name}</h4>
|
|
2483
|
+
<span>
|
|
2484
|
+
{
|
|
2485
|
+
info.description
|
|
2486
|
+
}
|
|
2487
|
+
</span>
|
|
2488
|
+
</div>
|
|
2489
|
+
</div>
|
|
2490
|
+
|
|
2491
|
+
<div
|
|
2492
|
+
className={
|
|
2493
|
+
styles.columnList
|
|
2494
|
+
}
|
|
2495
|
+
>
|
|
2496
|
+
{columns.map(
|
|
2497
|
+
(column) => {
|
|
2498
|
+
const isSelected =
|
|
2499
|
+
selectedColumns.some(
|
|
2500
|
+
(col) =>
|
|
2501
|
+
col.table ===
|
|
2502
|
+
selectedTable &&
|
|
2503
|
+
col.column ===
|
|
2504
|
+
column.name
|
|
2505
|
+
);
|
|
2506
|
+
|
|
2507
|
+
return (
|
|
2508
|
+
<label
|
|
2509
|
+
key={
|
|
2510
|
+
column.name
|
|
2511
|
+
}
|
|
2512
|
+
className={
|
|
2513
|
+
styles.columnItem
|
|
2514
|
+
}
|
|
2515
|
+
>
|
|
2516
|
+
<input
|
|
2517
|
+
type="checkbox"
|
|
2518
|
+
checked={
|
|
2519
|
+
isSelected
|
|
2520
|
+
}
|
|
2521
|
+
onChange={() =>
|
|
2522
|
+
handleColumnToggle(
|
|
2523
|
+
column,
|
|
2524
|
+
selectedTable
|
|
2525
|
+
)
|
|
2526
|
+
}
|
|
2527
|
+
/>
|
|
2528
|
+
<span
|
|
2529
|
+
className={
|
|
2530
|
+
styles.columnName
|
|
2531
|
+
}
|
|
2532
|
+
>
|
|
2533
|
+
{formatName(
|
|
2534
|
+
column.name
|
|
2535
|
+
)}
|
|
2536
|
+
</span>
|
|
2537
|
+
<span
|
|
2538
|
+
className={
|
|
2539
|
+
styles.columnType
|
|
2540
|
+
}
|
|
2541
|
+
>
|
|
2542
|
+
{getFriendlyTerm(
|
|
2543
|
+
column.type
|
|
2544
|
+
)}
|
|
2545
|
+
</span>
|
|
2546
|
+
</label>
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
)}
|
|
2550
|
+
</div>
|
|
2551
|
+
</div>
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
)}
|
|
2555
|
+
</div>
|
|
2556
|
+
) : (
|
|
2557
|
+
// Joined tables: simple list
|
|
2558
|
+
<div className={styles.columnList}>
|
|
2559
|
+
{tableData.columns
|
|
2560
|
+
.filter(
|
|
2561
|
+
(col) =>
|
|
2562
|
+
!shouldHideField(
|
|
2563
|
+
col.name,
|
|
2564
|
+
col.type
|
|
2565
|
+
)
|
|
2566
|
+
)
|
|
2567
|
+
.map((column) => {
|
|
2568
|
+
const isSelected =
|
|
2569
|
+
selectedColumns.some(
|
|
2570
|
+
(col) =>
|
|
2571
|
+
col.table ===
|
|
2572
|
+
tableData.tableName &&
|
|
2573
|
+
col.column ===
|
|
2574
|
+
column.name
|
|
2575
|
+
);
|
|
2576
|
+
|
|
2577
|
+
return (
|
|
2578
|
+
<label
|
|
2579
|
+
key={column.name}
|
|
2580
|
+
className={
|
|
2581
|
+
styles.columnItem
|
|
2582
|
+
}
|
|
2583
|
+
>
|
|
2584
|
+
<input
|
|
2585
|
+
type="checkbox"
|
|
2586
|
+
checked={isSelected}
|
|
2587
|
+
onChange={() =>
|
|
2588
|
+
handleColumnToggle(
|
|
2589
|
+
column,
|
|
2590
|
+
tableData.tableName
|
|
2591
|
+
)
|
|
2592
|
+
}
|
|
2593
|
+
/>
|
|
2594
|
+
<span
|
|
2595
|
+
className={
|
|
2596
|
+
styles.columnName
|
|
2597
|
+
}
|
|
2598
|
+
>
|
|
2599
|
+
{formatName(
|
|
2600
|
+
column.name
|
|
2601
|
+
)}
|
|
2602
|
+
</span>
|
|
2603
|
+
<span
|
|
2604
|
+
className={
|
|
2605
|
+
styles.columnType
|
|
2606
|
+
}
|
|
2607
|
+
>
|
|
2608
|
+
{getFriendlyTerm(
|
|
2609
|
+
column.type
|
|
2610
|
+
)}
|
|
2611
|
+
</span>
|
|
2612
|
+
</label>
|
|
2613
|
+
);
|
|
2614
|
+
})}
|
|
2615
|
+
</div>
|
|
2616
|
+
)}
|
|
2617
|
+
</div>
|
|
2618
|
+
))}
|
|
2619
|
+
</div>
|
|
2620
|
+
|
|
2621
|
+
{selectedColumns.length > 0 && (
|
|
2622
|
+
<div className={styles.selectedSummary}>
|
|
2623
|
+
<strong>
|
|
2624
|
+
{selectedColumns.length} fields selected
|
|
2625
|
+
</strong>
|
|
2626
|
+
</div>
|
|
2627
|
+
)}
|
|
2628
|
+
</div>
|
|
2629
|
+
);
|
|
2630
|
+
};
|
|
2631
|
+
|
|
2632
|
+
// Render relationships (simplified)
|
|
2633
|
+
const renderRelationships = () => {
|
|
2634
|
+
const tableSuggestions = suggestedJoins[selectedTable] || [];
|
|
2635
|
+
const isLoading = isLoadingSuggestedJoins[selectedTable] || false;
|
|
2636
|
+
|
|
2637
|
+
return (
|
|
2638
|
+
<div className={styles.relationshipsSection}>
|
|
2639
|
+
<div className={styles.helpBox}>
|
|
2640
|
+
<Info size={20} />
|
|
2641
|
+
<div>
|
|
2642
|
+
<h4>What are relationships?</h4>
|
|
2643
|
+
<p>
|
|
2644
|
+
Relationships allow you to include information from
|
|
2645
|
+
related data. For example, if you're reporting on
|
|
2646
|
+
invoices, you might want to include the client's
|
|
2647
|
+
name from the clients table.
|
|
2648
|
+
</p>
|
|
2649
|
+
</div>
|
|
2650
|
+
</div>
|
|
2651
|
+
|
|
2652
|
+
{/* Current Joins */}
|
|
2653
|
+
{joins.length > 0 && (
|
|
2654
|
+
<div className={styles.currentJoins}>
|
|
2655
|
+
<h3>Connected Data</h3>
|
|
2656
|
+
{joins.map((join, index) => (
|
|
2657
|
+
<div key={index} className={styles.joinItem}>
|
|
2658
|
+
<div className={styles.joinInfo}>
|
|
2659
|
+
<span className={styles.joinDescription}>
|
|
2660
|
+
{getTableDisplayName(
|
|
2661
|
+
selectedTable,
|
|
2662
|
+
definition
|
|
2663
|
+
)}{' '}
|
|
2664
|
+
→{' '}
|
|
2665
|
+
{getTableDisplayName(
|
|
2666
|
+
join.targetTable,
|
|
2667
|
+
definition
|
|
2668
|
+
)}
|
|
2669
|
+
</span>
|
|
2670
|
+
<span className={styles.joinDetails}>
|
|
2671
|
+
via {formatName(join.sourceColumn)} →{' '}
|
|
2672
|
+
{formatName(join.targetColumn)}
|
|
2673
|
+
</span>
|
|
2674
|
+
</div>
|
|
2675
|
+
<button
|
|
2676
|
+
className={styles.removeJoinBtn}
|
|
2677
|
+
onClick={() => removeJoin(index)}
|
|
2678
|
+
>
|
|
2679
|
+
×
|
|
2680
|
+
</button>
|
|
2681
|
+
</div>
|
|
2682
|
+
))}
|
|
2683
|
+
</div>
|
|
2684
|
+
)}
|
|
2685
|
+
|
|
2686
|
+
{/* Suggested Relationships */}
|
|
2687
|
+
{tableSuggestions.length > 0 && (
|
|
2688
|
+
<div className={styles.suggestedJoins}>
|
|
2689
|
+
<h3>Suggested Connections</h3>
|
|
2690
|
+
<p>
|
|
2691
|
+
We found these possible connections to other data:
|
|
2692
|
+
</p>
|
|
2693
|
+
{tableSuggestions.map((suggestion, index) => (
|
|
2694
|
+
<div key={index} className={styles.suggestionCard}>
|
|
2695
|
+
<div className={styles.suggestionInfo}>
|
|
2696
|
+
<div className={styles.suggestionTitle}>
|
|
2697
|
+
<LinkChain size={16} />
|
|
2698
|
+
<span>
|
|
2699
|
+
Connect to{' '}
|
|
2700
|
+
{getTableDisplayName(
|
|
2701
|
+
suggestion.targetTable,
|
|
2702
|
+
definition
|
|
2703
|
+
)}
|
|
2704
|
+
</span>
|
|
2705
|
+
<div
|
|
2706
|
+
className={styles.confidenceRating}
|
|
2707
|
+
>
|
|
2708
|
+
{'⭐'.repeat(
|
|
2709
|
+
Math.floor(
|
|
2710
|
+
suggestion.confidence / 20
|
|
2711
|
+
)
|
|
2712
|
+
)}
|
|
2713
|
+
</div>
|
|
2714
|
+
</div>
|
|
2715
|
+
<p className={styles.suggestionDescription}>
|
|
2716
|
+
{suggestion.description ||
|
|
2717
|
+
`Link ${getTableDisplayName(
|
|
2718
|
+
selectedTable,
|
|
2719
|
+
definition
|
|
2720
|
+
)} with ${getTableDisplayName(
|
|
2721
|
+
suggestion.targetTable,
|
|
2722
|
+
definition
|
|
2723
|
+
)}`}
|
|
2724
|
+
</p>
|
|
2725
|
+
</div>
|
|
2726
|
+
<button
|
|
2727
|
+
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
2728
|
+
onClick={() => addJoin(suggestion)}
|
|
2729
|
+
disabled={joins.some(
|
|
2730
|
+
(j) =>
|
|
2731
|
+
j.targetTable ===
|
|
2732
|
+
suggestion.targetTable
|
|
2733
|
+
)}
|
|
2734
|
+
>
|
|
2735
|
+
{joins.some(
|
|
2736
|
+
(j) =>
|
|
2737
|
+
j.targetTable ===
|
|
2738
|
+
suggestion.targetTable
|
|
2739
|
+
)
|
|
2740
|
+
? 'Connected'
|
|
2741
|
+
: 'Connect'}
|
|
2742
|
+
</button>
|
|
2743
|
+
</div>
|
|
2744
|
+
))}
|
|
2745
|
+
</div>
|
|
2746
|
+
)}
|
|
2747
|
+
|
|
2748
|
+
{isLoading && (
|
|
2749
|
+
<div className={styles.loading}>
|
|
2750
|
+
Finding possible connections...
|
|
2751
|
+
</div>
|
|
2752
|
+
)}
|
|
2753
|
+
|
|
2754
|
+
{/* Manual Relationship Creation */}
|
|
2755
|
+
<div className={styles.manualJoinSection}>
|
|
2756
|
+
<h3>Create Manual Relationship</h3>
|
|
2757
|
+
<p>
|
|
2758
|
+
Can't find the connection you need? Create a custom
|
|
2759
|
+
relationship between tables.
|
|
2760
|
+
</p>
|
|
2761
|
+
|
|
2762
|
+
{!showManualJoinForm ? (
|
|
2763
|
+
<button
|
|
2764
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
2765
|
+
onClick={() => {
|
|
2766
|
+
setShowManualJoinForm(true);
|
|
2767
|
+
setManualJoin((prev) => ({
|
|
2768
|
+
...prev,
|
|
2769
|
+
sourceTable: selectedTable,
|
|
2770
|
+
}));
|
|
2771
|
+
if (selectedTable) {
|
|
2772
|
+
fetchManualJoinColumns(
|
|
2773
|
+
selectedTable,
|
|
2774
|
+
'source'
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
}}
|
|
2778
|
+
>
|
|
2779
|
+
<CirclePlus size={16} />
|
|
2780
|
+
Add Manual Relationship
|
|
2781
|
+
</button>
|
|
2782
|
+
) : (
|
|
2783
|
+
<div className={styles.manualJoinForm}>
|
|
2784
|
+
<div className={styles.manualJoinGrid}>
|
|
2785
|
+
<div className={styles.manualJoinField}>
|
|
2786
|
+
<label>Join Type:</label>
|
|
2787
|
+
<select
|
|
2788
|
+
className={styles.filterSelect}
|
|
2789
|
+
value={manualJoin.joinType}
|
|
2790
|
+
onChange={(e) =>
|
|
2791
|
+
handleManualJoinChange(
|
|
2792
|
+
'joinType',
|
|
2793
|
+
e.target.value
|
|
2794
|
+
)
|
|
2795
|
+
}
|
|
2796
|
+
>
|
|
2797
|
+
<option value="INNER JOIN">
|
|
2798
|
+
Inner Join (Required match)
|
|
2799
|
+
</option>
|
|
2800
|
+
<option value="LEFT JOIN">
|
|
2801
|
+
Left Join (Optional match)
|
|
2802
|
+
</option>
|
|
2803
|
+
<option value="RIGHT JOIN">
|
|
2804
|
+
Right Join
|
|
2805
|
+
</option>
|
|
2806
|
+
<option value="FULL JOIN">
|
|
2807
|
+
Full Join
|
|
2808
|
+
</option>
|
|
2809
|
+
</select>
|
|
2810
|
+
</div>
|
|
2811
|
+
|
|
2812
|
+
<div className={styles.manualJoinField}>
|
|
2813
|
+
<label>Source Table:</label>
|
|
2814
|
+
<select
|
|
2815
|
+
className={styles.filterSelect}
|
|
2816
|
+
value={manualJoin.sourceTable}
|
|
2817
|
+
onChange={(e) =>
|
|
2818
|
+
handleManualJoinChange(
|
|
2819
|
+
'sourceTable',
|
|
2820
|
+
e.target.value
|
|
2821
|
+
)
|
|
2822
|
+
}
|
|
2823
|
+
>
|
|
2824
|
+
<option value={selectedTable}>
|
|
2825
|
+
{getTableDisplayName(
|
|
2826
|
+
selectedTable,
|
|
2827
|
+
definition
|
|
2828
|
+
)}
|
|
2829
|
+
</option>
|
|
2830
|
+
{joins.map((join, idx) => (
|
|
2831
|
+
<option
|
|
2832
|
+
key={idx}
|
|
2833
|
+
value={join.targetTable}
|
|
2834
|
+
>
|
|
2835
|
+
{getTableDisplayName(
|
|
2836
|
+
join.targetTable,
|
|
2837
|
+
definition
|
|
2838
|
+
)}
|
|
2839
|
+
</option>
|
|
2840
|
+
))}
|
|
2841
|
+
</select>
|
|
2842
|
+
</div>
|
|
2843
|
+
|
|
2844
|
+
<div className={styles.manualJoinField}>
|
|
2845
|
+
<label>Source Column:</label>
|
|
2846
|
+
<select
|
|
2847
|
+
className={styles.filterSelect}
|
|
2848
|
+
value={manualJoin.sourceColumn}
|
|
2849
|
+
onChange={(e) =>
|
|
2850
|
+
handleManualJoinChange(
|
|
2851
|
+
'sourceColumn',
|
|
2852
|
+
e.target.value
|
|
2853
|
+
)
|
|
2854
|
+
}
|
|
2855
|
+
disabled={!manualJoin.sourceTable}
|
|
2856
|
+
>
|
|
2857
|
+
<option value="">Select Column</option>
|
|
2858
|
+
{manualJoinColumns.source.map((col) => (
|
|
2859
|
+
<option
|
|
2860
|
+
key={col.name}
|
|
2861
|
+
value={col.name}
|
|
2862
|
+
>
|
|
2863
|
+
{formatName(col.name)}
|
|
2864
|
+
</option>
|
|
2865
|
+
))}
|
|
2866
|
+
</select>
|
|
2867
|
+
</div>
|
|
2868
|
+
|
|
2869
|
+
<div className={styles.manualJoinField}>
|
|
2870
|
+
<label>Target Table:</label>
|
|
2871
|
+
<select
|
|
2872
|
+
className={styles.filterSelect}
|
|
2873
|
+
value={manualJoin.targetTable}
|
|
2874
|
+
onChange={(e) =>
|
|
2875
|
+
handleManualJoinChange(
|
|
2876
|
+
'targetTable',
|
|
2877
|
+
e.target.value
|
|
2878
|
+
)
|
|
2879
|
+
}
|
|
2880
|
+
>
|
|
2881
|
+
<option value="">Select Table</option>
|
|
2882
|
+
{availableTables
|
|
2883
|
+
.filter(
|
|
2884
|
+
(table) =>
|
|
2885
|
+
table !==
|
|
2886
|
+
manualJoin.sourceTable
|
|
2887
|
+
)
|
|
2888
|
+
.map((table) => (
|
|
2889
|
+
<option
|
|
2890
|
+
key={table}
|
|
2891
|
+
value={table}
|
|
2892
|
+
>
|
|
2893
|
+
{getTableDisplayName(
|
|
2894
|
+
table,
|
|
2895
|
+
definition
|
|
2896
|
+
)}
|
|
2897
|
+
</option>
|
|
2898
|
+
))}
|
|
2899
|
+
</select>
|
|
2900
|
+
</div>
|
|
2901
|
+
|
|
2902
|
+
<div className={styles.manualJoinField}>
|
|
2903
|
+
<label>Target Column:</label>
|
|
2904
|
+
<select
|
|
2905
|
+
className={styles.filterSelect}
|
|
2906
|
+
value={manualJoin.targetColumn}
|
|
2907
|
+
onChange={(e) =>
|
|
2908
|
+
handleManualJoinChange(
|
|
2909
|
+
'targetColumn',
|
|
2910
|
+
e.target.value
|
|
2911
|
+
)
|
|
2912
|
+
}
|
|
2913
|
+
disabled={!manualJoin.targetTable}
|
|
2914
|
+
>
|
|
2915
|
+
<option value="">Select Column</option>
|
|
2916
|
+
{manualJoinColumns.target.map((col) => (
|
|
2917
|
+
<option
|
|
2918
|
+
key={col.name}
|
|
2919
|
+
value={col.name}
|
|
2920
|
+
>
|
|
2921
|
+
{formatName(col.name)}
|
|
2922
|
+
</option>
|
|
2923
|
+
))}
|
|
2924
|
+
</select>
|
|
2925
|
+
</div>
|
|
2926
|
+
</div>
|
|
2927
|
+
|
|
2928
|
+
<div className={styles.manualJoinActions}>
|
|
2929
|
+
<button
|
|
2930
|
+
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
2931
|
+
onClick={addManualJoin}
|
|
2932
|
+
disabled={
|
|
2933
|
+
!manualJoin.sourceTable ||
|
|
2934
|
+
!manualJoin.targetTable ||
|
|
2935
|
+
!manualJoin.sourceColumn ||
|
|
2936
|
+
!manualJoin.targetColumn
|
|
2937
|
+
}
|
|
2938
|
+
>
|
|
2939
|
+
Add Relationship
|
|
2940
|
+
</button>
|
|
2941
|
+
<button
|
|
2942
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
2943
|
+
onClick={() => {
|
|
2944
|
+
setShowManualJoinForm(false);
|
|
2945
|
+
setManualJoin({
|
|
2946
|
+
sourceTable: '',
|
|
2947
|
+
targetTable: '',
|
|
2948
|
+
sourceColumn: '',
|
|
2949
|
+
targetColumn: '',
|
|
2950
|
+
joinType: 'INNER JOIN',
|
|
2951
|
+
});
|
|
2952
|
+
setManualJoinColumns({
|
|
2953
|
+
source: [],
|
|
2954
|
+
target: [],
|
|
2955
|
+
});
|
|
2956
|
+
}}
|
|
2957
|
+
>
|
|
2958
|
+
Cancel
|
|
2959
|
+
</button>
|
|
2960
|
+
</div>
|
|
2961
|
+
</div>
|
|
2962
|
+
)}
|
|
2963
|
+
</div>
|
|
2964
|
+
|
|
2965
|
+
<div className={styles.skipOption}>
|
|
2966
|
+
<p>
|
|
2967
|
+
This is an optional step. You can skip it if you only
|
|
2968
|
+
need data from the{' '}
|
|
2969
|
+
{getTableDisplayName(selectedTable, definition)} table.
|
|
2970
|
+
</p>
|
|
2971
|
+
<button
|
|
2972
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
2973
|
+
onClick={goToNextStep}
|
|
2974
|
+
>
|
|
2975
|
+
Skip This Step
|
|
2976
|
+
</button>
|
|
2977
|
+
</div>
|
|
2978
|
+
</div>
|
|
2979
|
+
);
|
|
2980
|
+
};
|
|
2981
|
+
|
|
2982
|
+
// Advanced filter management functions (from original GenericReport)
|
|
2983
|
+
|
|
2984
|
+
// Add a new filter group
|
|
2985
|
+
const handleAddFilterGroup = () => {
|
|
2986
|
+
const updatedFilterCriteria = JSON.parse(
|
|
2987
|
+
JSON.stringify(filterCriteria)
|
|
2988
|
+
);
|
|
2989
|
+
updatedFilterCriteria.groups.push({
|
|
2990
|
+
operator: 'AND',
|
|
2991
|
+
filters: [],
|
|
2992
|
+
});
|
|
2993
|
+
setFilterCriteria(updatedFilterCriteria);
|
|
2994
|
+
setShowFilteringSection(true);
|
|
2995
|
+
};
|
|
2996
|
+
|
|
2997
|
+
// Remove a filter group
|
|
2998
|
+
const handleRemoveFilterGroup = (groupIndex) => {
|
|
2999
|
+
const updatedFilterCriteria = JSON.parse(
|
|
3000
|
+
JSON.stringify(filterCriteria)
|
|
3001
|
+
);
|
|
3002
|
+
updatedFilterCriteria.groups.splice(groupIndex, 1);
|
|
3003
|
+
|
|
3004
|
+
if (updatedFilterCriteria.groups.length === 0) {
|
|
3005
|
+
updatedFilterCriteria.groups.push({
|
|
3006
|
+
operator: 'AND',
|
|
3007
|
+
filters: [],
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
setFilterCriteria(updatedFilterCriteria);
|
|
3012
|
+
|
|
3013
|
+
const allGroupsEmpty = updatedFilterCriteria.groups.every(
|
|
3014
|
+
(group) => group.filters.length === 0
|
|
3015
|
+
);
|
|
3016
|
+
|
|
3017
|
+
if (allGroupsEmpty) {
|
|
3018
|
+
setShowFilteringSection(false);
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
|
|
3022
|
+
// Add a filter criterion to a group
|
|
3023
|
+
const handleAddFilterCriterion = (groupIndex = 0) => {
|
|
3024
|
+
const defaultColumn =
|
|
3025
|
+
selectedColumns.length > 0
|
|
3026
|
+
? {
|
|
3027
|
+
table: selectedColumns[0].table,
|
|
3028
|
+
column: selectedColumns[0].column,
|
|
3029
|
+
displayName: selectedColumns[0].displayName,
|
|
3030
|
+
}
|
|
3031
|
+
: null;
|
|
3032
|
+
|
|
3033
|
+
const newFilterCriterion = {
|
|
3034
|
+
table: defaultColumn?.table || '',
|
|
3035
|
+
column: defaultColumn?.column || '',
|
|
3036
|
+
operator: '=',
|
|
3037
|
+
value: '',
|
|
3038
|
+
};
|
|
3039
|
+
|
|
3040
|
+
const updatedFilterCriteria = JSON.parse(
|
|
3041
|
+
JSON.stringify(filterCriteria)
|
|
3042
|
+
);
|
|
3043
|
+
updatedFilterCriteria.groups[groupIndex].filters.push(
|
|
3044
|
+
newFilterCriterion
|
|
3045
|
+
);
|
|
3046
|
+
setFilterCriteria(updatedFilterCriteria);
|
|
3047
|
+
setShowFilteringSection(true);
|
|
3048
|
+
};
|
|
3049
|
+
|
|
3050
|
+
// Remove a filter criterion
|
|
3051
|
+
const handleRemoveFilterCriterion = (groupIndex, filterIndex) => {
|
|
3052
|
+
const updatedFilterCriteria = JSON.parse(
|
|
3053
|
+
JSON.stringify(filterCriteria)
|
|
3054
|
+
);
|
|
3055
|
+
updatedFilterCriteria.groups[groupIndex].filters.splice(filterIndex, 1);
|
|
3056
|
+
setFilterCriteria(updatedFilterCriteria);
|
|
3057
|
+
|
|
3058
|
+
const allGroupsEmpty = updatedFilterCriteria.groups.every(
|
|
3059
|
+
(group) => group.filters.length === 0
|
|
3060
|
+
);
|
|
3061
|
+
|
|
3062
|
+
if (allGroupsEmpty) {
|
|
3063
|
+
setShowFilteringSection(false);
|
|
3064
|
+
}
|
|
3065
|
+
};
|
|
3066
|
+
|
|
3067
|
+
// Update filter criterion
|
|
3068
|
+
const handleFilterCriterionChange = (
|
|
3069
|
+
groupIndex,
|
|
3070
|
+
filterIndex,
|
|
3071
|
+
field,
|
|
3072
|
+
value
|
|
3073
|
+
) => {
|
|
3074
|
+
const updatedFilterCriteria = JSON.parse(
|
|
3075
|
+
JSON.stringify(filterCriteria)
|
|
3076
|
+
);
|
|
3077
|
+
updatedFilterCriteria.groups[groupIndex].filters[filterIndex][field] =
|
|
3078
|
+
value;
|
|
3079
|
+
setFilterCriteria(updatedFilterCriteria);
|
|
3080
|
+
};
|
|
3081
|
+
|
|
3082
|
+
// Update main filter operator (AND/OR between groups)
|
|
3083
|
+
const handleMainFilterOperatorChange = (newOperator) => {
|
|
3084
|
+
const updatedFilterCriteria = JSON.parse(
|
|
3085
|
+
JSON.stringify(filterCriteria)
|
|
3086
|
+
);
|
|
3087
|
+
updatedFilterCriteria.operator = newOperator;
|
|
3088
|
+
setFilterCriteria(updatedFilterCriteria);
|
|
3089
|
+
};
|
|
3090
|
+
|
|
3091
|
+
// Update filter group operator (AND/OR within group)
|
|
3092
|
+
const handleFilterGroupOperatorChange = (groupIndex, newOperator) => {
|
|
3093
|
+
const updatedFilterCriteria = JSON.parse(
|
|
3094
|
+
JSON.stringify(filterCriteria)
|
|
3095
|
+
);
|
|
3096
|
+
updatedFilterCriteria.groups[groupIndex].operator = newOperator;
|
|
3097
|
+
setFilterCriteria(updatedFilterCriteria);
|
|
3098
|
+
};
|
|
3099
|
+
|
|
3100
|
+
// Apply suggested filter
|
|
3101
|
+
const applySuggestedFilter = (suggestedFilter) => {
|
|
3102
|
+
const updatedFilterCriteria = JSON.parse(
|
|
3103
|
+
JSON.stringify(filterCriteria)
|
|
3104
|
+
);
|
|
3105
|
+
|
|
3106
|
+
if (updatedFilterCriteria.groups.length === 0) {
|
|
3107
|
+
updatedFilterCriteria.groups.push({
|
|
3108
|
+
operator: 'AND',
|
|
3109
|
+
filters: [],
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
updatedFilterCriteria.groups[0].filters.push(suggestedFilter.filter);
|
|
3114
|
+
setFilterCriteria(updatedFilterCriteria);
|
|
3115
|
+
setShowFilteringSection(true);
|
|
3116
|
+
|
|
3117
|
+
toast.success(`Added filter: ${suggestedFilter.name}`);
|
|
3118
|
+
};
|
|
3119
|
+
|
|
3120
|
+
// Legacy functions for backward compatibility
|
|
3121
|
+
const addFilter = (filter) => {
|
|
3122
|
+
const updatedCriteria = { ...filterCriteria };
|
|
3123
|
+
updatedCriteria.groups[0].filters.push(filter);
|
|
3124
|
+
setFilterCriteria(updatedCriteria);
|
|
3125
|
+
};
|
|
3126
|
+
|
|
3127
|
+
const removeFilter = (groupIndex, filterIndex) => {
|
|
3128
|
+
handleRemoveFilterCriterion(groupIndex, filterIndex);
|
|
3129
|
+
};
|
|
3130
|
+
|
|
3131
|
+
// Add quick filter based on common patterns
|
|
3132
|
+
const addQuickFilter = (type) => {
|
|
3133
|
+
const availableColumns = [...selectedColumns];
|
|
3134
|
+
joins.forEach((join) => {
|
|
3135
|
+
// Add columns from joined tables (simplified)
|
|
3136
|
+
availableColumns.push({
|
|
3137
|
+
table: join.targetTable,
|
|
3138
|
+
column: 'status',
|
|
3139
|
+
displayName: 'Status',
|
|
3140
|
+
});
|
|
3141
|
+
});
|
|
3142
|
+
|
|
3143
|
+
let filter = null;
|
|
3144
|
+
|
|
3145
|
+
switch (type) {
|
|
3146
|
+
case 'last30days':
|
|
3147
|
+
const dateColumn = availableColumns.find(
|
|
3148
|
+
(col) =>
|
|
3149
|
+
col.column.toLowerCase().includes('date') ||
|
|
3150
|
+
col.column.toLowerCase().includes('created_at')
|
|
3151
|
+
);
|
|
3152
|
+
if (dateColumn) {
|
|
3153
|
+
filter = {
|
|
3154
|
+
table: dateColumn.table,
|
|
3155
|
+
column: dateColumn.column,
|
|
3156
|
+
operator: '>=',
|
|
3157
|
+
value: moment()
|
|
3158
|
+
.subtract(30, 'days')
|
|
3159
|
+
.format('YYYY-MM-DD'),
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
3162
|
+
break;
|
|
3163
|
+
case 'active':
|
|
3164
|
+
const statusColumn = availableColumns.find(
|
|
3165
|
+
(col) =>
|
|
3166
|
+
col.column.toLowerCase().includes('status') ||
|
|
3167
|
+
col.column.toLowerCase().includes('active')
|
|
3168
|
+
);
|
|
3169
|
+
if (statusColumn) {
|
|
3170
|
+
filter = {
|
|
3171
|
+
table: statusColumn.table,
|
|
3172
|
+
column: statusColumn.column,
|
|
3173
|
+
operator: '=',
|
|
3174
|
+
value: '1',
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
break;
|
|
3178
|
+
case 'my_records':
|
|
3179
|
+
const userColumn = availableColumns.find(
|
|
3180
|
+
(col) =>
|
|
3181
|
+
col.column.toLowerCase().includes('user_id') ||
|
|
3182
|
+
col.column.toLowerCase().includes('created_by')
|
|
3183
|
+
);
|
|
3184
|
+
if (userColumn) {
|
|
3185
|
+
filter = {
|
|
3186
|
+
table: userColumn.table,
|
|
3187
|
+
column: userColumn.column,
|
|
3188
|
+
operator: '=',
|
|
3189
|
+
value: 'current_user', // This would be replaced by backend
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
break;
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
if (filter) {
|
|
3196
|
+
addFilter(filter);
|
|
3197
|
+
toast.success('Filter added successfully!');
|
|
3198
|
+
} else {
|
|
3199
|
+
toast.warning('No suitable column found for this filter type.');
|
|
3200
|
+
}
|
|
3201
|
+
};
|
|
3202
|
+
|
|
3203
|
+
// Add custom filter
|
|
3204
|
+
const addCustomFilter = () => {
|
|
3205
|
+
// Get all available columns from all tables
|
|
3206
|
+
const allColumns = [];
|
|
3207
|
+
|
|
3208
|
+
// Add main table columns
|
|
3209
|
+
tableColumns.forEach((col) => {
|
|
3210
|
+
allColumns.push({
|
|
3211
|
+
table: selectedTable,
|
|
3212
|
+
tableName: getTableDisplayName(selectedTable, definition),
|
|
3213
|
+
column: col.name,
|
|
3214
|
+
columnName: formatName(col.name),
|
|
3215
|
+
type: col.type,
|
|
3216
|
+
});
|
|
3217
|
+
});
|
|
3218
|
+
|
|
3219
|
+
// Add joined table columns
|
|
3220
|
+
joins.forEach((join) => {
|
|
3221
|
+
if (join.availableColumns) {
|
|
3222
|
+
join.availableColumns.forEach((col) => {
|
|
3223
|
+
allColumns.push({
|
|
3224
|
+
table: join.targetTable,
|
|
3225
|
+
tableName: getTableDisplayName(
|
|
3226
|
+
join.targetTable,
|
|
3227
|
+
definition
|
|
3228
|
+
),
|
|
3229
|
+
column: col.name,
|
|
3230
|
+
columnName: formatName(col.name),
|
|
3231
|
+
type: col.type,
|
|
3232
|
+
});
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
});
|
|
3236
|
+
|
|
3237
|
+
if (allColumns.length === 0) {
|
|
3238
|
+
toast.warning(
|
|
3239
|
+
'No columns available for filtering. Please select tables and columns first.'
|
|
3240
|
+
);
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
const newFilter = {
|
|
3245
|
+
table: allColumns[0].table,
|
|
3246
|
+
column: allColumns[0].column,
|
|
3247
|
+
operator: '=',
|
|
3248
|
+
value: '',
|
|
3249
|
+
};
|
|
3250
|
+
|
|
3251
|
+
const updatedCriteria = { ...filterCriteria };
|
|
3252
|
+
updatedCriteria.groups[0].filters.push(newFilter);
|
|
3253
|
+
setFilterCriteria(updatedCriteria);
|
|
3254
|
+
};
|
|
3255
|
+
|
|
3256
|
+
// Update filter value
|
|
3257
|
+
const updateFilter = (groupIndex, filterIndex, field, value) => {
|
|
3258
|
+
const updatedCriteria = { ...filterCriteria };
|
|
3259
|
+
updatedCriteria.groups[groupIndex].filters[filterIndex][field] = value;
|
|
3260
|
+
setFilterCriteria(updatedCriteria);
|
|
3261
|
+
};
|
|
3262
|
+
|
|
3263
|
+
// Get available columns for filters
|
|
3264
|
+
const getAvailableFilterColumns = () => {
|
|
3265
|
+
const allColumns = [];
|
|
3266
|
+
|
|
3267
|
+
// Add main table columns
|
|
3268
|
+
tableColumns.forEach((col) => {
|
|
3269
|
+
if (!shouldHideField(col.name, col.type)) {
|
|
3270
|
+
allColumns.push({
|
|
3271
|
+
table: selectedTable,
|
|
3272
|
+
tableName: getTableDisplayName(selectedTable, definition),
|
|
3273
|
+
column: col.name,
|
|
3274
|
+
columnName: formatName(col.name),
|
|
3275
|
+
type: col.type,
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
});
|
|
3279
|
+
|
|
3280
|
+
// Add joined table columns
|
|
3281
|
+
joins.forEach((join) => {
|
|
3282
|
+
if (join.availableColumns) {
|
|
3283
|
+
join.availableColumns.forEach((col) => {
|
|
3284
|
+
if (!shouldHideField(col.name, col.type)) {
|
|
3285
|
+
allColumns.push({
|
|
3286
|
+
table: join.targetTable,
|
|
3287
|
+
tableName: getTableDisplayName(
|
|
3288
|
+
join.targetTable,
|
|
3289
|
+
definition
|
|
3290
|
+
),
|
|
3291
|
+
column: col.name,
|
|
3292
|
+
columnName: formatName(col.name),
|
|
3293
|
+
type: col.type,
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
});
|
|
3299
|
+
|
|
3300
|
+
return allColumns;
|
|
3301
|
+
};
|
|
3302
|
+
|
|
3303
|
+
// Advanced filter rendering (from original GenericReport)
|
|
3304
|
+
const renderFilters = () => {
|
|
3305
|
+
const availableColumns = getAvailableFilterColumns();
|
|
3306
|
+
|
|
3307
|
+
return (
|
|
3308
|
+
<div className={styles.filtersSection}>
|
|
3309
|
+
<div className={styles.helpBox}>
|
|
3310
|
+
<Info size={20} />
|
|
3311
|
+
<div>
|
|
3312
|
+
<h4>Advanced Filter Builder</h4>
|
|
3313
|
+
<p>
|
|
3314
|
+
Create sophisticated filters with multiple groups
|
|
3315
|
+
and logical operators. Use filter groups to create
|
|
3316
|
+
complex conditions like "(A AND B) OR (C AND D)".
|
|
3317
|
+
</p>
|
|
3318
|
+
</div>
|
|
3319
|
+
</div>
|
|
3320
|
+
|
|
3321
|
+
{/* Main Filter Operator */}
|
|
3322
|
+
<div className={styles.filterOperatorContainer}>
|
|
3323
|
+
<div className={styles.mainFilterOperator}>
|
|
3324
|
+
<label>Match:</label>
|
|
3325
|
+
<div className={styles.operatorOptions}>
|
|
3326
|
+
<div
|
|
3327
|
+
className={`${styles.operatorOption} ${
|
|
3328
|
+
filterCriteria.operator === 'AND'
|
|
3329
|
+
? styles.operatorOptionSelected
|
|
3330
|
+
: ''
|
|
3331
|
+
}`}
|
|
3332
|
+
onClick={() =>
|
|
3333
|
+
handleMainFilterOperatorChange('AND')
|
|
3334
|
+
}
|
|
3335
|
+
>
|
|
3336
|
+
<div className={styles.operatorName}>ALL</div>
|
|
3337
|
+
<div className={styles.operatorDescription}>
|
|
3338
|
+
All filter groups must match
|
|
3339
|
+
</div>
|
|
3340
|
+
</div>
|
|
3341
|
+
<div
|
|
3342
|
+
className={`${styles.operatorOption} ${
|
|
3343
|
+
filterCriteria.operator === 'OR'
|
|
3344
|
+
? styles.operatorOptionSelected
|
|
3345
|
+
: ''
|
|
3346
|
+
}`}
|
|
3347
|
+
onClick={() =>
|
|
3348
|
+
handleMainFilterOperatorChange('OR')
|
|
3349
|
+
}
|
|
3350
|
+
>
|
|
3351
|
+
<div className={styles.operatorName}>ANY</div>
|
|
3352
|
+
<div className={styles.operatorDescription}>
|
|
3353
|
+
Any filter group can match
|
|
3354
|
+
</div>
|
|
3355
|
+
</div>
|
|
3356
|
+
</div>
|
|
3357
|
+
</div>
|
|
3358
|
+
|
|
3359
|
+
<div className={styles.filterActionButtons}>
|
|
3360
|
+
<button
|
|
3361
|
+
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
3362
|
+
onClick={handleAddFilterGroup}
|
|
3363
|
+
disabled={selectedColumns.length === 0}
|
|
3364
|
+
>
|
|
3365
|
+
Add Filter Group
|
|
3366
|
+
</button>
|
|
3367
|
+
</div>
|
|
3368
|
+
</div>
|
|
3369
|
+
|
|
3370
|
+
{/* Filter Groups */}
|
|
3371
|
+
{filterCriteria.groups.map((group, groupIndex) => (
|
|
3372
|
+
<div
|
|
3373
|
+
key={`group-${groupIndex}`}
|
|
3374
|
+
className={styles.filterGroup}
|
|
3375
|
+
>
|
|
3376
|
+
<div className={styles.filterGroupHeader}>
|
|
3377
|
+
<div className={styles.filterGroupTitle}>
|
|
3378
|
+
<h4>Filter Group {groupIndex + 1}</h4>
|
|
3379
|
+
<div className={styles.filterGroupOperator}>
|
|
3380
|
+
<label>Match:</label>
|
|
3381
|
+
<div className={styles.operatorButtonGroup}>
|
|
3382
|
+
<button
|
|
3383
|
+
type="button"
|
|
3384
|
+
className={`${
|
|
3385
|
+
styles.operatorButton
|
|
3386
|
+
} ${
|
|
3387
|
+
group.operator === 'AND'
|
|
3388
|
+
? styles.operatorButtonSelected
|
|
3389
|
+
: ''
|
|
3390
|
+
}`}
|
|
3391
|
+
onClick={() =>
|
|
3392
|
+
handleFilterGroupOperatorChange(
|
|
3393
|
+
groupIndex,
|
|
3394
|
+
'AND'
|
|
3395
|
+
)
|
|
3396
|
+
}
|
|
3397
|
+
>
|
|
3398
|
+
ALL
|
|
3399
|
+
</button>
|
|
3400
|
+
<button
|
|
3401
|
+
type="button"
|
|
3402
|
+
className={`${
|
|
3403
|
+
styles.operatorButton
|
|
3404
|
+
} ${
|
|
3405
|
+
group.operator === 'OR'
|
|
3406
|
+
? styles.operatorButtonSelected
|
|
3407
|
+
: ''
|
|
3408
|
+
}`}
|
|
3409
|
+
onClick={() =>
|
|
3410
|
+
handleFilterGroupOperatorChange(
|
|
3411
|
+
groupIndex,
|
|
3412
|
+
'OR'
|
|
3413
|
+
)
|
|
3414
|
+
}
|
|
3415
|
+
>
|
|
3416
|
+
ANY
|
|
3417
|
+
</button>
|
|
3418
|
+
</div>
|
|
3419
|
+
</div>
|
|
3420
|
+
</div>
|
|
3421
|
+
|
|
3422
|
+
<div className={styles.filterGroupActions}>
|
|
3423
|
+
<button
|
|
3424
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
3425
|
+
onClick={() =>
|
|
3426
|
+
handleAddFilterCriterion(groupIndex)
|
|
3427
|
+
}
|
|
3428
|
+
disabled={selectedColumns.length === 0}
|
|
3429
|
+
>
|
|
3430
|
+
Add Filter
|
|
3431
|
+
</button>
|
|
3432
|
+
{filterCriteria.groups.length > 1 && (
|
|
3433
|
+
<button
|
|
3434
|
+
className={styles.removeJoinBtn}
|
|
3435
|
+
onClick={() =>
|
|
3436
|
+
handleRemoveFilterGroup(groupIndex)
|
|
3437
|
+
}
|
|
3438
|
+
title="Remove Filter Group"
|
|
3439
|
+
>
|
|
3440
|
+
×
|
|
3441
|
+
</button>
|
|
3442
|
+
)}
|
|
3443
|
+
</div>
|
|
3444
|
+
</div>
|
|
3445
|
+
|
|
3446
|
+
{/* Filter Criteria in Group */}
|
|
3447
|
+
{group.filters.length > 0 ? (
|
|
3448
|
+
<div className={styles.filterCriteriaContainer}>
|
|
3449
|
+
{group.filters.map((criterion, filterIndex) => (
|
|
3450
|
+
<div
|
|
3451
|
+
key={`filter-${groupIndex}-${filterIndex}`}
|
|
3452
|
+
className={styles.filterCriterion}
|
|
3453
|
+
>
|
|
3454
|
+
<div
|
|
3455
|
+
className={
|
|
3456
|
+
styles.filterCriterionHeader
|
|
3457
|
+
}
|
|
3458
|
+
>
|
|
3459
|
+
<h4>
|
|
3460
|
+
Filter {filterIndex + 1}:{' '}
|
|
3461
|
+
{criterion.table &&
|
|
3462
|
+
criterion.column
|
|
3463
|
+
? `${formatName(
|
|
3464
|
+
criterion.table
|
|
3465
|
+
)}.${formatName(
|
|
3466
|
+
criterion.column
|
|
3467
|
+
)}`
|
|
3468
|
+
: `Criterion #${
|
|
3469
|
+
filterIndex + 1
|
|
3470
|
+
}`}
|
|
3471
|
+
</h4>
|
|
3472
|
+
<button
|
|
3473
|
+
className={styles.removeJoinBtn}
|
|
3474
|
+
onClick={() =>
|
|
3475
|
+
handleRemoveFilterCriterion(
|
|
3476
|
+
groupIndex,
|
|
3477
|
+
filterIndex
|
|
3478
|
+
)
|
|
3479
|
+
}
|
|
3480
|
+
title="Remove Filter Criterion"
|
|
3481
|
+
>
|
|
3482
|
+
×
|
|
3483
|
+
</button>
|
|
3484
|
+
</div>
|
|
3485
|
+
|
|
3486
|
+
<div
|
|
3487
|
+
className={
|
|
3488
|
+
styles.filterCriterionFields
|
|
3489
|
+
}
|
|
3490
|
+
>
|
|
3491
|
+
{/* Table.Column Selection */}
|
|
3492
|
+
<div className={styles.filterField}>
|
|
3493
|
+
<label>Column:</label>
|
|
3494
|
+
<select
|
|
3495
|
+
className={
|
|
3496
|
+
styles.filterSelect
|
|
3497
|
+
}
|
|
3498
|
+
value={
|
|
3499
|
+
criterion.table &&
|
|
3500
|
+
criterion.column
|
|
3501
|
+
? `${criterion.table}.${criterion.column}`
|
|
3502
|
+
: ''
|
|
3503
|
+
}
|
|
3504
|
+
onChange={(e) => {
|
|
3505
|
+
if (e.target.value) {
|
|
3506
|
+
const dotIndex =
|
|
3507
|
+
e.target.value.indexOf(
|
|
3508
|
+
'.'
|
|
3509
|
+
);
|
|
3510
|
+
if (
|
|
3511
|
+
dotIndex !== -1
|
|
3512
|
+
) {
|
|
3513
|
+
const table =
|
|
3514
|
+
e.target.value.substring(
|
|
3515
|
+
0,
|
|
3516
|
+
dotIndex
|
|
3517
|
+
);
|
|
3518
|
+
const column =
|
|
3519
|
+
e.target.value.substring(
|
|
3520
|
+
dotIndex +
|
|
3521
|
+
1
|
|
3522
|
+
);
|
|
3523
|
+
handleFilterCriterionChange(
|
|
3524
|
+
groupIndex,
|
|
3525
|
+
filterIndex,
|
|
3526
|
+
'table',
|
|
3527
|
+
table
|
|
3528
|
+
);
|
|
3529
|
+
handleFilterCriterionChange(
|
|
3530
|
+
groupIndex,
|
|
3531
|
+
filterIndex,
|
|
3532
|
+
'column',
|
|
3533
|
+
column
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
}}
|
|
3538
|
+
>
|
|
3539
|
+
<option value="">
|
|
3540
|
+
Select a column...
|
|
3541
|
+
</option>
|
|
3542
|
+
{availableColumns.map(
|
|
3543
|
+
(col) => (
|
|
3544
|
+
<option
|
|
3545
|
+
key={`${col.table}.${col.column}`}
|
|
3546
|
+
value={`${col.table}.${col.column}`}
|
|
3547
|
+
>
|
|
3548
|
+
{col.tableName}.
|
|
3549
|
+
{col.columnName}
|
|
3550
|
+
</option>
|
|
3551
|
+
)
|
|
3552
|
+
)}
|
|
3553
|
+
</select>
|
|
3554
|
+
</div>
|
|
3555
|
+
|
|
3556
|
+
{/* Operator Selection */}
|
|
3557
|
+
<div className={styles.filterField}>
|
|
3558
|
+
<label>Operator:</label>
|
|
3559
|
+
<select
|
|
3560
|
+
className={
|
|
3561
|
+
styles.filterSelect
|
|
3562
|
+
}
|
|
3563
|
+
value={criterion.operator}
|
|
3564
|
+
onChange={(e) =>
|
|
3565
|
+
handleFilterCriterionChange(
|
|
3566
|
+
groupIndex,
|
|
3567
|
+
filterIndex,
|
|
3568
|
+
'operator',
|
|
3569
|
+
e.target.value
|
|
3570
|
+
)
|
|
3571
|
+
}
|
|
3572
|
+
>
|
|
3573
|
+
<option value="=">
|
|
3574
|
+
equals
|
|
3575
|
+
</option>
|
|
3576
|
+
<option value="!=">
|
|
3577
|
+
does not equal
|
|
3578
|
+
</option>
|
|
3579
|
+
<option value="LIKE">
|
|
3580
|
+
contains
|
|
3581
|
+
</option>
|
|
3582
|
+
<option value="NOT LIKE">
|
|
3583
|
+
does not contain
|
|
3584
|
+
</option>
|
|
3585
|
+
<option value=">">
|
|
3586
|
+
greater than
|
|
3587
|
+
</option>
|
|
3588
|
+
<option value="<">
|
|
3589
|
+
less than
|
|
3590
|
+
</option>
|
|
3591
|
+
<option value=">=">
|
|
3592
|
+
greater than or equal to
|
|
3593
|
+
</option>
|
|
3594
|
+
<option value="<=">
|
|
3595
|
+
less than or equal to
|
|
3596
|
+
</option>
|
|
3597
|
+
<option value="IN">
|
|
3598
|
+
is one of
|
|
3599
|
+
</option>
|
|
3600
|
+
<option value="NOT IN">
|
|
3601
|
+
is not one of
|
|
3602
|
+
</option>
|
|
3603
|
+
<option value="BETWEEN">
|
|
3604
|
+
is between
|
|
3605
|
+
</option>
|
|
3606
|
+
<option value="IS NULL">
|
|
3607
|
+
is empty
|
|
3608
|
+
</option>
|
|
3609
|
+
<option value="IS NOT NULL">
|
|
3610
|
+
is not empty
|
|
3611
|
+
</option>
|
|
3612
|
+
</select>
|
|
3613
|
+
</div>
|
|
3614
|
+
|
|
3615
|
+
{/* Value Input */}
|
|
3616
|
+
{![
|
|
3617
|
+
'IS NULL',
|
|
3618
|
+
'IS NOT NULL',
|
|
3619
|
+
].includes(criterion.operator) && (
|
|
3620
|
+
<div
|
|
3621
|
+
className={
|
|
3622
|
+
styles.filterField
|
|
3623
|
+
}
|
|
3624
|
+
>
|
|
3625
|
+
<label>Value:</label>
|
|
3626
|
+
<input
|
|
3627
|
+
type="text"
|
|
3628
|
+
className={
|
|
3629
|
+
styles.filterInput
|
|
3630
|
+
}
|
|
3631
|
+
value={
|
|
3632
|
+
criterion.value ||
|
|
3633
|
+
''
|
|
3634
|
+
}
|
|
3635
|
+
onChange={(e) =>
|
|
3636
|
+
handleFilterCriterionChange(
|
|
3637
|
+
groupIndex,
|
|
3638
|
+
filterIndex,
|
|
3639
|
+
'value',
|
|
3640
|
+
e.target.value
|
|
3641
|
+
)
|
|
3642
|
+
}
|
|
3643
|
+
placeholder={
|
|
3644
|
+
criterion.operator ===
|
|
3645
|
+
'BETWEEN'
|
|
3646
|
+
? 'Enter values separated by comma (e.g., 10,100)'
|
|
3647
|
+
: criterion.operator ===
|
|
3648
|
+
'IN' ||
|
|
3649
|
+
criterion.operator ===
|
|
3650
|
+
'NOT IN'
|
|
3651
|
+
? 'Enter values separated by comma'
|
|
3652
|
+
: 'Enter filter value'
|
|
3653
|
+
}
|
|
3654
|
+
/>
|
|
3655
|
+
</div>
|
|
3656
|
+
)}
|
|
3657
|
+
</div>
|
|
3658
|
+
</div>
|
|
3659
|
+
))}
|
|
3660
|
+
</div>
|
|
3661
|
+
) : (
|
|
3662
|
+
<div className={styles.emptyFilterGroup}>
|
|
3663
|
+
<p>
|
|
3664
|
+
No filters in this group. Click "Add Filter"
|
|
3665
|
+
to add one.
|
|
3666
|
+
</p>
|
|
3667
|
+
</div>
|
|
3668
|
+
)}
|
|
3669
|
+
</div>
|
|
3670
|
+
))}
|
|
3671
|
+
|
|
3672
|
+
<div className={styles.skipOption}>
|
|
3673
|
+
<p>
|
|
3674
|
+
This is an optional step. You can skip it to see all
|
|
3675
|
+
records.
|
|
3676
|
+
</p>
|
|
3677
|
+
<button
|
|
3678
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
3679
|
+
onClick={goToNextStep}
|
|
3680
|
+
>
|
|
3681
|
+
Skip Filters
|
|
3682
|
+
</button>
|
|
3683
|
+
</div>
|
|
3684
|
+
</div>
|
|
3685
|
+
);
|
|
3686
|
+
};
|
|
3687
|
+
|
|
3688
|
+
// Export report in different formats (replicating GenericReport pattern)
|
|
3689
|
+
const exportReport = async (format) => {
|
|
3690
|
+
if (!selectedTable || selectedColumns.length === 0) {
|
|
3691
|
+
toast.error('Please generate a report first before exporting.');
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
if (!reportName || !reportName.trim()) {
|
|
3696
|
+
toast.error('Please save your report first before exporting.');
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
try {
|
|
3701
|
+
// Use exact same payload format as GenericReport
|
|
3702
|
+
const queryConfig = {
|
|
3703
|
+
mainTable: selectedTable,
|
|
3704
|
+
columns: selectedColumns.map((col) => ({
|
|
3705
|
+
table: col.table,
|
|
3706
|
+
column: col.column,
|
|
3707
|
+
alias: col.alias || col.displayName,
|
|
3708
|
+
})),
|
|
3709
|
+
joins: joins.map((join) => ({
|
|
3710
|
+
joinType: join.joinType,
|
|
3711
|
+
targetTable: join.targetTable,
|
|
3712
|
+
sourceColumn: join.sourceColumn,
|
|
3713
|
+
targetColumn: join.targetColumn,
|
|
3714
|
+
})),
|
|
3715
|
+
filters: flattenFilterCriteria(filterCriteria),
|
|
3716
|
+
sorting: sortCriteria,
|
|
3717
|
+
};
|
|
3718
|
+
|
|
3719
|
+
const exportData = {
|
|
3720
|
+
query: queryConfig,
|
|
3721
|
+
format: format === 'excel' ? 'xlsx' : format, // Convert excel to xlsx
|
|
3722
|
+
};
|
|
3723
|
+
|
|
3724
|
+
// Generate filename with timestamp
|
|
3725
|
+
const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss');
|
|
3726
|
+
const fileName = `${reportName.replace(
|
|
3727
|
+
/[^a-zA-Z0-9]/g,
|
|
3728
|
+
'_'
|
|
3729
|
+
)}_${timestamp}.${format === 'excel' ? 'xlsx' : format}`;
|
|
3730
|
+
|
|
3731
|
+
// Use Download component like GenericReport
|
|
3732
|
+
const result = await Download(exportUrl, 'POST', exportData);
|
|
3733
|
+
|
|
3734
|
+
if (result && result.data) {
|
|
3735
|
+
// Use saveAs to trigger download
|
|
3736
|
+
saveAs(result.data, fileName);
|
|
3737
|
+
toast.success(`Report exported as ${format.toUpperCase()}`);
|
|
3738
|
+
} else {
|
|
3739
|
+
toast.error(
|
|
3740
|
+
`Failed to export report as ${format.toUpperCase()}`
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3743
|
+
} catch (error) {
|
|
3744
|
+
toast.error(`Failed to export report as ${format.toUpperCase()}`);
|
|
3745
|
+
console.error('Export error:', error);
|
|
3746
|
+
}
|
|
3747
|
+
};
|
|
3748
|
+
|
|
3749
|
+
// Save report configuration (with save/save-as functionality)
|
|
3750
|
+
const saveReport = async (saveAs = false) => {
|
|
3751
|
+
if (!reportName.trim()) {
|
|
3752
|
+
const defaultName = `${formatName(
|
|
3753
|
+
selectedTable
|
|
3754
|
+
)} Report - ${moment().format('YYYY-MM-DD')}`;
|
|
3755
|
+
setReportName(defaultName);
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
// Determine if this is a save or save-as operation
|
|
3759
|
+
const isExistingReport = loadedReportId && !saveAs;
|
|
3760
|
+
const dialogTitle = isExistingReport ? 'Update Report' : 'Save Report';
|
|
3761
|
+
const confirmButtonText = isExistingReport
|
|
3762
|
+
? 'Update Report'
|
|
3763
|
+
: 'Save Report';
|
|
3764
|
+
|
|
3765
|
+
const result = await Swal.fire({
|
|
3766
|
+
title: dialogTitle,
|
|
3767
|
+
html: `
|
|
3768
|
+
<div style="text-align: left;">
|
|
3769
|
+
${
|
|
3770
|
+
isExistingReport
|
|
3771
|
+
? `<div style="background: #e3f2fd; border: 1px solid #2196f3; border-radius: 4px; padding: 12px; margin-bottom: 16px;">
|
|
3772
|
+
<strong>⚠️ This will overwrite the existing report:</strong><br>
|
|
3773
|
+
"${reportName}"
|
|
3774
|
+
</div>`
|
|
3775
|
+
: ''
|
|
3776
|
+
}
|
|
3777
|
+
<label for="report-name" style="display: block; margin-bottom: 8px; font-weight: 500;">Report Name:</label>
|
|
3778
|
+
<input
|
|
3779
|
+
id="report-name"
|
|
3780
|
+
type="text"
|
|
3781
|
+
value="${
|
|
3782
|
+
saveAs
|
|
3783
|
+
? `${reportName} (Copy)`
|
|
3784
|
+
: reportName ||
|
|
3785
|
+
`${formatName(
|
|
3786
|
+
selectedTable
|
|
3787
|
+
)} Report - ${moment().format('YYYY-MM-DD')}`
|
|
3788
|
+
}"
|
|
3789
|
+
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 16px;"
|
|
3790
|
+
/>
|
|
3791
|
+
<label style="display: flex; align-items: center; gap: 8px;">
|
|
3792
|
+
<input type="checkbox" id="is-public" ${
|
|
3793
|
+
isPublic ? 'checked' : ''
|
|
3794
|
+
} />
|
|
3795
|
+
<span>Make this report public (visible to other users)</span>
|
|
3796
|
+
</label>
|
|
3797
|
+
</div>
|
|
3798
|
+
`,
|
|
3799
|
+
showCancelButton: true,
|
|
3800
|
+
confirmButtonText: confirmButtonText,
|
|
3801
|
+
cancelButtonText: 'Cancel',
|
|
3802
|
+
confirmButtonColor: isExistingReport ? '#ff9800' : '#2563eb',
|
|
3803
|
+
preConfirm: () => {
|
|
3804
|
+
const reportNameInput = document.getElementById('report-name');
|
|
3805
|
+
const isPublicInput = document.getElementById('is-public');
|
|
3806
|
+
return {
|
|
3807
|
+
name: reportNameInput.value,
|
|
3808
|
+
isPublic: isPublicInput.checked,
|
|
3809
|
+
};
|
|
3810
|
+
},
|
|
3811
|
+
});
|
|
3812
|
+
|
|
3813
|
+
if (result.isConfirmed) {
|
|
3814
|
+
try {
|
|
3815
|
+
const reportConfig = {
|
|
3816
|
+
label: result.value.name,
|
|
3817
|
+
is_public: result.value.isPublic,
|
|
3818
|
+
detail: {
|
|
3819
|
+
mainTable: selectedTable,
|
|
3820
|
+
columns: selectedColumns,
|
|
3821
|
+
joins: joins,
|
|
3822
|
+
filters: filterCriteria,
|
|
3823
|
+
sorting: sortCriteria,
|
|
3824
|
+
},
|
|
3825
|
+
};
|
|
3826
|
+
|
|
3827
|
+
let saveResult;
|
|
3828
|
+
if (isExistingReport) {
|
|
3829
|
+
// Update existing report using PUT
|
|
3830
|
+
saveResult = await CustomFetch(
|
|
3831
|
+
`${reportsUrl}/${loadedReportId}`,
|
|
3832
|
+
'PUT',
|
|
3833
|
+
reportConfig
|
|
3834
|
+
);
|
|
3835
|
+
} else {
|
|
3836
|
+
// Create new report using POST
|
|
3837
|
+
saveResult = await CustomFetch(
|
|
3838
|
+
reportsUrl,
|
|
3839
|
+
'POST',
|
|
3840
|
+
reportConfig
|
|
3841
|
+
);
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
if (saveResult.error) {
|
|
3845
|
+
toast.error(saveResult.error);
|
|
3846
|
+
} else if (saveResult.data?.success || saveResult.success) {
|
|
3847
|
+
const successMessage = isExistingReport
|
|
3848
|
+
? 'Report updated successfully!'
|
|
3849
|
+
: 'Report saved successfully!';
|
|
3850
|
+
toast.success(successMessage);
|
|
3851
|
+
setReportName(result.value.name);
|
|
3852
|
+
setIsPublic(result.value.isPublic);
|
|
3853
|
+
|
|
3854
|
+
// If this was a save-as operation, update the loaded report ID
|
|
3855
|
+
if (saveAs && saveResult.data?.data?.id) {
|
|
3856
|
+
setLoadedReportId(saveResult.data.data.id);
|
|
3857
|
+
} else if (!isExistingReport && saveResult.data?.data?.id) {
|
|
3858
|
+
setLoadedReportId(saveResult.data.data.id);
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
fetchSavedReports(); // Refresh the saved reports list
|
|
3862
|
+
} else {
|
|
3863
|
+
toast.error(
|
|
3864
|
+
saveResult.data?.message ||
|
|
3865
|
+
saveResult.message ||
|
|
3866
|
+
'Failed to save report'
|
|
3867
|
+
);
|
|
3868
|
+
}
|
|
3869
|
+
} catch (error) {
|
|
3870
|
+
toast.error('Failed to save report');
|
|
3871
|
+
console.error('Save error:', error);
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
};
|
|
3875
|
+
|
|
3876
|
+
// Render preview
|
|
3877
|
+
const renderPreview = () => {
|
|
3878
|
+
return (
|
|
3879
|
+
<div className={styles.previewSection}>
|
|
3880
|
+
<div className={styles.previewActions}>
|
|
3881
|
+
<button
|
|
3882
|
+
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
3883
|
+
onClick={() => {
|
|
3884
|
+
resetPagination();
|
|
3885
|
+
executeReport();
|
|
3886
|
+
}}
|
|
3887
|
+
disabled={isLoading}
|
|
3888
|
+
>
|
|
3889
|
+
{isLoading ? 'Loading...' : 'Generate Report'}
|
|
3890
|
+
</button>
|
|
3891
|
+
</div>
|
|
3892
|
+
|
|
3893
|
+
{previewData.length > 0 && (
|
|
3894
|
+
<>
|
|
3895
|
+
<div className={styles.gridContainer}>
|
|
3896
|
+
<ReactDataGrid
|
|
3897
|
+
columns={gridColumns}
|
|
3898
|
+
dataSource={previewData}
|
|
3899
|
+
style={{ height: 800 }}
|
|
3900
|
+
pagination="local"
|
|
3901
|
+
limit={pageSize}
|
|
3902
|
+
skip={(currentPage - 1) * pageSize}
|
|
3903
|
+
count={totalResults}
|
|
3904
|
+
onLimitChange={(newLimit) => {
|
|
3905
|
+
handlePageSizeChange(newLimit);
|
|
3906
|
+
}}
|
|
3907
|
+
onSkipChange={(newSkip) => {
|
|
3908
|
+
const newPage =
|
|
3909
|
+
Math.floor(newSkip / pageSize) + 1;
|
|
3910
|
+
handlePageChange(newPage);
|
|
3911
|
+
}}
|
|
3912
|
+
pageSizes={[10, 15, 25, 50]}
|
|
3913
|
+
defaultLimit={25}
|
|
3914
|
+
// Row height configuration for JSON content
|
|
3915
|
+
rowHeight={null} // Allow dynamic row height
|
|
3916
|
+
estimatedRowHeight={60} // Estimated height for performance
|
|
3917
|
+
minRowHeight={40} // Minimum row height
|
|
3918
|
+
// No maxRowHeight to allow full expansion for JSON content
|
|
3919
|
+
// Enable text wrapping and proper sizing
|
|
3920
|
+
showCellBorders="horizontal"
|
|
3921
|
+
showZebraRows={false}
|
|
3922
|
+
// Ensure proper rendering of content
|
|
3923
|
+
virtualizeColumns={false}
|
|
3924
|
+
enableRowspan={false}
|
|
3925
|
+
/>
|
|
3926
|
+
</div>
|
|
3927
|
+
|
|
3928
|
+
<div className={styles.reportActions}>
|
|
3929
|
+
<div className={styles.saveSection}>
|
|
3930
|
+
<h3>Save Your Report</h3>
|
|
3931
|
+
<p>
|
|
3932
|
+
Save this report configuration to use again
|
|
3933
|
+
later.
|
|
3934
|
+
</p>
|
|
3935
|
+
<div className={styles.saveButtons}>
|
|
3936
|
+
<button
|
|
3937
|
+
className={`${styles.btn} ${styles.btnSuccess}`}
|
|
3938
|
+
onClick={() => saveReport(false)}
|
|
3939
|
+
>
|
|
3940
|
+
<Save size={16} />
|
|
3941
|
+
{loadedReportId
|
|
3942
|
+
? 'Update Report'
|
|
3943
|
+
: 'Save Report'}
|
|
3944
|
+
</button>
|
|
3945
|
+
{loadedReportId && (
|
|
3946
|
+
<button
|
|
3947
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
3948
|
+
onClick={() => saveReport(true)}
|
|
3949
|
+
>
|
|
3950
|
+
<Save size={16} />
|
|
3951
|
+
Save As New
|
|
3952
|
+
</button>
|
|
3953
|
+
)}
|
|
3954
|
+
</div>
|
|
3955
|
+
</div>
|
|
3956
|
+
|
|
3957
|
+
<div className={styles.exportOptions}>
|
|
3958
|
+
<h3>Export Your Report</h3>
|
|
3959
|
+
<p>
|
|
3960
|
+
Download your report data in various
|
|
3961
|
+
formats.
|
|
3962
|
+
</p>
|
|
3963
|
+
<div className={styles.exportButtons}>
|
|
3964
|
+
<button
|
|
3965
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
3966
|
+
onClick={() => exportReport('excel')}
|
|
3967
|
+
disabled={
|
|
3968
|
+
isLoading ||
|
|
3969
|
+
!reportName ||
|
|
3970
|
+
!reportName.trim()
|
|
3971
|
+
}
|
|
3972
|
+
title={
|
|
3973
|
+
!reportName
|
|
3974
|
+
? 'Please save your report first'
|
|
3975
|
+
: ''
|
|
3976
|
+
}
|
|
3977
|
+
>
|
|
3978
|
+
<DownloadIcon size={16} />
|
|
3979
|
+
Export as Excel
|
|
3980
|
+
</button>
|
|
3981
|
+
<button
|
|
3982
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
3983
|
+
onClick={() => exportReport('csv')}
|
|
3984
|
+
disabled={
|
|
3985
|
+
isLoading ||
|
|
3986
|
+
!reportName ||
|
|
3987
|
+
!reportName.trim()
|
|
3988
|
+
}
|
|
3989
|
+
title={
|
|
3990
|
+
!reportName
|
|
3991
|
+
? 'Please save your report first'
|
|
3992
|
+
: ''
|
|
3993
|
+
}
|
|
3994
|
+
>
|
|
3995
|
+
<DownloadIcon size={16} />
|
|
3996
|
+
Export as CSV
|
|
3997
|
+
</button>
|
|
3998
|
+
<button
|
|
3999
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
4000
|
+
onClick={() => exportReport('pdf')}
|
|
4001
|
+
disabled={
|
|
4002
|
+
isLoading ||
|
|
4003
|
+
!reportName ||
|
|
4004
|
+
!reportName.trim()
|
|
4005
|
+
}
|
|
4006
|
+
title={
|
|
4007
|
+
!reportName
|
|
4008
|
+
? 'Please save your report first'
|
|
4009
|
+
: ''
|
|
4010
|
+
}
|
|
4011
|
+
>
|
|
4012
|
+
<DownloadIcon size={16} />
|
|
4013
|
+
Export as PDF
|
|
4014
|
+
</button>
|
|
4015
|
+
</div>
|
|
4016
|
+
</div>
|
|
4017
|
+
</div>
|
|
4018
|
+
</>
|
|
4019
|
+
)}
|
|
4020
|
+
</div>
|
|
4021
|
+
);
|
|
4022
|
+
};
|
|
4023
|
+
|
|
4024
|
+
// Render advanced mode (using original GenericReport)
|
|
4025
|
+
const renderAdvancedMode = () => {
|
|
4026
|
+
return (
|
|
4027
|
+
<div className={styles.advancedMode}>
|
|
4028
|
+
<GenericReport setting={setting} definition={definition} />
|
|
4029
|
+
</div>
|
|
4030
|
+
);
|
|
4031
|
+
};
|
|
4032
|
+
|
|
4033
|
+
return (
|
|
4034
|
+
<div className={styles.reportBuilder}>
|
|
4035
|
+
{/* Header */}
|
|
4036
|
+
<div className={styles.header}>
|
|
4037
|
+
<div className={styles.headerLeft}>
|
|
4038
|
+
<h1>Report Builder</h1>
|
|
4039
|
+
<button
|
|
4040
|
+
className={styles.helpButton}
|
|
4041
|
+
onClick={showGuidedHelp}
|
|
4042
|
+
>
|
|
4043
|
+
<Question size={16} />
|
|
4044
|
+
Help
|
|
4045
|
+
</button>
|
|
4046
|
+
</div>
|
|
4047
|
+
|
|
4048
|
+
<div className={styles.headerRight}>
|
|
4049
|
+
<div className={styles.modeToggle}>
|
|
4050
|
+
<span className={beginnerMode ? styles.active : ''}>
|
|
4051
|
+
<BookOpen size={16} />
|
|
4052
|
+
Beginner
|
|
4053
|
+
</span>
|
|
4054
|
+
<label className={styles.switch}>
|
|
4055
|
+
<input
|
|
4056
|
+
type="checkbox"
|
|
4057
|
+
checked={!beginnerMode}
|
|
4058
|
+
onChange={(e) =>
|
|
4059
|
+
setBeginnerMode(!e.target.checked)
|
|
4060
|
+
}
|
|
4061
|
+
/>
|
|
4062
|
+
<span className={styles.slider}></span>
|
|
4063
|
+
</label>
|
|
4064
|
+
<span className={!beginnerMode ? styles.active : ''}>
|
|
4065
|
+
<SettingsVertical size={16} />
|
|
4066
|
+
Advanced
|
|
4067
|
+
</span>
|
|
4068
|
+
</div>
|
|
4069
|
+
</div>
|
|
4070
|
+
</div>
|
|
4071
|
+
|
|
4072
|
+
{/* Main content */}
|
|
4073
|
+
<div className={styles.content}>{renderContent()}</div>
|
|
4074
|
+
</div>
|
|
4075
|
+
);
|
|
4076
|
+
};
|
|
4077
|
+
|
|
4078
|
+
GenericReportImproved.propTypes = {
|
|
4079
|
+
setting: PropTypes.object,
|
|
4080
|
+
definition: PropTypes.object,
|
|
4081
|
+
};
|
|
4082
|
+
|
|
4083
|
+
export default GenericReportImproved;
|