filter
This commit is contained in:
		
							parent
							
								
									0e3aa9b903
								
							
						
					
					
						commit
						f60657ca64
					
				| @ -0,0 +1,687 @@ | |||||||
|  | # Common Filter System - Example Usage | ||||||
|  | 
 | ||||||
|  | ## Dashboard Layout | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | +-----------------------------------------------------+ | ||||||
|  | | Common Filter Widget (Draggable)                    | | ||||||
|  | |                                                     | | ||||||
|  | | [Category ▼] [Status ▼] [Date Range] [Active -toggle]| | ||||||
|  | | [Save Preset] [Preset Selector] [Reset]             | | ||||||
|  | +-----------------------------------------------------+ | ||||||
|  | | +-----------+  +-----------+  +-----------+          | | ||||||
|  | | | Bar Chart |  | Line Chart|  | Pie Chart |          | | ||||||
|  | | |           |  |           |  |           |          | | ||||||
|  | | |           |  |           |  |           |          | | ||||||
|  | | +-----------+  +-----------+  +-----------+          | | ||||||
|  | |                                                     | | ||||||
|  | | +-----------+  +-----------+  +-----------+          | | ||||||
|  | | | Table     |  | Map       |  | KPI Cards |          | | ||||||
|  | | |           |  |           |  |           |          | | ||||||
|  | | |           |  |           |  |           |          | | ||||||
|  | | +-----------+  +-----------+  +-----------+          | | ||||||
|  | +-----------------------------------------------------+ | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Filter Configuration Example | ||||||
|  | 
 | ||||||
|  | ### 1. Creating Filter Definitions | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // In CommonFilterComponent or dashboard initialization | ||||||
|  | const filters: Filter[] = [ | ||||||
|  |   { | ||||||
|  |     id: 'category', | ||||||
|  |     field: 'product_category', | ||||||
|  |     label: 'Category', | ||||||
|  |     type: 'dropdown', | ||||||
|  |     options: ['Electronics', 'Clothing', 'Home & Garden', 'Books', 'Sports'] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: 'status', | ||||||
|  |     field: 'order_status', | ||||||
|  |     label: 'Status', | ||||||
|  |     type: 'multiselect', | ||||||
|  |     options: ['Pending', 'Processing', 'Shipped', 'Delivered', 'Cancelled'] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: 'date_range', | ||||||
|  |     field: 'order_date', | ||||||
|  |     label: 'Order Date', | ||||||
|  |     type: 'date-range' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: 'active', | ||||||
|  |     field: 'is_active', | ||||||
|  |     label: 'Active Orders', | ||||||
|  |     type: 'toggle' | ||||||
|  |   } | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | // Set filters in the service | ||||||
|  | filterService.setFilters(filters); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 2. Setting Initial Filter Values | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // Set initial values | ||||||
|  | filterService.updateFilterValue('category', 'Electronics'); | ||||||
|  | filterService.updateFilterValue('status', ['Processing', 'Shipped']); | ||||||
|  | filterService.updateFilterValue('date_range', { | ||||||
|  |   start: '2023-01-01', | ||||||
|  |   end: '2023-12-31' | ||||||
|  | }); | ||||||
|  | filterService.updateFilterValue('active', true); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 3. Chart Component Integration | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // In bar-chart.component.ts | ||||||
|  | export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||||
|  |   private subscriptions: Subscription[] = []; | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     private dashboardService: Dashboard3Service, | ||||||
|  |     private filterService: FilterService | ||||||
|  |   ) { } | ||||||
|  | 
 | ||||||
