filter
This commit is contained in:
		
							parent
							
								
									98e0908920
								
							
						
					
					
						commit
						c6ad8b5c2f
					
				| @ -0,0 +1,113 @@ | ||||
| <!-- Configuration Mode --> | ||||
| <div class="compact-filter-config" *ngIf="isConfigMode"> | ||||
|   <div class="config-header"> | ||||
|     <h5>Compact Filter Configuration</h5> | ||||
|     <button class="btn btn-sm btn-link" (click)="cancelConfiguration()"> | ||||
|       <clr-icon shape="close"></clr-icon> | ||||
|     </button> | ||||
|   </div> | ||||
|    | ||||
|   <div class="config-form"> | ||||
|     <div class="clr-form-control"> | ||||
|       <label class="clr-control-label">API URL</label> | ||||
|       <input type="text" [(ngModel)]="configApiUrl" (ngModelChange)="onApiUrlChange($event)" placeholder="Enter API URL" class="clr-input"> | ||||
|     </div> | ||||
|      | ||||
|     <div class="clr-form-control"> | ||||
|       <label class="clr-control-label">Filter Key</label> | ||||
|       <select [(ngModel)]="configFilterKey" (ngModelChange)="onFilterKeyChange($event)" class="clr-select"> | ||||
|         <option value="">Select a key</option> | ||||
|         <option *ngFor="let key of availableKeys" [value]="key">{{ key }}</option> | ||||
|       </select> | ||||
|     </div> | ||||
|      | ||||
|     <div class="clr-form-control"> | ||||
|       <label class="clr-control-label">Filter Type</label> | ||||
|       <select [(ngModel)]="configFilterType" (ngModelChange)="onFilterTypeChange($event)" class="clr-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> | ||||
|      | ||||
|     <!-- Options will be automatically populated for dropdown/multiselect based on API data --> | ||||
|     <div class="clr-form-control" *ngIf="configFilterType === 'dropdown' || configFilterType === 'multiselect'"> | ||||
|       <label class="clr-control-label">Available Values (comma separated)</label> | ||||
|       <div class="available-values"> | ||||
|         {{ availableValues.join(', ') }} | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <div class="config-actions"> | ||||
|       <button class="btn btn-sm btn-outline" (click)="cancelConfiguration()">Cancel</button> | ||||
|       <button class="btn btn-sm btn-primary" (click)="saveConfiguration()">Save</button> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <!-- Display Mode --> | ||||
| <div class="compact-filter" *ngIf="!isConfigMode"> | ||||
|   <div class="filter-header"> | ||||
|     <span class="filter-label" *ngIf="filterLabel">{{ filterLabel }}</span> | ||||
|     <span class="filter-key" *ngIf="!filterLabel && filterKey">{{ filterKey }}</span> | ||||
|     <span class="filter-type">({{ filterType }})</span> | ||||
|     <button class="btn btn-icon btn-sm" (click)="toggleConfigMode()"> | ||||
|       <clr-icon shape="cog"></clr-icon> | ||||
|     </button> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Text Filter --> | ||||
|   <div class="filter-control" *ngIf="filterType === 'text'"> | ||||
|     <input type="text"  | ||||
|            [(ngModel)]="filterValue"  | ||||
|            (ngModelChange)="onFilterValueChange($event)" | ||||
|            [placeholder]="filterLabel || filterKey" | ||||
|            class="clr-input compact-input"> | ||||
|   </div> | ||||
| 
 | ||||
|   <!-- Dropdown Filter --> | ||||
|   <div class="filter-control" *ngIf="filterType === 'dropdown'"> | ||||
|     <select [(ngModel)]="filterValue"  | ||||
|             (ngModelChange)="onFilterValueChange($event)" | ||||
|             class="clr-select compact-select"> | ||||
|       <option value="">{{ filterLabel || filterKey }}</option> | ||||
|       <option *ngFor="let option of filterOptions" [value]="option">{{ option }}</option> | ||||
|     </select> | ||||
|   </div> | ||||
| 
 | ||||
|   <!-- Multi-Select Filter --> | ||||
|   <div class="filter-control" *ngIf="filterType === 'multiselect'"> | ||||
|     <select [(ngModel)]="filterValue"  | ||||
|             (ngModelChange)="onFilterValueChange($event)" | ||||
|             multiple  | ||||
|             class="clr-select compact-multiselect"> | ||||
|       <option *ngFor="let option of filterOptions" [value]="option">{{ option }}</option> | ||||
|     </select> | ||||
|   </div> | ||||
| 
 | ||||
|   <!-- Date Range Filter --> | ||||
|   <div class="filter-control date-range" *ngIf="filterType === 'date-range'"> | ||||
|     <input type="date"  | ||||
|            [(ngModel)]="filterValue.start"  | ||||
|            (ngModelChange)="onDateRangeChange({ start: $event, end: filterValue.end })" | ||||
|            placeholder="Start Date" | ||||
|            class="clr-input compact-date"> | ||||
|     <input type="date"  | ||||
|            [(ngModel)]="filterValue.end"  | ||||
|            (ngModelChange)="onDateRangeChange({ start: filterValue.start, end: $event })" | ||||
|            placeholder="End Date" | ||||
|            class="clr-input compact-date"> | ||||
|   </div> | ||||
| 
 | ||||
