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'; | ||||
| // Add the SureconnectService import
 | ||||
| import { SureconnectService } from '../sureconnect/sureconnect.service'; | ||||
| // Add the CommonFilterComponent import
 | ||||
| import { CommonFilterComponent } from '../common-filter/common-filter.component'; | ||||
| 
 | ||||
| function isNullArray(arr) { | ||||
|   return !Array.isArray(arr) || arr.length === 0; | ||||
| @ -45,6 +47,10 @@ export class EditnewdashComponent implements OnInit { | ||||
|   public commonFilterForm: FormGroup; // Add common filter form
 | ||||
| 
 | ||||
|   WidgetsMock: WidgetModel[] = [ | ||||
|     { | ||||
|       name: 'Common Filter', | ||||
|       identifier: 'common_filter' | ||||
|     }, | ||||
|     { | ||||
|       name: 'Radar Chart', | ||||
|       identifier: 'radar_chart' | ||||
| @ -104,6 +110,7 @@ export class EditnewdashComponent implements OnInit { | ||||
|   public dashArr: []; | ||||
| 
 | ||||
|   protected componentCollection = [ | ||||
|     { name: "Common Filter", componentInstance: CommonFilterComponent }, | ||||
|     { name: "Line Chart", componentInstance: LineChartComponent }, | ||||
|     { name: "Doughnut Chart", componentInstance: DoughnutChartComponent }, | ||||
|     { name: "Radar Chart", componentInstance: RadarChartComponent }, | ||||
| @ -500,6 +507,16 @@ export class EditnewdashComponent implements OnInit { | ||||
|           component: ToDoChartComponent, | ||||
|           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": | ||||
|         return this.dashboardArray.push({ | ||||
|           cols: 5, | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; | ||||
| import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { FilterService } from '../../common-filter/filter.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-bar-chart', | ||||
| @ -57,9 +58,20 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||
|   // Subscriptions to unsubscribe on destroy
 | ||||
|   private subscriptions: Subscription[] = []; | ||||
| 
 | ||||
|   constructor(private dashboardService: Dashboard3Service) { } | ||||
|   constructor( | ||||
|     private dashboardService: Dashboard3Service, | ||||
|     private filterService: FilterService | ||||
|   ) { } | ||||
| 
 | ||||
|   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
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| @ -135,7 +147,49 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||
|           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
 | ||||
|       // 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( | ||||
| @ -313,6 +367,36 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||
|         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); | ||||
|      | ||||
|     // 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
 | ||||
|   private syncLabelAndDataArrays(): void { | ||||
|     // 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 { FilterService } from '../../common-filter/filter.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-doughnut-chart', | ||||
| @ -96,10 +98,24 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|    | ||||
|   // Flag to prevent infinite loops
 | ||||
|   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 { | ||||
|     // Subscribe to filter changes
 | ||||
|     this.subscriptions.push( | ||||
|       this.filterService.filterState$.subscribe(filters => { | ||||
|         // When filters change, refresh the chart data
 | ||||
|         this.fetchChartData(); | ||||
|       }) | ||||
|     ); | ||||
|      | ||||
|     // Validate initial data
 | ||||
|     this.validateChartData(); | ||||
|     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 { | ||||
|     // Set flag to prevent recursive calls
 | ||||
|     this.isFetchingData = true; | ||||
| @ -212,7 +237,49 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|           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
 | ||||
|       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 { FilterService } from '../../common-filter/filter.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-line-chart', | ||||
| @ -82,10 +84,24 @@ export class LineChartComponent implements OnInit, OnChanges { | ||||
|    | ||||
|   // Flag to prevent infinite loops
 | ||||
|   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 { | ||||
|     // 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
 | ||||
|     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 { | ||||
|     // Set flag to prevent recursive calls
 | ||||
|     this.isFetchingData = true; | ||||
| @ -154,7 +179,49 @@ export class LineChartComponent implements OnInit, OnChanges { | ||||
|           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
 | ||||
|       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
 | ||||
|     const url = `chart/getdashjson/line?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
|     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 { FilterService } from '../../common-filter/filter.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-pie-chart', | ||||
| @ -95,8 +97,14 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
|    | ||||
|   // Flag to prevent infinite loops
 | ||||
|   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 | ||||
| @ -108,6 +116,14 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
|   } | ||||
|    | ||||
|   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 }); | ||||
|     // Validate initial data
 | ||||
|     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 { | ||||
|     // Set flag to prevent recursive calls
 | ||||
|     this.isFetchingData = true; | ||||
| @ -172,7 +197,49 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
|           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
 | ||||
|       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
 | ||||
|     const url = `chart/getdashjson/pie?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
|     console.log('Drilldown data URL:', url); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user