|  |   ngOnInit(): void { | ||||||
|  |     // Subscribe to filter changes | ||||||
|  |     this.subscriptions.push( | ||||||
|  |       this.filterService.filterState$.subscribe(filters => { | ||||||
|  |         this.refreshData(); | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |      | ||||||
|  |     // Load initial data | ||||||
|  |     this.fetchChartData(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   fetchChartData(): void { | ||||||
|  |     // Get current filter values | ||||||
|  |     const filterValues = this.filterService.getFilterValues(); | ||||||
|  |      | ||||||
|  |     // Build filter parameters for API | ||||||
|  |     let filterParams = ''; | ||||||
|  |     if (Object.keys(filterValues).length > 0) { | ||||||
|  |       const filterObj = {}; | ||||||
|  |        | ||||||
|  |       // Add base filters first | ||||||
|  |       if (this.baseFilters && this.baseFilters.length > 0) { | ||||||
|  |         this.baseFilters.forEach(filter => { | ||||||
|  |           if (filter.field && filter.value) { | ||||||
|  |             filterObj[filter.field] = filter.value; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Add common filters | ||||||
|  |       Object.keys(filterValues).forEach(key => { | ||||||
|  |         const value = filterValues[key]; | ||||||
|  |         if (value !== undefined && value !== null && value !== '') { | ||||||
|  |           filterObj[key] = value; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       if (Object.keys(filterObj).length > 0) { | ||||||
|  |         filterParams = JSON.stringify(filterObj); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Make API call with filters | ||||||
|  |     this.dashboardService.getChartData( | ||||||
|  |       this.table,  | ||||||
|  |       'bar',  | ||||||
|  |       this.xAxis,  | ||||||
|  |       this.yAxis,  | ||||||
|  |       this.connection,  | ||||||
|  |       '',  | ||||||
|  |       '',  | ||||||
|  |       filterParams | ||||||
|  |     ).subscribe(data => { | ||||||
|  |       // Handle response | ||||||
|  |       this.processChartData(data); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   refreshData(): void { | ||||||
|  |     this.fetchChartData(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ngOnDestroy(): void { | ||||||
|  |     this.subscriptions.forEach(sub => sub.unsubscribe()); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 4. API Endpoint Example | ||||||
|  | 
 | ||||||
|  | ```javascript | ||||||
|  | // Backend API endpoint example | ||||||
|  | app.get('/chart/getdashjson/bar', (req, res) => { | ||||||
|  |   const { | ||||||
|  |     tableName, | ||||||
|  |     xAxis, | ||||||
|  |     yAxes, | ||||||
|  |     sureId, | ||||||
|  |     filters // JSON string of filters | ||||||
|  |   } = req.query; | ||||||
|  |    | ||||||
|  |   // Parse filters | ||||||
|  |   let filterObj = {}; | ||||||
|  |   if (filters) { | ||||||
|  |     try { | ||||||
|  |       filterObj = JSON.parse(filters); | ||||||
|  |     } catch (e) { | ||||||
|  |       console.error('Failed to parse filters:', e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Build database query with filters | ||||||
|  |   let query = `SELECT ${xAxis}, ${yAxes} FROM ${tableName}`; | ||||||
|  |   const whereConditions = []; | ||||||
|  |    | ||||||
|  |   // Add filter conditions | ||||||
|  |   Object.keys(filterObj).forEach(field => { | ||||||
|  |     const value = filterObj[field]; | ||||||
|  |     if (Array.isArray(value)) { | ||||||
|  |       // Handle multiselect (IN clause) | ||||||
|  |       const values = value.map(v => `'${v}'`).join(','); | ||||||
|  |       whereConditions.push(`${field} IN (${values})`); | ||||||
|  |     } else if (typeof value === 'object' && value.start && value.end) { | ||||||
|  |       // Handle date range | ||||||
|  |       whereConditions.push(`${field} BETWEEN '${value.start}' AND '${value.end}'`); | ||||||
|  |     } else if (typeof value === 'boolean') { | ||||||
|  |       // Handle boolean | ||||||
|  |       whereConditions.push(`${field} = ${value}`); | ||||||
|  |     } else { | ||||||
|  |       // Handle text and other values | ||||||
|  |       whereConditions.push(`${field} = '${value}'`); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   if (whereConditions.length > 0) { | ||||||
|  |     query += ` WHERE ${whereConditions.join(' AND ')}`; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Execute query and return results | ||||||
|  |   database.query(query, (err, results) => { | ||||||
|  |     if (err) { | ||||||
|  |       res.status(500).json({ error: err.message }); | ||||||
|  |     } else { | ||||||
|  |       res.json({ | ||||||
|  |         chartLabels: results.map(row => row[xAxis]), | ||||||
|  |         chartData: [{ | ||||||
|  |           data: results.map(row => row[yAxes]), | ||||||
|  |           label: yAxes | ||||||
|  |         }] | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Filter Presets Example | ||||||
|  | 
 | ||||||
|  | ### Saving a Preset | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // Save current filter state as a preset | ||||||
|  | const presetName = 'Q4 2023 Sales'; | ||||||
|  | filterService.savePreset(presetName); | ||||||
|  | 
 | ||||||
|  | // Preset is now available in the presets list | ||||||
|  | const presets = filterService.getPresets(); // ['Q4 2023 Sales', ...] | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Loading a Preset | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // Load a saved preset | ||||||
|  | filterService.loadPreset('Q4 2023 Sales'); | ||||||
|  | 
 | ||||||
|  | // All charts will automatically refresh with the preset filters | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Cross-Filtering Example | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // In a chart component, handle click events | ||||||
|  | onBarClick(event: any): void { | ||||||
|  |   const clickedCategory = event.active[0]._model.label; | ||||||
|  |    | ||||||
|  |   // Update the category filter | ||||||
|  |   this.filterService.updateFilterValue('category', clickedCategory); | ||||||
|  |    | ||||||
|  |   // All other charts will automatically update | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## URL Synchronization Example | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // Update URL when filters change | ||||||
|  | this.filterService.filterState$.subscribe(filters => { | ||||||
|  |   const queryParams = new URLSearchParams(); | ||||||
|  |    | ||||||
|  |   Object.keys(filters).forEach(key => { | ||||||
|  |     const value = filters[key]; | ||||||
|  |     if (value !== undefined && value !== null && value !== '') { | ||||||
|  |       if (typeof value === 'object') { | ||||||
|  |         if (value.hasOwnProperty('start') && value.hasOwnProperty('end')) { | ||||||
|  |           // Date range | ||||||
|  |           if (value.start) queryParams.append(`${key}_start`, value.start); | ||||||
|  |           if (value.end) queryParams.append(`${key}_end`, value.end); | ||||||
|  |         } else { | ||||||
|  |           // Other objects as JSON | ||||||
|  |           queryParams.append(key, JSON.stringify(value)); | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         // Simple values | ||||||
|  |         queryParams.append(key, value.toString()); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Update browser URL | ||||||
|  |   const newUrl = `${window.location.pathname}?${queryParams.toString()}`; | ||||||
|  |   window.history.replaceState({}, '', newUrl); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | // Load filters from URL on page load | ||||||
|  | ngOnInit(): void { | ||||||
|  |   const urlParams = new URLSearchParams(window.location.search); | ||||||
|  |   const filterState: any = {}; | ||||||
|  |    | ||||||
|  |   // Parse URL parameters back into filter state | ||||||
|  |   for (const [key, value] of urlParams.entries()) { | ||||||
|  |     if (key.endsWith('_start')) { | ||||||
|  |       const fieldName = key.replace('_start', ''); | ||||||
|  |       filterState[fieldName] = filterState[fieldName] || {}; | ||||||
|  |       filterState[fieldName].start = value; | ||||||
|  |     } else if (key.endsWith('_end')) { | ||||||
|  |       const fieldName = key.replace('_end', ''); | ||||||
|  |       filterState[fieldName] = filterState[fieldName] || {}; | ||||||
|  |       filterState[fieldName].end = value; | ||||||
|  |     } else { | ||||||
|  |       try { | ||||||
|  |         filterState[key] = JSON.parse(value); | ||||||
|  |       } catch (e) { | ||||||
|  |         filterState[key] = value; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Apply URL filters | ||||||
|  |   Object.keys(filterState).forEach(key => { | ||||||
|  |     this.filterService.updateFilterValue(key, filterState[key]); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Data Flow Diagram | ||||||
|  | 
 | ||||||
|  | ```mermaid | ||||||
|  | sequenceDiagram | ||||||
|  |     participant User | ||||||
|  |     participant CommonFilterComponent | ||||||
|  |     participant FilterService | ||||||
|  |     participant ChartComponent | ||||||
|  |     participant API | ||||||
|  | 
 | ||||||
|  |     User->>CommonFilterComponent: Configure filters | ||||||
|  |     CommonFilterComponent->>FilterService: Update filter state | ||||||
|  |     FilterService->>ChartComponent: Notify filter change | ||||||
|  |     ChartComponent->>FilterService: Request current filters | ||||||
|  |     FilterService-->>ChartComponent: Return filter values | ||||||
|  |     ChartComponent->>API: Fetch data with filters | ||||||
|  |     API-->>ChartComponent: Return filtered data | ||||||
|  |     ChartComponent->>User: Display updated chart | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | This implementation provides a robust, scalable solution for managing common filters across multiple dashboard components with real-time updates and flexible configuration options.# Common Filter System - Example Usage | ||||||
|  | 
 | ||||||
|  | ## Dashboard Layout | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | +-----------------------------------------------------+ | ||||||
|  | | Common Filter Widget (Draggable)                    | | ||||||
|  | |                                                     | | ||||||
|  | | [Category ▼] [Status ▼] [Date Range] [Active -toggle]| | ||||||
|  | | [Save Preset] [Preset Selector] [Reset]             | | ||||||
|  | +-----------------------------------------------------+ | ||||||
|  | | +-----------+  +-----------+  +-----------+          | | ||||||
|  | | | Bar Chart |  | Line Chart|  | Pie Chart |          | | ||||||
|  | | |           |  |           |  |           |          | | ||||||
|  | | |           |  |           |  |           |          | | ||||||
|  | | +-----------+  +-----------+  +-----------+          | | ||||||
|  | |                                                     | | ||||||
|  | | +-----------+  +-----------+  +-----------+          | | ||||||
|  | | | Table     |  | Map       |  | KPI Cards |          | | ||||||
|  | | |           |  |           |  |           |          | | ||||||
|  | | |           |  |           |  |           |          | | ||||||
|  | | +-----------+  +-----------+  +-----------+          | | ||||||
|  | +-----------------------------------------------------+ | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Filter Configuration Example | ||||||
|  | 
 | ||||||
|  | ### 1. Creating Filter Definitions | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // In CommonFilterComponent or dashboard initialization | ||||||
|  | const filters: Filter[] = [ | ||||||
|  |   { | ||||||
|  |     id: 'category', | ||||||
|  |     field: 'product_category', | ||||||
|  |     label: 'Category', | ||||||
|  |     type: 'dropdown', | ||||||
|  |     options: ['Electronics', 'Clothing', 'Home & Garden', 'Books', 'Sports'] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: 'status', | ||||||
|  |     field: 'order_status', | ||||||
|  |     label: 'Status', | ||||||
|  |     type: 'multiselect', | ||||||
|  |     options: ['Pending', 'Processing', 'Shipped', 'Delivered', 'Cancelled'] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: 'date_range', | ||||||
|  |     field: 'order_date', | ||||||
|  |     label: 'Order Date', | ||||||
|  |     type: 'date-range' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: 'active', | ||||||
|  |     field: 'is_active', | ||||||
|  |     label: 'Active Orders', | ||||||
|  |     type: 'toggle' | ||||||
|  |   } | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | // Set filters in the service | ||||||
|  | filterService.setFilters(filters); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 2. Setting Initial Filter Values | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // Set initial values | ||||||
|  | filterService.updateFilterValue('category', 'Electronics'); | ||||||
|  | filterService.updateFilterValue('status', ['Processing', 'Shipped']); | ||||||
|  | filterService.updateFilterValue('date_range', { | ||||||
|  |   start: '2023-01-01', | ||||||
|  |   end: '2023-12-31' | ||||||
|  | }); | ||||||
|  | filterService.updateFilterValue('active', true); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 3. Chart Component Integration | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // In bar-chart.component.ts | ||||||
|  | export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||||
|  |   private subscriptions: Subscription[] = []; | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     private dashboardService: Dashboard3Service, | ||||||
|  |     private filterService: FilterService | ||||||
|  |   ) { } | ||||||
|  | 
 | ||||||
|  |   ngOnInit(): void { | ||||||
|  |     // Subscribe to filter changes | ||||||
|  |     this.subscriptions.push( | ||||||
|  |       this.filterService.filterState$.subscribe(filters => { | ||||||
|  |         this.refreshData(); | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |      | ||||||
|  |     // Load initial data | ||||||
|  |     this.fetchChartData(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   fetchChartData(): void { | ||||||
|  |     // Get current filter values | ||||||
|  |     const filterValues = this.filterService.getFilterValues(); | ||||||
|  |      | ||||||
|  |     // Build filter parameters for API | ||||||
|  |     let filterParams = ''; | ||||||
|  |     if (Object.keys(filterValues).length > 0) { | ||||||
|  |       const filterObj = {}; | ||||||
|  |        | ||||||
|  |       // Add base filters first | ||||||
|  |       if (this.baseFilters && this.baseFilters.length > 0) { | ||||||
|  |         this.baseFilters.forEach(filter => { | ||||||
|  |           if (filter.field && filter.value) { | ||||||
|  |             filterObj[filter.field] = filter.value; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Add common filters | ||||||
|  |       Object.keys(filterValues).forEach(key => { | ||||||
|  |         const value = filterValues[key]; | ||||||
|  |         if (value !== undefined && value !== null && value !== '') { | ||||||
|  |           filterObj[key] = value; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       if (Object.keys(filterObj).length > 0) { | ||||||
|  |         filterParams = JSON.stringify(filterObj); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Make API call with filters | ||||||
|  |     this.dashboardService.getChartData( | ||||||
|  |       this.table,  | ||||||
|  |       'bar',  | ||||||
|  |       this.xAxis,  | ||||||
|  |       this.yAxis,  | ||||||
|  |       this.connection,  | ||||||
|  |       '',  | ||||||
|  |       '',  | ||||||
|  |       filterParams | ||||||
|  |     ).subscribe(data => { | ||||||
|  |       // Handle response | ||||||
|  |       this.processChartData(data); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   refreshData(): void { | ||||||
|  |     this.fetchChartData(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ngOnDestroy(): void { | ||||||
|  |     this.subscriptions.forEach(sub => sub.unsubscribe()); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 4. API Endpoint Example | ||||||
|  | 
 | ||||||
|  | ```javascript | ||||||
|  | // Backend API endpoint example | ||||||
|  | app.get('/chart/getdashjson/bar', (req, res) => { | ||||||
|  |   const { | ||||||
|  |     tableName, | ||||||
|  |     xAxis, | ||||||
|  |     yAxes, | ||||||
|  |     sureId, | ||||||
|  |     filters // JSON string of filters | ||||||
|  |   } = req.query; | ||||||
|  |    | ||||||
|  |   // Parse filters | ||||||
|  |   let filterObj = {}; | ||||||
|  |   if (filters) { | ||||||
|  |     try { | ||||||
|  |       filterObj = JSON.parse(filters); | ||||||
|  |     } catch (e) { | ||||||
|  |       console.error('Failed to parse filters:', e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Build database query with filters | ||||||
|  |   let query = `SELECT ${xAxis}, ${yAxes} FROM ${tableName}`; | ||||||
|  |   const whereConditions = []; | ||||||
|  |    | ||||||
|  |   // Add filter conditions | ||||||
|  |   Object.keys(filterObj).forEach(field => { | ||||||
|  |     const value = filterObj[field]; | ||||||
|  |     if (Array.isArray(value)) { | ||||||
|  |       // Handle multiselect (IN clause) | ||||||
|  |       const values = value.map(v => `'${v}'`).join(','); | ||||||
|  |       whereConditions.push(`${field} IN (${values})`); | ||||||
|  |     } else if (typeof value === 'object' && value.start && value.end) { | ||||||
|  |       // Handle date range | ||||||
|  |       whereConditions.push(`${field} BETWEEN '${value.start}' AND '${value.end}'`); | ||||||
|  |     } else if (typeof value === 'boolean') { | ||||||
|  |       // Handle boolean | ||||||
|  |       whereConditions.push(`${field} = ${value}`); | ||||||
|  |     } else { | ||||||
|  |       // Handle text and other values | ||||||
|  |       whereConditions.push(`${field} = '${value}'`); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   if (whereConditions.length > 0) { | ||||||
|  |     query += ` WHERE ${whereConditions.join(' AND ')}`; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Execute query and return results | ||||||
|  |   database.query(query, (err, results) => { | ||||||
|  |     if (err) { | ||||||
|  |       res.status(500).json({ error: err.message }); | ||||||
|  |     } else { | ||||||
|  |       res.json({ | ||||||
|  |         chartLabels: results.map(row => row[xAxis]), | ||||||
|  |         chartData: [{ | ||||||
|  |           data: results.map(row => row[yAxes]), | ||||||
|  |           label: yAxes | ||||||
|  |         }] | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Filter Presets Example | ||||||
|  | 
 | ||||||
|  | ### Saving a Preset | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // Save current filter state as a preset | ||||||
|  | const presetName = 'Q4 2023 Sales'; | ||||||
|  | filterService.savePreset(presetName); | ||||||
|  | 
 | ||||||
|  | // Preset is now available in the presets list | ||||||
|  | const presets = filterService.getPresets(); // ['Q4 2023 Sales', ...] | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Loading a Preset | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // Load a saved preset | ||||||
|  | filterService.loadPreset('Q4 2023 Sales'); | ||||||
|  | 
 | ||||||
|  | // All charts will automatically refresh with the preset filters | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Cross-Filtering Example | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // In a chart component, handle click events | ||||||
|  | onBarClick(event: any): void { | ||||||
|  |   const clickedCategory = event.active[0]._model.label; | ||||||
|  |    | ||||||
|  |   // Update the category filter | ||||||
|  |   this.filterService.updateFilterValue('category', clickedCategory); | ||||||
|  |    | ||||||
|  |   // All other charts will automatically update | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## URL Synchronization Example | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // Update URL when filters change | ||||||
|  | this.filterService.filterState$.subscribe(filters => { | ||||||
|  |   const queryParams = new URLSearchParams(); | ||||||
|  |    | ||||||
|  |   Object.keys(filters).forEach(key => { | ||||||
|  |     const value = filters[key]; | ||||||
|  |     if (value !== undefined && value !== null && value !== '') { | ||||||
|  |       if (typeof value === 'object') { | ||||||
|  |         if (value.hasOwnProperty('start') && value.hasOwnProperty('end')) { | ||||||
|  |           // Date range | ||||||
|  |           if (value.start) queryParams.append(`${key}_start`, value.start); | ||||||
|  |           if (value.end) queryParams.append(`${key}_end`, value.end); | ||||||
|  |         } else { | ||||||
|  |           // Other objects as JSON | ||||||
|  |           queryParams.append(key, JSON.stringify(value)); | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         // Simple values | ||||||
|  |         queryParams.append(key, value.toString()); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Update browser URL | ||||||
|  |   const newUrl = `${window.location.pathname}?${queryParams.toString()}`; | ||||||
|  |   window.history.replaceState({}, '', newUrl); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | // Load filters from URL on page load | ||||||
|  | ngOnInit(): void { | ||||||
|  |   const urlParams = new URLSearchParams(window.location.search); | ||||||
|  |   const filterState: any = {}; | ||||||
|  |    | ||||||
|  |   // Parse URL parameters back into filter state | ||||||
|  |   for (const [key, value] of urlParams.entries()) { | ||||||
|  |     if (key.endsWith('_start')) { | ||||||
|  |       const fieldName = key.replace('_start', ''); | ||||||
|  |       filterState[fieldName] = filterState[fieldName] || {}; | ||||||
|  |       filterState[fieldName].start = value; | ||||||
|  |     } else if (key.endsWith('_end')) { | ||||||
|  |       const fieldName = key.replace('_end', ''); | ||||||
|  |       filterState[fieldName] = filterState[fieldName] || {}; | ||||||
|  |       filterState[fieldName].end = value; | ||||||
|  |     } else { | ||||||
|  |       try { | ||||||
|  |         filterState[key] = JSON.parse(value); | ||||||
|  |       } catch (e) { | ||||||
|  |         filterState[key] = value; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Apply URL filters | ||||||
|  |   Object.keys(filterState).forEach(key => { | ||||||
|  |     this.filterService.updateFilterValue(key, filterState[key]); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Data Flow Diagram | ||||||
|  | 
 | ||||||
|  | ```mermaid | ||||||
|  | sequenceDiagram | ||||||
|  |     participant User | ||||||
|  |     participant CommonFilterComponent | ||||||
|  |     participant FilterService | ||||||
|  |     participant ChartComponent | ||||||
|  |     participant API | ||||||
|  | 
 | ||||||
|  |     User->>CommonFilterComponent: Configure filters | ||||||
|  |     CommonFilterComponent->>FilterService: Update filter state | ||||||
|  |     FilterService->>ChartComponent: Notify filter change | ||||||
|  |     ChartComponent->>FilterService: Request current filters | ||||||
|  |     FilterService-->>ChartComponent: Return filter values | ||||||
|  |     ChartComponent->>API: Fetch data with filters | ||||||
|  |     API-->>ChartComponent: Return filtered data | ||||||
|  |     ChartComponent->>User: Display updated chart | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | This implementation provides a robust, scalable solution for managing common filters across multiple dashboard components with real-time updates and flexible configuration options. | ||||||
| @ -0,0 +1,219 @@ | |||||||
|  | # Common Filter Implementation Guide | ||||||
|  | 
 | ||||||
|  | ## Overview | ||||||
|  | 
 | ||||||
|  | This implementation provides a centralized filter system that allows users to control multiple charts simultaneously through a draggable filter widget. The system consists of: | ||||||
|  | 
 | ||||||
|  | 1. **FilterService** - Central service managing filter definitions and state | ||||||
|  | 2. **CommonFilterComponent** - Draggable widget for configuring filters | ||||||
|  | 3. **Chart Components** - Updated chart components that subscribe to filter changes | ||||||
|  | 
 | ||||||
|  | ## Architecture | ||||||
|  | 
 | ||||||
|  | ```mermaid | ||||||
|  | graph TB | ||||||
|  |     A[FilterService] --> B[CommonFilterComponent] | ||||||
|  |     A --> C[BarChartComponent] | ||||||
|  |     A --> D[LineChartComponent] | ||||||
|  |     A --> E[OtherChartComponents] | ||||||
|  |     F[User Interaction] --> B | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Key Components | ||||||
|  | 
 | ||||||
|  | ### 1. FilterService | ||||||
|  | 
 | ||||||
|  | The central service that manages: | ||||||
|  | - Filter definitions (type, options, labels) | ||||||
|  | - Current filter values | ||||||
|  | - Filter presets | ||||||
|  | - Query parameter generation for API calls | ||||||
|  | 
 | ||||||
|  | #### Methods: | ||||||
|  | - `addFilter()` - Add a new filter | ||||||
|  | - `removeFilter()` - Remove a filter | ||||||
|  | - `updateFilterValue()` - Update a filter's value | ||||||
|  | - `getFilterValues()` - Get current filter values | ||||||
|  | - `resetFilters()` - Reset all filters to default values | ||||||
|  | - `savePreset()` - Save current filter state as a preset | ||||||
|  | - `loadPreset()` - Load a saved preset | ||||||
|  | - `buildQueryParams()` - Generate query parameters for API calls | ||||||
|  | 
 | ||||||
|  | ### 2. CommonFilterComponent | ||||||
|  | 
 | ||||||
|  | A draggable widget that provides the UI for: | ||||||
|  | - Adding/removing filters | ||||||
|  | - Configuring filter properties | ||||||
|  | - Setting filter values | ||||||
|  | - Managing presets | ||||||
|  | 
 | ||||||
|  | ### 3. Chart Components | ||||||
|  | 
 | ||||||
|  | Updated chart components that: | ||||||
|  | - Subscribe to filter changes | ||||||
|  | - Automatically refresh when filters change | ||||||
|  | - Include filter values in API calls | ||||||
|  | 
 | ||||||
|  | ## Implementation Details | ||||||
|  | 
 | ||||||
|  | ### Filter Types Supported | ||||||
|  | 
 | ||||||
|  | 1. **Text** - Simple text input | ||||||
|  | 2. **Dropdown** - Single selection from options | ||||||
|  | 3. **Multiselect** - Multiple selection from options | ||||||
|  | 4. **Date Range** - Start and end date selection | ||||||
|  | 5. **Toggle** - Boolean on/off switch | ||||||
|  | 
 | ||||||
|  | ### Data Flow | ||||||
|  | 
 | ||||||
|  | 1. User adds/configures filters in CommonFilterComponent | ||||||
|  | 2. FilterService stores filter definitions and values | ||||||
|  | 3. Chart components subscribe to FilterService.filterState$ | ||||||
|  | 4. When filters change, charts automatically refresh | ||||||
|  | 5. Filter values are included in API calls as query parameters | ||||||
|  | 
 | ||||||
|  | ### API Integration | ||||||
|  | 
 | ||||||
|  | Filters are passed to the backend as query parameters: | ||||||
|  | - Text filter: `name=John` | ||||||
|  | - Dropdown filter: `category=A` | ||||||
|  | - Multiselect filter: `tags=["tag1","tag2"]` | ||||||
|  | - Date range filter: `date_start=2023-01-01&date_end=2023-12-31` | ||||||
|  | - Toggle filter: `isActive=true` | ||||||
|  | 
 | ||||||
|  | ## Usage Examples | ||||||
|  | 
 | ||||||
|  | ### Adding the Common Filter Widget | ||||||
|  | 
 | ||||||
|  | 1. Drag the "Common Filter" widget from the component palette | ||||||
|  | 2. Place it at the top of the dashboard | ||||||
|  | 3. Configure filters as needed | ||||||
|  | 
 | ||||||
|  | ### Creating Filters | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // Add a text filter | ||||||
|  | const textFilter: Filter = { | ||||||
|  |   id: 'name', | ||||||
|  |   field: 'name', | ||||||
|  |   label: 'Name', | ||||||
|  |   type: 'text' | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Add a dropdown filter | ||||||
|  | const dropdownFilter: Filter = { | ||||||
|  |   id: 'category', | ||||||
|  |   field: 'category', | ||||||
|  |   label: 'Category', | ||||||
|  |   type: 'dropdown', | ||||||
|  |   options: ['A', 'B', 'C'] | ||||||
|  | }; | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Consuming Filters in Charts | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | // In chart component | ||||||
|  | constructor(private filterService: FilterService) {} | ||||||
|  | 
 | ||||||
|  | ngOnInit(): void { | ||||||
|  |   // Subscribe to filter changes | ||||||
|  |   this.subscriptions.push( | ||||||
|  |     this.filterService.filterState$.subscribe(filters => { | ||||||
|  |       this.fetchChartData(); | ||||||
|  |     }) | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fetchChartData(): void { | ||||||
|  |   // Get current filter values | ||||||
|  |   const filterValues = this.filterService.getFilterValues(); | ||||||
|  |    | ||||||
|  |   // Include in API call | ||||||
|  |   const queryParams = this.filterService.buildQueryParams(); | ||||||
|  |   // Use queryParams in API call | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## JSON Structures | ||||||
|  | 
 | ||||||
|  | ### Filter Definition | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "id": "filter_123456", | ||||||
|  |   "field": "category", | ||||||
|  |   "label": "Category", | ||||||
|  |   "type": "dropdown", | ||||||
|  |   "options": ["Electronics", "Clothing", "Books"], | ||||||
|  |   "value": "Electronics" | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Filter State | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "category": "Electronics", | ||||||
|  |   "price_range": { | ||||||
|  |     "start": 100, | ||||||
|  |     "end": 500 | ||||||
|  |   }, | ||||||
|  |   "in_stock": true, | ||||||
|  |   "tags": ["sale", "featured"] | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### API Response Format | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "chartLabels": ["Jan", "Feb", "Mar"], | ||||||
|  |   "chartData": [ | ||||||
|  |     { | ||||||
|  |       "data": [10, 20, 30], | ||||||
|  |       "label": "Sales" | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Cross-Filtering Support | ||||||
|  | 
 | ||||||
|  | Charts can also support cross-filtering where clicking on one chart updates the common filters: | ||||||
|  | 
 | ||||||
|  | 1. Implement click handlers in chart components | ||||||
|  | 2. Update filter values through FilterService | ||||||
|  | 3. All other charts automatically refresh | ||||||
|  | 
 | ||||||
|  | Example: | ||||||
|  | ```typescript | ||||||
|  | onChartClick(dataPoint: any): void { | ||||||
|  |   // Update a filter based on chart click | ||||||
|  |   this.filterService.updateFilterValue('category', dataPoint.category); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Preset Management | ||||||
|  | 
 | ||||||
|  | Users can save and load filter presets: | ||||||
|  | 
 | ||||||
|  | 1. Configure desired filters | ||||||
|  | 2. Enter a preset name and click "Save Preset" | ||||||
|  | 3. Select preset from dropdown to load | ||||||
|  | 4. Delete presets as needed | ||||||
|  | 
 | ||||||
|  | ## URL Synchronization | ||||||
|  | 
 | ||||||
|  | Filter states can be synchronized with URL query parameters for shareable dashboards: | ||||||
|  | 
 | ||||||
|  | 1. On filter change, update URL query parameters | ||||||
|  | 2. On page load, read filters from URL | ||||||
|  | 3. Apply filters to charts | ||||||
|  | 
 | ||||||
|  | ## Optional Enhancements | ||||||
|  | 
 | ||||||
|  | 1. **Real-time Updates** - WebSocket integration for live data | ||||||
|  | 2. **Advanced Filtering** - Custom filter expressions | ||||||
|  | 3. **Filter Dependencies** - Conditional filters based on other filter values | ||||||
|  | 4. **Analytics** - Track most used filters and combinations | ||||||
| @ -0,0 +1,23 @@ | |||||||
|  | .chart-wrapper { | ||||||
|  |   height: 100%; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |    | ||||||
|  |   .chart-header { | ||||||
|  |     padding: 10px 15px; | ||||||
|  |     background: #f8f8f8; | ||||||
|  |     border-bottom: 1px solid #eee; | ||||||
|  |      | ||||||
|  |     h5 { | ||||||
|  |       margin: 0; | ||||||
|  |       color: #333; | ||||||
|  |       font-size: 16px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .chart-container { | ||||||
|  |     flex: 1; | ||||||
|  |     padding: 15px; | ||||||
|  |     overflow: auto; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,39 @@ | |||||||
|  | import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||||
|  | import { ChartWrapperComponent } from './chart-wrapper.component'; | ||||||
|  | import { FilterService } from './filter.service'; | ||||||
|  | 
 | ||||||
|  | describe('ChartWrapperComponent', () => { | ||||||
|  |   let component: ChartWrapperComponent; | ||||||
|  |   let fixture: ComponentFixture<ChartWrapperComponent>; | ||||||
|  |   let filterService: FilterService; | ||||||
|  | 
 | ||||||
|  |   beforeEach(async () => { | ||||||
|  |     await TestBed.configureTestingModule({ | ||||||
|  |       declarations: [ChartWrapperComponent], | ||||||
|  |       providers: [FilterService] | ||||||
|  |     }) | ||||||
|  |     .compileComponents(); | ||||||
|  |      | ||||||
|  |     fixture = TestBed.createComponent(ChartWrapperComponent); | ||||||
|  |     component = fixture.componentInstance; | ||||||
|  |     filterService = TestBed.inject(FilterService); | ||||||
|  |     fixture.detectChanges(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should create', () => { | ||||||
|  |     expect(component).toBeTruthy(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should subscribe to filter changes on init', () => { | ||||||
|  |     spyOn(filterService.filterState$, 'subscribe'); | ||||||
|  |     component.ngOnInit(); | ||||||
|  |     expect(filterService.filterState$.subscribe).toHaveBeenCalled(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should unsubscribe from filter changes on destroy', () => { | ||||||
|  |     component.ngOnInit(); | ||||||
|  |     spyOn(component['filterSubscription']!, 'unsubscribe'); | ||||||
|  |     component.ngOnDestroy(); | ||||||
|  |     expect(component['filterSubscription']!.unsubscribe).toHaveBeenCalled(); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -0,0 +1,90 @@ | |||||||
|  | import { Component, Input, OnInit, OnDestroy, ComponentRef, ViewChild, ViewContainerRef } from '@angular/core'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
|  | import { FilterService } from './filter.service'; | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-chart-wrapper', | ||||||
|  |   template: ` | ||||||
|  |     <div class="chart-wrapper"> | ||||||
|  |       <div class="chart-header" *ngIf="chartTitle"> | ||||||
|  |         <h5>{{ chartTitle }}</h5> | ||||||
|  |       </div> | ||||||
|  |       <div class="chart-container"> | ||||||
|  |         <ng-container #chartContainer></ng-container> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   `,
 | ||||||
|  |   styleUrls: ['./chart-wrapper.component.scss'] | ||||||
|  | }) | ||||||
|  | export class ChartWrapperComponent implements OnInit, OnDestroy { | ||||||
|  |   @Input() chartComponent: any; | ||||||
|  |   @Input() chartInputs: any = {}; | ||||||
|  |   @Input() chartTitle: string = ''; | ||||||
|  | 
 | ||||||
|  |   @ViewChild('chartContainer', { read: ViewContainerRef }) chartContainer!: ViewContainerRef; | ||||||
|  | 
 | ||||||
|  |   private componentRef: ComponentRef<any> | null = null; | ||||||
|  |   private filterSubscription: Subscription | null = null; | ||||||
|  | 
 | ||||||
|  |   constructor(private filterService: FilterService) { } | ||||||
|  | 
 | ||||||
|  |   ngOnInit(): void { | ||||||
|  |     this.loadChartComponent(); | ||||||
|  |     this.subscribeToFilters(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ngOnDestroy(): void { | ||||||
|  |     if (this.filterSubscription) { | ||||||
|  |       this.filterSubscription.unsubscribe(); | ||||||
|  |     } | ||||||
|  |     if (this.componentRef) { | ||||||
|  |       this.componentRef.destroy(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private loadChartComponent(): void { | ||||||
|  |     if (this.chartContainer && this.chartComponent) { | ||||||
|  |       this.chartContainer.clear(); | ||||||
|  |       const factory = this.chartContainer.createComponent(this.chartComponent); | ||||||
|  |       this.componentRef = factory; | ||||||
|  |        | ||||||
|  |       // Set initial inputs
 | ||||||
|  |       Object.keys(this.chartInputs).forEach(key => { | ||||||
|  |         factory.instance[key] = this.chartInputs[key]; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private subscribeToFilters(): void { | ||||||
|  |     this.filterSubscription = this.filterService.filterState$.subscribe(filterValues => { | ||||||
|  |       this.updateChartWithFilters(filterValues); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private updateChartWithFilters(filterValues: any): void { | ||||||
|  |     if (this.componentRef) { | ||||||
|  |       // Add filter values to chart inputs
 | ||||||
|  |       const updatedInputs = { | ||||||
|  |         ...this.chartInputs, | ||||||
|  |         filterValues: filterValues, | ||||||
|  |         // Pass the query params string for easy API integration
 | ||||||
|  |         filterQueryParams: this.filterService.buildQueryParams() | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       // Update chart component inputs
 | ||||||
|  |       Object.keys(updatedInputs).forEach(key => { | ||||||
|  |         this.componentRef!.instance[key] = updatedInputs[key]; | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       // Trigger change detection if the component has a method for it
 | ||||||
|  |       if (this.componentRef!.instance.ngOnChanges) { | ||||||
|  |         // We can't easily trigger ngOnChanges manually, but the input update should trigger it
 | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // If the chart component has a method to refresh data, call it
 | ||||||
|  |       if (this.componentRef!.instance.refreshData) { | ||||||
|  |         this.componentRef!.instance.refreshData(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,128 @@ | |||||||
|  | <div class="common-filter-container"> | ||||||
|  |   <!-- Filter Header --> | ||||||
|  |   <div class="filter-header"> | ||||||
|  |     <h4>Common Filters</h4> | ||||||
|  |     <button class="btn btn-sm btn-primary" (click)="addFilter()"> | ||||||
|  |       <clr-icon shape="plus"></clr-icon> Add Filter | ||||||
|  |     </button> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  |   <!-- Presets Section --> | ||||||
|  |   <div class="presets-section" *ngIf="presets.length > 0"> | ||||||
|  |     <div class="preset-controls"> | ||||||
|  |       <select [(ngModel)]="activePreset" (change)="loadPreset(activePreset || '')" class="clr-select"> | ||||||
|  |         <option value="">Select Preset</option> | ||||||
|  |         <option *ngFor="let preset of presets" [value]="preset">{{ preset }}</option> | ||||||
|  |       </select> | ||||||
|  |       <button class="btn btn-sm btn-danger" (click)="resetFilters()"> | ||||||
|  |         <clr-icon shape="undo"></clr-icon> Reset | ||||||
|  |       </button> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  |   <!-- Save Preset Section --> | ||||||
|  |   <div class="save-preset-section"> | ||||||
|  |     <div class="clr-input-group"> | ||||||
|  |       <input type="text" [(ngModel)]="newPresetName" placeholder="Preset name" class="clr-input"> | ||||||
|  |       <div class="clr-input-group-btn"> | ||||||
|  |         <button class="btn btn-sm btn-success" (click)="savePreset()" [disabled]="!newPresetName.trim()"> | ||||||
|  |           <clr-icon shape="floppy"></clr-icon> Save Preset | ||||||
|  |         </button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  |   <!-- Filters Form --> | ||||||
|  |   <form [formGroup]="filterForm" class="filters-form"> | ||||||
|  |     <div class="filters-grid"> | ||||||
|  |       <div *ngFor="let filter of filters" class="filter-item"> | ||||||
|  |         <div class="filter-header"> | ||||||
|  |           <span class="filter-label">{{ filter.label }}</span> | ||||||
|  |           <button class="btn btn-icon btn-danger btn-sm" (click)="removeFilter(filter.id)"> | ||||||
|  |             <clr-icon shape="trash"></clr-icon> | ||||||
|  |           </button> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <!-- Filter Type Selector --> | ||||||
|  |         <div class="clr-form-control"> | ||||||
|  |           <select [(ngModel)]="filter.type" (ngModelChange)="updateFilter(filter.id, 'type', $event)"  | ||||||
|  |                   [ngModelOptions]="{standalone: true}" class="clr-select filter-type-select"> | ||||||
|  |             <option value="text">Text</option> | ||||||
|  |             <option value="dropdown">Dropdown</option> | ||||||
|  |             <option value="multiselect">Multi-Select</option> | ||||||
|  |             <option value="date-range">Date Range</option> | ||||||
|  |             <option value="toggle">Toggle</option> | ||||||
|  |           </select> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <!-- Filter Label Input --> | ||||||
|  |         <div class="clr-form-control"> | ||||||
|  |           <input type="text" [(ngModel)]="filter.label" (ngModelChange)="updateFilter(filter.id, 'label', $event)"  | ||||||
|  |                  [ngModelOptions]="{standalone: true}" placeholder="Filter Label" class="clr-input"> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <!-- Filter Field Input --> | ||||||
|  |         <div class="clr-form-control"> | ||||||
|  |           <input type="text" [(ngModel)]="filter.field" (ngModelChange)="updateFilter(filter.id, 'field', $event)"  | ||||||
|  |                  [ngModelOptions]="{standalone: true}" placeholder="Field Name" class="clr-input"> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <!-- Filter Options (for dropdown and multiselect) --> | ||||||
|  |         <div class="clr-form-control" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'"> | ||||||
|  |           <textarea [(ngModel)]="filter.options" (ngModelChange)="updateFilter(filter.id, 'options', $event ? $event.split(',') : [])"  | ||||||
|  |                     [ngModelOptions]="{standalone: true}" placeholder="Options (comma separated)" class="clr-textarea"></textarea> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <!-- Filter Controls Based on Type --> | ||||||
|  |         <div class="filter-control" [ngSwitch]="filter.type"> | ||||||
|  |           <!-- Text Filter --> | ||||||
|  |           <div *ngSwitchCase="'text'"> | ||||||
|  |             <input type="text" [formControlName]="filter.id" placeholder="Enter text" class="clr-input"> | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           <!-- Dropdown Filter --> | ||||||
|  |           <div *ngSwitchCase="'dropdown'"> | ||||||
|  |             <select [formControlName]="filter.id" class="clr-select"> | ||||||
|  |               <option value="">Select an option</option> | ||||||
|  |               <option *ngFor="let option of filter.options" [value]="option">{{ option }}</option> | ||||||
|  |             </select> | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           <!-- Multi-Select Filter --> | ||||||
|  |           <div *ngSwitchCase="'multiselect'"> | ||||||
|  |             <select [formControlName]="filter.id" multiple class="clr-select multiselect"> | ||||||
|  |               <option *ngFor="let option of filter.options" [value]="option">{{ option }}</option> | ||||||
|  |             </select> | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           <!-- Date Range Filter --> | ||||||
|  |           <div *ngSwitchCase="'date-range'" class="date-range-controls"> | ||||||
|  |             <div class="clr-form-control"> | ||||||
|  |               <label>Start Date</label> | ||||||
|  |               <input type="date" [ngModel]="filterForm.get(filter.id + '.start')?.value"  | ||||||
|  |                      (ngModelChange)="onDateRangeChange(filter.id, { start: $event, end: filterForm.get(filter.id + '.end')?.value })"  | ||||||
|  |                      [ngModelOptions]="{standalone: true}" class="clr-input"> | ||||||
|  |             </div> | ||||||
|  |             <div class="clr-form-control"> | ||||||
|  |               <label>End Date</label> | ||||||
|  |               <input type="date" [ngModel]="filterForm.get(filter.id + '.end')?.value"  | ||||||
|  |                      (ngModelChange)="onDateRangeChange(filter.id, { start: filterForm.get(filter.id + '.start')?.value, end: $event })"  | ||||||
|  |                      [ngModelOptions]="{standalone: true}" class="clr-input"> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           <!-- Toggle Filter --> | ||||||
|  |           <div *ngSwitchCase="'toggle'" class="toggle-control"> | ||||||
|  |             <input type="checkbox" [formControlName]="filter.id" clrToggle class="clr-toggle"> | ||||||
|  |             <label>{{ filter.label }}</label> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </form> | ||||||
|  | 
 | ||||||
|  |   <!-- No Filters Message --> | ||||||
|  |   <div class="no-filters" *ngIf="filters.length === 0"> | ||||||
|  |     <p>No filters added yet. Click "Add Filter" to create your first filter.</p> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
| @ -0,0 +1,103 @@ | |||||||
|  | .common-filter-container { | ||||||
|  |   padding: 15px; | ||||||
|  |   background: #f5f5f5; | ||||||
|  |   border: 1px solid #ddd; | ||||||
|  |   border-radius: 4px; | ||||||
|  |   margin-bottom: 20px; | ||||||
|  | 
 | ||||||
|  |   .filter-header { | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: space-between; | ||||||
|  |     align-items: center; | ||||||
|  |     margin-bottom: 15px; | ||||||
|  | 
 | ||||||
|  |     h4 { | ||||||
|  |       margin: 0; | ||||||
|  |       color: #333; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .presets-section { | ||||||
|  |     margin-bottom: 15px; | ||||||
|  |      | ||||||
|  |     .preset-controls { | ||||||
|  |       display: flex; | ||||||
|  |       gap: 10px; | ||||||
|  |       align-items: center; | ||||||
|  |        | ||||||
|  |       select { | ||||||
|  |         flex: 1; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .save-preset-section { | ||||||
|  |     margin-bottom: 15px; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .filters-form { | ||||||
|  |     .filters-grid { | ||||||
|  |       display: grid; | ||||||
|  |       grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | ||||||
|  |       gap: 15px; | ||||||
|  |        | ||||||
|  |       .filter-item { | ||||||
|  |         background: white; | ||||||
|  |         border: 1px solid #e0e0e0; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         padding: 15px; | ||||||
|  |          | ||||||
|  |         .filter-header { | ||||||
|  |           display: flex; | ||||||
|  |           justify-content: space-between; | ||||||
|  |           align-items: center; | ||||||
|  |           margin-bottom: 10px; | ||||||
|  |            | ||||||
|  |           .filter-label { | ||||||
|  |             font-weight: bold; | ||||||
|  |             color: #333; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .filter-type-select { | ||||||
|  |           width: 100%; | ||||||
|  |           margin-bottom: 10px; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .clr-form-control { | ||||||
|  |           margin-bottom: 10px; | ||||||
|  |            | ||||||
|  |           input, select, textarea { | ||||||
|  |             width: 100%; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .date-range-controls { | ||||||
|  |           display: flex; | ||||||
|  |           gap: 10px; | ||||||
|  |            | ||||||
|  |           .clr-form-control { | ||||||
|  |             flex: 1; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .toggle-control { | ||||||
|  |           display: flex; | ||||||
|  |           align-items: center; | ||||||
|  |           gap: 8px; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .multiselect { | ||||||
|  |           height: 100px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .no-filters { | ||||||
|  |     text-align: center; | ||||||
|  |     padding: 20px; | ||||||
|  |     color: #666; | ||||||
|  |     font-style: italic; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,89 @@ | |||||||
|  | import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||||
|  | import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; | ||||||
|  | import { CommonFilterComponent } from './common-filter.component'; | ||||||
|  | import { FilterService } from './filter.service'; | ||||||
|  | 
 | ||||||
|  | describe('CommonFilterComponent', () => { | ||||||
|  |   let component: CommonFilterComponent; | ||||||
|  |   let fixture: ComponentFixture<CommonFilterComponent>; | ||||||
|  |   let filterService: FilterService; | ||||||
|  | 
 | ||||||
|  |   beforeEach(async () => { | ||||||
|  |     await TestBed.configureTestingModule({ | ||||||
|  |       imports: [FormsModule, ReactiveFormsModule], | ||||||
|  |       declarations: [CommonFilterComponent], | ||||||
|  |       providers: [FormBuilder, FilterService] | ||||||
|  |     }) | ||||||
|  |     .compileComponents(); | ||||||
|  |      | ||||||
|  |     fixture = TestBed.createComponent(CommonFilterComponent); | ||||||
|  |     component = fixture.componentInstance; | ||||||
|  |     filterService = TestBed.inject(FilterService); | ||||||
|  |     fixture.detectChanges(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should create', () => { | ||||||
|  |     expect(component).toBeTruthy(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should add a new filter', () => { | ||||||
|  |     const initialFilters = filterService.getFilters().length; | ||||||
|  |     component.addFilter(); | ||||||
|  |     const updatedFilters = filterService.getFilters().length; | ||||||
|  |     expect(updatedFilters).toBe(initialFilters + 1); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should remove a filter', () => { | ||||||
|  |     // Add a filter first
 | ||||||
|  |     component.addFilter(); | ||||||
|  |     const filterId = filterService.getFilters()[0].id; | ||||||
|  |      | ||||||
|  |     // Then remove it
 | ||||||
|  |     const initialFilters = filterService.getFilters().length; | ||||||
|  |     component.removeFilter(filterId); | ||||||
|  |     const updatedFilters = filterService.getFilters().length; | ||||||
|  |     expect(updatedFilters).toBe(initialFilters - 1); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should update filter properties', () => { | ||||||
|  |     // Add a filter
 | ||||||
|  |     component.addFilter(); | ||||||
|  |     const filter = filterService.getFilters()[0]; | ||||||
|  |      | ||||||
|  |     // Update the filter label
 | ||||||
|  |     component.updateFilter(filter.id, 'label', 'Updated Label'); | ||||||
|  |     const updatedFilters = filterService.getFilters(); | ||||||
|  |     expect(updatedFilters[0].label).toBe('Updated Label'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should handle filter value changes', () => { | ||||||
|  |     const filterId = 'test-filter'; | ||||||
|  |     const testValue = 'test value'; | ||||||
|  |      | ||||||
|  |     // Mock the filter service method
 | ||||||
|  |     spyOn(filterService, 'updateFilterValue'); | ||||||
|  |      | ||||||
|  |     component.onFilterChange(filterId, testValue); | ||||||
|  |     expect(filterService.updateFilterValue).toHaveBeenCalledWith(filterId, testValue); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should reset filters', () => { | ||||||
|  |     spyOn(filterService, 'resetFilters'); | ||||||
|  |     component.resetFilters(); | ||||||
|  |     expect(filterService.resetFilters).toHaveBeenCalled(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should save and load presets', () => { | ||||||
|  |     spyOn(filterService, 'savePreset'); | ||||||
|  |     spyOn(filterService, 'loadPreset'); | ||||||
|  |      | ||||||
|  |     // Test save preset
 | ||||||
|  |     component.newPresetName = 'Test Preset'; | ||||||
|  |     component.savePreset(); | ||||||
|  |     expect(filterService.savePreset).toHaveBeenCalledWith('Test Preset'); | ||||||
|  |      | ||||||
|  |     // Test load preset
 | ||||||
|  |     component.loadPreset('Test Preset'); | ||||||
|  |     expect(filterService.loadPreset).toHaveBeenCalledWith('Test Preset'); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -0,0 +1,173 @@ | |||||||
|  | import { Component, OnInit, OnDestroy } from '@angular/core'; | ||||||
|  | import { FormBuilder, FormGroup } from '@angular/forms'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
|  | import { Filter, FilterService, FilterType } from './filter.service'; | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-common-filter', | ||||||
|  |   templateUrl: './common-filter.component.html', | ||||||
|  |   styleUrls: ['./common-filter.component.scss'] | ||||||
|  | }) | ||||||
|  | export class CommonFilterComponent implements OnInit, OnDestroy { | ||||||
|  |   filters: Filter[] = []; | ||||||
|  |   filterForm: FormGroup; | ||||||
|  |   presets: string[] = []; | ||||||
|  |   activePreset: string | null = null; | ||||||
|  |   newPresetName: string = ''; | ||||||
|  |    | ||||||
|  |   private subscriptions: Subscription[] = []; | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     private filterService: FilterService, | ||||||
|  |     private fb: FormBuilder | ||||||
|  |   ) { | ||||||
|  |     this.filterForm = this.fb.group({}); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ngOnInit(): void { | ||||||
|  |     // Subscribe to filter definitions
 | ||||||
|  |     this.subscriptions.push( | ||||||
|  |       this.filterService.filters$.subscribe(filters => { | ||||||
|  |         this.filters = filters; | ||||||
|  |         this.buildForm(); | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |      | ||||||
|  |     // Subscribe to filter state changes
 | ||||||
|  |     this.subscriptions.push( | ||||||
|  |       this.filterService.filterState$.subscribe(state => { | ||||||
|  |         this.updateFormValues(state); | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |      | ||||||
|  |     // Subscribe to preset changes
 | ||||||
|  |     this.subscriptions.push( | ||||||
|  |       this.filterService.activePreset$.subscribe(preset => { | ||||||
|  |         this.activePreset = preset; | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |      | ||||||
|  |     // Get initial presets
 | ||||||
|  |     this.presets = this.filterService.getPresets(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ngOnDestroy(): void { | ||||||
|  |     this.subscriptions.forEach(sub => sub.unsubscribe()); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Build the form based on current filters
 | ||||||
|  |   private buildForm(): void { | ||||||
|  |     // Clear existing form controls
 | ||||||
|  |     Object.keys(this.filterForm.controls).forEach(key => { | ||||||
|  |       this.filterForm.removeControl(key); | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     // Add controls for each filter
 | ||||||
|  |     this.filters.forEach(filter => { | ||||||
|  |       let initialValue: any; | ||||||
|  |        | ||||||
|  |       switch (filter.type) { | ||||||
|  |         case 'multiselect': | ||||||
|  |           initialValue = filter.value || []; | ||||||
|  |           break; | ||||||
|  |         case 'date-range': | ||||||
|  |           initialValue = filter.value || { start: null, end: null }; | ||||||
|  |           break; | ||||||
|  |         case 'toggle': | ||||||
|  |           initialValue = filter.value || false; | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           initialValue = filter.value || ''; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       const control = this.fb.control(initialValue); | ||||||
|  |        | ||||||
|  |       // Subscribe to value changes for this control
 | ||||||
|  |       control.valueChanges.subscribe(value => { | ||||||
|  |         this.onFilterChange(filter.id, value); | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       this.filterForm.addControl(filter.id, control); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Update form values based on filter state
 | ||||||
|  |   private updateFormValues(state: any): void { | ||||||
|  |     Object.keys(state).forEach(key => { | ||||||
|  |       if (this.filterForm.contains(key)) { | ||||||
|  |         this.filterForm.get(key)?.setValue(state[key], { emitEvent: false }); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Handle filter value changes
 | ||||||
|  |   onFilterChange(filterId: string, value: any): void { | ||||||
|  |     this.filterService.updateFilterValue(filterId, value); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Handle multiselect changes
 | ||||||
|  |   onMultiselectChange(filterId: string, selectedValues: string[]): void { | ||||||
|  |     this.filterService.updateFilterValue(filterId, selectedValues); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Handle date range changes
 | ||||||
|  |   onDateRangeChange(filterId: string, dateRange: { start: string | null, end: string | null }): void { | ||||||
|  |     this.filterService.updateFilterValue(filterId, dateRange); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Handle toggle changes
 | ||||||
|  |   onToggleChange(filterId: string, checked: boolean): void { | ||||||
|  |     this.filterService.updateFilterValue(filterId, checked); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Reset all filters
 | ||||||
|  |   resetFilters(): void { | ||||||
|  |     this.filterService.resetFilters(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Save current filter state as preset
 | ||||||
|  |   savePreset(): void { | ||||||
|  |     if (this.newPresetName.trim()) { | ||||||
|  |       this.filterService.savePreset(this.newPresetName.trim()); | ||||||
|  |       this.presets = this.filterService.getPresets(); | ||||||
|  |       this.newPresetName = ''; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Load a preset
 | ||||||
|  |   loadPreset(presetName: string): void { | ||||||
|  |     this.filterService.loadPreset(presetName); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Delete a preset
 | ||||||
|  |   deletePreset(presetName: string): void { | ||||||
|  |     this.filterService.deletePreset(presetName); | ||||||
|  |     this.presets = this.filterService.getPresets(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Add a new filter
 | ||||||
|  |   addFilter(): void { | ||||||
|  |     const newFilter: Filter = { | ||||||
|  |       id: `filter_${Date.now()}`, | ||||||
|  |       field: '', | ||||||
|  |       label: 'New Filter', | ||||||
|  |       type: 'text' | ||||||
|  |     }; | ||||||
|  |     this.filterService.addFilter(newFilter); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Remove a filter
 | ||||||
|  |   removeFilter(filterId: string): void { | ||||||
|  |     this.filterService.removeFilter(filterId); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Update filter properties
 | ||||||
|  |   updateFilter(filterId: string, property: string, value: any): void { | ||||||
|  |     const filterIndex = this.filters.findIndex(f => f.id === filterId); | ||||||
|  |     if (filterIndex !== -1) { | ||||||
|  |       const updatedFilters = [...this.filters]; | ||||||
|  |       (updatedFilters[filterIndex] as any)[property] = value; | ||||||
|  |       this.filterService.setFilters(updatedFilters); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,129 @@ | |||||||
|  | import { TestBed } from '@angular/core/testing'; | ||||||
|  | import { FilterService, Filter } from './filter.service'; | ||||||
|  | 
 | ||||||
|  | describe('FilterService', () => { | ||||||
|  |   let service: FilterService; | ||||||
|  | 
 | ||||||
|  |   beforeEach(() => { | ||||||
|  |     TestBed.configureTestingModule({}); | ||||||
|  |     service = TestBed.inject(FilterService); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should be created', () => { | ||||||
|  |     expect(service).toBeTruthy(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should add and remove filters', () => { | ||||||
|  |     const filter: Filter = { | ||||||
|  |       id: 'test-filter', | ||||||
|  |       field: 'testField', | ||||||
|  |       label: 'Test Field', | ||||||
|  |       type: 'text' | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // Add filter
 | ||||||
|  |     service.addFilter(filter); | ||||||
|  |     const filters = service.getFilters(); | ||||||
|  |     expect(filters.length).toBe(1); | ||||||
|  |     expect(filters[0]).toEqual(filter); | ||||||
|  | 
 | ||||||
|  |     // Remove filter
 | ||||||
|  |     service.removeFilter('test-filter'); | ||||||
|  |     const updatedFilters = service.getFilters(); | ||||||
|  |     expect(updatedFilters.length).toBe(0); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should update filter values', () => { | ||||||
|  |     const filter: Filter = { | ||||||
|  |       id: 'name-filter', | ||||||
|  |       field: 'name', | ||||||
|  |       label: 'Name', | ||||||
|  |       type: 'text' | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     service.addFilter(filter); | ||||||
|  |     service.updateFilterValue('name-filter', 'John Doe'); | ||||||
|  | 
 | ||||||
|  |     const filterValues = service.getFilterValues(); | ||||||
|  |     expect(filterValues['name-filter']).toBe('John Doe'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should reset filters', () => { | ||||||
|  |     const textFilter: Filter = { | ||||||
|  |       id: 'name', | ||||||
|  |       field: 'name', | ||||||
|  |       label: 'Name', | ||||||
|  |       type: 'text', | ||||||
|  |       value: 'John' | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const toggleFilter: Filter = { | ||||||
|  |       id: 'active', | ||||||
|  |       field: 'isActive', | ||||||
|  |       label: 'Active', | ||||||
|  |       type: 'toggle', | ||||||
|  |       value: true | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     service.setFilters([textFilter, toggleFilter]); | ||||||
|  |     service.resetFilters(); | ||||||
|  | 
 | ||||||
|  |     const filterValues = service.getFilterValues(); | ||||||
|  |     expect(filterValues['name']).toBe(''); | ||||||
|  |     expect(filterValues['active']).toBe(false); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should manage presets', () => { | ||||||
|  |     const filter: Filter = { | ||||||
|  |       id: 'category', | ||||||
|  |       field: 'category', | ||||||
|  |       label: 'Category', | ||||||
|  |       type: 'dropdown', | ||||||
|  |       value: 'Electronics' | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     service.addFilter(filter); | ||||||
|  |      | ||||||
|  |     // Save preset
 | ||||||
|  |     service.savePreset('Electronics View'); | ||||||
|  |     const presets = service.getPresets(); | ||||||
|  |     expect(presets).toContain('Electronics View'); | ||||||
|  | 
 | ||||||
|  |     // Update filter and load preset
 | ||||||
|  |     service.updateFilterValue('category', 'Clothing'); | ||||||
|  |     service.loadPreset('Electronics View'); | ||||||
|  |      | ||||||
|  |     const filterValues = service.getFilterValues(); | ||||||
|  |     expect(filterValues['category']).toBe('Electronics'); | ||||||
|  | 
 | ||||||
|  |     // Delete preset
 | ||||||
|  |     service.deletePreset('Electronics View'); | ||||||
|  |     const updatedPresets = service.getPresets(); | ||||||
|  |     expect(updatedPresets).not.toContain('Electronics View'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should build query parameters', () => { | ||||||
|  |     const textFilter: Filter = { | ||||||
|  |       id: 'name', | ||||||
|  |       field: 'name', | ||||||
|  |       label: 'Name', | ||||||
|  |       type: 'text' | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const dateFilter: Filter = { | ||||||
|  |       id: 'dateRange', | ||||||
|  |       field: 'date', | ||||||
|  |       label: 'Date Range', | ||||||
|  |       type: 'date-range' | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     service.setFilters([textFilter, dateFilter]); | ||||||
|  |     service.updateFilterValue('name', 'John Doe'); | ||||||
|  |     service.updateFilterValue('dateRange', { start: '2023-01-01', end: '2023-12-31' }); | ||||||
|  | 
 | ||||||
|  |     const queryParams = service.buildQueryParams(); | ||||||
|  |     expect(queryParams).toContain('name=John%20Doe'); | ||||||
|  |     expect(queryParams).toContain('dateRange_start=2023-01-01'); | ||||||
|  |     expect(queryParams).toContain('dateRange_end=2023-12-31'); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -0,0 +1,191 @@ | |||||||
|  | import { Injectable } from '@angular/core'; | ||||||
|  | import { BehaviorSubject, Observable } from 'rxjs'; | ||||||
|  | 
 | ||||||
|  | // Define the filter types
 | ||||||
|  | export type FilterType = 'dropdown' | 'multiselect' | 'date-range' | 'text' | 'toggle'; | ||||||
|  | 
 | ||||||
|  | // Define the filter interface
 | ||||||
|  | export interface Filter { | ||||||
|  |   id: string; | ||||||
|  |   field: string; | ||||||
|  |   label: string; | ||||||
|  |   type: FilterType; | ||||||
|  |   options?: string[]; // For dropdown and multiselect
 | ||||||
|  |   value?: any; // Current value
 | ||||||
|  |   placeholder?: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Define the filter state
 | ||||||
|  | export interface FilterState { | ||||||
|  |   [key: string]: any; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Injectable({ | ||||||
|  |   providedIn: 'root' | ||||||
|  | }) | ||||||
|  | export class FilterService { | ||||||
|  |   // Store the filter definitions
 | ||||||
|  |   private filtersSubject = new BehaviorSubject<Filter[]>([]); | ||||||
|  |   public filters$ = this.filtersSubject.asObservable(); | ||||||
|  | 
 | ||||||
|  |   // Store the current filter values
 | ||||||
|  |   private filterStateSubject = new BehaviorSubject<FilterState>({}); | ||||||
|  |   public filterState$ = this.filterStateSubject.asObservable(); | ||||||
|  | 
 | ||||||
|  |   // Store the active filter presets
 | ||||||
|  |   private activePresetSubject = new BehaviorSubject<string | null>(null); | ||||||
|  |   public activePreset$ = this.activePresetSubject.asObservable(); | ||||||
|  | 
 | ||||||
|  |   // Store filter presets
 | ||||||
|  |   private presets: { [key: string]: FilterState } = {}; | ||||||
|  | 
 | ||||||
|  |   constructor() { } | ||||||
|  | 
 | ||||||
|  |   // Add a new filter
 | ||||||
|  |   addFilter(filter: Filter): void { | ||||||
|  |     const currentFilters = this.filtersSubject.value; | ||||||
|  |     this.filtersSubject.next([...currentFilters, filter]); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Remove a filter
 | ||||||
|  |   removeFilter(filterId: string): void { | ||||||
|  |     const currentFilters = this.filtersSubject.value; | ||||||
|  |     const updatedFilters = currentFilters.filter(f => f.id !== filterId); | ||||||
|  |     this.filtersSubject.next(updatedFilters); | ||||||
|  |      | ||||||
|  |     // Also remove the filter value from state
 | ||||||
|  |     const currentState = this.filterStateSubject.value; | ||||||
|  |     const newState = { ...currentState }; | ||||||
|  |     delete newState[filterId]; | ||||||
|  |     this.filterStateSubject.next(newState); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Update filter value
 | ||||||
|  |   updateFilterValue(filterId: string, value: any): void { | ||||||
|  |     const currentState = this.filterStateSubject.value; | ||||||
|  |     this.filterStateSubject.next({ | ||||||
|  |       ...currentState, | ||||||
|  |       [filterId]: value | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Get current filter values
 | ||||||
|  |   getFilterValues(): FilterState { | ||||||
|  |     return this.filterStateSubject.value; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Reset all filters
 | ||||||
|  |   resetFilters(): void { | ||||||
|  |     const currentFilters = this.filtersSubject.value; | ||||||
|  |     const resetState: FilterState = {}; | ||||||
|  |      | ||||||
|  |     // Initialize all filters with empty/default values
 | ||||||
|  |     currentFilters.forEach(filter => { | ||||||
|  |       switch (filter.type) { | ||||||
|  |         case 'multiselect': | ||||||
|  |           resetState[filter.id] = []; | ||||||
|  |           break; | ||||||
|  |         case 'date-range': | ||||||
|  |           resetState[filter.id] = { start: null, end: null }; | ||||||
|  |           break; | ||||||
|  |         case 'toggle': | ||||||
|  |           resetState[filter.id] = false; | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           resetState[filter.id] = ''; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     this.filterStateSubject.next(resetState); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Save current filter state as a preset
 | ||||||
|  |   savePreset(name: string): void { | ||||||
|  |     this.presets[name] = this.filterStateSubject.value; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Load a preset
 | ||||||
|  |   loadPreset(name: string): void { | ||||||
|  |     if (this.presets[name]) { | ||||||
|  |       this.filterStateSubject.next(this.presets[name]); | ||||||
|  |       this.activePresetSubject.next(name); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Get all presets
 | ||||||
|  |   getPresets(): string[] { | ||||||
|  |     return Object.keys(this.presets); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Delete a preset
 | ||||||
|  |   deletePreset(name: string): void { | ||||||
|  |     delete this.presets[name]; | ||||||
|  |     if (this.activePresetSubject.value === name) { | ||||||
|  |       this.activePresetSubject.next(null); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Clear all presets
 | ||||||
|  |   clearPresets(): void { | ||||||
|  |     this.presets = {}; | ||||||
|  |     this.activePresetSubject.next(null); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Build query parameters for API calls
 | ||||||
|  |   buildQueryParams(): string { | ||||||
|  |     const filterValues = this.getFilterValues(); | ||||||
|  |     const params = new URLSearchParams(); | ||||||
|  |      | ||||||
|  |     Object.keys(filterValues).forEach(key => { | ||||||
|  |       const value = filterValues[key]; | ||||||
|  |       if (value !== undefined && value !== null && value !== '') { | ||||||
|  |         if (typeof value === 'object') { | ||||||
|  |           // Handle date ranges and other objects
 | ||||||
|  |           if (value.hasOwnProperty('start') && value.hasOwnProperty('end')) { | ||||||
|  |             // Date range
 | ||||||
|  |             if (value.start) params.append(`${key}_start`, value.start); | ||||||
|  |             if (value.end) params.append(`${key}_end`, value.end); | ||||||
|  |           } else { | ||||||
|  |             // Other objects as JSON
 | ||||||
|  |             params.append(key, JSON.stringify(value)); | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           // Simple values
 | ||||||
|  |           params.append(key, value.toString()); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     return params.toString(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Get filter definitions
 | ||||||
|  |   getFilters(): Filter[] { | ||||||
|  |     return this.filtersSubject.value; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Update filter definitions
 | ||||||
|  |   setFilters(filters: Filter[]): void { | ||||||
|  |     this.filtersSubject.next(filters); | ||||||
|  |      | ||||||
|  |     // Initialize filter state with default values
 | ||||||
|  |     const initialState: FilterState = {}; | ||||||
|  |     filters.forEach(filter => { | ||||||
|  |       switch (filter.type) { | ||||||
|  |         case 'multiselect': | ||||||
|  |           initialState[filter.id] = filter.value || []; | ||||||
|  |           break; | ||||||
|  |         case 'date-range': | ||||||
|  |           initialState[filter.id] = filter.value || { start: null, end: null }; | ||||||
|  |           break; | ||||||
|  |         case 'toggle': | ||||||
|  |           initialState[filter.id] = filter.value || false; | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           initialState[filter.id] = filter.value || ''; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     this.filterStateSubject.next(initialState); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,3 @@ | |||||||
|  | export * from './filter.service'; | ||||||
|  | export * from './common-filter.component'; | ||||||
|  | export * from './chart-wrapper.component'; | ||||||
| @ -22,6 +22,8 @@ import { AlertsService } from 'src/app/services/fnd/alerts.service'; | |||||||
| import { isArray } from 'highcharts'; | import { isArray } from 'highcharts'; | ||||||
| // Add the SureconnectService import
 | // Add the SureconnectService import
 | ||||||
| import { SureconnectService } from '../sureconnect/sureconnect.service'; | import { SureconnectService } from '../sureconnect/sureconnect.service'; | ||||||
|  | // Add the CommonFilterComponent import
 | ||||||
|  | import { CommonFilterComponent } from '../common-filter/common-filter.component'; | ||||||
| 
 | 
 | ||||||
| function isNullArray(arr) { | function isNullArray(arr) { | ||||||
|   return !Array.isArray(arr) || arr.length === 0; |   return !Array.isArray(arr) || arr.length === 0; | ||||||
| @ -45,6 +47,10 @@ export class EditnewdashComponent implements OnInit { | |||||||
|   public commonFilterForm: FormGroup; // Add common filter form
 |   public commonFilterForm: FormGroup; // Add common filter form
 | ||||||
| 
 | 
 | ||||||
|   WidgetsMock: WidgetModel[] = [ |   WidgetsMock: WidgetModel[] = [ | ||||||
|  |     { | ||||||
|  |       name: 'Common Filter', | ||||||
|  |       identifier: 'common_filter' | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|       name: 'Radar Chart', |       name: 'Radar Chart', | ||||||
|       identifier: 'radar_chart' |       identifier: 'radar_chart' | ||||||
| @ -104,6 +110,7 @@ export class EditnewdashComponent implements OnInit { | |||||||
|   public dashArr: []; |   public dashArr: []; | ||||||
| 
 | 
 | ||||||
|   protected componentCollection = [ |   protected componentCollection = [ | ||||||
|  |     { name: "Common Filter", componentInstance: CommonFilterComponent }, | ||||||
|     { name: "Line Chart", componentInstance: LineChartComponent }, |     { name: "Line Chart", componentInstance: LineChartComponent }, | ||||||
|     { name: "Doughnut Chart", componentInstance: DoughnutChartComponent }, |     { name: "Doughnut Chart", componentInstance: DoughnutChartComponent }, | ||||||
|     { name: "Radar Chart", componentInstance: RadarChartComponent }, |     { name: "Radar Chart", componentInstance: RadarChartComponent }, | ||||||
| @ -500,6 +507,16 @@ export class EditnewdashComponent implements OnInit { | |||||||
|           component: ToDoChartComponent, |           component: ToDoChartComponent, | ||||||
|           name: "To Do Chart" |           name: "To Do Chart" | ||||||
|         }); |         }); | ||||||
|  |       case "common_filter": | ||||||
|  |         return this.dashboardArray.push({ | ||||||
|  |           cols: 10, | ||||||
|  |           rows: 3, | ||||||
|  |           x: 0, | ||||||
|  |           y: 0, | ||||||
|  |           chartid: maxChartId + 1, | ||||||
|  |           component: CommonFilterComponent, | ||||||
|  |           name: "Common Filter" | ||||||
|  |         }); | ||||||
|       case "grid_view": |       case "grid_view": | ||||||
|         return this.dashboardArray.push({ |         return this.dashboardArray.push({ | ||||||
|           cols: 5, |           cols: 5, | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; | import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; | ||||||
| import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service'; | import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service'; | ||||||
| import { Subscription } from 'rxjs'; | import { Subscription } from 'rxjs'; | ||||||
|  | import { FilterService } from '../../common-filter/filter.service'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-bar-chart', |   selector: 'app-bar-chart', | ||||||
| @ -57,9 +58,20 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|   // Subscriptions to unsubscribe on destroy
 |   // Subscriptions to unsubscribe on destroy
 | ||||||
|   private subscriptions: Subscription[] = []; |   private subscriptions: Subscription[] = []; | ||||||
| 
 | 
 | ||||||
|   constructor(private dashboardService: Dashboard3Service) { } |   constructor( | ||||||
|  |     private dashboardService: Dashboard3Service, | ||||||
|  |     private filterService: FilterService | ||||||
|  |   ) { } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|  |     // Subscribe to filter changes
 | ||||||
|  |     this.subscriptions.push( | ||||||
|  |       this.filterService.filterState$.subscribe(filters => { | ||||||
|  |         // When filters change, refresh the chart data
 | ||||||
|  |         this.fetchChartData(); | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |      | ||||||
|     // Initialize with default data
 |     // Initialize with default data
 | ||||||
|     this.fetchChartData(); |     this.fetchChartData(); | ||||||
|   } |   } | ||||||
| @ -135,7 +147,49 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|           filterParams = JSON.stringify(filterObj); |           filterParams = JSON.stringify(filterObj); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       console.log('Base filter parameters:', filterParams); |        | ||||||
|  |       // Add common filters to filter parameters
 | ||||||
|  |       const commonFilters = this.filterService.getFilterValues(); | ||||||
|  |       console.log('Common filters from service:', commonFilters); | ||||||
|  |        | ||||||
|  |       if (Object.keys(commonFilters).length > 0) { | ||||||
|  |         // Merge common filters with base filters
 | ||||||
|  |         const mergedFilterObj = {}; | ||||||
|  |          | ||||||
|  |         // Add base filters first
 | ||||||
|  |         if (filterParams) { | ||||||
|  |           try { | ||||||
|  |             const baseFilterObj = JSON.parse(filterParams); | ||||||
|  |             Object.assign(mergedFilterObj, baseFilterObj); | ||||||
|  |           } catch (e) { | ||||||
|  |             console.warn('Failed to parse base filter parameters:', e); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Add common filters using the field name as the key, not the filter id
 | ||||||
|  |         Object.keys(commonFilters).forEach(filterId => { | ||||||
|  |           const filterValue = commonFilters[filterId]; | ||||||
|  |           // Find the filter definition to get the field name
 | ||||||
|  |           const filterDef = this.filterService.getFilters().find(f => f.id === filterId); | ||||||
|  |           if (filterDef && filterDef.field) { | ||||||
|  |             const fieldName = filterDef.field; | ||||||
|  |             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||||
|  |               mergedFilterObj[fieldName] = filterValue; | ||||||
|  |             } | ||||||
|  |           } else { | ||||||
|  |             // Fallback to using filterId as field name if no field is defined
 | ||||||
|  |             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||||
|  |               mergedFilterObj[filterId] = filterValue; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         if (Object.keys(mergedFilterObj).length > 0) { | ||||||
|  |           filterParams = JSON.stringify(mergedFilterObj); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       console.log('Final filter parameters:', filterParams); | ||||||
|       // Fetch data from the dashboard service with parameter field and value
 |       // Fetch data from the dashboard service with parameter field and value
 | ||||||
|       // For base level, we pass empty parameter and value, but now also pass filters
 |       // For base level, we pass empty parameter and value, but now also pass filters
 | ||||||
|       const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe( |       const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe( | ||||||
| @ -313,6 +367,36 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|         drilldownFilterParams = JSON.stringify(filterObj); |         drilldownFilterParams = JSON.stringify(filterObj); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     // Add common filters to drilldown filter parameters
 | ||||||
|  |     const commonFilters = this.filterService.getFilterValues(); | ||||||
|  |     if (Object.keys(commonFilters).length > 0) { | ||||||
|  |       // Merge common filters with drilldown filters
 | ||||||
|  |       const mergedFilterObj = {}; | ||||||
|  |        | ||||||
|  |       // Add drilldown filters first
 | ||||||
|  |       if (drilldownFilterParams) { | ||||||
|  |         try { | ||||||
|  |           const drilldownFilterObj = JSON.parse(drilldownFilterParams); | ||||||
|  |           Object.assign(mergedFilterObj, drilldownFilterObj); | ||||||
|  |         } catch (e) { | ||||||
|  |           console.warn('Failed to parse drilldown filter parameters:', e); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Add common filters
 | ||||||
|  |       Object.keys(commonFilters).forEach(key => { | ||||||
|  |         const value = commonFilters[key]; | ||||||
|  |         if (value !== undefined && value !== null && value !== '') { | ||||||
|  |           mergedFilterObj[key] = value; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       if (Object.keys(mergedFilterObj).length > 0) { | ||||||
|  |         drilldownFilterParams = JSON.stringify(mergedFilterObj); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|     console.log('Drilldown filter parameters:', drilldownFilterParams); |     console.log('Drilldown filter parameters:', drilldownFilterParams); | ||||||
|      |      | ||||||
|     // For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
 |     // For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
 | ||||||
| @ -426,6 +510,11 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   // Public method to refresh data when filters change
 | ||||||
|  |   refreshData(): void { | ||||||
|  |     this.fetchChartData(); | ||||||
|  |   } | ||||||
|  |    | ||||||
|   // Ensure labels and data arrays have the same length
 |   // Ensure labels and data arrays have the same length
 | ||||||
|   private syncLabelAndDataArrays(): void { |   private syncLabelAndDataArrays(): void { | ||||||
|     // For bar charts, we need to ensure all datasets have the same number of data points
 |     // For bar charts, we need to ensure all datasets have the same number of data points
 | ||||||
|  | |||||||
| @ -1,5 +1,7 @@ | |||||||
| import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core'; | import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core'; | ||||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||||
|  | import { FilterService } from '../../common-filter/filter.service'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-doughnut-chart', |   selector: 'app-doughnut-chart', | ||||||
| @ -96,10 +98,24 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|    |    | ||||||
|   // Flag to prevent infinite loops
 |   // Flag to prevent infinite loops
 | ||||||
|   private isFetchingData: boolean = false; |   private isFetchingData: boolean = false; | ||||||
|  |    | ||||||
|  |   // Subscriptions to unsubscribe on destroy
 | ||||||
|  |   private subscriptions: Subscription[] = []; | ||||||
| 
 | 
 | ||||||
|   constructor(private dashboardService: Dashboard3Service) { } |   constructor( | ||||||
|  |     private dashboardService: Dashboard3Service, | ||||||
|  |     private filterService: FilterService | ||||||
|  |   ) { } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|  |     // Subscribe to filter changes
 | ||||||
|  |     this.subscriptions.push( | ||||||
|  |       this.filterService.filterState$.subscribe(filters => { | ||||||
|  |         // When filters change, refresh the chart data
 | ||||||
|  |         this.fetchChartData(); | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |      | ||||||
|     // Validate initial data
 |     // Validate initial data
 | ||||||
|     this.validateChartData(); |     this.validateChartData(); | ||||||
|     this.fetchChartData(); |     this.fetchChartData(); | ||||||
| @ -180,6 +196,15 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   ngOnDestroy(): void { | ||||||
|  |     this.subscriptions.forEach(sub => sub.unsubscribe()); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Public method to refresh data when filters change
 | ||||||
|  |   refreshData(): void { | ||||||
|  |     this.fetchChartData(); | ||||||
|  |   } | ||||||
|  |    | ||||||
|   fetchChartData(): void { |   fetchChartData(): void { | ||||||
|     // Set flag to prevent recursive calls
 |     // Set flag to prevent recursive calls
 | ||||||
|     this.isFetchingData = true; |     this.isFetchingData = true; | ||||||
| @ -212,7 +237,49 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|           filterParams = JSON.stringify(filterObj); |           filterParams = JSON.stringify(filterObj); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       console.log('Base filter parameters:', filterParams); |        | ||||||
|  |       // Add common filters to filter parameters
 | ||||||
|  |       const commonFilters = this.filterService.getFilterValues(); | ||||||
|  |       console.log('Common filters from service:', commonFilters); | ||||||
|  |        | ||||||
|  |       if (Object.keys(commonFilters).length > 0) { | ||||||
|  |         // Merge common filters with base filters
 | ||||||
|  |         const mergedFilterObj = {}; | ||||||
|  |          | ||||||
|  |         // Add base filters first
 | ||||||
|  |         if (filterParams) { | ||||||
|  |           try { | ||||||
|  |             const baseFilterObj = JSON.parse(filterParams); | ||||||
|  |             Object.assign(mergedFilterObj, baseFilterObj); | ||||||
|  |           } catch (e) { | ||||||
|  |             console.warn('Failed to parse base filter parameters:', e); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Add common filters using the field name as the key, not the filter id
 | ||||||
|  |         Object.keys(commonFilters).forEach(filterId => { | ||||||
|  |           const filterValue = commonFilters[filterId]; | ||||||
|  |           // Find the filter definition to get the field name
 | ||||||
|  |           const filterDef = this.filterService.getFilters().find(f => f.id === filterId); | ||||||
|  |           if (filterDef && filterDef.field) { | ||||||
|  |             const fieldName = filterDef.field; | ||||||
|  |             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||||
|  |               mergedFilterObj[fieldName] = filterValue; | ||||||
|  |             } | ||||||
|  |           } else { | ||||||
|  |             // Fallback to using filterId as field name if no field is defined
 | ||||||
|  |             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||||
|  |               mergedFilterObj[filterId] = filterValue; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         if (Object.keys(mergedFilterObj).length > 0) { | ||||||
|  |           filterParams = JSON.stringify(mergedFilterObj); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       console.log('Final filter parameters:', filterParams); | ||||||
|        |        | ||||||
|       // Log the URL that will be called
 |       // Log the URL that will be called
 | ||||||
|       const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; |       const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||||
|  | |||||||
| @ -1,5 +1,7 @@ | |||||||
| import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | import { Component, OnInit, Input, OnChanges, SimpleChanges, OnDestroy } from '@angular/core'; | ||||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||||
|  | import { FilterService } from '../../common-filter/filter.service'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-line-chart', |   selector: 'app-line-chart', | ||||||
| @ -82,10 +84,24 @@ export class LineChartComponent implements OnInit, OnChanges { | |||||||
|    |    | ||||||
|   // Flag to prevent infinite loops
 |   // Flag to prevent infinite loops
 | ||||||
|   private isFetchingData: boolean = false; |   private isFetchingData: boolean = false; | ||||||
|  |    | ||||||
|  |   // Subscriptions to unsubscribe on destroy
 | ||||||
|  |   private subscriptions: Subscription[] = []; | ||||||
| 
 | 
 | ||||||
|   constructor(private dashboardService: Dashboard3Service) { } |   constructor( | ||||||
|  |     private dashboardService: Dashboard3Service, | ||||||
|  |     private filterService: FilterService | ||||||
|  |   ) { } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|  |     // Subscribe to filter changes
 | ||||||
|  |     this.subscriptions.push( | ||||||
|  |       this.filterService.filterState$.subscribe(filters => { | ||||||
|  |         // When filters change, refresh the chart data
 | ||||||
|  |         this.fetchChartData(); | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |      | ||||||
|     // Initialize with default data
 |     // Initialize with default data
 | ||||||
|     this.fetchChartData(); |     this.fetchChartData(); | ||||||
|   } |   } | ||||||
| @ -122,6 +138,15 @@ export class LineChartComponent implements OnInit, OnChanges { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   ngOnDestroy(): void { | ||||||
|  |     this.subscriptions.forEach(sub => sub.unsubscribe()); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Public method to refresh data when filters change
 | ||||||
|  |   refreshData(): void { | ||||||
|  |     this.fetchChartData(); | ||||||
|  |   } | ||||||
|  |    | ||||||
|   fetchChartData(): void { |   fetchChartData(): void { | ||||||
|     // Set flag to prevent recursive calls
 |     // Set flag to prevent recursive calls
 | ||||||
|     this.isFetchingData = true; |     this.isFetchingData = true; | ||||||
| @ -154,7 +179,49 @@ export class LineChartComponent implements OnInit, OnChanges { | |||||||
|           filterParams = JSON.stringify(filterObj); |           filterParams = JSON.stringify(filterObj); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       console.log('Base filter parameters:', filterParams); |        | ||||||
|  |       // Add common filters to filter parameters
 | ||||||
|  |       const commonFilters = this.filterService.getFilterValues(); | ||||||
|  |       console.log('Common filters from service:', commonFilters); | ||||||
|  |        | ||||||
|  |       if (Object.keys(commonFilters).length > 0) { | ||||||
|  |         // Merge common filters with base filters
 | ||||||
|  |         const mergedFilterObj = {}; | ||||||
|  |          | ||||||
|  |         // Add base filters first
 | ||||||
|  |         if (filterParams) { | ||||||
|  |           try { | ||||||
|  |             const baseFilterObj = JSON.parse(filterParams); | ||||||
|  |             Object.assign(mergedFilterObj, baseFilterObj); | ||||||
|  |           } catch (e) { | ||||||
|  |             console.warn('Failed to parse base filter parameters:', e); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Add common filters using the field name as the key, not the filter id
 | ||||||
|  |         Object.keys(commonFilters).forEach(filterId => { | ||||||
|  |           const filterValue = commonFilters[filterId]; | ||||||
|  |           // Find the filter definition to get the field name
 | ||||||
|  |           const filterDef = this.filterService.getFilters().find(f => f.id === filterId); | ||||||
|  |           if (filterDef && filterDef.field) { | ||||||
|  |             const fieldName = filterDef.field; | ||||||
|  |             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||||
|  |               mergedFilterObj[fieldName] = filterValue; | ||||||
|  |             } | ||||||
|  |           } else { | ||||||
|  |             // Fallback to using filterId as field name if no field is defined
 | ||||||
|  |             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||||
|  |               mergedFilterObj[filterId] = filterValue; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         if (Object.keys(mergedFilterObj).length > 0) { | ||||||
|  |           filterParams = JSON.stringify(mergedFilterObj); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       console.log('Final filter parameters:', filterParams); | ||||||
|        |        | ||||||
|       // Log the URL that will be called
 |       // Log the URL that will be called
 | ||||||
|       const url = `chart/getdashjson/line?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; |       const url = `chart/getdashjson/line?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||||
| @ -313,6 +380,35 @@ export class LineChartComponent implements OnInit, OnChanges { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     // Add common filters to drilldown filter parameters
 | ||||||
|  |     const commonFilters = this.filterService.getFilterValues(); | ||||||
|  |     if (Object.keys(commonFilters).length > 0) { | ||||||
|  |       // Merge common filters with drilldown filters
 | ||||||
|  |       const mergedFilterObj = {}; | ||||||
|  |        | ||||||
|  |       // Add drilldown filters first
 | ||||||
|  |       if (filterParams) { | ||||||
|  |         try { | ||||||
|  |           const drilldownFilterObj = JSON.parse(filterParams); | ||||||
|  |           Object.assign(mergedFilterObj, drilldownFilterObj); | ||||||
|  |         } catch (e) { | ||||||
|  |           console.warn('Failed to parse drilldown filter parameters:', e); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Add common filters
 | ||||||
|  |       Object.keys(commonFilters).forEach(key => { | ||||||
|  |         const value = commonFilters[key]; | ||||||
|  |         if (value !== undefined && value !== null && value !== '') { | ||||||
|  |           mergedFilterObj[key] = value; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       if (Object.keys(mergedFilterObj).length > 0) { | ||||||
|  |         filterParams = JSON.stringify(mergedFilterObj); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|     // Log the URL that will be called
 |     // Log the URL that will be called
 | ||||||
|     const url = `chart/getdashjson/line?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; |     const url = `chart/getdashjson/line?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||||
|     console.log('Drilldown data URL:', url); |     console.log('Drilldown data URL:', url); | ||||||
|  | |||||||
| @ -1,5 +1,7 @@ | |||||||
| import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core'; | import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core'; | ||||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||||
|  | import { FilterService } from '../../common-filter/filter.service'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-pie-chart', |   selector: 'app-pie-chart', | ||||||
| @ -95,8 +97,14 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | |||||||
|    |    | ||||||
|   // Flag to prevent infinite loops
 |   // Flag to prevent infinite loops
 | ||||||
|   private isFetchingData: boolean = false; |   private isFetchingData: boolean = false; | ||||||
|  |    | ||||||
|  |   // Subscriptions to unsubscribe on destroy
 | ||||||
|  |   private subscriptions: Subscription[] = []; | ||||||
| 
 | 
 | ||||||
|   constructor(private dashboardService: Dashboard3Service) { } |   constructor( | ||||||
|  |     private dashboardService: Dashboard3Service, | ||||||
|  |     private filterService: FilterService | ||||||
|  |   ) { } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Force chart redraw |    * Force chart redraw | ||||||
| @ -108,6 +116,14 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | |||||||
|   } |   } | ||||||
|    |    | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|  |     // Subscribe to filter changes
 | ||||||
|  |     this.subscriptions.push( | ||||||
|  |       this.filterService.filterState$.subscribe(filters => { | ||||||
|  |         // When filters change, refresh the chart data
 | ||||||
|  |         this.fetchChartData(); | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |      | ||||||
|     console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData }); |     console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData }); | ||||||
|     // Validate initial data
 |     // Validate initial data
 | ||||||
|     this.validateChartData(); |     this.validateChartData(); | ||||||
| @ -140,6 +156,15 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   ngOnDestroy(): void { | ||||||
|  |     this.subscriptions.forEach(sub => sub.unsubscribe()); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Public method to refresh data when filters change
 | ||||||
|  |   refreshData(): void { | ||||||
|  |     this.fetchChartData(); | ||||||
|  |   } | ||||||
|  |    | ||||||
|   fetchChartData(): void { |   fetchChartData(): void { | ||||||
|     // Set flag to prevent recursive calls
 |     // Set flag to prevent recursive calls
 | ||||||
|     this.isFetchingData = true; |     this.isFetchingData = true; | ||||||
| @ -172,7 +197,49 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | |||||||
|           filterParams = JSON.stringify(filterObj); |           filterParams = JSON.stringify(filterObj); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       console.log('Base filter parameters:', filterParams); |        | ||||||
|  |       // Add common filters to filter parameters
 | ||||||
|  |       const commonFilters = this.filterService.getFilterValues(); | ||||||
|  |       console.log('Common filters from service:', commonFilters); | ||||||
|  |        | ||||||
|  |       if (Object.keys(commonFilters).length > 0) { | ||||||
|  |         // Merge common filters with base filters
 | ||||||
|  |         const mergedFilterObj = {}; | ||||||
|  |          | ||||||
|  |         // Add base filters first
 | ||||||
|  |         if (filterParams) { | ||||||
|  |           try { | ||||||
|  |             const baseFilterObj = JSON.parse(filterParams); | ||||||
|  |             Object.assign(mergedFilterObj, baseFilterObj); | ||||||
|  |           } catch (e) { | ||||||
|  |             console.warn('Failed to parse base filter parameters:', e); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Add common filters using the field name as the key, not the filter id
 | ||||||
|  |         Object.keys(commonFilters).forEach(filterId => { | ||||||
|  |           const filterValue = commonFilters[filterId]; | ||||||
|  |           // Find the filter definition to get the field name
 | ||||||
|  |           const filterDef = this.filterService.getFilters().find(f => f.id === filterId); | ||||||
|  |           if (filterDef && filterDef.field) { | ||||||
|  |             const fieldName = filterDef.field; | ||||||
|  |             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||||
|  |               mergedFilterObj[fieldName] = filterValue; | ||||||
|  |             } | ||||||
|  |           } else { | ||||||
|  |             // Fallback to using filterId as field name if no field is defined
 | ||||||
|  |             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||||
|  |               mergedFilterObj[filterId] = filterValue; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         if (Object.keys(mergedFilterObj).length > 0) { | ||||||
|  |           filterParams = JSON.stringify(mergedFilterObj); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       console.log('Final filter parameters:', filterParams); | ||||||
|        |        | ||||||
|       // Log the URL that will be called
 |       // Log the URL that will be called
 | ||||||
|       const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; |       const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||||
| @ -362,6 +429,35 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     // Add common filters to drilldown filter parameters
 | ||||||
|  |     const commonFilters = this.filterService.getFilterValues(); | ||||||
|  |     if (Object.keys(commonFilters).length > 0) { | ||||||
|  |       // Merge common filters with drilldown filters
 | ||||||
|  |       const mergedFilterObj = {}; | ||||||
|  |        | ||||||
|  |       // Add drilldown filters first
 | ||||||
|  |       if (filterParams) { | ||||||
|  |         try { | ||||||
|  |           const drilldownFilterObj = JSON.parse(filterParams); | ||||||
|  |           Object.assign(mergedFilterObj, drilldownFilterObj); | ||||||
|  |         } catch (e) { | ||||||
|  |           console.warn('Failed to parse drilldown filter parameters:', e); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Add common filters
 | ||||||
|  |       Object.keys(commonFilters).forEach(key => { | ||||||
|  |         const value = commonFilters[key]; | ||||||
|  |         if (value !== undefined && value !== null && value !== '') { | ||||||
|  |           mergedFilterObj[key] = value; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       if (Object.keys(mergedFilterObj).length > 0) { | ||||||
|  |         filterParams = JSON.stringify(mergedFilterObj); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|     // Log the URL that will be called
 |     // Log the URL that will be called
 | ||||||
|     const url = `chart/getdashjson/pie?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; |     const url = `chart/getdashjson/pie?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||||
|     console.log('Drilldown data URL:', url); |     console.log('Drilldown data URL:', url); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user