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 './filter.service';
|
||||||
export * from './common-filter.component';
|
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';
|
import { SureconnectService } from '../sureconnect/sureconnect.service';
|
||||||
// Add the CommonFilterComponent import
|
// Add the CommonFilterComponent import
|
||||||
import { CommonFilterComponent } from '../common-filter/common-filter.component';
|
import { CommonFilterComponent } from '../common-filter/common-filter.component';
|
||||||
|
// Add the CompactFilterComponent import
|
||||||
|
import { CompactFilterComponent } from '../common-filter';
|
||||||
|
|
||||||
function isNullArray(arr) {
|
function isNullArray(arr) {
|
||||||
return !Array.isArray(arr) || arr.length === 0;
|
return !Array.isArray(arr) || arr.length === 0;
|
||||||
@ -98,6 +100,10 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
{
|
{
|
||||||
name: 'Grid View',
|
name: 'Grid View',
|
||||||
identifier: '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: "Financial Chart", componentInstance: FinancialChartComponent },
|
||||||
{ name: "To Do Chart", componentInstance: ToDoChartComponent },
|
{ name: "To Do Chart", componentInstance: ToDoChartComponent },
|
||||||
{ name: "Grid View", componentInstance: GridViewComponent },
|
{ name: "Grid View", componentInstance: GridViewComponent },
|
||||||
|
{ name: "Compact Filter", componentInstance: CompactFilterComponent }, // Add this line
|
||||||
];
|
];
|
||||||
model: any;
|
model: any;
|
||||||
linesdata: any;
|
linesdata: any;
|
||||||
@ -168,7 +175,12 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
drilldownLayers: [] as any[],
|
drilldownLayers: [] as any[],
|
||||||
// Common filter properties
|
// Common filter properties
|
||||||
commonFilterEnabled: false,
|
commonFilterEnabled: false,
|
||||||
commonFilterEnabledDrilldown: false
|
commonFilterEnabledDrilldown: false,
|
||||||
|
// Compact filter properties
|
||||||
|
filterKey: '',
|
||||||
|
filterType: 'text',
|
||||||
|
filterLabel: '',
|
||||||
|
filterOptions: [] as string[]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add sureconnect data property
|
// Add sureconnect data property
|
||||||
@ -519,6 +531,21 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
component: CommonFilterComponent,
|
component: CommonFilterComponent,
|
||||||
name: "Common Filter"
|
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":
|
case "grid_view":
|
||||||
return this.dashboardArray.push({
|
return this.dashboardArray.push({
|
||||||
cols: 5,
|
cols: 5,
|
||||||
@ -561,6 +588,19 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
if (item['commonFilterEnabledDrilldown'] === undefined) {
|
if (item['commonFilterEnabledDrilldown'] === undefined) {
|
||||||
this.gadgetsEditdata['commonFilterEnabledDrilldown'] = false;
|
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();
|
this.getStores();
|
||||||
|
|
||||||
// Set default connection if none is set and we have connections
|
// 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.drilldownLayers = this.gadgetsEditdata.drilldownLayers;
|
||||||
xyz.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
|
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);
|
console.log(xyz);
|
||||||
return xyz;
|
return xyz;
|
||||||
}
|
}
|
||||||
@ -809,6 +857,16 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
chartInputs['connection'] = item['connection'] || undefined;
|
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
|
// Remove undefined properties to avoid passing unnecessary data
|
||||||
Object.keys(chartInputs).forEach(key => {
|
Object.keys(chartInputs).forEach(key => {
|
||||||
if (chartInputs[key] === undefined) {
|
if (chartInputs[key] === undefined) {
|
||||||
@ -863,6 +921,16 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
updatedItem.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
|
updatedItem.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
|
||||||
updatedItem.commonFilterEnabledDrilldown = this.gadgetsEditdata.commonFilterEnabledDrilldown; // Add drilldown 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);
|
console.log('Updated item:', updatedItem);
|
||||||
return updatedItem;
|
return updatedItem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,32 +1,50 @@
|
|||||||
<div style="display: block; height: 100%; width: 100%;">
|
<div class="chart-container">
|
||||||
<!-- No filter controls needed with the new simplified approach -->
|
<!-- Compact Filters -->
|
||||||
<!-- Filters are now configured at the drilldown level -->
|
<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 -->
|
<!-- Drilldown mode indicator -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<span class="drilldown-text">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;">
|
<button class="btn btn-secondary btn-sm" (click)="navigateBack()">
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
Back to Level {{currentDrilldownLevel - 1}}
|
||||||
</button>
|
</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
|
Back to Main View
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No data message -->
|
<div class="chart-header">
|
||||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
<h3>{{ charttitle || 'Bar Chart' }}</h3>
|
||||||
No data available
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chart display -->
|
<div class="chart-wrapper">
|
||||||
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
<div class="chart-content" [class.loading]="barChartData.length === 0 && barChartLabels.length === 0 && !noDataAvailable">
|
||||||
<canvas baseChart
|
<!-- No data message -->
|
||||||
[datasets]="barChartData"
|
<div class="no-data-message" *ngIf="noDataAvailable">
|
||||||
[labels]="barChartLabels"
|
<p>No data available</p>
|
||||||
[type]="barChartType"
|
</div>
|
||||||
[options]="barChartOptions"
|
|
||||||
(chartHover)="chartHovered($event)"
|
<!-- Chart display -->
|
||||||
(chartClick)="chartClicked($event)">
|
<canvas baseChart
|
||||||
</canvas>
|
*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>
|
||||||
</div>
|
</div>
|
||||||
@ -5,27 +5,214 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar-chart-container {
|
.chart-container {
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
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 {
|
.chart-container:hover {
|
||||||
display: block;
|
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
|
||||||
max-width: 100%;
|
transform: translateY(-2px);
|
||||||
max-height: 100%;
|
}
|
||||||
|
|
||||||
|
.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
|
// Responsive design for chart container
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.bar-chart-container {
|
.chart-container {
|
||||||
height: 300px;
|
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) {
|
@media (max-width: 480px) {
|
||||||
.bar-chart-container {
|
.chart-container {
|
||||||
height: 250px;
|
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);
|
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 {
|
fetchChartData(): void {
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
|
|||||||
@ -1,39 +1,53 @@
|
|||||||
<div class="doughnut-chart-container">
|
<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 -->
|
<!-- Drilldown mode indicator -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<span class="drilldown-text">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;">
|
<button class="btn btn-secondary btn-sm" (click)="navigateBack()">
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
Back to Level {{currentDrilldownLevel - 1}}
|
||||||
</button>
|
</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
|
Back to Main View
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
<div class="chart-header">
|
||||||
<div class="chart-wrapper">
|
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||||
<!-- 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>
|
</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="chart-legend" *ngIf="!noDataAvailable && showlabel && doughnutChartLabels && doughnutChartLabels.length > 0">
|
||||||
<div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index">
|
<div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index">
|
||||||
<span class="legend-color" [style.background-color]="getLegendColor(i)"></span>
|
<span class="legend-color" [style.background-color]="getLegendColor(i)"></span>
|
||||||
|
|||||||
@ -17,17 +17,78 @@
|
|||||||
transform: translateY(-2px);
|
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 {
|
||||||
.chart-title {
|
background-color: #e0e0e0;
|
||||||
font-size: 26px;
|
padding: 10px;
|
||||||
font-weight: 700;
|
margin-bottom: 15px;
|
||||||
color: #2c3e50;
|
border-radius: 8px;
|
||||||
margin-bottom: 20px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-bottom: 15px;
|
display: flex;
|
||||||
border-bottom: 2px solid #3498db;
|
justify-content: center;
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
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 {
|
.chart-wrapper {
|
||||||
@ -56,6 +117,62 @@
|
|||||||
transition: all 0.3s ease;
|
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 {
|
.chart-legend {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -119,36 +236,13 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-indicator, .no-data-message {
|
@keyframes shimmer {
|
||||||
text-align: center;
|
0% {
|
||||||
padding: 30px;
|
background-position: -200% 0;
|
||||||
color: #666;
|
}
|
||||||
font-size: 18px;
|
100% {
|
||||||
font-style: italic;
|
background-position: 200% 0;
|
||||||
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); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive design */
|
/* Responsive design */
|
||||||
@ -157,9 +251,17 @@
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-title {
|
.chart-header .chart-title {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
margin-bottom: 15px;
|
}
|
||||||
|
|
||||||
|
.drilldown-indicator {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drilldown-text {
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper {
|
.chart-wrapper {
|
||||||
@ -181,4 +283,8 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 20px;
|
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());
|
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
|
// Public method to refresh data when filters change
|
||||||
refreshData(): void {
|
refreshData(): void {
|
||||||
this.fetchChartData();
|
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',
|
name: 'Quarterwise Flow',
|
||||||
identifier: 'line_chart'
|
identifier: 'line_chart'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Compact Filter',
|
||||||
|
identifier: 'compact_filter'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -300,6 +304,20 @@ export class ShieldDashboardComponent implements OnInit {
|
|||||||
drilldownLayers: []
|
drilldownLayers: []
|
||||||
};
|
};
|
||||||
break;
|
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:
|
default:
|
||||||
newItem = {
|
newItem = {
|
||||||
cols: 5,
|
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