From f60657ca64e3ce85e37529f73c34aceb2e976f5d Mon Sep 17 00:00:00 2001 From: Gaurav Kumar Date: Sat, 25 Oct 2025 10:33:16 +0530 Subject: [PATCH] filter --- .../common-filter/EXAMPLE_USAGE.md | 687 ++++++++++++++++++ .../common-filter/IMPLEMENTATION_GUIDE.md | 219 ++++++ .../chart-wrapper.component.scss | 23 + .../chart-wrapper.component.spec.ts | 39 + .../common-filter/chart-wrapper.component.ts | 90 +++ .../common-filter.component.html | 128 ++++ .../common-filter.component.scss | 103 +++ .../common-filter.component.spec.ts | 89 +++ .../common-filter/common-filter.component.ts | 173 +++++ .../common-filter/filter.service.spec.ts | 129 ++++ .../common-filter/filter.service.ts | 191 +++++ .../dashboardnew/common-filter/index.ts | 3 + .../editnewdash/editnewdash.component.ts | 17 + .../gadgets/bar-chart/bar-chart.component.ts | 93 ++- .../doughnut-chart.component.ts | 73 +- .../line-chart/line-chart.component.ts | 102 ++- .../gadgets/pie-chart/pie-chart.component.ts | 102 ++- 17 files changed, 2250 insertions(+), 11 deletions(-) create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/EXAMPLE_USAGE.md create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/IMPLEMENTATION_GUIDE.md create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/chart-wrapper.component.scss create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/chart-wrapper.component.spec.ts create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/chart-wrapper.component.ts create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.html create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.scss create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.spec.ts create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.ts create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/filter.service.spec.ts create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/filter.service.ts create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/index.ts diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/EXAMPLE_USAGE.md b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/EXAMPLE_USAGE.md new file mode 100644 index 0000000..3d536da --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/EXAMPLE_USAGE.md @@ -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. \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/IMPLEMENTATION_GUIDE.md b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/IMPLEMENTATION_GUIDE.md new file mode 100644 index 0000000..b56127f --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/IMPLEMENTATION_GUIDE.md @@ -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 \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/chart-wrapper.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/chart-wrapper.component.scss new file mode 100644 index 0000000..b0b316e --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/chart-wrapper.component.scss @@ -0,0 +1,23 @@ +.chart-wrapper { + height: 100%; + display: flex; + flex-direction: column; + + .chart-header { + padding: 10px 15px; + background: #f8f8f8; + border-bottom: 1px solid #eee; + + h5 { + margin: 0; + color: #333; + font-size: 16px; + } + } + + .chart-container { + flex: 1; + padding: 15px; + overflow: auto; + } +} \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/chart-wrapper.component.spec.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/chart-wrapper.component.spec.ts new file mode 100644 index 0000000..74d20dd --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/chart-wrapper.component.spec.ts @@ -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; + 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(); + }); +}); \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/chart-wrapper.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/chart-wrapper.component.ts new file mode 100644 index 0000000..1572bbf --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/chart-wrapper.component.ts @@ -0,0 +1,90 @@ +import { Component, Input, OnInit, OnDestroy, ComponentRef, ViewChild, ViewContainerRef } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { FilterService } from './filter.service'; + +@Component({ + selector: 'app-chart-wrapper', + template: ` +
+
+
{{ chartTitle }}
+
+
+ +
+
+ `, + 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 | null = null; + private filterSubscription: Subscription | null = null; + + constructor(private filterService: FilterService) { } + + ngOnInit(): void { + this.loadChartComponent(); + this.subscribeToFilters(); + } + + ngOnDestroy(): void { + if (this.filterSubscription) { + this.filterSubscription.unsubscribe(); + } + if (this.componentRef) { + this.componentRef.destroy(); + } + } + + private loadChartComponent(): void { + if (this.chartContainer && this.chartComponent) { + this.chartContainer.clear(); + const factory = this.chartContainer.createComponent(this.chartComponent); + this.componentRef = factory; + + // Set initial inputs + Object.keys(this.chartInputs).forEach(key => { + factory.instance[key] = this.chartInputs[key]; + }); + } + } + + private subscribeToFilters(): void { + this.filterSubscription = this.filterService.filterState$.subscribe(filterValues => { + this.updateChartWithFilters(filterValues); + }); + } + + private updateChartWithFilters(filterValues: any): void { + if (this.componentRef) { + // Add filter values to chart inputs + const updatedInputs = { + ...this.chartInputs, + filterValues: filterValues, + // Pass the query params string for easy API integration + filterQueryParams: this.filterService.buildQueryParams() + }; + + // Update chart component inputs + Object.keys(updatedInputs).forEach(key => { + this.componentRef!.instance[key] = updatedInputs[key]; + }); + + // Trigger change detection if the component has a method for it + if (this.componentRef!.instance.ngOnChanges) { + // We can't easily trigger ngOnChanges manually, but the input update should trigger it + } + + // If the chart component has a method to refresh data, call it + if (this.componentRef!.instance.refreshData) { + this.componentRef!.instance.refreshData(); + } + } + } +} \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.html new file mode 100644 index 0000000..9fa578a --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.html @@ -0,0 +1,128 @@ +
+ +
+

Common Filters

+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ + +
+
+
+
+ {{ filter.label }} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+
+
+
+ + +
+

No filters added yet. Click "Add Filter" to create your first filter.

+
+
\ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.scss new file mode 100644 index 0000000..444e788 --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.scss @@ -0,0 +1,103 @@ +.common-filter-container { + padding: 15px; + background: #f5f5f5; + border: 1px solid #ddd; + border-radius: 4px; + margin-bottom: 20px; + + .filter-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + + h4 { + margin: 0; + color: #333; + } + } + + .presets-section { + margin-bottom: 15px; + + .preset-controls { + display: flex; + gap: 10px; + align-items: center; + + select { + flex: 1; + } + } + } + + .save-preset-section { + margin-bottom: 15px; + } + + .filters-form { + .filters-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 15px; + + .filter-item { + background: white; + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 15px; + + .filter-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + + .filter-label { + font-weight: bold; + color: #333; + } + } + + .filter-type-select { + width: 100%; + margin-bottom: 10px; + } + + .clr-form-control { + margin-bottom: 10px; + + input, select, textarea { + width: 100%; + } + } + + .date-range-controls { + display: flex; + gap: 10px; + + .clr-form-control { + flex: 1; + } + } + + .toggle-control { + display: flex; + align-items: center; + gap: 8px; + } + + .multiselect { + height: 100px; + } + } + } + } + + .no-filters { + text-align: center; + padding: 20px; + color: #666; + font-style: italic; + } +} \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.spec.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.spec.ts new file mode 100644 index 0000000..3a65cec --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.spec.ts @@ -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; + 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'); + }); +}); \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.ts new file mode 100644 index 0000000..bf7209e --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/common-filter.component.ts @@ -0,0 +1,173 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { Filter, FilterService, FilterType } from './filter.service'; + +@Component({ + selector: 'app-common-filter', + templateUrl: './common-filter.component.html', + styleUrls: ['./common-filter.component.scss'] +}) +export class CommonFilterComponent implements OnInit, OnDestroy { + filters: Filter[] = []; + filterForm: FormGroup; + presets: string[] = []; + activePreset: string | null = null; + newPresetName: string = ''; + + private subscriptions: Subscription[] = []; + + constructor( + private filterService: FilterService, + private fb: FormBuilder + ) { + this.filterForm = this.fb.group({}); + } + + ngOnInit(): void { + // Subscribe to filter definitions + this.subscriptions.push( + this.filterService.filters$.subscribe(filters => { + this.filters = filters; + this.buildForm(); + }) + ); + + // Subscribe to filter state changes + this.subscriptions.push( + this.filterService.filterState$.subscribe(state => { + this.updateFormValues(state); + }) + ); + + // Subscribe to preset changes + this.subscriptions.push( + this.filterService.activePreset$.subscribe(preset => { + this.activePreset = preset; + }) + ); + + // Get initial presets + this.presets = this.filterService.getPresets(); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + // Build the form based on current filters + private buildForm(): void { + // Clear existing form controls + Object.keys(this.filterForm.controls).forEach(key => { + this.filterForm.removeControl(key); + }); + + // Add controls for each filter + this.filters.forEach(filter => { + let initialValue: any; + + switch (filter.type) { + case 'multiselect': + initialValue = filter.value || []; + break; + case 'date-range': + initialValue = filter.value || { start: null, end: null }; + break; + case 'toggle': + initialValue = filter.value || false; + break; + default: + initialValue = filter.value || ''; + } + + const control = this.fb.control(initialValue); + + // Subscribe to value changes for this control + control.valueChanges.subscribe(value => { + this.onFilterChange(filter.id, value); + }); + + this.filterForm.addControl(filter.id, control); + }); + } + + // Update form values based on filter state + private updateFormValues(state: any): void { + Object.keys(state).forEach(key => { + if (this.filterForm.contains(key)) { + this.filterForm.get(key)?.setValue(state[key], { emitEvent: false }); + } + }); + } + + // Handle filter value changes + onFilterChange(filterId: string, value: any): void { + this.filterService.updateFilterValue(filterId, value); + } + + // Handle multiselect changes + onMultiselectChange(filterId: string, selectedValues: string[]): void { + this.filterService.updateFilterValue(filterId, selectedValues); + } + + // Handle date range changes + onDateRangeChange(filterId: string, dateRange: { start: string | null, end: string | null }): void { + this.filterService.updateFilterValue(filterId, dateRange); + } + + // Handle toggle changes + onToggleChange(filterId: string, checked: boolean): void { + this.filterService.updateFilterValue(filterId, checked); + } + + // Reset all filters + resetFilters(): void { + this.filterService.resetFilters(); + } + + // Save current filter state as preset + savePreset(): void { + if (this.newPresetName.trim()) { + this.filterService.savePreset(this.newPresetName.trim()); + this.presets = this.filterService.getPresets(); + this.newPresetName = ''; + } + } + + // Load a preset + loadPreset(presetName: string): void { + this.filterService.loadPreset(presetName); + } + + // Delete a preset + deletePreset(presetName: string): void { + this.filterService.deletePreset(presetName); + this.presets = this.filterService.getPresets(); + } + + // Add a new filter + addFilter(): void { + const newFilter: Filter = { + id: `filter_${Date.now()}`, + field: '', + label: 'New Filter', + type: 'text' + }; + this.filterService.addFilter(newFilter); + } + + // Remove a filter + removeFilter(filterId: string): void { + this.filterService.removeFilter(filterId); + } + + // Update filter properties + updateFilter(filterId: string, property: string, value: any): void { + const filterIndex = this.filters.findIndex(f => f.id === filterId); + if (filterIndex !== -1) { + const updatedFilters = [...this.filters]; + (updatedFilters[filterIndex] as any)[property] = value; + this.filterService.setFilters(updatedFilters); + } + } +} \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/filter.service.spec.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/filter.service.spec.ts new file mode 100644 index 0000000..72e070f --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/filter.service.spec.ts @@ -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'); + }); +}); \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/filter.service.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/filter.service.ts new file mode 100644 index 0000000..1e246c1 --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/filter.service.ts @@ -0,0 +1,191 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; + +// Define the filter types +export type FilterType = 'dropdown' | 'multiselect' | 'date-range' | 'text' | 'toggle'; + +// Define the filter interface +export interface Filter { + id: string; + field: string; + label: string; + type: FilterType; + options?: string[]; // For dropdown and multiselect + value?: any; // Current value + placeholder?: string; +} + +// Define the filter state +export interface FilterState { + [key: string]: any; +} + +@Injectable({ + providedIn: 'root' +}) +export class FilterService { + // Store the filter definitions + private filtersSubject = new BehaviorSubject([]); + public filters$ = this.filtersSubject.asObservable(); + + // Store the current filter values + private filterStateSubject = new BehaviorSubject({}); + public filterState$ = this.filterStateSubject.asObservable(); + + // Store the active filter presets + private activePresetSubject = new BehaviorSubject(null); + public activePreset$ = this.activePresetSubject.asObservable(); + + // Store filter presets + private presets: { [key: string]: FilterState } = {}; + + constructor() { } + + // Add a new filter + addFilter(filter: Filter): void { + const currentFilters = this.filtersSubject.value; + this.filtersSubject.next([...currentFilters, filter]); + } + + // Remove a filter + removeFilter(filterId: string): void { + const currentFilters = this.filtersSubject.value; + const updatedFilters = currentFilters.filter(f => f.id !== filterId); + this.filtersSubject.next(updatedFilters); + + // Also remove the filter value from state + const currentState = this.filterStateSubject.value; + const newState = { ...currentState }; + delete newState[filterId]; + this.filterStateSubject.next(newState); + } + + // Update filter value + updateFilterValue(filterId: string, value: any): void { + const currentState = this.filterStateSubject.value; + this.filterStateSubject.next({ + ...currentState, + [filterId]: value + }); + } + + // Get current filter values + getFilterValues(): FilterState { + return this.filterStateSubject.value; + } + + // Reset all filters + resetFilters(): void { + const currentFilters = this.filtersSubject.value; + const resetState: FilterState = {}; + + // Initialize all filters with empty/default values + currentFilters.forEach(filter => { + switch (filter.type) { + case 'multiselect': + resetState[filter.id] = []; + break; + case 'date-range': + resetState[filter.id] = { start: null, end: null }; + break; + case 'toggle': + resetState[filter.id] = false; + break; + default: + resetState[filter.id] = ''; + } + }); + + this.filterStateSubject.next(resetState); + } + + // Save current filter state as a preset + savePreset(name: string): void { + this.presets[name] = this.filterStateSubject.value; + } + + // Load a preset + loadPreset(name: string): void { + if (this.presets[name]) { + this.filterStateSubject.next(this.presets[name]); + this.activePresetSubject.next(name); + } + } + + // Get all presets + getPresets(): string[] { + return Object.keys(this.presets); + } + + // Delete a preset + deletePreset(name: string): void { + delete this.presets[name]; + if (this.activePresetSubject.value === name) { + this.activePresetSubject.next(null); + } + } + + // Clear all presets + clearPresets(): void { + this.presets = {}; + this.activePresetSubject.next(null); + } + + // Build query parameters for API calls + buildQueryParams(): string { + const filterValues = this.getFilterValues(); + const params = new URLSearchParams(); + + Object.keys(filterValues).forEach(key => { + const value = filterValues[key]; + if (value !== undefined && value !== null && value !== '') { + if (typeof value === 'object') { + // Handle date ranges and other objects + if (value.hasOwnProperty('start') && value.hasOwnProperty('end')) { + // Date range + if (value.start) params.append(`${key}_start`, value.start); + if (value.end) params.append(`${key}_end`, value.end); + } else { + // Other objects as JSON + params.append(key, JSON.stringify(value)); + } + } else { + // Simple values + params.append(key, value.toString()); + } + } + }); + + return params.toString(); + } + + // Get filter definitions + getFilters(): Filter[] { + return this.filtersSubject.value; + } + + // Update filter definitions + setFilters(filters: Filter[]): void { + this.filtersSubject.next(filters); + + // Initialize filter state with default values + const initialState: FilterState = {}; + filters.forEach(filter => { + switch (filter.type) { + case 'multiselect': + initialState[filter.id] = filter.value || []; + break; + case 'date-range': + initialState[filter.id] = filter.value || { start: null, end: null }; + break; + case 'toggle': + initialState[filter.id] = filter.value || false; + break; + default: + initialState[filter.id] = filter.value || ''; + } + }); + + this.filterStateSubject.next(initialState); + } +} \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/index.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/index.ts new file mode 100644 index 0000000..401a53d --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/common-filter/index.ts @@ -0,0 +1,3 @@ +export * from './filter.service'; +export * from './common-filter.component'; +export * from './chart-wrapper.component'; \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/editnewdash/editnewdash.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/editnewdash/editnewdash.component.ts index dd020a6..641a480 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/editnewdash/editnewdash.component.ts +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/editnewdash/editnewdash.component.ts @@ -22,6 +22,8 @@ import { AlertsService } from 'src/app/services/fnd/alerts.service'; import { isArray } from 'highcharts'; // Add the SureconnectService import import { SureconnectService } from '../sureconnect/sureconnect.service'; +// Add the CommonFilterComponent import +import { CommonFilterComponent } from '../common-filter/common-filter.component'; function isNullArray(arr) { return !Array.isArray(arr) || arr.length === 0; @@ -45,6 +47,10 @@ export class EditnewdashComponent implements OnInit { public commonFilterForm: FormGroup; // Add common filter form WidgetsMock: WidgetModel[] = [ + { + name: 'Common Filter', + identifier: 'common_filter' + }, { name: 'Radar Chart', identifier: 'radar_chart' @@ -104,6 +110,7 @@ export class EditnewdashComponent implements OnInit { public dashArr: []; protected componentCollection = [ + { name: "Common Filter", componentInstance: CommonFilterComponent }, { name: "Line Chart", componentInstance: LineChartComponent }, { name: "Doughnut Chart", componentInstance: DoughnutChartComponent }, { name: "Radar Chart", componentInstance: RadarChartComponent }, @@ -500,6 +507,16 @@ export class EditnewdashComponent implements OnInit { component: ToDoChartComponent, name: "To Do Chart" }); + case "common_filter": + return this.dashboardArray.push({ + cols: 10, + rows: 3, + x: 0, + y: 0, + chartid: maxChartId + 1, + component: CommonFilterComponent, + name: "Common Filter" + }); case "grid_view": return this.dashboardArray.push({ cols: 5, diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.ts index ee9938d..3bf405f 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.ts +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.ts @@ -1,6 +1,7 @@ import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service'; import { Subscription } from 'rxjs'; +import { FilterService } from '../../common-filter/filter.service'; @Component({ selector: 'app-bar-chart', @@ -57,9 +58,20 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { // Subscriptions to unsubscribe on destroy private subscriptions: Subscription[] = []; - constructor(private dashboardService: Dashboard3Service) { } + constructor( + private dashboardService: Dashboard3Service, + private filterService: FilterService + ) { } ngOnInit(): void { + // Subscribe to filter changes + this.subscriptions.push( + this.filterService.filterState$.subscribe(filters => { + // When filters change, refresh the chart data + this.fetchChartData(); + }) + ); + // Initialize with default data this.fetchChartData(); } @@ -135,7 +147,49 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { filterParams = JSON.stringify(filterObj); } } - console.log('Base filter parameters:', filterParams); + + // Add common filters to filter parameters + const commonFilters = this.filterService.getFilterValues(); + console.log('Common filters from service:', commonFilters); + + if (Object.keys(commonFilters).length > 0) { + // Merge common filters with base filters + const mergedFilterObj = {}; + + // Add base filters first + if (filterParams) { + try { + const baseFilterObj = JSON.parse(filterParams); + Object.assign(mergedFilterObj, baseFilterObj); + } catch (e) { + console.warn('Failed to parse base filter parameters:', e); + } + } + + // Add common filters using the field name as the key, not the filter id + Object.keys(commonFilters).forEach(filterId => { + const filterValue = commonFilters[filterId]; + // Find the filter definition to get the field name + const filterDef = this.filterService.getFilters().find(f => f.id === filterId); + if (filterDef && filterDef.field) { + const fieldName = filterDef.field; + if (filterValue !== undefined && filterValue !== null && filterValue !== '') { + mergedFilterObj[fieldName] = filterValue; + } + } else { + // Fallback to using filterId as field name if no field is defined + if (filterValue !== undefined && filterValue !== null && filterValue !== '') { + mergedFilterObj[filterId] = filterValue; + } + } + }); + + if (Object.keys(mergedFilterObj).length > 0) { + filterParams = JSON.stringify(mergedFilterObj); + } + } + + console.log('Final filter parameters:', filterParams); // Fetch data from the dashboard service with parameter field and value // For base level, we pass empty parameter and value, but now also pass filters const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe( @@ -313,6 +367,36 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { drilldownFilterParams = JSON.stringify(filterObj); } } + + // Add common filters to drilldown filter parameters + const commonFilters = this.filterService.getFilterValues(); + if (Object.keys(commonFilters).length > 0) { + // Merge common filters with drilldown filters + const mergedFilterObj = {}; + + // Add drilldown filters first + if (drilldownFilterParams) { + try { + const drilldownFilterObj = JSON.parse(drilldownFilterParams); + Object.assign(mergedFilterObj, drilldownFilterObj); + } catch (e) { + console.warn('Failed to parse drilldown filter parameters:', e); + } + } + + // Add common filters + Object.keys(commonFilters).forEach(key => { + const value = commonFilters[key]; + if (value !== undefined && value !== null && value !== '') { + mergedFilterObj[key] = value; + } + }); + + if (Object.keys(mergedFilterObj).length > 0) { + drilldownFilterParams = JSON.stringify(mergedFilterObj); + } + } + console.log('Drilldown filter parameters:', drilldownFilterParams); // For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters @@ -426,6 +510,11 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { } } + // Public method to refresh data when filters change + refreshData(): void { + this.fetchChartData(); + } + // Ensure labels and data arrays have the same length private syncLabelAndDataArrays(): void { // For bar charts, we need to ensure all datasets have the same number of data points diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component.ts index 6582acc..9153a55 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component.ts +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component.ts @@ -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}` : ''}`; diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.ts index 92b8826..b54710e 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.ts +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.ts @@ -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); diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.ts index 523feb4..d074ef5 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.ts +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.ts @@ -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);