|   <!-- Toggle Filter --> | ||||
|   <div class="filter-control toggle" *ngIf="filterType === 'toggle'"> | ||||
|     <input type="checkbox"  | ||||
|            [(ngModel)]="filterValue"  | ||||
|            (ngModelChange)="onToggleChange($event)" | ||||
|            clrToggle  | ||||
|            class="clr-toggle"> | ||||
|     <label class="toggle-label">{{ filterLabel || filterKey }}</label> | ||||
|   </div> | ||||
| </div> | ||||
| @ -0,0 +1,189 @@ | ||||
| .compact-filter { | ||||
|   display: inline-block; | ||||
|   min-width: 120px; | ||||
|   max-width: 200px; | ||||
|   margin: 5px; | ||||
|   padding: 5px; | ||||
|   background: #f8f9fa; | ||||
|   border: 1px solid #e9ecef; | ||||
|   border-radius: 4px; | ||||
|   font-size: 12px; | ||||
|    | ||||
|   .filter-header { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|     margin-bottom: 5px; | ||||
|      | ||||
|     .filter-label, .filter-key { | ||||
|       font-weight: bold; | ||||
|       white-space: nowrap; | ||||
|       overflow: hidden; | ||||
|       text-overflow: ellipsis; | ||||
|       flex-grow: 1; | ||||
|     } | ||||
|      | ||||
|     .filter-type { | ||||
|       font-size: 10px; | ||||
|       color: #6c757d; | ||||
|       margin-left: 5px; | ||||
|     } | ||||
|      | ||||
|     .btn-icon { | ||||
|       padding: 2px; | ||||
|       min-width: auto; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   .filter-control { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 5px; | ||||
|      | ||||
|     &.date-range { | ||||
|       flex-direction: column; | ||||
|     } | ||||
|      | ||||
|     &.toggle { | ||||
|       justify-content: center; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   .compact-input, | ||||
|   .compact-select, | ||||
|   .compact-multiselect, | ||||
|   .compact-date { | ||||
|     width: 100%; | ||||
|     padding: 4px 6px; | ||||
|     font-size: 12px; | ||||
|     border-radius: 3px; | ||||
|     min-height: 24px; | ||||
|   } | ||||
|    | ||||
|   .compact-select, | ||||
|   .compact-multiselect { | ||||
|     height: 24px; | ||||
|   } | ||||
|    | ||||
|   .compact-multiselect { | ||||
|     height: auto; | ||||
|     min-height: 24px; | ||||
|   } | ||||
|    | ||||
|   .toggle-label { | ||||
|     margin: 0; | ||||
|     font-size: 12px; | ||||
|     white-space: nowrap; | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     max-width: 100px; | ||||
|   } | ||||
|    | ||||
|   .clr-toggle { | ||||
|     margin: 0 5px 0 0; | ||||
|   } | ||||
|    | ||||
|   /* Responsive design */ | ||||
|   @media (max-width: 768px) { | ||||
|     min-width: 100px; | ||||
|     max-width: 150px; | ||||
|      | ||||
|     .compact-input, | ||||
|     .compact-select, | ||||
|     .compact-multiselect, | ||||
|     .compact-date { | ||||
|       font-size: 11px; | ||||
|       padding: 3px 5px; | ||||
|     } | ||||
|      | ||||
|     .toggle-label { | ||||
|       font-size: 11px; | ||||
|       max-width: 80px; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   @media (max-width: 480px) { | ||||
|     min-width: 80px; | ||||
|     max-width: 120px; | ||||
|      | ||||
|     .compact-input, | ||||
|     .compact-select, | ||||
|     .compact-multiselect, | ||||
|     .compact-date { | ||||
|       font-size: 10px; | ||||
|       padding: 2px 4px; | ||||
|     } | ||||
|      | ||||
|     .toggle-label { | ||||
|       font-size: 10px; | ||||
|       max-width: 60px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .compact-filter-config { | ||||
|   min-width: 200px; | ||||
|   max-width: 300px; | ||||
|   padding: 10px; | ||||
|   background: #f8f9fa; | ||||
|   border: 1px solid #e9ecef; | ||||
|   border-radius: 4px; | ||||
|    | ||||
|   .config-header { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|     margin-bottom: 10px; | ||||
|      | ||||
|     h5 { | ||||
|       margin: 0; | ||||
|     } | ||||
|      | ||||
|     .btn-link { | ||||
|       padding: 2px; | ||||
|       min-width: auto; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   .config-form { | ||||
|     .clr-form-control { | ||||
|       margin-bottom: 8px; | ||||
|        | ||||
|       .clr-control-label { | ||||
|         font-size: 12px; | ||||
|       } | ||||
|        | ||||
|       .clr-input, | ||||
|       .clr-select, | ||||
|       .clr-textarea { | ||||
|         font-size: 12px; | ||||
|         padding: 4px 6px; | ||||
|       } | ||||
|        | ||||
|       .clr-textarea { | ||||
|         min-height: 60px; | ||||
|       } | ||||
|        | ||||
|       .available-values { | ||||
|         background: #e9ecef; | ||||
|         border-radius: 3px; | ||||
|         padding: 6px; | ||||
|         font-size: 11px; | ||||
|         margin-top: 4px; | ||||
|         word-break: break-all; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     .config-actions { | ||||
|       display: flex; | ||||
|       justify-content: flex-end; | ||||
|       gap: 5px; | ||||
|       margin-top: 10px; | ||||
|        | ||||
|       .btn { | ||||
|         font-size: 12px; | ||||
|         padding: 4px 8px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,218 @@ | ||||
| import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; | ||||
| import { FilterService, Filter } from './filter.service'; | ||||
| import { AlertsService } from 'src/app/services/fnd/alerts.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-compact-filter', | ||||
|   templateUrl: './compact-filter.component.html', | ||||
|   styleUrls: ['./compact-filter.component.scss'] | ||||
| }) | ||||
| export class CompactFilterComponent implements OnInit { | ||||
|   @Input() filterKey: string = ''; | ||||
|   @Input() filterType: string = 'text'; | ||||
|   @Input() filterOptions: string[] = []; | ||||
|   @Input() filterLabel: string = ''; | ||||
|   @Input() apiUrl: string = ''; | ||||
|   @Input() connectionId: number | undefined; | ||||
|   @Output() filterChange = new EventEmitter<any>(); | ||||
|   @Output() configChange = new EventEmitter<any>(); | ||||
|    | ||||
|   selectedFilter: Filter | null = null; | ||||
|   filterValue: any = ''; | ||||
|   availableFilters: Filter[] = []; | ||||
|   availableKeys: string[] = []; | ||||
|   availableValues: string[] = []; | ||||
|    | ||||
|   // Configuration properties
 | ||||
|   isConfigMode: boolean = false; | ||||
|   configFilterKey: string = ''; | ||||
|   configFilterType: string = 'text'; | ||||
|   configFilterOptions: string = ''; | ||||
|   configFilterLabel: string = ''; | ||||
|   configApiUrl: string = ''; | ||||
|   configConnectionId: number | undefined; | ||||
|    | ||||
|   constructor( | ||||
|     private filterService: FilterService, | ||||
|     private alertService: AlertsService | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     // Subscribe to filter definitions to get available filters
 | ||||
|     this.filterService.filters$.subscribe(filters => { | ||||
|       this.availableFilters = filters; | ||||
|       this.updateSelectedFilter(); | ||||
|     }); | ||||
|      | ||||
|     // Subscribe to filter state changes
 | ||||
|     this.filterService.filterState$.subscribe(state => { | ||||
|       if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) { | ||||
|         this.filterValue = state[this.selectedFilter.id]; | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Initialize configuration from inputs
 | ||||
|     this.configFilterKey = this.filterKey; | ||||
|     this.configFilterType = this.filterType; | ||||
|     this.configFilterLabel = this.filterLabel; | ||||
|     this.configFilterOptions = this.filterOptions.join(','); | ||||
|     this.configApiUrl = this.apiUrl; | ||||
|     this.configConnectionId = this.connectionId; | ||||
|      | ||||
|     // Load available keys and values if API URL and filter key are provided
 | ||||
|     if (this.apiUrl) { | ||||
|       this.loadAvailableKeys(); | ||||
|       // Load available values for the current filter key if it's a dropdown or multiselect
 | ||||
|       if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) { | ||||
|         this.loadAvailableValues(this.filterKey); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   updateSelectedFilter(): void { | ||||
|     if (this.filterKey && this.availableFilters.length > 0) { | ||||
|       this.selectedFilter = this.availableFilters.find(f => f.field === this.filterKey) || null; | ||||
|       if (this.selectedFilter) { | ||||
|         // Get current value for this filter
 | ||||
|         const currentState = this.filterService.getFilterValues(); | ||||
|         this.filterValue = currentState[this.selectedFilter.id] || ''; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   onFilterValueChange(value: any): void { | ||||
|     if (this.selectedFilter) { | ||||
|       this.filterValue = value; | ||||
|       this.filterService.updateFilterValue(this.selectedFilter.id, value); | ||||
|       this.filterChange.emit({ filterId: this.selectedFilter.id, value: value }); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   onToggleChange(checked: boolean): void { | ||||
|     this.onFilterValueChange(checked); | ||||
|   } | ||||
|    | ||||
|   onDateRangeChange(dateRange: { start: string | null, end: string | null }): void { | ||||
|     this.onFilterValueChange(dateRange); | ||||
|   } | ||||
|    | ||||
|   // Load available keys from API
 | ||||
|   loadAvailableKeys(): void { | ||||
|     if (this.apiUrl) { | ||||
|       this.alertService.getColumnfromurl(this.apiUrl, this.connectionId).subscribe( | ||||
|         (keys: string[]) => { | ||||
|           this.availableKeys = keys; | ||||
|         }, | ||||
|         (error) => { | ||||
|           console.error('Error loading available keys:', error); | ||||
|           this.availableKeys = []; | ||||
|         } | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Load available values for a specific key
 | ||||
|   loadAvailableValues(key: string): void { | ||||
|     if (this.apiUrl && key) { | ||||
|       this.alertService.getValuesFromUrl(this.apiUrl, this.connectionId, key).subscribe( | ||||
|         (values: string[]) => { | ||||
|           this.availableValues = values; | ||||
|           // Update filter options if this is a dropdown or multiselect
 | ||||
|           if (this.filterType === 'dropdown' || this.filterType === 'multiselect') { | ||||
|             this.filterOptions = values; | ||||
|           } | ||||
|         }, | ||||
|         (error) => { | ||||
|           console.error('Error loading available values:', error); | ||||
|           this.availableValues = []; | ||||
|         } | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Configuration methods
 | ||||
|   toggleConfigMode(): void { | ||||
|     this.isConfigMode = !this.isConfigMode; | ||||
|     if (this.isConfigMode) { | ||||
|       // Initialize config values
 | ||||
|       this.configFilterKey = this.filterKey; | ||||
|       this.configFilterType = this.filterType; | ||||
|       this.configFilterLabel = this.filterLabel; | ||||
|       this.configFilterOptions = this.filterOptions.join(','); | ||||
|       this.configApiUrl = this.apiUrl; | ||||
|       this.configConnectionId = this.connectionId; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   saveConfiguration(): void { | ||||
|     const config = { | ||||
|       filterKey: this.configFilterKey, | ||||
|       filterType: this.configFilterType, | ||||
|       filterLabel: this.configFilterLabel, | ||||
|       filterOptions: this.configFilterOptions.split(',').map(opt => opt.trim()).filter(opt => opt), | ||||
|       apiUrl: this.configApiUrl, | ||||
|       connectionId: this.configConnectionId | ||||
|     }; | ||||
|      | ||||
|     // Emit configuration change
 | ||||
|     this.configChange.emit(config); | ||||
|      | ||||
|     // Update local properties
 | ||||
|     this.filterKey = config.filterKey; | ||||
|     this.filterType = config.filterType; | ||||
|     this.filterLabel = config.filterLabel; | ||||
|     this.filterOptions = config.filterOptions; | ||||
|     this.apiUrl = config.apiUrl; | ||||
|     this.connectionId = config.connectionId; | ||||
|      | ||||
|     // Load available keys if API URL is provided
 | ||||
|     if (this.apiUrl) { | ||||
|       this.loadAvailableKeys(); | ||||
|     } | ||||
|      | ||||
|     // Load available values for the selected key if it's a dropdown or multiselect
 | ||||
|     if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) { | ||||
|       this.loadAvailableValues(this.filterKey); | ||||
|     } | ||||
|      | ||||
|     // Update selected filter
 | ||||
|     this.updateSelectedFilter(); | ||||
|      | ||||
|     // Exit config mode
 | ||||
|     this.isConfigMode = false; | ||||
|   } | ||||
|    | ||||
|   cancelConfiguration(): void { | ||||
|     this.isConfigMode = false; | ||||
|   } | ||||
|    | ||||
|   // Handle filter key change in configuration
 | ||||
|   onFilterKeyChange(key: string): void { | ||||
|     this.configFilterKey = key; | ||||
|     // Load available values for the selected key if it's a dropdown or multiselect
 | ||||
|     if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && key) { | ||||
|       this.loadAvailableValues(key); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Handle API URL change in configuration
 | ||||
|   onApiUrlChange(url: string): void { | ||||
|     this.configApiUrl = url; | ||||
|     // Load available keys when API URL changes
 | ||||
|     if (url) { | ||||
|       this.loadAvailableKeys(); | ||||
|       // Also clear available values since the API has changed
 | ||||
|       this.availableValues = []; | ||||
|       this.filterOptions = []; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Handle filter type change in configuration
 | ||||
|   onFilterTypeChange(type: string): void { | ||||
|     this.configFilterType = type; | ||||
|     // If changing to dropdown or multiselect and we have a key selected, load values
 | ||||
|     if ((type === 'dropdown' || type === 'multiselect') && this.configFilterKey) { | ||||
|       this.loadAvailableValues(this.configFilterKey); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,3 +1,4 @@ | ||||
| export * from './filter.service'; | ||||
| export * from './common-filter.component'; | ||||
| export * from './chart-wrapper.component'; | ||||
| export * from './chart-wrapper.component'; | ||||
| export * from './compact-filter.component'; | ||||
| @ -24,6 +24,8 @@ import { isArray } from 'highcharts'; | ||||
| import { SureconnectService } from '../sureconnect/sureconnect.service'; | ||||
| // Add the CommonFilterComponent import
 | ||||
| import { CommonFilterComponent } from '../common-filter/common-filter.component'; | ||||
| // Add the CompactFilterComponent import
 | ||||
| import { CompactFilterComponent } from '../common-filter'; | ||||
| 
 | ||||
| function isNullArray(arr) { | ||||
|   return !Array.isArray(arr) || arr.length === 0; | ||||
| @ -98,6 +100,10 @@ export class EditnewdashComponent implements OnInit { | ||||
|     { | ||||
|       name: 'Grid View', | ||||
|       identifier: 'grid_view' | ||||
|     }, | ||||
|     { | ||||
|       name: 'Compact Filter', | ||||
|       identifier: 'compact_filter' | ||||
|     } | ||||
|   ] | ||||
| 
 | ||||
| @ -123,6 +129,7 @@ export class EditnewdashComponent implements OnInit { | ||||
|     { name: "Financial Chart", componentInstance: FinancialChartComponent }, | ||||
|     { name: "To Do Chart", componentInstance: ToDoChartComponent }, | ||||
|     { name: "Grid View", componentInstance: GridViewComponent }, | ||||
|     { name: "Compact Filter", componentInstance: CompactFilterComponent }, // Add this line
 | ||||
|   ]; | ||||
|   model: any; | ||||
|   linesdata: any; | ||||
| @ -168,7 +175,12 @@ export class EditnewdashComponent implements OnInit { | ||||
|     drilldownLayers: [] as any[], | ||||
|     // Common filter properties
 | ||||
|     commonFilterEnabled: false, | ||||
|     commonFilterEnabledDrilldown: false | ||||
|     commonFilterEnabledDrilldown: false, | ||||
|     // Compact filter properties
 | ||||
|     filterKey: '', | ||||
|     filterType: 'text', | ||||
|     filterLabel: '', | ||||
|     filterOptions: [] as string[] | ||||
|   }; | ||||
| 
 | ||||
|   // Add sureconnect data property
 | ||||
| @ -519,6 +531,21 @@ export class EditnewdashComponent implements OnInit { | ||||
|           component: CommonFilterComponent, | ||||
|           name: "Common Filter" | ||||
|         }); | ||||
|       case "compact_filter": | ||||
|         return this.dashboardArray.push({ | ||||
|           cols: 3, | ||||
|           rows: 2, | ||||
|           x: 0, | ||||
|           y: 0, | ||||
|           chartid: maxChartId + 1, | ||||
|           component: CompactFilterComponent, | ||||
|           name: "Compact Filter", | ||||
|           // Add default configuration for compact filter
 | ||||
|           filterKey: '', | ||||
|           filterType: 'text', | ||||
|           filterLabel: '', | ||||
|           filterOptions: [] | ||||
|         }); | ||||
|       case "grid_view": | ||||
|         return this.dashboardArray.push({ | ||||
|           cols: 5, | ||||
| @ -561,6 +588,19 @@ export class EditnewdashComponent implements OnInit { | ||||
|     if (item['commonFilterEnabledDrilldown'] === undefined) {  | ||||
|       this.gadgetsEditdata['commonFilterEnabledDrilldown'] = false;  | ||||
|     } | ||||
|     // Initialize compact filter properties if not present
 | ||||
|     if (item['filterKey'] === undefined) {  | ||||
|       this.gadgetsEditdata['filterKey'] = '';  | ||||
|     } | ||||
|     if (item['filterType'] === undefined) {  | ||||
|       this.gadgetsEditdata['filterType'] = 'text';  | ||||
|     } | ||||
|     if (item['filterLabel'] === undefined) {  | ||||
|       this.gadgetsEditdata['filterLabel'] = '';  | ||||
|     } | ||||
|     if (item['filterOptions'] === undefined) {  | ||||
|       this.gadgetsEditdata['filterOptions'] = [];  | ||||
|     } | ||||
|     this.getStores(); | ||||
|      | ||||
|     // Set default connection if none is set and we have connections
 | ||||
| @ -736,6 +776,14 @@ export class EditnewdashComponent implements OnInit { | ||||
|         xyz.drilldownLayers = this.gadgetsEditdata.drilldownLayers; | ||||
|         xyz.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
 | ||||
|          | ||||
|         // For compact filter, preserve filter configuration properties
 | ||||
|         if (item.component && item.component.name === 'CompactFilterComponent') { | ||||
|           xyz.filterKey = this.gadgetsEditdata.filterKey || ''; | ||||
|           xyz.filterType = this.gadgetsEditdata.filterType || 'text'; | ||||
|           xyz.filterLabel = this.gadgetsEditdata.filterLabel || ''; | ||||
|           xyz.filterOptions = this.gadgetsEditdata.filterOptions || []; | ||||
|         } | ||||
|          | ||||
|         console.log(xyz); | ||||
|         return xyz; | ||||
|       } | ||||
| @ -809,6 +857,16 @@ export class EditnewdashComponent implements OnInit { | ||||
|       chartInputs['connection'] = item['connection'] || undefined; | ||||
|     } | ||||
|      | ||||
|     // For CompactFilterComponent, pass filter configuration properties
 | ||||
|     if (item.component && item.component.name === 'CompactFilterComponent') { | ||||
|       chartInputs['filterKey'] = item['filterKey'] || ''; | ||||
|       chartInputs['filterType'] = item['filterType'] || 'text'; | ||||
|       chartInputs['filterLabel'] = item['filterLabel'] || ''; | ||||
|       chartInputs['filterOptions'] = item['filterOptions'] || []; | ||||
|       chartInputs['apiUrl'] = item['table'] || ''; // Use table as API URL
 | ||||
|       chartInputs['connectionId'] = item['connection'] ? parseInt(item['connection'], 10) : undefined; | ||||
|     } | ||||
|      | ||||
|     // Remove undefined properties to avoid passing unnecessary data
 | ||||
|     Object.keys(chartInputs).forEach(key => { | ||||
|       if (chartInputs[key] === undefined) { | ||||
| @ -863,6 +921,16 @@ export class EditnewdashComponent implements OnInit { | ||||
|         updatedItem.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
 | ||||
|         updatedItem.commonFilterEnabledDrilldown = this.gadgetsEditdata.commonFilterEnabledDrilldown; // Add drilldown common filter property
 | ||||
|          | ||||
|         // For compact filter, preserve filter configuration properties
 | ||||
|         if (item.component && item.component.name === 'CompactFilterComponent') { | ||||
|           updatedItem.filterKey = this.gadgetsEditdata.filterKey || ''; | ||||
|           updatedItem.filterType = this.gadgetsEditdata.filterType || 'text'; | ||||
|           updatedItem.filterLabel = this.gadgetsEditdata.filterLabel || ''; | ||||
|           updatedItem.filterOptions = this.gadgetsEditdata.filterOptions || []; | ||||
|           updatedItem.table = this.gadgetsEditdata.table || ''; // API URL
 | ||||
|           updatedItem.connection = this.gadgetsEditdata.connection || undefined; // Connection ID
 | ||||
|         } | ||||
|          | ||||
|         console.log('Updated item:', updatedItem); | ||||
|         return updatedItem; | ||||
|       } | ||||
|  | ||||
| @ -1,32 +1,50 @@ | ||||
| <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 --> | ||||
| <div class="chart-container"> | ||||
|   <!-- Compact Filters --> | ||||
|   <div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0"> | ||||
|     <app-compact-filter  | ||||
|       *ngFor="let filter of baseFilters"  | ||||
|       [filterKey]="filter.field" | ||||
|       (filterChange)="onFilterChange($event)"> | ||||
|     </app-compact-filter> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Drilldown mode indicator --> | ||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> | ||||
|     <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> | ||||
|     <button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> | ||||
|   <div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator"> | ||||
|     <span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span> | ||||
|     <button class="btn btn-secondary btn-sm" (click)="navigateBack()"> | ||||
|       Back to Level {{currentDrilldownLevel - 1}} | ||||
|     </button> | ||||
|     <button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> | ||||
|     <button class="btn btn-danger btn-sm" (click)="resetToOriginalData()"> | ||||
|       Back to Main View | ||||
|     </button> | ||||
|   </div> | ||||
|    | ||||
|   <!-- No data message --> | ||||
|   <div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;"> | ||||
|     No data available | ||||
|   <div class="chart-header"> | ||||
|     <h3>{{ charttitle || 'Bar Chart' }}</h3> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Chart display --> | ||||
|   <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);"> | ||||
|     <canvas baseChart | ||||
|       [datasets]="barChartData" | ||||
|       [labels]="barChartLabels" | ||||
|       [type]="barChartType" | ||||
|       [options]="barChartOptions" | ||||
|       (chartHover)="chartHovered($event)" | ||||
|       (chartClick)="chartClicked($event)"> | ||||
|     </canvas> | ||||
|   <div class="chart-wrapper"> | ||||
|     <div class="chart-content" [class.loading]="barChartData.length === 0 && barChartLabels.length === 0 && !noDataAvailable"> | ||||
|       <!-- No data message --> | ||||
|       <div class="no-data-message" *ngIf="noDataAvailable"> | ||||
|         <p>No data available</p> | ||||
|       </div> | ||||
|        | ||||
|       <!-- Chart display --> | ||||
|       <canvas baseChart | ||||
|         *ngIf="!noDataAvailable" | ||||
|         [datasets]="barChartData" | ||||
|         [labels]="barChartLabels" | ||||
|         [type]="barChartType" | ||||
|         [options]="barChartOptions" | ||||
|         (chartHover)="chartHovered($event)" | ||||
|         (chartClick)="chartClicked($event)"> | ||||
|       </canvas> | ||||
|        | ||||
|       <!-- Loading overlay --> | ||||
|       <div class="loading-overlay" *ngIf="barChartData.length === 0 && barChartLabels.length === 0 && !noDataAvailable"> | ||||
|         <div class="shimmer-bar"></div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| @ -5,27 +5,214 @@ | ||||
|   width: 100%; | ||||
| } | ||||
| 
 | ||||
| .bar-chart-container { | ||||
|   position: relative; | ||||
| .chart-container { | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); | ||||
|   border-radius: 12px; | ||||
|   box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); | ||||
|   font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||||
|   transition: all 0.3s ease; | ||||
|   border: 1px solid #eaeaea; | ||||
|   padding: 20px; | ||||
| } | ||||
| 
 | ||||
| canvas { | ||||
|   display: block; | ||||
|   max-width: 100%; | ||||
|   max-height: 100%; | ||||
| .chart-container:hover { | ||||
|   box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2); | ||||
|   transform: translateY(-2px); | ||||
| } | ||||
| 
 | ||||
| .compact-filters-container { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   gap: 5px; | ||||
|   margin-bottom: 10px; | ||||
|   padding: 5px; | ||||
|   background: #ffffff; | ||||
|   border: 1px solid #e9ecef; | ||||
|   border-radius: 6px; | ||||
|   min-height: 40px; | ||||
| } | ||||
| 
 | ||||
| .drilldown-indicator { | ||||
|   background-color: #e0e0e0; | ||||
|   padding: 10px; | ||||
|   margin-bottom: 15px; | ||||
|   border-radius: 8px; | ||||
|   text-align: center; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   gap: 10px; | ||||
|   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||||
| } | ||||
| 
 | ||||
| .drilldown-text { | ||||
|   font-weight: bold; | ||||
|   color: #333; | ||||
|   font-size: 16px; | ||||
| } | ||||
| 
 | ||||
| .btn { | ||||
|   padding: 6px 12px; | ||||
|   border: none; | ||||
|   border-radius: 4px; | ||||
|   cursor: pointer; | ||||
|   font-size: 14px; | ||||
|   transition: all 0.3s ease; | ||||
| } | ||||
| 
 | ||||
| .btn:hover { | ||||
|   transform: translateY(-2px); | ||||
|   box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); | ||||
| } | ||||
| 
 | ||||
| .btn-sm { | ||||
|   padding: 4px 8px; | ||||
|   font-size: 12px; | ||||
| } | ||||
| 
 | ||||
| .btn-secondary { | ||||
|   background-color: #007cba; | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| .btn-danger { | ||||
|   background-color: #dc3545; | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| .chart-header { | ||||
|   margin-bottom: 20px; | ||||
|    | ||||
|   h3 { | ||||
|     font-size: 22px; | ||||
|     font-weight: 600; | ||||
|     color: #0a192f; | ||||
|     margin: 0; | ||||
|     text-align: center; | ||||
|     padding-bottom: 10px; | ||||
|     border-bottom: 2px solid #3498db; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .chart-wrapper { | ||||
|   flex: 1; | ||||
|   position: relative; | ||||
|    | ||||
|   .chart-content { | ||||
|     position: relative; | ||||
|     height: 100%; | ||||
|     background: #f8f9fa; | ||||
|     border: 1px solid #e9ecef; | ||||
|     border-radius: 8px; | ||||
|     padding: 10px; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|      | ||||
|     &.loading { | ||||
|       opacity: 0.7; | ||||
|        | ||||
|       canvas { | ||||
|         filter: blur(2px); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     canvas { | ||||
|       max-width: 100%; | ||||
|       max-height: 100%; | ||||
|       transition: filter 0.3s ease; | ||||
|       filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1)); | ||||
|     } | ||||
|      | ||||
|     canvas:hover { | ||||
|       filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15)); | ||||
|       transform: scale(1.02); | ||||
|       transition: all 0.3s ease; | ||||
|     } | ||||
|      | ||||
|     .no-data-message { | ||||
|       text-align: center; | ||||
|       padding: 30px; | ||||
|       color: #666; | ||||
|       font-size: 18px; | ||||
|       font-style: italic; | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       align-items: center; | ||||
|       justify-content: center; | ||||
|       height: 100%; | ||||
|       width: 100%; | ||||
|     } | ||||
|      | ||||
|     .no-data-message p { | ||||
|       margin: 0; | ||||
|     } | ||||
|      | ||||
|     .loading-overlay { | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       left: 0; | ||||
|       right: 0; | ||||
|       bottom: 0; | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: center; | ||||
|       background: rgba(255, 255, 255, 0.8); | ||||
|        | ||||
|       .shimmer-bar { | ||||
|         width: 80%; | ||||
|         height: 20px; | ||||
|         background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); | ||||
|         background-size: 200% 100%; | ||||
|         animation: shimmer 1.5s infinite; | ||||
|         border-radius: 4px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes shimmer { | ||||
|   0% { | ||||
|     background-position: -200% 0; | ||||
|   } | ||||
|   100% { | ||||
|     background-position: 200% 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Responsive design for chart container | ||||
| @media (max-width: 768px) { | ||||
|   .bar-chart-container { | ||||
|     height: 300px; | ||||
|   .chart-container { | ||||
|     padding: 15px; | ||||
|   } | ||||
|    | ||||
|   .chart-header h3 { | ||||
|     font-size: 18px; | ||||
|   } | ||||
|    | ||||
|   .drilldown-indicator { | ||||
|     flex-direction: column; | ||||
|     gap: 5px; | ||||
|   } | ||||
|    | ||||
|   .drilldown-text { | ||||
|     font-size: 14px; | ||||
|   } | ||||
|    | ||||
|   .compact-filters-container { | ||||
|     flex-wrap: wrap; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 480px) { | ||||
|   .bar-chart-container { | ||||
|     height: 250px; | ||||
|   .chart-container { | ||||
|     padding: 10px; | ||||
|   } | ||||
|    | ||||
|   .chart-header h3 { | ||||
|     font-size: 16px; | ||||
|   } | ||||
| } | ||||
| @ -140,6 +140,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy { | ||||
|       console.log('Chart legend changed to:', this.barChartLegend); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Handle filter changes from compact filters
 | ||||
|   onFilterChange(event: { filterId: string, value: any }): void { | ||||
|     console.log('Compact filter changed:', event); | ||||
|     // The filter service will automatically trigger chart updates through the subscription
 | ||||
|   } | ||||
| 
 | ||||
|   fetchChartData(): void { | ||||
|     // Set flag to prevent recursive calls
 | ||||
|  | ||||
| @ -1,39 +1,53 @@ | ||||
| <div class="doughnut-chart-container"> | ||||
|   <!-- Compact Filters --> | ||||
|   <div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0"> | ||||
|     <app-compact-filter  | ||||
|       *ngFor="let filter of baseFilters"  | ||||
|       [filterKey]="filter.field" | ||||
|       (filterChange)="onFilterChange($event)"> | ||||
|     </app-compact-filter> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Drilldown mode indicator --> | ||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> | ||||
|     <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> | ||||
|     <button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> | ||||
|   <div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator"> | ||||
|     <span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span> | ||||
|     <button class="btn btn-secondary btn-sm" (click)="navigateBack()"> | ||||
|       Back to Level {{currentDrilldownLevel - 1}} | ||||
|     </button> | ||||
|     <button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> | ||||
|     <button class="btn btn-danger btn-sm" (click)="resetToOriginalData()"> | ||||
|       Back to Main View | ||||
|     </button> | ||||
|   </div> | ||||
|    | ||||
|   <h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3> | ||||
|   <div class="chart-wrapper"> | ||||
|     <!-- Show loading indicator --> | ||||
|     <div class="loading-indicator" *ngIf="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable"> | ||||
|       <div class="spinner"></div> | ||||
|       <p>Loading chart data...</p> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Show no data message --> | ||||
|     <div class="no-data-message" *ngIf="noDataAvailable"> | ||||
|       <p>No chart data available</p> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Show chart when data is available --> | ||||
|     <canvas baseChart  | ||||
|       *ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0" | ||||
|       [data]="doughnutChartData"  | ||||
|       [labels]="doughnutChartLabels"  | ||||
|       [type]="doughnutChartType" | ||||
|       [options]="doughnutChartOptions" | ||||
|       (chartHover)="chartHovered($event)" | ||||
|       (chartClick)="chartClicked($event)"> | ||||
|     </canvas> | ||||
|   <div class="chart-header"> | ||||
|     <h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3> | ||||
|   </div> | ||||
|    | ||||
|   <div class="chart-wrapper"> | ||||
|     <div class="chart-content" [class.loading]="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable"> | ||||
|       <!-- Show no data message --> | ||||
|       <div class="no-data-message" *ngIf="noDataAvailable"> | ||||
|         <p>No chart data available</p> | ||||
|       </div> | ||||
|        | ||||
|       <!-- Show chart when data is available --> | ||||
|       <canvas baseChart  | ||||
|         *ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0" | ||||
|         [data]="doughnutChartData"  | ||||
|         [labels]="doughnutChartLabels"  | ||||
|         [type]="doughnutChartType" | ||||
|         [options]="doughnutChartOptions" | ||||
|         (chartHover)="chartHovered($event)" | ||||
|         (chartClick)="chartClicked($event)"> | ||||
|       </canvas> | ||||
|        | ||||
|       <!-- Loading overlay --> | ||||
|       <div class="loading-overlay" *ngIf="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable"> | ||||
|         <div class="shimmer-donut"></div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <div class="chart-legend" *ngIf="!noDataAvailable && showlabel && doughnutChartLabels && doughnutChartLabels.length > 0"> | ||||
|     <div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index"> | ||||
|       <span class="legend-color" [style.background-color]="getLegendColor(i)"></span> | ||||
|  | ||||
| @ -17,17 +17,78 @@ | ||||
|   transform: translateY(-2px); | ||||
| } | ||||
| 
 | ||||
| .compact-filters-container { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   gap: 5px; | ||||
|   margin-bottom: 10px; | ||||
|   padding: 5px; | ||||
|   background: #ffffff; | ||||
|   border: 1px solid #e9ecef; | ||||
|   border-radius: 6px; | ||||
|   min-height: 40px; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| .chart-title { | ||||
|   font-size: 26px; | ||||
|   font-weight: 700; | ||||
|   color: #2c3e50; | ||||
|   margin-bottom: 20px; | ||||
| .drilldown-indicator { | ||||
|   background-color: #e0e0e0; | ||||
|   padding: 10px; | ||||
|   margin-bottom: 15px; | ||||
|   border-radius: 8px; | ||||
|   text-align: center; | ||||
|   padding-bottom: 15px; | ||||
|   border-bottom: 2px solid #3498db; | ||||
|   text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   gap: 10px; | ||||
|   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||||
| } | ||||
| 
 | ||||
| .drilldown-text { | ||||
|   font-weight: bold; | ||||
|   color: #333; | ||||
|   font-size: 16px; | ||||
| } | ||||
| 
 | ||||
| .btn { | ||||
|   padding: 6px 12px; | ||||
|   border: none; | ||||
|   border-radius: 4px; | ||||
|   cursor: pointer; | ||||
|   font-size: 14px; | ||||
|   transition: all 0.3s ease; | ||||
| } | ||||
| 
 | ||||
| .btn:hover { | ||||
|   transform: translateY(-2px); | ||||
|   box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); | ||||
| } | ||||
| 
 | ||||
| .btn-sm { | ||||
|   padding: 4px 8px; | ||||
|   font-size: 12px; | ||||
| } | ||||
| 
 | ||||
| .btn-secondary { | ||||
|   background-color: #007cba; | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| .btn-danger { | ||||
|   background-color: #dc3545; | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| .chart-header { | ||||
|   margin-bottom: 20px; | ||||
|    | ||||
|   .chart-title { | ||||
|     font-size: 22px; | ||||
|     font-weight: 600; | ||||
|     color: #0a192f; | ||||
|     margin: 0; | ||||
|     text-align: center; | ||||
|     padding-bottom: 10px; | ||||
|     border-bottom: 2px solid #3498db; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .chart-wrapper { | ||||
| @ -56,6 +117,62 @@ | ||||
|   transition: all 0.3s ease; | ||||
| } | ||||
| 
 | ||||
| .chart-content { | ||||
|   position: relative; | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|    | ||||
|   &.loading { | ||||
|     opacity: 0.7; | ||||
|      | ||||
|     canvas { | ||||
|       filter: blur(2px); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   .no-data-message { | ||||
|     text-align: center; | ||||
|     padding: 30px; | ||||
|     color: #666; | ||||
|     font-size: 18px; | ||||
|     font-style: italic; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     height: 100%; | ||||
|     width: 100%; | ||||
|   } | ||||
|    | ||||
|   .no-data-message p { | ||||
|     margin: 0; | ||||
|   } | ||||
|    | ||||
|   .loading-overlay { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     bottom: 0; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     background: rgba(255, 255, 255, 0.8); | ||||
|      | ||||
|     .shimmer-donut { | ||||
|       width: 120px; | ||||
|       height: 120px; | ||||
|       border-radius: 50%; | ||||
|       background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); | ||||
|       background-size: 200% 100%; | ||||
|       animation: shimmer 1.5s infinite; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .chart-legend { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
| @ -119,36 +236,13 @@ | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .loading-indicator, .no-data-message { | ||||
|   text-align: center; | ||||
|   padding: 30px; | ||||
|   color: #666; | ||||
|   font-size: 18px; | ||||
|   font-style: italic; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   height: 100%; | ||||
| } | ||||
| 
 | ||||
| .loading-indicator p, .no-data-message p { | ||||
|   margin: 10px 0 0 0; | ||||
| } | ||||
| 
 | ||||
| .spinner { | ||||
|   border: 4px solid #f3f3f3; | ||||
|   border-top: 4px solid #3498db; | ||||
|   border-radius: 50%; | ||||
|   width: 40px; | ||||
|   height: 40px; | ||||
|   animation: spin 1s linear infinite; | ||||
|   margin-bottom: 10px; | ||||
| } | ||||
| 
 | ||||
| @keyframes spin { | ||||
|   0% { transform: rotate(0deg); } | ||||
|   100% { transform: rotate(360deg); } | ||||
| @keyframes shimmer { | ||||
|   0% { | ||||
|     background-position: -200% 0; | ||||
|   } | ||||
|   100% { | ||||
|     background-position: 200% 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /* Responsive design */ | ||||
| @ -157,9 +251,17 @@ | ||||
|     padding: 15px; | ||||
|   } | ||||
|    | ||||
|   .chart-title { | ||||
|     font-size: 20px; | ||||
|     margin-bottom: 15px; | ||||
|   .chart-header .chart-title { | ||||
|     font-size: 18px; | ||||
|   } | ||||
|    | ||||
|   .drilldown-indicator { | ||||
|     flex-direction: column; | ||||
|     gap: 5px; | ||||
|   } | ||||
|    | ||||
|   .drilldown-text { | ||||
|     font-size: 14px; | ||||
|   } | ||||
|    | ||||
|   .chart-wrapper { | ||||
| @ -181,4 +283,8 @@ | ||||
|     font-size: 16px; | ||||
|     padding: 20px; | ||||
|   } | ||||
|    | ||||
|   .compact-filters-container { | ||||
|     flex-wrap: wrap; | ||||
|   } | ||||
| } | ||||
| @ -200,6 +200,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|     this.subscriptions.forEach(sub => sub.unsubscribe()); | ||||
|   } | ||||
|    | ||||
|   // Handle filter changes from compact filters
 | ||||
|   onFilterChange(event: { filterId: string, value: any }): void { | ||||
|     console.log('Compact filter changed:', event); | ||||
|     // The filter service will automatically trigger chart updates through the subscription
 | ||||
|   } | ||||
|    | ||||
|   // Public method to refresh data when filters change
 | ||||
|   refreshData(): void { | ||||
|     this.fetchChartData(); | ||||
|  | ||||
| @ -0,0 +1,25 @@ | ||||
| <div class="sidebar-filters"> | ||||
|   <!-- Component Palette Button --> | ||||
|   <div class="component-palette-section"> | ||||
|     <button class="component-palette-button" (click)="toggleComponentPalette()"> | ||||
|       <clr-icon shape="plus-circle"></clr-icon> | ||||
|       Add Components | ||||
|     </button> | ||||
|      | ||||
|     <!-- Component Palette (hidden by default) --> | ||||
|     <div class="component-palette" *ngIf="showComponentPalette"> | ||||
|       <h3 class="palette-title">Available Components</h3> | ||||
|       <div class="component-list"> | ||||
|         <div  | ||||
|           *ngFor="let component of availableComponents"  | ||||
|           class="component-item" | ||||
|           draggable="true" | ||||
|           (dragstart)="onComponentDragStart($event, component)" | ||||
|           (dragend)="onComponentDragEnd($event)"> | ||||
|           <clr-icon shape="drag-handle" class="drag-icon"></clr-icon> | ||||
|           {{ component.name }} | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| @ -0,0 +1,16 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { DashboardFilterService } from '../../services/dashboard-filter.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-shield-sidebar-filters', | ||||
|   templateUrl: './sidebar-filters.component.html', | ||||
|   styleUrls: ['./sidebar-filters.component.scss'] | ||||
| }) | ||||
| export class SidebarFiltersComponent implements OnInit { | ||||
| 
 | ||||
|   constructor(private filterService: DashboardFilterService) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     // Component initialization | ||||
|   } | ||||
| } | ||||
| @ -74,6 +74,10 @@ export class ShieldDashboardComponent implements OnInit { | ||||
|     { | ||||
|       name: 'Quarterwise Flow', | ||||
|       identifier: 'line_chart' | ||||
|     }, | ||||
|     { | ||||
|       name: 'Compact Filter', | ||||
|       identifier: 'compact_filter' | ||||
|     } | ||||
|   ]; | ||||
|    | ||||
| @ -300,6 +304,20 @@ export class ShieldDashboardComponent implements OnInit { | ||||
|           drilldownLayers: [] | ||||
|         }; | ||||
|         break; | ||||
|       case "compact_filter": | ||||
|         newItem = {  | ||||
|           cols: 3,  | ||||
|           rows: 2,  | ||||
|           y: 0,  | ||||
|           x: 0,  | ||||
|           chartType: 'compact-filter',  | ||||
|           name: 'Compact Filter',  | ||||
|           id: newId, | ||||
|           baseFilters: [], | ||||
|           drilldownEnabled: false, | ||||
|           drilldownLayers: [] | ||||
|         }; | ||||
|         break; | ||||
|       default: | ||||
|         newItem = {  | ||||
|           cols: 5,  | ||||
|  | ||||
| @ -0,0 +1,223 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { GridsterConfig, GridsterItem } from 'angular-gridster2'; | ||||
| 
 | ||||
| interface ShieldDashboardItem extends GridsterItem { | ||||
|   chartType: string; | ||||
|   name: string; | ||||
|   id: number; | ||||
|   component?: any; | ||||
| } | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-shield-dashboard', | ||||
|   templateUrl: './shield-dashboard.component.html', | ||||
|   styleUrls: ['./shield-dashboard.component.scss'] | ||||
| }) | ||||
| export class ShieldDashboardComponent implements OnInit { | ||||
|   options: GridsterConfig; | ||||
|   dashboard: Array<ShieldDashboardItem>; | ||||
|    | ||||
|   // Keep track of deleted items | ||||
|   deletedItems: Array<ShieldDashboardItem> = []; | ||||
| 
 | ||||
|   constructor() { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.options = { | ||||
|       gridType: 'fit', | ||||
|       enableEmptyCellDrop: true, | ||||
|       emptyCellDropCallback: this.onDrop, | ||||
|       pushItems: true, | ||||
|       swap: true, | ||||
|       pushDirections: { north: true, east: true, south: true, west: true }, | ||||
|       resizable: { enabled: true }, | ||||
|       itemChangeCallback: this.itemChange.bind(this), | ||||
|       draggable: { | ||||
|         enabled: true, | ||||
|         ignoreContent: true, | ||||
|         dropOverItems: true, | ||||
|         dragHandleClass: 'drag-handler', | ||||
|         ignoreContentClass: 'no-drag', | ||||
|       }, | ||||
|       displayGrid: 'always', | ||||
|       minCols: 10, | ||||
|       minRows: 10, | ||||
|       itemResizeCallback: this.itemResize.bind(this) | ||||
|     }; | ||||
| 
 | ||||
|     // Initialize the dashboard with empty canvas | ||||
|     this.dashboard = []; | ||||
|   } | ||||
| 
 | ||||
|   onDrop = (event: any) => { | ||||
|     // Handle dropping new components onto the dashboard | ||||
|     console.log('Item dropped:', event); | ||||
|      | ||||
|     // Get the component identifier from the drag event | ||||
|     const componentType = event.dataTransfer ? event.dataTransfer.getData('widgetIdentifier') : ''; | ||||
|     console.log('Component type dropped:', componentType); | ||||
|      | ||||
|     if (componentType) { | ||||
|       this.addComponentToDashboard(componentType); | ||||
|     } else { | ||||
|       console.log('No component type found in drag data'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Add a new component to the dashboard | ||||
|   addComponentToDashboard(componentType: string) { | ||||
|     // Generate a new ID for the component | ||||
|     const newId = this.dashboard.length > 0 ? Math.max(...this.dashboard.map(item => item.id), 0) + 1 : 1; | ||||
|      | ||||
|     let newItem: ShieldDashboardItem; | ||||
|      | ||||
|     switch (componentType) { | ||||
|       case "bar_chart": | ||||
|         newItem = {  | ||||
|           cols: 5,  | ||||
|           rows: 6,  | ||||
|           y: 0,  | ||||
|           x: 0,  | ||||
|           chartType: 'bar-chart',  | ||||
|           name: 'Bar Chart',  | ||||
|           id: newId | ||||
|         }; | ||||
|         break; | ||||
|       case "doughnut_chart": | ||||
|         // For doughnut charts, we'll need to determine which one based on existing items | ||||
|         const donutCount = this.dashboard.filter(item => item.chartType === 'donut-chart').length; | ||||
|         if (donutCount % 2 === 0) { | ||||
|           newItem = {  | ||||
|             cols: 5,  | ||||
|             rows: 6,  | ||||
|             y: 0,  | ||||
|             x: 0,  | ||||
|             chartType: 'donut-chart',  | ||||
|             name: 'End Customer Donut',  | ||||
|             id: newId | ||||
|           }; | ||||
|         } else { | ||||
|           newItem = {  | ||||
|             cols: 5,  | ||||
|             rows: 6,  | ||||
|             y: 0,  | ||||
|             x: 0,  | ||||
|             chartType: 'donut-chart',  | ||||
|             name: 'Segment Penetration Donut',  | ||||
|             id: newId | ||||
|           }; | ||||
|         } | ||||
|         break; | ||||
|       case "map_chart": | ||||
|         newItem = {  | ||||
|           cols: 5,  | ||||
|           rows: 6,  | ||||
|           y: 0,  | ||||
|           x: 0,  | ||||
|           chartType: 'map-chart',  | ||||
|           name: 'Map Chart',  | ||||
|           id: newId | ||||
|         }; | ||||
|         break; | ||||
|       case "grid_view": | ||||
|         newItem = {  | ||||
|           cols: 10,  | ||||
|           rows: 6,  | ||||
|           y: 0,  | ||||
|           x: 0,  | ||||
|           chartType: 'data-table',  | ||||
|           name: 'Data Table',  | ||||
|           id: newId | ||||
|         }; | ||||
|         break; | ||||
|       case "to_do_chart": | ||||
|         newItem = {  | ||||
|           cols: 5,  | ||||
|           rows: 6,  | ||||
|           y: 0,  | ||||
|           x: 0,  | ||||
|           chartType: 'deal-details',  | ||||
|           name: 'Deal Details',  | ||||
|           id: newId | ||||
|         }; | ||||
|         break; | ||||
|       case "line_chart": | ||||
|         newItem = {  | ||||
|           cols: 5,  | ||||
|           rows: 6,  | ||||
|           y: 0,  | ||||
|           x: 0,  | ||||
|           chartType: 'quarterwise-flow',  | ||||
|           name: 'Quarterwise Flow',  | ||||
|           id: newId | ||||
|         }; | ||||
|         break; | ||||
|       default: | ||||
|         newItem = {  | ||||
|           cols: 5,  | ||||
|           rows: 6,  | ||||
|           y: 0,  | ||||
|           x: 0,  | ||||
|           chartType: componentType,  | ||||
|           name: componentType,  | ||||
|           id: newId | ||||
|         }; | ||||
|     } | ||||
|      | ||||
|     // Add the new item to the dashboard | ||||
|     this.dashboard.push(newItem); | ||||
|   } | ||||
| 
 | ||||
|   removeItem(item: ShieldDashboardItem) { | ||||
|     // Add the item to deleted items list before removing | ||||
|     this.deletedItems.push({...item}); | ||||
|      | ||||
|     // Remove the item from the dashboard | ||||
|     this.dashboard.splice(this.dashboard.indexOf(item), 1); | ||||
|   } | ||||
|    | ||||
|   // Restore a deleted item | ||||
|   restoreItem(item: ShieldDashboardItem) { | ||||
|     // Remove from deleted items | ||||
|     this.deletedItems.splice(this.deletedItems.indexOf(item), 1); | ||||
|      | ||||
|     // Add back to dashboard | ||||
|     this.dashboard.push(item); | ||||
|   } | ||||
|    | ||||
|   // Clear all deleted items | ||||
|   clearDeletedItems() { | ||||
|     this.deletedItems = []; | ||||
|   } | ||||
| 
 | ||||
|   itemChange() { | ||||
|     console.log('Item changed:', this.dashboard); | ||||
|   } | ||||
|    | ||||
|   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')); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Extract only the relevant chart configuration properties to pass to chart components | ||||
|    * This prevents errors when trying to set properties that don't exist on the components | ||||
|    */ | ||||
|   getChartInputs(item: any): any { | ||||
|     // Only pass properties that are relevant to chart components | ||||
|     const chartInputs = { | ||||
|       chartType: item.chartType, | ||||
|       name: item.name | ||||
|     }; | ||||
|      | ||||
|     // Remove undefined properties to avoid passing unnecessary data | ||||
|     Object.keys(chartInputs).forEach(key => { | ||||
|       if (chartInputs[key] === undefined) { | ||||
|         delete chartInputs[key]; | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     return chartInputs; | ||||
|   } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user