@visns-studio/visns-components 5.9.8 → 5.9.10

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