@visns-studio/visns-components 5.9.7 → 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;