Compare commits
	
		
			3 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| aade12d6ff | |||
| 6c01e71d04 | |||
| f60657ca64 | 
| @ -23,6 +23,20 @@ export interface DashboardContentModel { | ||||
|   component?: any; | ||||
|   name: string; | ||||
|   type?:string; | ||||
|   // Common properties
 | ||||
|   table?: string; | ||||
|   connection?: string; | ||||
|   baseFilters?: any[]; | ||||
|   // Common filter properties
 | ||||
|   commonFilterEnabled?: boolean; | ||||
|   // Drilldown properties
 | ||||
|   drilldownEnabled?: boolean; | ||||
|   drilldownApiUrl?: string; | ||||
|   drilldownXAxis?: string; | ||||
|   drilldownYAxis?: string; | ||||
|   drilldownParameter?: string; | ||||
|   drilldownFilters?: any[]; | ||||
|   drilldownLayers?: any[]; | ||||
| } | ||||
| 
 | ||||
| export interface DashboardModel { | ||||
| @ -67,6 +81,10 @@ export class value1{ | ||||
| 
 | ||||
| 
 | ||||
| export const WidgetsMock: WidgetModel[] = [ | ||||
|   { | ||||
|       name: 'Common Filter', | ||||
|       identifier: 'common_filter' | ||||
|   }, | ||||
|   { | ||||
|       name: 'Radar Chart', | ||||
|       identifier: 'radar_chart' | ||||
| @ -111,4 +129,4 @@ export const WidgetsMock: WidgetModel[] = [ | ||||
|   name: 'To Do', | ||||
|   identifier: 'to_do_chart' | ||||
|   } | ||||
| ] | ||||
| ] | ||||
| @ -97,6 +97,7 @@ export class LoginPageComponent implements OnInit { | ||||
|       .subscribe( | ||||
|         resp => { | ||||
|           console.log('API Response received:', resp); | ||||
|           // Always reset loading state when response is received
 | ||||
|           this.isLoading = false; | ||||
|            | ||||
|           // Handle different response formats
 | ||||
| @ -106,7 +107,7 @@ export class LoginPageComponent implements OnInit { | ||||
|             return; | ||||
|           } | ||||
|           // Handle different response formats
 | ||||
|           if (resp.success === 'false') { | ||||
|           if (resp.success === 'false' || resp.success === false) { | ||||
|             this.isError = true; | ||||
|             this.errMsg = resp.message || 'Login failed'; | ||||
|             return; | ||||
| @ -123,6 +124,7 @@ export class LoginPageComponent implements OnInit { | ||||
|         }, | ||||
|         (errResponse: HttpErrorResponse) => { | ||||
|           console.log('API Error received:', errResponse); | ||||
|           // Always reset loading state when error occurs
 | ||||
|           this.isLoading = false; | ||||
|           this.isError = true; | ||||
|            | ||||
| @ -167,4 +169,4 @@ export class LoginPageComponent implements OnInit { | ||||
|     this.showPassword = !this.showPassword; | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| } | ||||
| @ -1,20 +0,0 @@ | ||||
| <!DOCTYPE html> | ||||
| 
 | ||||
| <html> | ||||
| <head> | ||||
| <title>gaurav</title> | ||||
| 
 | ||||
| </head> | ||||
| 
 | ||||
| <body> | ||||
|     <h1>this is h1</h1> | ||||
|     <h2>this is h1</h2> | ||||
|     <h3>this is h1</h3> | ||||
|     <h4>this is h1</h4> | ||||
|     <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsa fuga, asperiores mollitia iste vitae repellendus adipisci atque eum corrupti ad placeat unde voluptatum quia perferendis neque expedita, sequi iure quo. Ut error adipisci ex cum sint, suscipit, voluptatem repellat nemo dolorum unde dolores quasi aut. A earum quo mollitia voluptatibus!</p> | ||||
|     | ||||
| 
 | ||||
| </body> | ||||
| 
 | ||||
| </html> | ||||
| 
 | ||||
										
											Binary file not shown.
										
									
								
							| @ -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,68 @@ | ||||
| .chart-wrapper { | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   box-sizing: border-box; | ||||
|    | ||||
|   .chart-header { | ||||
|     padding: 10px 15px; | ||||
|     background: #f8f8f8; | ||||
|     border-bottom: 1px solid #eee; | ||||
|      | ||||
|     h5 { | ||||
|       margin: 0; | ||||
|       color: #333; | ||||
|       font-size: 16px; | ||||
|       white-space: nowrap; | ||||
|       overflow: hidden; | ||||
|       text-overflow: ellipsis; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   .chart-container { | ||||
|     flex: 1; | ||||
|     padding: 15px; | ||||
|     overflow: auto; | ||||
|     position: relative; | ||||
|      | ||||
|     // Ensure chart containers fill available space | ||||
|     ::ng-deep canvas { | ||||
|       max-width: 100%; | ||||
|       max-height: 100%; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Responsive adjustments | ||||
| @media (max-width: 768px) { | ||||
|   .chart-wrapper { | ||||
|     .chart-header { | ||||
|       padding: 8px 12px; | ||||
|        | ||||
|       h5 { | ||||
|         font-size: 14px; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     .chart-container { | ||||
|       padding: 10px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 480px) { | ||||
|   .chart-wrapper { | ||||
|     .chart-header { | ||||
|       padding: 6px 10px; | ||||
|        | ||||
|       h5 { | ||||
|         font-size: 13px; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     .chart-container { | ||||
|       padding: 8px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -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,111 @@ | ||||
| import { Component, Input, OnInit, OnDestroy, ComponentRef, ViewChild, ViewContainerRef, HostListener } 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(); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Handle window resize events
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   onResize(event: any) { | ||||
|     // Notify the chart component to resize if it has a resize method
 | ||||
|     if (this.componentRef && this.componentRef.instance) { | ||||
|       const chartInstance = this.componentRef.instance; | ||||
|        | ||||
|       // If it's a chart component with an onResize method, call it
 | ||||
|       if (chartInstance.onResize && typeof chartInstance.onResize === 'function') { | ||||
|         chartInstance.onResize(); | ||||
|       } | ||||
|        | ||||
|       // If it's a chart component with a chart property (from BaseChartDirective), resize it
 | ||||
|       if (chartInstance.chart && typeof chartInstance.chart.resize === 'function') { | ||||
|         setTimeout(() => { | ||||
|           chartInstance.chart.resize(); | ||||
|         }, 100); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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,192 @@ | ||||
| .common-filter-container { | ||||
|   padding: 15px; | ||||
|   background: #f5f5f5; | ||||
|   border: 1px solid #ddd; | ||||
|   border-radius: 4px; | ||||
|   margin-bottom: 20px; | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
|   box-sizing: border-box; | ||||
| 
 | ||||
|   .filter-header { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|     margin-bottom: 15px; | ||||
|     flex-wrap: wrap; | ||||
|     gap: 10px; | ||||
| 
 | ||||
|     h4 { | ||||
|       margin: 0; | ||||
|       color: #333; | ||||
|       flex: 1; | ||||
|     } | ||||
|      | ||||
|     .btn { | ||||
|       white-space: nowrap; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .presets-section { | ||||
|     margin-bottom: 15px; | ||||
|      | ||||
|     .preset-controls { | ||||
|       display: flex; | ||||
|       gap: 10px; | ||||
|       align-items: center; | ||||
|       flex-wrap: wrap; | ||||
|        | ||||
|       select { | ||||
|         flex: 1; | ||||
|         min-width: 150px; | ||||
|       } | ||||
|        | ||||
|       .btn { | ||||
|         white-space: nowrap; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .save-preset-section { | ||||
|     margin-bottom: 15px; | ||||
|      | ||||
|     .clr-input-group { | ||||
|       display: flex; | ||||
|       flex-wrap: wrap; | ||||
|       gap: 10px; | ||||
|        | ||||
|       .clr-input { | ||||
|         flex: 1; | ||||
|         min-width: 150px; | ||||
|       } | ||||
|        | ||||
|       .clr-input-group-btn { | ||||
|         .btn { | ||||
|           white-space: nowrap; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .filters-form { | ||||
|     .filters-grid { | ||||
|       display: grid; | ||||
|       grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); | ||||
|       gap: 15px; | ||||
|        | ||||
|       .filter-item { | ||||
|         background: white; | ||||
|         border: 1px solid #e0e0e0; | ||||
|         border-radius: 4px; | ||||
|         padding: 15px; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|          | ||||
|         .filter-header { | ||||
|           display: flex; | ||||
|           justify-content: space-between; | ||||
|           align-items: center; | ||||
|           margin-bottom: 10px; | ||||
|            | ||||
|           .filter-label { | ||||
|             font-weight: bold; | ||||
|             color: #333; | ||||
|             flex: 1; | ||||
|             white-space: nowrap; | ||||
|             overflow: hidden; | ||||
|             text-overflow: ellipsis; | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         .filter-type-select { | ||||
|           width: 100%; | ||||
|           margin-bottom: 10px; | ||||
|         } | ||||
|          | ||||
|         .clr-form-control { | ||||
|           margin-bottom: 10px; | ||||
|            | ||||
|           input, select, textarea { | ||||
|             width: 100%; | ||||
|             min-width: 0; // Allow flexbox to shrink items | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         .date-range-controls { | ||||
|           display: flex; | ||||
|           gap: 10px; | ||||
|           flex-direction: column; | ||||
|            | ||||
|           .clr-form-control { | ||||
|             flex: 1; | ||||
|           } | ||||
|            | ||||
|           @media (min-width: 480px) { | ||||
|             flex-direction: row; | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         .toggle-control { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
|           gap: 8px; | ||||
|           flex-wrap: wrap; | ||||
|         } | ||||
|          | ||||
|         .multiselect { | ||||
|           height: 100px; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .no-filters { | ||||
|     text-align: center; | ||||
|     padding: 20px; | ||||
|     color: #666; | ||||
|     font-style: italic; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Responsive design for smaller screens | ||||
| @media (max-width: 768px) { | ||||
|   .common-filter-container { | ||||
|     padding: 10px; | ||||
|      | ||||
|     .filters-form { | ||||
|       .filters-grid { | ||||
|         grid-template-columns: 1fr; | ||||
|         gap: 10px; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     .filter-header { | ||||
|       flex-direction: column; | ||||
|       align-items: stretch; | ||||
|     } | ||||
|      | ||||
|     .presets-section { | ||||
|       .preset-controls { | ||||
|         flex-direction: column; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     .save-preset-section { | ||||
|       .clr-input-group { | ||||
|         flex-direction: column; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 480px) { | ||||
|   .common-filter-container { | ||||
|     .date-range-controls { | ||||
|       flex-direction: column; | ||||
|     } | ||||
|      | ||||
|     .filter-item { | ||||
|       padding: 10px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -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,197 @@ | ||||
| import { Component, OnInit, OnDestroy, Input, HostListener } 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 { | ||||
|   @Input() baseFilters: any[] = []; | ||||
|   @Input() drilldownFilters: any[] = []; | ||||
|   @Input() drilldownLayers: any[] = []; | ||||
|   @Input() fieldName: string; | ||||
|   @Input() connection: number; | ||||
|    | ||||
|   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()); | ||||
|   } | ||||
|    | ||||
|   // Handle window resize events
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   onResize(event: any) { | ||||
|     // Trigger change detection to reflow the layout
 | ||||
|     setTimeout(() => { | ||||
|       // This will cause the grid to recalculate its layout
 | ||||
|       this.filters = [...this.filters]; | ||||
|     }, 100); | ||||
|   } | ||||
| 
 | ||||
|   // 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 { | ||||
|     console.log('=== COMMON FILTER DEBUG INFO ==='); | ||||
|     console.log('Filter value changed for ID:', filterId); | ||||
|     console.log('New value:', value); | ||||
|      | ||||
|     const filterDef = this.filters.find(f => f.id === filterId); | ||||
|     console.log('Filter definition:', filterDef); | ||||
|      | ||||
|     this.filterService.updateFilterValue(filterId, value); | ||||
|     console.log('=== END COMMON FILTER DEBUG ==='); | ||||
|   } | ||||
| 
 | ||||
|   // 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,199 @@ | ||||
| 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 { | ||||
|     console.log('=== FILTER SERVICE DEBUG INFO ==='); | ||||
|     console.log('Updating filter value for ID:', filterId); | ||||
|     console.log('New value:', value); | ||||
|      | ||||
|     const currentState = this.filterStateSubject.value; | ||||
|     const newState = { | ||||
|       ...currentState, | ||||
|       [filterId]: value | ||||
|     }; | ||||
|      | ||||
|     console.log('New filter state:', newState); | ||||
|     this.filterStateSubject.next(newState); | ||||
|     console.log('=== END FILTER SERVICE DEBUG ==='); | ||||
|   } | ||||
| 
 | ||||
|   // 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 }, | ||||
| @ -201,7 +208,9 @@ export class EditnewdashComponent implements OnInit { | ||||
|       }, | ||||
|       displayGrid: "always", | ||||
|       minCols: 10, | ||||
|       minRows: 10 | ||||
|       minRows: 10, | ||||
|       // Add resize callback to handle chart resizing
 | ||||
|       itemResizeCallback: this.itemResize.bind(this) | ||||
|     }; | ||||
| 
 | ||||
|     this.editId = this.route.snapshot.params.id; | ||||
| @ -500,6 +509,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, | ||||
| @ -781,6 +800,15 @@ export class EditnewdashComponent implements OnInit { | ||||
|       drilldownLayers: item['drilldownLayers'] || [] | ||||
|     }; | ||||
|      | ||||
|     // For CommonFilterComponent, also pass baseFilters, drilldownFilters, drilldownLayers, fieldName, and connection
 | ||||
|     if (item.component && item.component.name === 'CommonFilterComponent') { | ||||
|       chartInputs['baseFilters'] = item['baseFilters'] || []; | ||||
|       chartInputs['drilldownFilters'] = item['drilldownFilters'] || []; | ||||
|       chartInputs['drilldownLayers'] = item['drilldownLayers'] || []; | ||||
|       chartInputs['fieldName'] = item['name'] || ''; | ||||
|       chartInputs['connection'] = item['connection'] || undefined; | ||||
|     } | ||||
|      | ||||
|     // Remove undefined properties to avoid passing unnecessary data
 | ||||
|     Object.keys(chartInputs).forEach(key => { | ||||
|       if (chartInputs[key] === undefined) { | ||||
| @ -1247,4 +1275,17 @@ export class EditnewdashComponent implements OnInit { | ||||
|       // When disabling, the user can edit the filters normally
 | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Add method to handle item resize events
 | ||||
|   itemResize(item: any, itemComponent: any) { | ||||
|     console.log('Item resized:', item); | ||||
|     // Trigger a window resize event to notify charts to resize
 | ||||
|     window.dispatchEvent(new Event('resize')); | ||||
|      | ||||
|     // Also try to directly notify the chart component if possible
 | ||||
|     if (itemComponent && itemComponent.item && itemComponent.item.component) { | ||||
|       // If the resized item contains a chart, we could try to call its resize method directly
 | ||||
|       // This would require the chart component to have a public resize method
 | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <div style="display: block"> | ||||
| <div style="display: block; height: 100%; width: 100%;"> | ||||
|   <!-- No filter controls needed with the new simplified approach --> | ||||
|   <!-- Filters are now configured at the drilldown level --> | ||||
|    | ||||
| @ -19,13 +19,14 @@ | ||||
|   </div> | ||||
|    | ||||
|   <!-- Chart display --> | ||||
|   <div *ngIf="!noDataAvailable"> | ||||
|   <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);"> | ||||
|     <canvas baseChart | ||||
|     [datasets]="barChartData" | ||||
|     [labels]="barChartLabels" | ||||
|     [type]="barChartType" | ||||
|     (chartHover)="chartHovered($event)" | ||||
|     (chartClick)="chartClicked($event)"> | ||||
|   </canvas> | ||||
|       [datasets]="barChartData" | ||||
|       [labels]="barChartLabels" | ||||
|       [type]="barChartType" | ||||
|       [options]="barChartOptions" | ||||
|       (chartHover)="chartHovered($event)" | ||||
|       (chartClick)="chartClicked($event)"> | ||||
|     </canvas> | ||||
|   </div> | ||||
| </div> | ||||
| @ -0,0 +1,31 @@ | ||||
| // Bar Chart Component Styles | ||||
| :host { | ||||
|   display: block; | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
| } | ||||
| 
 | ||||
| .bar-chart-container { | ||||
|   position: relative; | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
| } | ||||
| 
 | ||||
| canvas { | ||||
|   display: block; | ||||
|   max-width: 100%; | ||||
|   max-height: 100%; | ||||
| } | ||||
| 
 | ||||
| // Responsive design for chart container | ||||
| @media (max-width: 768px) { | ||||
|   .bar-chart-container { | ||||
|     height: 300px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 480px) { | ||||
|   .bar-chart-container { | ||||
|     height: 250px; | ||||
|   } | ||||
| } | ||||
| @ -1,6 +1,9 @@ | ||||
| import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; | ||||
| import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core'; | ||||
| import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { FilterService } from '../../common-filter/filter.service'; | ||||
| // Add BaseChartDirective import for chart resizing
 | ||||
| import { BaseChartDirective } from 'ng2-charts'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-bar-chart', | ||||
| @ -34,6 +37,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||
|   // Multi-layer drilldown configuration inputs
 | ||||
|   @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
 | ||||
| 
 | ||||
|   // Add ViewChild to access the chart directive
 | ||||
|   @ViewChild(BaseChartDirective) chart?: BaseChartDirective; | ||||
| 
 | ||||
|   barChartLabels: string[] = ['Apple', 'Banana', 'Kiwifruit', 'Blueberry', 'Orange', 'Grapes']; | ||||
|   barChartType: string = 'bar'; | ||||
|   barChartPlugins = []; | ||||
| @ -42,6 +48,33 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||
|   ]; | ||||
|   barChartLegend: boolean = true; | ||||
|    | ||||
|   // Add responsive chart options
 | ||||
|   barChartOptions: any = { | ||||
|     responsive: true, | ||||
|     maintainAspectRatio: false, | ||||
|     scales: { | ||||
|       x: { | ||||
|         ticks: { | ||||
|           autoSkip: false, | ||||
|           maxRotation: 45, | ||||
|           minRotation: 45 | ||||
|         } | ||||
|       }, | ||||
|       y: { | ||||
|         beginAtZero: true | ||||
|       } | ||||
|     }, | ||||
|     plugins: { | ||||
|       legend: { | ||||
|         display: true, | ||||
|         position: 'top', | ||||
|       }, | ||||
|       tooltip: { | ||||
|         enabled: true | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|    | ||||
|   // Multi-layer drilldown state tracking
 | ||||
|   drilldownStack: any[] = []; // Stack to track drilldown navigation history
 | ||||
|   currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
 | ||||
| @ -57,9 +90,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(); | ||||
|   } | ||||
| @ -92,6 +136,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     // Update legend visibility if it changed
 | ||||
|     if (changes.chartlegend !== undefined) { | ||||
|       this.barChartLegend = changes.chartlegend.currentValue; | ||||
|       this.barChartOptions.plugins.legend.display = this.barChartLegend; | ||||
|       console.log('Chart legend changed to:', this.barChartLegend); | ||||
|     } | ||||
|   } | ||||
| @ -110,10 +155,15 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||
|      | ||||
|     // If we have the necessary data, fetch chart data from the service
 | ||||
|     if (this.table && this.xAxis && this.yAxis) { | ||||
|       console.log('Fetching bar chart data for:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); | ||||
|       console.log('=== BAR CHART DEBUG INFO ==='); | ||||
|       console.log('Table:', this.table); | ||||
|       console.log('X-Axis:', this.xAxis); | ||||
|       console.log('Y-Axis:', this.yAxis); | ||||
|       console.log('Connection:', this.connection); | ||||
|        | ||||
|       // Convert yAxis to string if it's an array
 | ||||
|       const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis; | ||||
|       console.log('Y-Axis String:', yAxisString); | ||||
|        | ||||
|       // Get the parameter value from the drilldown stack for base level (should be empty)
 | ||||
|       let parameterValue = ''; | ||||
| @ -135,11 +185,66 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||
|           filterParams = JSON.stringify(filterObj); | ||||
|         } | ||||
|       } | ||||
|       console.log('Base filter parameters:', filterParams); | ||||
|       console.log('Base filters:', this.baseFilters); | ||||
|       console.log('Base filter params:', filterParams); | ||||
|        | ||||
|       // Add common filters to filter parameters
 | ||||
|       const commonFilters = this.filterService.getFilterValues(); | ||||
|       const filterDefinitions = this.filterService.getFilters(); | ||||
|       console.log('Common filters from service:', commonFilters); | ||||
|       console.log('Filter definitions:', filterDefinitions); | ||||
|        | ||||
|       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]; | ||||
|           console.log(`Processing filter ID: ${filterId}, Value:`, filterValue); | ||||
|            | ||||
|           // Find the filter definition to get the field name
 | ||||
|           const filterDef = this.filterService.getFilters().find(f => f.id === filterId); | ||||
|           console.log(`Filter definition for ${filterId}:`, filterDef); | ||||
|            | ||||
|           if (filterDef && filterDef.field) { | ||||
|             const fieldName = filterDef.field; | ||||
|             console.log(`Mapping filter ID ${filterId} to field name: ${fieldName}`); | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[fieldName] = filterValue; | ||||
|               console.log(`Added to merged filters: ${fieldName} =`, filterValue); | ||||
|             } | ||||
|           } else { | ||||
|             // Fallback to using filterId as field name if no field is defined
 | ||||
|             console.log(`No field name found for filter ID ${filterId}, using ID as field name`); | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[filterId] = filterValue; | ||||
|               console.log(`Added to merged filters: ${filterId} =`, filterValue); | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|          | ||||
|         if (Object.keys(mergedFilterObj).length > 0) { | ||||
|           filterParams = JSON.stringify(mergedFilterObj); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       console.log('Final merged filter object:', 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( | ||||
|         (data: any) => { | ||||
|           console.log('=== BAR CHART DATA RESPONSE ==='); | ||||
|           console.log('Received bar chart data:', data); | ||||
|           if (data === null) { | ||||
|             console.warn('Bar chart API returned null data. Check if the API endpoint is working correctly.'); | ||||
| @ -160,6 +265,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||
|             // Trigger change detection
 | ||||
|             // this.barChartData = [...this.barChartData];
 | ||||
|             console.log('Updated bar chart with data:', { labels: this.barChartLabels, data: this.barChartData }); | ||||
|             console.log('=== CHART UPDATED SUCCESSFULLY ==='); | ||||
|           } else if (data && data.labels && data.datasets) { | ||||
|             // Backend has already filtered the data, just display it
 | ||||
|             this.noDataAvailable = data.labels.length === 0; | ||||
| @ -168,6 +274,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||
|             // Trigger change detection
 | ||||
|             // this.barChartData = [...this.barChartData];
 | ||||
|             console.log('Updated bar chart with legacy data format:', { labels: this.barChartLabels, data: this.barChartData }); | ||||
|             console.log('=== CHART UPDATED SUCCESSFULLY (LEGACY) ==='); | ||||
|           } else { | ||||
|             console.warn('Bar chart received data does not have expected structure', data); | ||||
|             this.noDataAvailable = true; | ||||
| @ -178,6 +285,7 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||
|           this.isFetchingData = false; | ||||
|         }, | ||||
|         (error) => { | ||||
|           console.error('=== BAR CHART ERROR ==='); | ||||
|           console.error('Error fetching bar chart data:', error); | ||||
|           this.noDataAvailable = true; | ||||
|           this.barChartLabels = []; | ||||
| @ -313,6 +421,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 +564,18 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Public method to refresh data when filters change
 | ||||
|   refreshData(): void { | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
|    | ||||
|   // Method to handle window resize events
 | ||||
|   onResize(): void { | ||||
|     if (this.chart) { | ||||
|       this.chart.chart?.resize(); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // 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); | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { Router } from '@angular/router'; | ||||
| 
 | ||||
| import { Observable, BehaviorSubject } from 'rxjs'; | ||||
| import { Observable, BehaviorSubject, Subject } from 'rxjs'; | ||||
| import { UserInfoService, LoginInfoInStorage} from '../user-info.service'; | ||||
| import { ApiRequestService } from './api-request.service'; | ||||
| import { HttpClient } from '@angular/common/http'; | ||||
| @ -43,63 +43,70 @@ export class LoginService { | ||||
| 
 | ||||
|         */ | ||||
| 
 | ||||
|         let loginDataSubject:BehaviorSubject<any> = new BehaviorSubject<any>([]); // Will use this BehaviorSubject to emit data that we want after ajax login attempt
 | ||||
|         // let loginDataSubject:BehaviorSubject<any> = new BehaviorSubject<any>([]); // Will use this BehaviorSubject to emit data that we want after ajax login attempt
 | ||||
|        let loginDataSubject: Subject<any> = new Subject<any>(); | ||||
| 
 | ||||
|         let loginInfoReturn:LoginInfoInStorage; // Object that we want to send back to Login Page
 | ||||
| 
 | ||||
|         this.apiRequest.loginAuthentication('token/session', bodyData) | ||||
|             .subscribe(jsonResp => { | ||||
|                 console.log('login response in service : ', jsonResp); | ||||
|                 if (jsonResp.operationMessage=='Login Failed') { | ||||
|                   this.toastr.warning('Not Login Getting Error check your Username and password'); | ||||
|                } | ||||
|                 if (jsonResp !== undefined && jsonResp !== null && jsonResp.operationStatus === "SUCCESS"){ | ||||
|                     //Create a success object that we want to send back to login page
 | ||||
|                     ////"displayName": jsonResp.item.fullname,
 | ||||
|                     //"username"   : jsonResp.item.username,
 | ||||
|                     loginInfoReturn = { | ||||
|                         "success"    : true, | ||||
|                         "message"    : jsonResp.operationMessage, | ||||
|                         "landingPage": this.landingPage, | ||||
|                         "user"       : { | ||||
|                             "userId"     : jsonResp.item.userId, | ||||
|                             "email"      : jsonResp.item.email, | ||||
|                             "displayName": jsonResp.item.firstName, | ||||
|                             "username"   : jsonResp.item.username, | ||||
|                             "roles"      :  jsonResp.item.roles, | ||||
|                             "token"      : jsonResp.item.token, | ||||
| 
 | ||||
|                         }, | ||||
|                     }; | ||||
|                     console.log(loginInfoReturn.user); | ||||
|                     if(jsonResp !== undefined && jsonResp !== null && jsonResp.operationStatus === "SUCCESS"){ | ||||
|                       this.toastr.success(`Welcome To home Page!! your Role is ${jsonResp.item.roles}`); | ||||
|             .subscribe({ | ||||
|                 next: (jsonResp) => { | ||||
|                     console.log('login response in service : ', jsonResp); | ||||
|                     if (jsonResp.operationMessage=='Login Failed') { | ||||
|                       this.toastr.warning('Not Login Getting Error check your Username and password'); | ||||
|                     } | ||||
|                     // store username and jwt token in session storage to keep user logged in between page refreshes
 | ||||
|                     this.userInfoService.storeUserInfo(JSON.stringify(loginInfoReturn.user)); | ||||
|                 } | ||||
|                 else { | ||||
|                     //Create a faliure object that we want to send back to login page
 | ||||
|                     if (jsonResp !== undefined && jsonResp !== null && jsonResp.operationStatus === "SUCCESS"){ | ||||
|                         //Create a success object that we want to send back to login page
 | ||||
|                         ////"displayName": jsonResp.item.fullname,
 | ||||
|                         //"username"   : jsonResp.item.username,
 | ||||
|                         loginInfoReturn = { | ||||
|                             "success"    : true, | ||||
|                             "message"    : jsonResp.operationMessage, | ||||
|                             "landingPage": this.landingPage, | ||||
|                             "user"       : { | ||||
|                                 "userId"     : jsonResp.item.userId, | ||||
|                                 "email"      : jsonResp.item.email, | ||||
|                                 "displayName": jsonResp.item.firstName, | ||||
|                                 "username"   : jsonResp.item.username, | ||||
|                                 "roles"      :  jsonResp.item.roles, | ||||
|                                 "token"      : jsonResp.item.token, | ||||
| 
 | ||||
|                             }, | ||||
|                         }; | ||||
|                         console.log(loginInfoReturn.user); | ||||
|                         if(jsonResp !== undefined && jsonResp !== null && jsonResp.operationStatus === "SUCCESS"){ | ||||
|                           this.toastr.success(`Welcome To home Page!! your Role is ${jsonResp.item.roles}`); | ||||
|                         } | ||||
|                         // store username and jwt token in session storage to keep user logged in between page refreshes
 | ||||
|                         this.userInfoService.storeUserInfo(JSON.stringify(loginInfoReturn.user)); | ||||
|                     } | ||||
|                     else { | ||||
|                         //Create a failure object that we want to send back to login page
 | ||||
|                         loginInfoReturn = { | ||||
|                             "success":false, | ||||
|                             "message":jsonResp.operationMessage, | ||||
|                             "landingPage":"/login" | ||||
|                         }; | ||||
|                     } | ||||
|                     loginDataSubject.next(loginInfoReturn); | ||||
|                     loginDataSubject.complete(); // Complete the subject
 | ||||
|                 }, | ||||
|                 error: (err) => { | ||||
|                     console.log('login error ', err); | ||||
|                     loginInfoReturn = { | ||||
|                         "success":false, | ||||
|                         "message":jsonResp.operationMessage, | ||||
|                         "landingPage":"/login" | ||||
|                         "success": false, | ||||
|                         "message": err.url + " >>> " + err.statusText +  "[" + err.status +"]", | ||||
|                         "landingPage": "/login" | ||||
|                     }; | ||||
|                     if (err) { | ||||
|                         this.toastr.error('Getting Server Error'); | ||||
|                     } | ||||
|                     loginDataSubject.next(loginInfoReturn); // Send the error response
 | ||||
|                     loginDataSubject.complete(); // Complete the subject
 | ||||
|                 } | ||||
|                 loginDataSubject.next(loginInfoReturn); | ||||
|             }, | ||||
|             err => { | ||||
|                 console.log('login error ', err); | ||||
|               loginInfoReturn = { | ||||
|                 "success": false, | ||||
|                 "message": err.url + " >>> " + err.statusText +  "[" + err.status +"]", | ||||
|                 "landingPage": "/login" | ||||
|               }; | ||||
|               if (err) { | ||||
|                 this.toastr.error('Getting Server Error'); | ||||
|              } | ||||
|             }); | ||||
| 
 | ||||
|             return loginDataSubject; | ||||
|         return loginDataSubject; | ||||
|     } | ||||
| 
 | ||||
|     logout(navigatetoLogout=true): void { | ||||
|  | ||||
| @ -281,7 +281,7 @@ export class Dashboard3Service { | ||||
|     return this.apiRequest.get(`Dashboard/Dashboard`); | ||||
|   } | ||||
| 
 | ||||
|   public getChartData(tableName: string, jobType: string, xAxis?: any, yAxes?: any, sureId?: number, parameter?: string, parameterValue?: string): Observable<any> { | ||||
|   public getChartData(tableName: string, jobType: string, xAxis?: any, yAxes?: any, sureId?: number, parameter?: string, parameterValue?: string, filters?: string): Observable<any> { | ||||
|     let url = `${baseUrl}/chart/getdashjson/${jobType}?tableName=${tableName}&xAxis=${xAxis}&yAxes=${yAxes}`; | ||||
|     if (sureId) { | ||||
|       url += `&sureId=${sureId}`; | ||||
| @ -292,8 +292,27 @@ export class Dashboard3Service { | ||||
|     if (parameterValue) { | ||||
|       url += `¶meterValue=${encodeURIComponent(parameterValue)}`; | ||||
|     } | ||||
|      | ||||
|     console.log('=== DASHBOARD SERVICE DEBUG INFO ==='); | ||||
|     console.log('Base URL:', url); | ||||
|     console.log('Filters parameter:', filters); | ||||
| 
 | ||||
|     // Parse filters JSON and add as a single "filters" parameter
 | ||||
|     if (filters) { | ||||
|       try { | ||||
|         const filterObj = JSON.parse(filters); | ||||
|         console.log('Parsed filter object:', filterObj); | ||||
|          | ||||
|         // Add all filters as a single "filters" parameter with JSON object
 | ||||
|         url += `&filters=${encodeURIComponent(JSON.stringify(filterObj))}`; | ||||
|         console.log('Added filters parameter:', JSON.stringify(filterObj)); | ||||
|       } catch (e) { | ||||
|         console.warn('Failed to parse filter parameters:', e); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     console.log('Final constructed URL:', url); | ||||
|     console.log('=== END DASHBOARD SERVICE DEBUG ==='); | ||||
|     return this._http.get(url); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -372,5 +372,9 @@ | ||||
|   "select_Field2": "select_Field2", | ||||
|   "Password_Field": "Password_Field", | ||||
|   "age": "age", | ||||
|   "Button_Field": "Button_Field" | ||||
|   "Button_Field": "Button_Field", | ||||
|   "API_REGISTERY": "API Registry", | ||||
|   "API_REGISTERY_DESCRIPTION": "API Registry Description", | ||||
|   "TOKEN_REGISTERY": "Token Registry", | ||||
|   "TOKEN_REGISTERY_DESCRIPTION": "Token Registry Description" | ||||
| } | ||||
| @ -105,6 +105,8 @@ | ||||
|   "REPORT_DESCRIPTION": "रिपोर्ट विवरण", | ||||
|   "API_REGISTERY": "एपीआई रजिस्ट्री", | ||||
|   "API_REGISTERY_DESCRIPTION": "एपीआई रजिस्ट्री विवरण", | ||||
|   "TOKEN_REGISTERY": "टोकन रजिस्ट्री", | ||||
|   "TOKEN_REGISTERY_DESCRIPTION": "टोकन रजिस्ट्री विवरण", | ||||
|   "ACTIVE": "सक्रिय", | ||||
|   "FOLDER_NAME": "फ़ोल्डर नाम", | ||||
|   "ACTION": "क्रिया", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user