Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82425d5377 | ||
|
|
2995328ec1 | ||
|
|
afc2c1f8a1 | ||
|
|
418b02acd7 | ||
|
|
cdd752469c | ||
|
|
cdcf1e07c7 | ||
|
|
c6ad8b5c2f | ||
|
|
98e0908920 | ||
|
|
8266bfdc01 | ||
|
|
f2b6a4d145 | ||
|
|
02f37a1bc5 | ||
|
|
f24138cfbd |
@@ -11,6 +11,9 @@
|
||||
<h3>{{ 'Dashboard_builder' | translate }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-4" style="text-align: right;">
|
||||
<button class="btn btn-success" [routerLink]="['/cns-portal/shield-dashboard']">
|
||||
<clr-icon shape="shield"></clr-icon>Shield Dashboard
|
||||
</button>
|
||||
<button id="add" class="btn btn-primary" (click)="gotorunner()">
|
||||
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
|
||||
</button>
|
||||
@@ -140,6 +143,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<!-- 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'">
|
||||
<div class="compact-multiselect-checkboxes" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;">
|
||||
<div *ngFor="let option of filterOptions" class="clr-checkbox-wrapper" style="margin-bottom: 5px;">
|
||||
<input type="checkbox"
|
||||
[id]="'multiselect-' + option"
|
||||
[value]="option"
|
||||
[checked]="isOptionSelected(option)"
|
||||
(change)="onMultiselectOptionChange($event, option)">
|
||||
<label [for]="'multiselect-' + option" class="clr-control-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</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,224 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.clr-checkbox {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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,378 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } 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, OnChanges {
|
||||
@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 {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Register this filter with the filter service
|
||||
this.registerFilter();
|
||||
|
||||
// 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];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// If filterKey or filterType changes, re-register the filter
|
||||
if (changes.filterKey || changes.filterType) {
|
||||
// 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);
|
||||
}
|
||||
this.registerFilter();
|
||||
}
|
||||
|
||||
// Handle API URL changes
|
||||
if (changes.apiUrl && !changes.apiUrl.firstChange) {
|
||||
if (this.apiUrl) {
|
||||
this.loadAvailableKeys();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register this filter with the filter service
|
||||
registerFilter(): void {
|
||||
if (this.filterKey) {
|
||||
// Get current filter values from the service
|
||||
const currentFilterValues = this.filterService.getFilterValues();
|
||||
|
||||
// Create a filter definition for this compact filter
|
||||
const filterDef: Filter = {
|
||||
id: `${this.filterKey}`,
|
||||
field: this.filterKey,
|
||||
label: this.filterLabel || this.filterKey,
|
||||
type: this.filterType as any,
|
||||
options: this.filterOptions,
|
||||
value: this.filterValue // Use the current filter value
|
||||
};
|
||||
|
||||
// Get current filters
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
|
||||
// Check if this filter is already registered
|
||||
const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id);
|
||||
|
||||
if (existingFilterIndex >= 0) {
|
||||
// Preserve the existing filter configuration
|
||||
const existingFilter = currentFilters[existingFilterIndex];
|
||||
|
||||
// Preserve the existing filter value if it exists in the service
|
||||
if (currentFilterValues.hasOwnProperty(existingFilter.id)) {
|
||||
filterDef.value = currentFilterValues[existingFilter.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
} else if (existingFilter.value !== undefined) {
|
||||
// Fallback to existing filter's value if no service value
|
||||
filterDef.value = existingFilter.value;
|
||||
this.filterValue = filterDef.value;
|
||||
}
|
||||
|
||||
// Preserve other configuration properties
|
||||
filterDef.label = existingFilter.label;
|
||||
filterDef.options = existingFilter.options || this.filterOptions;
|
||||
|
||||
// Update existing filter
|
||||
currentFilters[existingFilterIndex] = filterDef;
|
||||
} else {
|
||||
// For new filters, check if there's already a value in the service
|
||||
if (currentFilterValues.hasOwnProperty(filterDef.id)) {
|
||||
filterDef.value = currentFilterValues[filterDef.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
}
|
||||
|
||||
// Add new filter
|
||||
currentFilters.push(filterDef);
|
||||
}
|
||||
|
||||
// Update the filter service with the new filter list
|
||||
this.filterService.setFilters(currentFilters);
|
||||
|
||||
// Update the selected filter reference
|
||||
this.selectedFilter = filterDef;
|
||||
}
|
||||
}
|
||||
|
||||
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 from the service
|
||||
const currentState = this.filterService.getFilterValues();
|
||||
const filterValue = currentState[this.selectedFilter.id];
|
||||
if (filterValue !== undefined) {
|
||||
this.filterValue = filterValue;
|
||||
} else if (this.selectedFilter.value !== undefined) {
|
||||
// Use the filter's default value if no service value
|
||||
this.filterValue = this.selectedFilter.value;
|
||||
} else {
|
||||
// Use the current filter value as fallback
|
||||
this.filterValue = this.filterValue || '';
|
||||
}
|
||||
|
||||
// Also update configuration properties from the selected filter
|
||||
this.configFilterKey = this.selectedFilter.field;
|
||||
this.configFilterType = this.selectedFilter.type;
|
||||
this.configFilterLabel = this.selectedFilter.label;
|
||||
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
// Update the filter definition in the service to reflect the new value
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
const filterIndex = currentFilters.findIndex(f => f.id === this.selectedFilter.id);
|
||||
if (filterIndex >= 0) {
|
||||
currentFilters[filterIndex].value = value;
|
||||
this.filterService.setFilters(currentFilters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 from current filter if available
|
||||
if (this.selectedFilter) {
|
||||
this.configFilterKey = this.selectedFilter.field;
|
||||
this.configFilterType = this.selectedFilter.type;
|
||||
this.configFilterLabel = this.selectedFilter.label;
|
||||
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
|
||||
} else {
|
||||
// Fallback to current properties
|
||||
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.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && this.configFilterKey) {
|
||||
this.loadAvailableValues(this.configFilterKey);
|
||||
}
|
||||
|
||||
// Register the updated filter with the filter service
|
||||
this.registerFilter();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to check if an option is selected for checkboxes
|
||||
isOptionSelected(option: string): boolean {
|
||||
if (!this.filterValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure filterValue is an array for multiselect
|
||||
if (!Array.isArray(this.filterValue)) {
|
||||
this.filterValue = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.filterValue.includes(option);
|
||||
}
|
||||
// need to check this
|
||||
// Add method to handle multiselect option change
|
||||
onMultiselectOptionChange(event: any, option: string): void {
|
||||
// Initialize filterValue array if it doesn't exist
|
||||
if (!this.filterValue) {
|
||||
this.filterValue = [];
|
||||
}
|
||||
|
||||
// Ensure filterValue is an array
|
||||
if (!Array.isArray(this.filterValue)) {
|
||||
this.filterValue = [];
|
||||
}
|
||||
|
||||
if (event.target.checked) {
|
||||
// Add option if not already in array
|
||||
if (!this.filterValue.includes(option)) {
|
||||
this.filterValue.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
const index = this.filterValue.indexOf(option);
|
||||
if (index > -1) {
|
||||
this.filterValue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit the change event
|
||||
this.onFilterValueChange(this.filterValue);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './filter.service';
|
||||
export * from './common-filter.component';
|
||||
export * from './chart-wrapper.component';
|
||||
export * from './compact-filter.component';
|
||||
@@ -90,11 +90,106 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Compact Filter Configuration (shown only for Compact Filter components) -->
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName === 'Compact Filter'">
|
||||
<div class="clr-col-sm-12">
|
||||
<h4>Compact Filter Configuration</h4>
|
||||
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="compactFilterConnection">Connection</label>
|
||||
<select id="compactFilterConnection" class="clr-select" [(ngModel)]="gadgetsEditdata.connection"
|
||||
(ngModelChange)="onCompactFilterConnectionChange($event)" [ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Connection</option>
|
||||
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||
{{conn.connection_name || conn.id}}
|
||||
</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select a connection for this compact filter</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="compactFilterApiUrl">API URL</label>
|
||||
<div>
|
||||
<input type="text" id="compactFilterApiUrl" class="clr-input" [(ngModel)]="gadgetsEditdata.table"
|
||||
(ngModelChange)="onCompactFilterApiUrlChange($event)" [ngModelOptions]="{standalone: true}"
|
||||
placeholder="Enter API URL">
|
||||
<span>
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;"
|
||||
(click)="loadAvailableKeys(gadgetsEditdata.table, gadgetsEditdata.connection)"
|
||||
[disabled]="!gadgetsEditdata.table">
|
||||
<clr-icon shape="redo"></clr-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clr-subtext">Enter the API URL to fetch data for this filter</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="filterKey">Filter Key</label>
|
||||
<select id="filterKey" class="clr-select" [(ngModel)]="gadgetsEditdata.filterKey"
|
||||
(ngModelChange)="onFilterKeyChange($event)" [ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Filter Key</option>
|
||||
<option *ngFor="let key of availableKeys" [value]="key">{{ key }}</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the field name to filter on</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="filterType">Filter Type</label>
|
||||
<select id="filterType" class="clr-select" [(ngModel)]="gadgetsEditdata.filterType"
|
||||
(ngModelChange)="onFilterTypeChange($event)" [ngModelOptions]="{standalone: true}">
|
||||
<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 class="clr-subtext">Select the type of filter control to display</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="filterLabel">Filter Label (Optional)</label>
|
||||
<input type="text" id="filterLabel" class="clr-input" [(ngModel)]="gadgetsEditdata.filterLabel"
|
||||
[ngModelOptions]="{standalone: true}" placeholder="Enter filter label">
|
||||
<div class="clr-subtext">Label to display for this filter in the UI (if not provided, filter key will be
|
||||
used)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;"
|
||||
*ngIf="gadgetsEditdata.filterType === 'dropdown' || gadgetsEditdata.filterType === 'multiselect'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="filterOptions">Filter Options (comma separated)</label>
|
||||
<input type="text" id="filterOptions" class="clr-input" [(ngModel)]="filterOptionsString"
|
||||
[ngModelOptions]="{standalone: true}" placeholder="Option1,Option2,Option3">
|
||||
<div class="clr-subtext">Comma-separated list of options for dropdown/multiselect filters</div>
|
||||
<div class="clr-subtext" *ngIf="gadgetsEditdata.filterKey">
|
||||
<strong>Available values for "{{ gadgetsEditdata.filterKey }}":</strong> {{ filterOptionsString }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName !== 'Compact Filter'"
|
||||
style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
|
||||
|
||||
<!-- Add Connection Selection Field -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="connection">Connection</label>
|
||||
<select id="connection" formControlName="connection" [(ngModel)]="gadgetsEditdata.connection" class="clr-select">
|
||||
<select id="connection" formControlName="connection" [(ngModel)]="gadgetsEditdata.connection"
|
||||
class="clr-select">
|
||||
<option value="">Select Connection</option>
|
||||
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||
{{conn.connection_name || conn.id}}
|
||||
@@ -149,8 +244,8 @@
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="table">Api Url</label>
|
||||
<div><input type="urk" id="table" formControlName="table" class="clr-input"
|
||||
[(ngModel)]="gadgetsEditdata.table" style="width:90%"> <span><button class="btn btn-icon btn-primary"
|
||||
style="margin: 0px;" (click)="callApi(gadgetsEditdata.table)">
|
||||
[(ngModel)]="gadgetsEditdata.table" style="width:90%"> <span><button
|
||||
class="btn btn-icon btn-primary" style="margin: 0px;" (click)="callApi(gadgetsEditdata.table)">
|
||||
<clr-icon shape="redo"></clr-icon> </button></span></div>
|
||||
<!-- <select id="table" formControlName="table" [(ngModel)]="gadgetsEditdata.table" (change)="tablename($event.target.value)">
|
||||
<option value="null">choose Table</option>
|
||||
@@ -223,8 +318,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Add Base Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addBaseFilter()" style="margin-top: 10px; margin-bottom: 10px;"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabled">
|
||||
<button class="btn btn-sm btn-primary" (click)="addBaseFilter()"
|
||||
style="margin-top: 10px; margin-bottom: 10px;" [disabled]="gadgetsEditdata.commonFilterEnabled">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
@@ -245,14 +340,14 @@
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabled">
|
||||
<option value="">Select Field</option>
|
||||
<!-- Base API filters should always use columnData, not drilldownColumnData -->
|
||||
<option *ngFor="let column of getAvailableFields(gadgetsEditdata.baseFilters, i, columnData)" [value]="column">{{column}}</option>
|
||||
<option *ngFor="let column of getAvailableFields(gadgetsEditdata.baseFilters, i, columnData)"
|
||||
[value]="column">{{column}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-5">
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
|
||||
class="clr-input" placeholder="Filter Value"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabled"/>
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabled" />
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-2">
|
||||
@@ -288,22 +383,25 @@
|
||||
<label for="drilldownApiUrl">Base Drilldown API URL</label>
|
||||
<div>
|
||||
<input type="text" id="drilldownApiUrl" formControlName="drilldownApiUrl" class="clr-input"
|
||||
[(ngModel)]="gadgetsEditdata.drilldownApiUrl" style="width:90%" [ngModelOptions]="{standalone: true}">
|
||||
[(ngModel)]="gadgetsEditdata.drilldownApiUrl" style="width:90%"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
<span>
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;"
|
||||
(click)="refreshBaseDrilldownColumns()" [disabled]="!gadgetsEditdata.drilldownApiUrl">
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;" (click)="refreshBaseDrilldownColumns()"
|
||||
[disabled]="!gadgetsEditdata.drilldownApiUrl">
|
||||
<clr-icon shape="redo"></clr-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clr-subtext">Enter the API URL for base drilldown data. Use angle brackets for parameters, e.g., http://api.example.com/data/<country></div>
|
||||
<div class="clr-subtext">Enter the API URL for base drilldown data. Use angle brackets for parameters, e.g.,
|
||||
http://api.example.com/data/<country></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="drilldownXAxis">Base Drilldown X-Axis</label>
|
||||
<select id="drilldownXAxis" formControlName="drilldownXAxis" [(ngModel)]="gadgetsEditdata.drilldownXAxis" [ngModelOptions]="{standalone: true}">
|
||||
<select id="drilldownXAxis" formControlName="drilldownXAxis" [(ngModel)]="gadgetsEditdata.drilldownXAxis"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select X-Axis Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
@@ -317,7 +415,8 @@
|
||||
gadgetsEditdata?.fieldName !== 'To Do Chart'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="drilldownYAxis">Base Drilldown Y-Axis</label>
|
||||
<select id="drilldownYAxis" formControlName="drilldownYAxis" [(ngModel)]="gadgetsEditdata.drilldownYAxis" [ngModelOptions]="{standalone: true}">
|
||||
<select id="drilldownYAxis" formControlName="drilldownYAxis" [(ngModel)]="gadgetsEditdata.drilldownYAxis"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Y-Axis Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
@@ -329,11 +428,14 @@
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="drilldownParameter">Base Drilldown Parameter</label>
|
||||
<select id="drilldownParameter" [(ngModel)]="gadgetsEditdata.drilldownParameter" [ngModelOptions]="{standalone: true}">
|
||||
<select id="drilldownParameter" [(ngModel)]="gadgetsEditdata.drilldownParameter"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Parameter Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in base drilldown</div>
|
||||
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in base
|
||||
drilldown
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -347,16 +449,18 @@
|
||||
<div class="clr-form-control" style="margin-top: 10px;">
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" id="commonFilterToggleDrilldown" [(ngModel)]="gadgetsEditdata.commonFilterEnabledDrilldown"
|
||||
(change)="onCommonFilterToggleDrilldown()" [ngModelOptions]="{standalone: true}" class="clr-checkbox" />
|
||||
<input type="checkbox" id="commonFilterToggleDrilldown"
|
||||
[(ngModel)]="gadgetsEditdata.commonFilterEnabledDrilldown"
|
||||
(change)="onCommonFilterToggleDrilldown()" [ngModelOptions]="{standalone: true}"
|
||||
class="clr-checkbox" />
|
||||
<label for="commonFilterToggleDrilldown" class="clr-control-label">Use Common Filter</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Drilldown Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addDrilldownFilter()" style="margin-top: 10px; margin-bottom: 10px;"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
||||
<button class="btn btn-sm btn-primary" (click)="addDrilldownFilter()"
|
||||
style="margin-top: 10px; margin-bottom: 10px;" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
@@ -376,14 +480,15 @@
|
||||
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
||||
<option value="">Select Field</option>
|
||||
<option *ngFor="let column of getAvailableFields(gadgetsEditdata.drilldownFilters, i, drilldownColumnData)" [value]="column">{{column}}</option>
|
||||
<option
|
||||
*ngFor="let column of getAvailableFields(gadgetsEditdata.drilldownFilters, i, drilldownColumnData)"
|
||||
[value]="column">{{column}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-5">
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
|
||||
class="clr-input" placeholder="Filter Value"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown"/>
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-2">
|
||||
@@ -400,7 +505,8 @@
|
||||
|
||||
|
||||
<!-- Multi-Layer Drilldown Configurations -->
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled" style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled"
|
||||
style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
|
||||
<div class="clr-col-sm-12">
|
||||
<h4>Multi-Layer Drilldown Configurations</h4>
|
||||
<button class="btn btn-sm btn-primary" (click)="addDrilldownLayer()">
|
||||
@@ -412,7 +518,8 @@
|
||||
|
||||
<!-- Dynamic Drilldown Layers -->
|
||||
<div class="clr-row" *ngFor="let layer of gadgetsEditdata.drilldownLayers; let i = index">
|
||||
<div class="clr-col-sm-12" style="margin-top: 15px; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
|
||||
<div class="clr-col-sm-12"
|
||||
style="margin-top: 15px; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h5>Drilldown Layer {{i + 1}}</h5>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownLayer(i)">
|
||||
@@ -423,7 +530,8 @@
|
||||
<div class="clr-form-control">
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" [id]="'layerEnabled' + i" [(ngModel)]="layer.enabled" class="clr-checkbox" [ngModelOptions]="{standalone: true}" />
|
||||
<input type="checkbox" [id]="'layerEnabled' + i" [(ngModel)]="layer.enabled" class="clr-checkbox"
|
||||
[ngModelOptions]="{standalone: true}" />
|
||||
<label [for]="'layerEnabled' + i" class="clr-control-label">Enable Layer {{i + 1}} Drilldown</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -433,8 +541,8 @@
|
||||
<div class="clr-col-sm-12">
|
||||
<label [for]="'layerApiUrl' + i">Layer {{i + 1}} API URL</label>
|
||||
<div>
|
||||
<input type="text" [id]="'layerApiUrl' + i" class="clr-input"
|
||||
[(ngModel)]="layer.apiUrl" style="width:90%" [ngModelOptions]="{standalone: true}">
|
||||
<input type="text" [id]="'layerApiUrl' + i" class="clr-input" [(ngModel)]="layer.apiUrl"
|
||||
style="width:90%" [ngModelOptions]="{standalone: true}">
|
||||
<span>
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;"
|
||||
(click)="refreshDrilldownLayerColumns(i)" [disabled]="!layer.apiUrl">
|
||||
@@ -442,7 +550,8 @@
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clr-subtext">Enter the API URL for layer {{i + 1}} drilldown data. Use angle brackets for parameters, e.g., http://api.example.com/data/<state></div>
|
||||
<div class="clr-subtext">Enter the API URL for layer {{i + 1}} drilldown data. Use angle brackets for
|
||||
parameters, e.g., http://api.example.com/data/<state></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -478,7 +587,9 @@
|
||||
<option value="">Select Parameter Column</option>
|
||||
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in layer {{i + 1}} drilldown</div>
|
||||
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in layer {{i
|
||||
+
|
||||
1}} drilldown</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -492,16 +603,17 @@
|
||||
<div class="clr-form-control" style="margin-top: 10px;">
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" [id]="'commonFilterToggleLayer' + i" [(ngModel)]="layer.commonFilterEnabled"
|
||||
(change)="onCommonFilterToggleLayer(i)" [ngModelOptions]="{standalone: true}" class="clr-checkbox" />
|
||||
<input type="checkbox" [id]="'commonFilterToggleLayer' + i"
|
||||
[(ngModel)]="layer.commonFilterEnabled" (change)="onCommonFilterToggleLayer(i)"
|
||||
[ngModelOptions]="{standalone: true}" class="clr-checkbox" />
|
||||
<label [for]="'commonFilterToggleLayer' + i" class="clr-control-label">Use Common Filter</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Layer Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addLayerFilter(i)" style="margin-top: 10px; margin-bottom: 10px;"
|
||||
[disabled]="layer.commonFilterEnabled">
|
||||
<button class="btn btn-sm btn-primary" (click)="addLayerFilter(i)"
|
||||
style="margin-top: 10px; margin-bottom: 10px;" [disabled]="layer.commonFilterEnabled">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
@@ -521,14 +633,14 @@
|
||||
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||
[disabled]="layer.commonFilterEnabled">
|
||||
<option value="">Select Field</option>
|
||||
<option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])" [value]="column">{{column}}</option>
|
||||
<option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])"
|
||||
[value]="column">{{column}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-5">
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
|
||||
class="clr-input" placeholder="Filter Value"
|
||||
[disabled]="layer.commonFilterEnabled"/>
|
||||
class="clr-input" placeholder="Filter Value" [disabled]="layer.commonFilterEnabled" />
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-2">
|
||||
@@ -550,6 +662,9 @@
|
||||
<input id="chartparameter" type="text" formControlName="chartparameter" class="clr-input" [(ngModel)]="gadgetsEditdata.chartparameter">
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="modeledit = false">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="applyChanges(modelid)">Apply</button>
|
||||
@@ -568,7 +683,8 @@
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="commonFilterConnection">Connection</label>
|
||||
<select id="commonFilterConnection" formControlName="connection" [(ngModel)]="commonFilterData.connection" class="clr-select">
|
||||
<select id="commonFilterConnection" formControlName="connection" [(ngModel)]="commonFilterData.connection"
|
||||
class="clr-select">
|
||||
<option value="">Select Connection</option>
|
||||
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||
{{conn.connection_name || conn.id}}
|
||||
@@ -584,8 +700,8 @@
|
||||
<input type="text" id="commonFilterApiUrl" formControlName="apiUrl" class="clr-input"
|
||||
[(ngModel)]="commonFilterData.apiUrl" style="width:90%">
|
||||
<span>
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;"
|
||||
(click)="refreshCommonFilterColumns()" [disabled]="!commonFilterData.apiUrl">
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;" (click)="refreshCommonFilterColumns()"
|
||||
[disabled]="!commonFilterData.apiUrl">
|
||||
<clr-icon shape="redo"></clr-icon>
|
||||
</button>
|
||||
</span>
|
||||
@@ -599,7 +715,8 @@
|
||||
<h5>Common Filters</h5>
|
||||
|
||||
<!-- Add Common Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addCommonFilter()" style="margin-top: 10px; margin-bottom: 10px;">
|
||||
<button class="btn btn-sm btn-primary" (click)="addCommonFilter()"
|
||||
style="margin-top: 10px; margin-bottom: 10px;">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
@@ -622,8 +739,8 @@
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-5">
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
|
||||
class="clr-input" placeholder="Filter Value" />
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||
placeholder="Filter Value" />
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-2">
|
||||
|
||||
@@ -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;
|
||||
@@ -46,6 +48,12 @@ export class EditnewdashComponent implements OnInit {
|
||||
public entryForm: FormGroup;
|
||||
public commonFilterForm: FormGroup; // Add common filter form
|
||||
|
||||
// Add filterOptionsString property for compact filter
|
||||
filterOptionsString: string = '';
|
||||
|
||||
// Add availableKeys property for compact filter
|
||||
availableKeys: string[] = [];
|
||||
|
||||
WidgetsMock: WidgetModel[] = [
|
||||
{
|
||||
name: 'Common Filter',
|
||||
@@ -98,6 +106,10 @@ export class EditnewdashComponent implements OnInit {
|
||||
{
|
||||
name: 'Grid View',
|
||||
identifier: 'grid_view'
|
||||
},
|
||||
{
|
||||
name: 'Compact Filter',
|
||||
identifier: 'compact_filter'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -123,6 +135,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 +181,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
|
||||
@@ -348,6 +366,16 @@ export class EditnewdashComponent implements OnInit {
|
||||
dashboard.component = component.componentInstance;
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure compact filter configuration properties are properly initialized
|
||||
if (dashboard.component === 'Compact Filter' || dashboard.name === 'Compact Filter') {
|
||||
// Make sure all compact filter properties exist
|
||||
if (dashboard.filterKey === undefined) dashboard.filterKey = '';
|
||||
if (dashboard.filterType === undefined) dashboard.filterType = 'text';
|
||||
if (dashboard.filterLabel === undefined) dashboard.filterLabel = '';
|
||||
if (dashboard.filterOptions === undefined) dashboard.filterOptions = [];
|
||||
// table and connection properties should already exist for all components
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -362,6 +390,16 @@ export class EditnewdashComponent implements OnInit {
|
||||
dashboard.component = component.name;
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure compact filter configuration properties are preserved
|
||||
if (dashboard.name === 'Compact Filter') {
|
||||
// Make sure all compact filter properties exist
|
||||
if (dashboard.filterKey === undefined) dashboard.filterKey = '';
|
||||
if (dashboard.filterType === undefined) dashboard.filterType = 'text';
|
||||
if (dashboard.filterLabel === undefined) dashboard.filterLabel = '';
|
||||
if (dashboard.filterOptions === undefined) dashboard.filterOptions = [];
|
||||
// table and connection properties should already exist for all components
|
||||
}
|
||||
});
|
||||
}
|
||||
// Add method to get available fields for a filter dropdown (excluding already selected fields)
|
||||
@@ -519,6 +557,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 +614,31 @@ 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'] = [];
|
||||
}
|
||||
|
||||
// Initialize filterOptionsString for compact filter
|
||||
if (item.name === 'Compact Filter') {
|
||||
this.filterOptionsString = this.gadgetsEditdata['filterOptions'].join(', ');
|
||||
// Load available keys when editing a compact filter
|
||||
if (this.gadgetsEditdata['table']) {
|
||||
this.loadAvailableKeys(this.gadgetsEditdata['table'], this.gadgetsEditdata['connection']);
|
||||
}
|
||||
} else {
|
||||
this.filterOptionsString = '';
|
||||
}
|
||||
|
||||
this.getStores();
|
||||
|
||||
// Set default connection if none is set and we have connections
|
||||
@@ -659,6 +737,9 @@ export class EditnewdashComponent implements OnInit {
|
||||
|
||||
//https://www.w3schools.com/js/tryit.asp?filename=tryjson_stringify_function_tostring
|
||||
|
||||
// First serialize the dashboard collection to ensure component names are properly set
|
||||
this.serialize(this.dashboardCollection.dashboard);
|
||||
|
||||
let cmp = this.dashboardCollection.dashboard.forEach(dashboard => {
|
||||
this.componentCollection.forEach(component => {
|
||||
if (dashboard.name === component.name) {
|
||||
@@ -679,8 +760,6 @@ export class EditnewdashComponent implements OnInit {
|
||||
//console.log(merged);
|
||||
console.log("temp data", typeof tmp);
|
||||
console.log(tmp);
|
||||
let parsed = JSON.parse(tmp);
|
||||
this.serialize(parsed.dashboard);
|
||||
this.dashbord1_Line.model = tmp;
|
||||
|
||||
// let obj = this.dashboardCollection;
|
||||
@@ -736,6 +815,21 @@ 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.name === 'Compact Filter') {
|
||||
xyz.filterKey = this.gadgetsEditdata.filterKey || '';
|
||||
xyz.filterType = this.gadgetsEditdata.filterType || 'text';
|
||||
xyz.filterLabel = this.gadgetsEditdata.filterLabel || '';
|
||||
// Convert filterOptionsString to array
|
||||
if (this.gadgetsEditdata.fieldName === 'Compact Filter') {
|
||||
xyz.filterOptions = this.filterOptionsString.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
} else {
|
||||
xyz.filterOptions = this.gadgetsEditdata.filterOptions || [];
|
||||
}
|
||||
xyz.table = this.gadgetsEditdata.table || '';
|
||||
xyz.connection = this.gadgetsEditdata.connection || undefined;
|
||||
}
|
||||
|
||||
console.log(xyz);
|
||||
return xyz;
|
||||
}
|
||||
@@ -770,8 +864,58 @@ export class EditnewdashComponent implements OnInit {
|
||||
* 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 = {
|
||||
// For CompactFilterComponent, pass only filter configuration properties
|
||||
if (item.name === 'Compact Filter') {
|
||||
const filterInputs = {
|
||||
filterKey: item['filterKey'] || '',
|
||||
filterType: item['filterType'] || 'text',
|
||||
filterLabel: item['filterLabel'] || '',
|
||||
filterOptions: item['filterOptions'] || [],
|
||||
apiUrl: item['table'] || '', // Use table as API URL
|
||||
connectionId: item['connection'] ? parseInt(item['connection'], 10) : undefined
|
||||
};
|
||||
|
||||
// Preserve configuration in the item itself
|
||||
item['filterKey'] = filterInputs['filterKey'];
|
||||
item['filterType'] = filterInputs['filterType'];
|
||||
item['filterLabel'] = filterInputs['filterLabel'];
|
||||
item['filterOptions'] = filterInputs['filterOptions'];
|
||||
item['table'] = filterInputs['apiUrl'];
|
||||
item['connection'] = item['connection'];
|
||||
|
||||
// Remove undefined properties to avoid passing unnecessary data
|
||||
Object.keys(filterInputs).forEach(key => {
|
||||
if (filterInputs[key] === undefined) {
|
||||
delete filterInputs[key];
|
||||
}
|
||||
});
|
||||
|
||||
return filterInputs;
|
||||
}
|
||||
|
||||
// For CommonFilterComponent, pass only filter-related properties
|
||||
if (item.component && item.component.name === 'CommonFilterComponent') {
|
||||
const commonFilterInputs = {
|
||||
baseFilters: item['baseFilters'] || [],
|
||||
drilldownFilters: item['drilldownFilters'] || [],
|
||||
drilldownLayers: item['drilldownLayers'] || [],
|
||||
fieldName: item['name'] || '',
|
||||
connection: item['connection'] || undefined
|
||||
};
|
||||
|
||||
// Remove undefined properties to avoid passing unnecessary data
|
||||
Object.keys(commonFilterInputs).forEach(key => {
|
||||
if (commonFilterInputs[key] === undefined) {
|
||||
delete commonFilterInputs[key];
|
||||
}
|
||||
});
|
||||
|
||||
return commonFilterInputs;
|
||||
}
|
||||
|
||||
// For GridViewComponent, pass chart properties with drilldown support
|
||||
if (item.component && item.component.name === 'GridViewComponent') {
|
||||
const gridInputs = {
|
||||
xAxis: item.xAxis,
|
||||
yAxis: item.yAxis,
|
||||
table: item.table,
|
||||
@@ -800,14 +944,45 @@ export class EditnewdashComponent implements OnInit {
|
||||
drilldownLayers: item['drilldownLayers'] || []
|
||||
};
|
||||
|
||||
// For CommonFilterComponent, also pass baseFilters, drilldownFilters, drilldownLayers, fieldName, and connection
|
||||
if (item.component && item.component.name === 'CommonFilterComponent') {
|
||||
chartInputs['baseFilters'] = item['baseFilters'] || [];
|
||||
chartInputs['drilldownFilters'] = item['drilldownFilters'] || [];
|
||||
chartInputs['drilldownLayers'] = item['drilldownLayers'] || [];
|
||||
chartInputs['fieldName'] = item['name'] || '';
|
||||
chartInputs['connection'] = item['connection'] || undefined;
|
||||
// Remove undefined properties to avoid passing unnecessary data
|
||||
Object.keys(gridInputs).forEach(key => {
|
||||
if (gridInputs[key] === undefined) {
|
||||
delete gridInputs[key];
|
||||
}
|
||||
});
|
||||
|
||||
return gridInputs;
|
||||
}
|
||||
|
||||
// For all other chart components, pass chart-specific properties
|
||||
const chartInputs = {
|
||||
xAxis: item.xAxis,
|
||||
yAxis: item.yAxis,
|
||||
table: item.table,
|
||||
datastore: item.datastore,
|
||||
charttitle: item.charttitle,
|
||||
chartlegend: item.chartlegend,
|
||||
showlabel: item.showlabel,
|
||||
chartcolor: item.chartcolor,
|
||||
slices: item.slices,
|
||||
donut: item.donut,
|
||||
charturl: item.charturl,
|
||||
chartparameter: item.chartparameter,
|
||||
datasource: item.datasource,
|
||||
fieldName: item.name, // Using item.name as fieldName
|
||||
connection: item['connection'], // Add connection field using bracket notation
|
||||
// Base drilldown configuration properties
|
||||
drilldownEnabled: item['drilldownEnabled'],
|
||||
drilldownApiUrl: item['drilldownApiUrl'],
|
||||
// Removed drilldownParameterKey since we're using URL templates
|
||||
drilldownXAxis: item['drilldownXAxis'],
|
||||
drilldownYAxis: item['drilldownYAxis'],
|
||||
drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
|
||||
baseFilters: item['baseFilters'] || [], // Add base filters
|
||||
drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters
|
||||
// Multi-layer drilldown configurations
|
||||
drilldownLayers: item['drilldownLayers'] || []
|
||||
};
|
||||
|
||||
// Remove undefined properties to avoid passing unnecessary data
|
||||
Object.keys(chartInputs).forEach(key => {
|
||||
@@ -863,6 +1038,27 @@ 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.name === 'Compact Filter') {
|
||||
updatedItem.filterKey = this.gadgetsEditdata.filterKey || '';
|
||||
updatedItem.filterType = this.gadgetsEditdata.filterType || 'text';
|
||||
updatedItem.filterLabel = this.gadgetsEditdata.filterLabel || '';
|
||||
// Convert filterOptionsString to array
|
||||
if (this.gadgetsEditdata.fieldName === 'Compact Filter') {
|
||||
updatedItem.filterOptions = this.filterOptionsString.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
} else {
|
||||
updatedItem.filterOptions = this.gadgetsEditdata.filterOptions || [];
|
||||
}
|
||||
updatedItem.table = this.gadgetsEditdata.table || ''; // API URL
|
||||
updatedItem.connection = this.gadgetsEditdata.connection || undefined; // Connection ID
|
||||
|
||||
// Also preserve these properties in gadgetsEditdata for consistency
|
||||
this.gadgetsEditdata.filterKey = updatedItem.filterKey;
|
||||
this.gadgetsEditdata.filterType = updatedItem.filterType;
|
||||
this.gadgetsEditdata.filterLabel = updatedItem.filterLabel;
|
||||
this.gadgetsEditdata.filterOptions = updatedItem.filterOptions;
|
||||
}
|
||||
|
||||
console.log('Updated item:', updatedItem);
|
||||
return updatedItem;
|
||||
}
|
||||
@@ -1288,4 +1484,80 @@ export class EditnewdashComponent implements OnInit {
|
||||
// This would require the chart component to have a public resize method
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to load available keys for compact filter
|
||||
loadAvailableKeys(apiUrl: string, connectionId: string | undefined) {
|
||||
if (apiUrl) {
|
||||
const connectionIdNum = connectionId ? parseInt(connectionId, 10) : undefined;
|
||||
this.alertService.getColumnfromurl(apiUrl, connectionIdNum).subscribe(
|
||||
(keys: string[]) => {
|
||||
this.availableKeys = keys;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available keys:', error);
|
||||
this.availableKeys = [];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to load available values for a specific key
|
||||
loadAvailableValues(key: string) {
|
||||
if (key && this.gadgetsEditdata['table']) {
|
||||
const connectionIdNum = this.gadgetsEditdata['connection'] ?
|
||||
parseInt(this.gadgetsEditdata['connection'], 10) : undefined;
|
||||
this.alertService.getValuesFromUrl(this.gadgetsEditdata['table'], connectionIdNum, key).subscribe(
|
||||
(values: string[]) => {
|
||||
// Update filter options string for dropdown/multiselect
|
||||
if (this.gadgetsEditdata['filterType'] === 'dropdown' ||
|
||||
this.gadgetsEditdata['filterType'] === 'multiselect') {
|
||||
this.filterOptionsString = values.join(', ');
|
||||
// Also update the gadgetsEditdata filterOptions array
|
||||
this.gadgetsEditdata['filterOptions'] = values;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available values:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to handle filter key change
|
||||
onFilterKeyChange(key: string) {
|
||||
this.gadgetsEditdata['filterKey'] = key;
|
||||
// Load available values when filter key changes
|
||||
if (key && (this.gadgetsEditdata['filterType'] === 'dropdown' ||
|
||||
this.gadgetsEditdata['filterType'] === 'multiselect')) {
|
||||
this.loadAvailableValues(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to handle filter type change
|
||||
onFilterTypeChange(type: string) {
|
||||
this.gadgetsEditdata['filterType'] = type;
|
||||
// Load available values when filter type changes to dropdown or multiselect
|
||||
if ((type === 'dropdown' || type === 'multiselect') && this.gadgetsEditdata['filterKey']) {
|
||||
this.loadAvailableValues(this.gadgetsEditdata['filterKey']);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to handle API URL change for compact filter
|
||||
onCompactFilterApiUrlChange(url: string) {
|
||||
this.gadgetsEditdata['table'] = url;
|
||||
// Load available keys when API URL changes
|
||||
if (url) {
|
||||
this.loadAvailableKeys(url, this.gadgetsEditdata['connection']);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to handle connection change for compact filter
|
||||
onCompactFilterConnectionChange(connectionId: string) {
|
||||
this.gadgetsEditdata['connection'] = connectionId;
|
||||
// Reload available keys when connection changes
|
||||
if (this.gadgetsEditdata['table']) {
|
||||
this.loadAvailableKeys(this.gadgetsEditdata['table'], connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -173,73 +173,41 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
console.log('Bar chart data URL:', url);
|
||||
|
||||
// Convert baseFilters to filter parameters
|
||||
let filterParams = '';
|
||||
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||
const filterObj = {};
|
||||
|
||||
// Add base filters
|
||||
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filters:', this.baseFilters);
|
||||
console.log('Base filter params:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
// Add common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
console.log('Filter definitions:', filterDefinitions);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
console.log(`Processing filter ID: ${filterId}, Value:`, filterValue);
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
console.log(`Filter definition for ${filterId}:`, filterDef);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
console.log(`Mapping filter ID ${filterId} to field name: ${fieldName}`);
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
console.log(`Added to merged filters: ${fieldName} =`, filterValue);
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
console.log(`No field name found for filter ID ${filterId}, using ID as field name`);
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
console.log(`Added to merged filters: ${filterId} =`, filterValue);
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('Final merged filter object:', filterParams);
|
||||
console.log('Final filter object:', filterObj);
|
||||
// Fetch data from the dashboard service with parameter field and value
|
||||
// For base level, we pass empty parameter and value, but now also pass filters
|
||||
const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
|
||||
@@ -394,61 +362,47 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
console.log('Drilldown data URL:', url);
|
||||
|
||||
// Convert drilldown layer filters to filter parameters (if applicable)
|
||||
let filterParams = '';
|
||||
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
|
||||
const filterObj = {};
|
||||
|
||||
// Add drilldown layer filters
|
||||
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
|
||||
drilldownConfig.filters.forEach((filter: any) => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Drilldown layer filter parameters:', filterParams);
|
||||
|
||||
// Convert drilldownFilters to filter parameters for drilldown level
|
||||
let drilldownFilterParams = '';
|
||||
// Add drilldownFilters
|
||||
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
|
||||
const filterObj = {};
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (drilldownFilterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(drilldownFilterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
// Convert to JSON string for API call
|
||||
let drilldownFilterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('Drilldown filter parameters:', drilldownFilterParams);
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
<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>
|
||||
|
||||
<div class="chart-header">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
@@ -33,7 +40,14 @@
|
||||
(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;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
.chart-title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
padding-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #3498db;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.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%;
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
|
||||
.loading-indicator p, .no-data-message p {
|
||||
margin: 10px 0 0 0;
|
||||
100% {
|
||||
background-position: 200% 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 */
|
||||
@@ -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();
|
||||
|
||||
@@ -4,9 +4,41 @@
|
||||
<div class="clr-col-8">
|
||||
<h3>{{charttitle || 'Data Grid'}}</h3>
|
||||
</div>
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<div class="clr-col-4" *ngIf="drilldownEnabled && drilldownStack.length > 0" style="text-align: right;">
|
||||
<button class="btn btn-sm btn-link" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedKey}} = {{drilldownStack[drilldownStack.length - 1].clickedValue}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<clr-datagrid [clrDgLoading]="loading">
|
||||
<clr-dg-placeholder> <ng-template #loadingSpinner><clr-spinner>Loading ... </clr-spinner></ng-template>
|
||||
<clr-dg-placeholder>
|
||||
<ng-template #loadingSpinner>
|
||||
<clr-spinner>Loading ... </clr-spinner>
|
||||
</ng-template>
|
||||
<div *ngIf="error;else loadingSpinner">{{error}}</div>
|
||||
</clr-dg-placeholder>
|
||||
|
||||
@@ -19,7 +51,7 @@
|
||||
|
||||
<clr-dg-row *clrDgItems="let item of givendata" [clrDgItem]="item">
|
||||
<!-- Dynamic cells based on response keys -->
|
||||
<clr-dg-cell *ngFor="let header of dynamicHeaders">
|
||||
<clr-dg-cell *ngFor="let header of dynamicHeaders" (click)="onRowClick(item, header.key)">
|
||||
{{item[header.key]}}
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
@import '../../../../../../../styles1.scss';
|
||||
input.ng-invalid.ng-touched {
|
||||
border-color: red;
|
||||
// Add styles for drilldown navigation
|
||||
.alert-info {
|
||||
background-color: #dcedf7;
|
||||
border-color: #a3d4f5;
|
||||
color: #21333b;
|
||||
}
|
||||
|
||||
.error_mess {
|
||||
color: red;
|
||||
.alert-info .alert-icon {
|
||||
color: #0072a3;
|
||||
}
|
||||
clr-datagrid{
|
||||
height: 400px; /* Adjust the height as needed */
|
||||
overflow-y: auto;
|
||||
|
||||
.btn-link {
|
||||
color: #0072a3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
color: #00567a;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dg-wrapper {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.clr-row {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { UsergrpmaintainceService } from 'src/app/services/admin/usergrpmaintaince.service';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-grid-view',
|
||||
templateUrl: './grid-view.component.html',
|
||||
styleUrls: ['./grid-view.component.scss']
|
||||
})
|
||||
export class GridViewComponent implements OnInit, OnChanges {
|
||||
export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() xAxis: string;
|
||||
@Input() yAxis: string | string[];
|
||||
@Input() table: string;
|
||||
@@ -23,6 +27,16 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
@Input() datasource: string;
|
||||
@Input() fieldName: string;
|
||||
@Input() connection: number; // Add connection input
|
||||
// Drilldown configuration inputs
|
||||
@Input() drilldownEnabled: boolean = false;
|
||||
@Input() drilldownApiUrl: string;
|
||||
@Input() drilldownXAxis: string;
|
||||
@Input() drilldownYAxis: string;
|
||||
@Input() drilldownParameter: string; // Add drilldown parameter input
|
||||
@Input() baseFilters: any[] = []; // Add base filters input
|
||||
@Input() drilldownFilters: any[] = []; // Add drilldown filters input
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
loading = false;
|
||||
givendata: any[] = [];
|
||||
@@ -38,13 +52,37 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
submitted = false;
|
||||
dynamicHeaders: any[] = [];
|
||||
|
||||
constructor(
|
||||
// Multi-layer drilldown state tracking
|
||||
drilldownStack: any[] = []; // Stack to track drilldown navigation history
|
||||
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
|
||||
originalGridData: any[] = [];
|
||||
|
||||
// No data state
|
||||
noDataAvailable: boolean = false;
|
||||
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private mainservice: UsergrpmaintainceService,
|
||||
private dashboardService: Dashboard3Service,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the grid data
|
||||
console.log('GridView: Filter state changed:', filters);
|
||||
this.fetchGridData();
|
||||
})
|
||||
);
|
||||
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
@@ -55,12 +93,21 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
const tableChanged = changes.table && !changes.table.firstChange;
|
||||
const connectionChanged = changes.connection && !changes.connection.firstChange; // Add connection change detection
|
||||
const connectionChanged = changes.connection && !changes.connection.firstChange;
|
||||
const baseFiltersChanged = changes.baseFilters && !changes.baseFilters.firstChange;
|
||||
// Drilldown configuration changes
|
||||
const drilldownEnabledChanged = changes.drilldownEnabled && !changes.drilldownEnabled.firstChange;
|
||||
const drilldownApiUrlChanged = changes.drilldownApiUrl && !changes.drilldownApiUrl.firstChange;
|
||||
const drilldownXAxisChanged = changes.drilldownXAxis && !changes.drilldownXAxis.firstChange;
|
||||
const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange;
|
||||
const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.firstChange;
|
||||
|
||||
// Respond to input changes
|
||||
if (xAxisChanged || yAxisChanged || tableChanged || connectionChanged) {
|
||||
console.log('X or Y axis or table or connection changed, fetching new data');
|
||||
// Only fetch data if xAxis, yAxis, table, or connection has changed (and it's not the first change)
|
||||
if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
|
||||
drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
|
||||
drilldownLayersChanged)) {
|
||||
console.log('X or Y axis or table or connection or base filters or drilldown config changed, fetching new data');
|
||||
// Only fetch data if xAxis, yAxis, table, connection, baseFilters or drilldown config has changed (and it's not the first change)
|
||||
this.fetchGridData();
|
||||
}
|
||||
}
|
||||
@@ -68,20 +115,87 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
// Dynamic headers for the grid
|
||||
|
||||
fetchGridData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
|
||||
// If we're in drilldown mode, fetch the appropriate drilldown data
|
||||
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
|
||||
this.fetchDrilldownData();
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have the necessary data, fetch grid data from the service
|
||||
if (this.table && this.xAxis) {
|
||||
console.log('Fetching grid data for:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
// if (this.table && this.xAxis) {
|
||||
if (this.table) {
|
||||
|
||||
console.log('=== GRID VIEW DEBUG INFO ===');
|
||||
console.log('Table:', this.table);
|
||||
console.log('X-Axis:', this.xAxis);
|
||||
console.log('Y-Axis:', this.yAxis);
|
||||
console.log('Connection:', this.connection);
|
||||
|
||||
// Convert yAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
|
||||
|
||||
// Get the parameter value from the drilldown stack for base level (should be empty)
|
||||
let parameterValue = '';
|
||||
|
||||
// Log the URL that will be called
|
||||
let url = `chart/getdashjson/grid?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Grid data URL:', url);
|
||||
|
||||
// Get filter parameters from base filters
|
||||
const filterObj = {};
|
||||
|
||||
// Add base filters
|
||||
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add common filters directly as key-value pairs
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Add common filters using the field name as the key
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('GridView: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service, similar to other chart components
|
||||
this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection).subscribe(
|
||||
this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
|
||||
(data: any) => {
|
||||
console.log('=== GRID VIEW DATA RESPONSE ===');
|
||||
console.log('Received grid data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Grid API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.error = "No data Available";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,27 +204,36 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
this.givendata = data.chartData;
|
||||
this.extractDynamicHeaders(data.chartData);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with data:', this.givendata);
|
||||
} else if (data && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
this.givendata = data.data;
|
||||
this.extractDynamicHeaders(data.data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with legacy data format:', this.givendata);
|
||||
} else if (Array.isArray(data)) {
|
||||
// Handle case where data is directly an array
|
||||
this.givendata = data;
|
||||
this.extractDynamicHeaders(data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with array data:', this.givendata);
|
||||
} else {
|
||||
console.warn('Grid received data does not have expected structure', data);
|
||||
this.error = "No valid data received";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
}
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}, (error) => {
|
||||
console.log('Error fetching grid data:', error);
|
||||
this.error = "Server Error";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
});
|
||||
} else if (this.table) {
|
||||
console.log('Missing xAxis, falling back to default data fetching');
|
||||
@@ -126,13 +249,342 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
this.givendata = Array.isArray(data) ? data : [];
|
||||
this.extractDynamicHeaders(data);
|
||||
this.error = this.givendata && this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata && this.givendata.length === 0;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}, (error) => {
|
||||
console.log(error);
|
||||
this.error = "Server Error";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
});
|
||||
} else {
|
||||
console.log('Missing required data for grid:', { table: this.table });
|
||||
this.error = "Table name is required";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch drilldown data based on current drilldown level
|
||||
fetchDrilldownData(): void {
|
||||
console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
|
||||
console.log('Drilldown stack:', this.drilldownStack);
|
||||
|
||||
// Get the current drilldown configuration based on the current level
|
||||
let drilldownConfig;
|
||||
if (this.currentDrilldownLevel === 1) {
|
||||
// Base drilldown level
|
||||
drilldownConfig = {
|
||||
apiUrl: this.drilldownApiUrl,
|
||||
xAxis: this.drilldownXAxis,
|
||||
yAxis: this.drilldownYAxis,
|
||||
parameter: this.drilldownParameter
|
||||
};
|
||||
} else {
|
||||
// Multi-layer drilldown level
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
|
||||
drilldownConfig = this.drilldownLayers[layerIndex];
|
||||
} else {
|
||||
console.warn('Invalid drilldown layer index:', layerIndex);
|
||||
this.error = "Invalid drilldown configuration";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Drilldown config for level', this.currentDrilldownLevel, ':', drilldownConfig);
|
||||
|
||||
// Check if we have valid drilldown configuration
|
||||
if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
|
||||
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
|
||||
this.error = "Missing drilldown configuration";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the parameter value from the drilldown stack
|
||||
let parameterValue = '';
|
||||
if (this.drilldownStack.length > 0) {
|
||||
const lastEntry = this.drilldownStack[this.drilldownStack.length - 1];
|
||||
parameterValue = lastEntry.clickedValue || '';
|
||||
console.log('Parameter value from last click:', parameterValue);
|
||||
}
|
||||
|
||||
// Get the parameter field from drilldown config
|
||||
const parameterField = drilldownConfig.parameter || '';
|
||||
console.log('Parameter field:', parameterField);
|
||||
|
||||
console.log('Fetching drilldown data for level:', this.currentDrilldownLevel, {
|
||||
apiUrl: drilldownConfig.apiUrl,
|
||||
xAxis: drilldownConfig.xAxis,
|
||||
yAxis: drilldownConfig.yAxis,
|
||||
parameterField: parameterField,
|
||||
parameterValue: parameterValue,
|
||||
connection: this.connection
|
||||
});
|
||||
|
||||
// Build the actual API URL with parameter replacement
|
||||
let actualApiUrl = drilldownConfig.apiUrl;
|
||||
console.log('Original API URL:', actualApiUrl);
|
||||
console.log('Parameter value to use:', parameterValue);
|
||||
console.log('Parameter field:', parameterField);
|
||||
|
||||
// Check if the URL contains angle brackets for parameter replacement
|
||||
const hasAngleBrackets = /<[^>]+>/.test(actualApiUrl);
|
||||
|
||||
if (hasAngleBrackets && parameterValue) {
|
||||
// Replace angle brackets placeholder with actual value
|
||||
console.log('Replacing angle brackets with parameter value');
|
||||
const encodedValue = encodeURIComponent(parameterValue);
|
||||
actualApiUrl = actualApiUrl.replace(/<[^>]+>/g, encodedValue);
|
||||
console.log('URL after angle bracket replacement:', actualApiUrl);
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
let url = `chart/getdashjson/grid?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
if (parameterField && parameterValue) {
|
||||
url += `¶meter=${encodeURIComponent(parameterField)}¶meterValue=${encodeURIComponent(parameterValue)}`;
|
||||
}
|
||||
console.log('Drilldown data URL:', url);
|
||||
|
||||
// Convert drilldown layer filters to filter parameters (if applicable)
|
||||
const filterObj = {};
|
||||
|
||||
// Add drilldown layer filters
|
||||
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
|
||||
drilldownConfig.filters.forEach((filter: any) => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add drilldownFilters
|
||||
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let drilldownFilterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('Drilldown filter parameters:', drilldownFilterParams);
|
||||
|
||||
// For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
|
||||
this.dashboardService.getChartData(
|
||||
drilldownConfig.apiUrl, 'grid',
|
||||
drilldownConfig.xAxis, drilldownConfig.yAxis,
|
||||
this.connection,
|
||||
parameterField, parameterValue,
|
||||
drilldownFilterParams
|
||||
).subscribe(
|
||||
(data: any) => {
|
||||
console.log('Received drilldown data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.error = "No data Available";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartData) {
|
||||
this.givendata = data.chartData;
|
||||
this.extractDynamicHeaders(data.chartData);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with drilldown data:', this.givendata);
|
||||
} else if (data && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
this.givendata = data.data;
|
||||
this.extractDynamicHeaders(data.data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with drilldown legacy data format:', this.givendata);
|
||||
} else if (Array.isArray(data)) {
|
||||
// Handle case where data is directly an array
|
||||
this.givendata = data;
|
||||
this.extractDynamicHeaders(data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with drilldown array data:', this.givendata);
|
||||
} else {
|
||||
console.warn('Drilldown received data does not have expected structure', data);
|
||||
this.error = "No valid data received";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching drilldown data:', error);
|
||||
this.error = "Server Error";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Reset to original data (go back to base level)
|
||||
resetToOriginalData(): void {
|
||||
console.log('Resetting to original data');
|
||||
console.log('Current stack before reset:', this.drilldownStack);
|
||||
console.log('Current level before reset:', this.currentDrilldownLevel);
|
||||
|
||||
this.currentDrilldownLevel = 0;
|
||||
this.drilldownStack = [];
|
||||
|
||||
if (this.originalGridData.length > 0) {
|
||||
// Create a deep copy to avoid reference issues
|
||||
this.givendata = JSON.parse(JSON.stringify(this.originalGridData));
|
||||
this.extractDynamicHeaders(this.givendata);
|
||||
console.log('Restored original data');
|
||||
}
|
||||
|
||||
console.log('After reset - data:', this.givendata);
|
||||
|
||||
// Re-fetch original data
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Navigate back to previous drilldown level
|
||||
navigateBack(): void {
|
||||
console.log('Navigating back, current stack:', this.drilldownStack);
|
||||
console.log('Current level:', this.currentDrilldownLevel);
|
||||
|
||||
if (this.drilldownStack.length > 0) {
|
||||
// Remove the last entry from the stack
|
||||
const removedEntry = this.drilldownStack.pop();
|
||||
console.log('Removed entry from stack:', removedEntry);
|
||||
|
||||
// Update the current drilldown level
|
||||
this.currentDrilldownLevel = this.drilldownStack.length;
|
||||
console.log('New level after pop:', this.currentDrilldownLevel);
|
||||
console.log('Stack after pop:', this.drilldownStack);
|
||||
|
||||
if (this.drilldownStack.length > 0) {
|
||||
// Fetch data for the previous level
|
||||
console.log('Fetching data for previous level');
|
||||
this.fetchDrilldownData();
|
||||
} else {
|
||||
// Back to base level
|
||||
console.log('Back to base level, resetting to original data');
|
||||
this.resetToOriginalData();
|
||||
}
|
||||
} else {
|
||||
// Already at base level, reset to original data
|
||||
console.log('Already at base level, resetting to original data');
|
||||
this.resetToOriginalData();
|
||||
}
|
||||
}
|
||||
|
||||
// Method to handle grid row clicks for drilldown
|
||||
onRowClick(item: any, key: string): void {
|
||||
console.log('Grid row clicked:', { item, key });
|
||||
|
||||
// If drilldown is enabled
|
||||
if (this.drilldownEnabled) {
|
||||
// Get the value for the clicked key
|
||||
const clickedValue = item[key];
|
||||
|
||||
console.log('Clicked on row value:', { key, value: clickedValue });
|
||||
|
||||
// If we're not at the base level, store original data
|
||||
if (this.currentDrilldownLevel === 0) {
|
||||
// Store original data before entering drilldown mode
|
||||
// Create a deep copy to avoid reference issues
|
||||
this.originalGridData = JSON.parse(JSON.stringify(this.givendata));
|
||||
console.log('Stored original data for drilldown');
|
||||
}
|
||||
|
||||
// Determine the next drilldown level
|
||||
const nextDrilldownLevel = this.currentDrilldownLevel + 1;
|
||||
|
||||
console.log('Next drilldown level will be:', nextDrilldownLevel);
|
||||
|
||||
// Check if there's a drilldown configuration for this level
|
||||
let hasDrilldownConfig = false;
|
||||
let drilldownConfig;
|
||||
|
||||
if (nextDrilldownLevel === 1) {
|
||||
// Base drilldown level
|
||||
drilldownConfig = {
|
||||
apiUrl: this.drilldownApiUrl,
|
||||
xAxis: this.drilldownXAxis,
|
||||
yAxis: this.drilldownYAxis,
|
||||
parameter: this.drilldownParameter
|
||||
};
|
||||
hasDrilldownConfig = !!this.drilldownApiUrl && !!this.drilldownXAxis && !!this.drilldownYAxis;
|
||||
} else {
|
||||
// Multi-layer drilldown level
|
||||
const layerIndex = nextDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length) {
|
||||
drilldownConfig = this.drilldownLayers[layerIndex];
|
||||
hasDrilldownConfig = drilldownConfig.enabled &&
|
||||
!!drilldownConfig.apiUrl &&
|
||||
!!drilldownConfig.xAxis &&
|
||||
!!drilldownConfig.yAxis;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Drilldown config for next level:', drilldownConfig);
|
||||
console.log('Has drilldown config:', hasDrilldownConfig);
|
||||
|
||||
// If there's a drilldown configuration for the next level, proceed
|
||||
if (hasDrilldownConfig) {
|
||||
// Add this click to the drilldown stack
|
||||
const stackEntry = {
|
||||
level: nextDrilldownLevel,
|
||||
clickedKey: key,
|
||||
clickedValue: clickedValue
|
||||
};
|
||||
|
||||
this.drilldownStack.push(stackEntry);
|
||||
|
||||
console.log('Added to drilldown stack:', stackEntry);
|
||||
console.log('Current drilldown stack:', this.drilldownStack);
|
||||
|
||||
// Update the current drilldown level
|
||||
this.currentDrilldownLevel = nextDrilldownLevel;
|
||||
|
||||
console.log('Entering drilldown level:', this.currentDrilldownLevel);
|
||||
|
||||
// Fetch drilldown data for the new level
|
||||
this.fetchDrilldownData();
|
||||
} else {
|
||||
console.log('No drilldown configuration for level:', nextDrilldownLevel);
|
||||
}
|
||||
} else {
|
||||
console.log('Drilldown not enabled');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,4 +627,23 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, str => str.toUpperCase());
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('GridViewComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
// Clear data to help with garbage collection
|
||||
this.givendata = [];
|
||||
this.dynamicHeaders = [];
|
||||
this.drilldownStack = [];
|
||||
this.originalGridData = [];
|
||||
|
||||
console.log('GridViewComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
# Shield Dashboard
|
||||
|
||||
A professional analytics dashboard UI similar to the "Shield Overall Dashboard" design.
|
||||
|
||||
## Components
|
||||
|
||||
### Layout
|
||||
- **Left Sidebar**: Contains application name, KPI metrics, and filter controls
|
||||
- **Main Dashboard Area**: Grid-based responsive layout with various data visualization components
|
||||
|
||||
### Components
|
||||
|
||||
1. **Sidebar Filters**
|
||||
- Application header with title
|
||||
- KPI metrics display (Total Leads, Total Deals)
|
||||
- Filter controls (Sales Rep, Partner, Traction Channel, Sub Product Line)
|
||||
- Reset filters button
|
||||
|
||||
2. **Bar Chart**
|
||||
- Title: "Deal Stage Wise Progress"
|
||||
- Visualizes deal progress across different stages
|
||||
- Responsive design with loading shimmer effect
|
||||
|
||||
3. **Donut Charts**
|
||||
- End Customer Stage Split
|
||||
- Segment Penetration
|
||||
- Interactive with tooltips
|
||||
|
||||
4. **Map Chart**
|
||||
- Dealer locations with colored markers
|
||||
- Interactive hover tooltips showing dealer status
|
||||
- Legend for status colors
|
||||
|
||||
5. **Data Table**
|
||||
- Cross/Up Selling Scope
|
||||
- Scrollable with sticky header
|
||||
- Alternating row colors
|
||||
- Sortable columns
|
||||
- Probability bars with color coding
|
||||
|
||||
6. **Deal Details Card**
|
||||
- Company Name, Stage, Description, Amount, Stage Days
|
||||
- Color-coded deal stages
|
||||
- Responsive grid layout
|
||||
|
||||
7. **Quarterwise Flow**
|
||||
- Placeholder for chart configuration
|
||||
- "Chart configuration incomplete" message
|
||||
|
||||
## Features
|
||||
|
||||
### Responsiveness
|
||||
- Grid-based layout adapts to different screen sizes
|
||||
- Mobile-friendly design with stacked layout on small screens
|
||||
- Flexible components that resize appropriately
|
||||
|
||||
### Filter Functionality
|
||||
- Global filters that affect all dashboard components
|
||||
- Shared state management using BehaviorSubject
|
||||
- Real-time updates when filters change
|
||||
- Reset filters functionality
|
||||
|
||||
### Visual Design
|
||||
- Dark Navy Blue for headers and bars
|
||||
- Bright Orange for highlights and KPI boxes
|
||||
- White background for charts and tables
|
||||
- Modern color scheme with consistent styling
|
||||
|
||||
### Interactive Elements
|
||||
- Chart tooltips on hover
|
||||
- Interactive map with dealer status information
|
||||
- Sortable data table
|
||||
- Loading shimmer effects during data updates
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Technologies Used
|
||||
- Angular 16
|
||||
- Chart.js with ng2-charts
|
||||
- Clarity Design System
|
||||
- SCSS for styling
|
||||
|
||||
### State Management
|
||||
- Shared service using BehaviorSubject for filter state
|
||||
- Reactive components that update based on filter changes
|
||||
- Simulated data updates (no backend integration)
|
||||
|
||||
### Responsive Design
|
||||
- CSS Grid and Flexbox layouts
|
||||
- Media queries for different screen sizes
|
||||
- Relative units for scalable components
|
||||
|
||||
## Component Structure
|
||||
|
||||
```
|
||||
shield-dashboard/
|
||||
├── shield-dashboard.component.ts|.html|.scss
|
||||
├── services/
|
||||
│ └── dashboard-filter.service.ts
|
||||
├── components/
|
||||
│ ├── sidebar-filters/
|
||||
│ │ ├── sidebar-filters.component.ts|.html|.scss
|
||||
│ ├── bar-chart/
|
||||
│ │ ├── bar-chart.component.ts|.html|.scss
|
||||
│ ├── donut-chart/
|
||||
│ │ ├── donut-chart.component.ts|.html|.scss
|
||||
│ ├── map-chart/
|
||||
│ │ ├── map-chart.component.ts|.html|.scss
|
||||
│ ├── data-table/
|
||||
│ │ ├── data-table.component.ts|.html|.scss
|
||||
│ ├── deal-details-card/
|
||||
│ │ ├── deal-details-card.component.ts|.html|.scss
|
||||
│ ├── quarterwise-flow/
|
||||
│ │ ├── quarterwise-flow.component.ts|.html|.scss
|
||||
│ └── loading-shimmer/
|
||||
│ ├── loading-shimmer.component.ts|.html|.scss
|
||||
└── shield-dashboard-routing.module.ts
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
To navigate to the Shield Dashboard, visit:
|
||||
`/cns-portal/shield-dashboard`
|
||||
|
||||
The dashboard is fully responsive and will adapt to different screen sizes. All components are interconnected through the shared filter service, so changing any filter will update all visualizations in real-time.
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>Deal Stage Wise Progress</h3>
|
||||
</div>
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-content" [class.loading]="isLoading">
|
||||
<canvas
|
||||
baseChart
|
||||
[data]="barChartData"
|
||||
[options]="barChartOptions"
|
||||
[type]="barChartType"
|
||||
(chartClick)="chartClicked($event)"
|
||||
(chartHover)="chartHovered($event)">
|
||||
</canvas>
|
||||
<div class="loading-overlay" *ngIf="isLoading">
|
||||
<div class="shimmer-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chart-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.chart-content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
|
||||
canvas {
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Component, OnInit, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
|
||||
import { BaseChartDirective } from 'ng2-charts';
|
||||
import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js';
|
||||
import { DashboardFilterService } from '../../services/dashboard-filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-bar-chart',
|
||||
templateUrl: './bar-chart.component.html',
|
||||
styleUrls: ['./bar-chart.component.scss']
|
||||
})
|
||||
export class BarChartComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChild(BaseChartDirective) chart?: BaseChartDirective;
|
||||
|
||||
private filterSubscription: Subscription = new Subscription();
|
||||
|
||||
// Loading state
|
||||
isLoading: boolean = false;
|
||||
|
||||
public barChartOptions: ChartConfiguration['options'] = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.05)'
|
||||
},
|
||||
ticks: {
|
||||
color: '#64748b'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.05)'
|
||||
},
|
||||
ticks: {
|
||||
color: '#64748b'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(10, 25, 47, 0.9)',
|
||||
titleColor: '#ff6b35',
|
||||
bodyColor: '#ffffff',
|
||||
borderColor: 'rgba(255, 107, 53, 0.3)',
|
||||
borderWidth: 1,
|
||||
cornerRadius: 6
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public barChartType: ChartType = 'bar';
|
||||
public barChartPlugins = [];
|
||||
|
||||
public barChartData: ChartData<'bar'> = {
|
||||
labels: ['Prospecting', 'Qualification', 'Needs Analysis', 'Value Proposition', 'Decision Making', 'Negotiation'],
|
||||
datasets: [
|
||||
{
|
||||
data: [65, 59, 80, 81, 56, 55],
|
||||
label: 'Deal Progress',
|
||||
backgroundColor: '#0a192f',
|
||||
borderColor: '#0a192f',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
constructor(private filterService: DashboardFilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.filterSubscription.add(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
this.updateChartData(filters);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
// Initial chart render
|
||||
this.updateChart();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Update chart data based on filters
|
||||
updateChartData(filters: any): void {
|
||||
// Show loading state
|
||||
this.isLoading = true;
|
||||
|
||||
// Simulate data change based on filters
|
||||
// In a real implementation, this would fetch new data from an API
|
||||
const baseData = [65, 59, 80, 81, 56, 55];
|
||||
|
||||
// Apply filter effects (simplified logic)
|
||||
let multiplier = 1;
|
||||
if (filters.salesRep) multiplier *= 0.9;
|
||||
if (filters.partner) multiplier *= 0.85;
|
||||
if (filters.tractionChannel) multiplier *= 0.95;
|
||||
if (filters.subProductLine) multiplier *= 0.8;
|
||||
|
||||
// Add a small delay to simulate loading
|
||||
setTimeout(() => {
|
||||
this.barChartData.datasets[0].data = baseData.map(value =>
|
||||
Math.floor(value * multiplier)
|
||||
);
|
||||
|
||||
// Update chart
|
||||
this.updateChart();
|
||||
|
||||
// Hide loading state
|
||||
this.isLoading = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Update chart with new data
|
||||
updateChart(): void {
|
||||
if (this.chart) {
|
||||
this.chart.update();
|
||||
}
|
||||
}
|
||||
|
||||
// events
|
||||
public chartClicked({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
|
||||
console.log(event, active);
|
||||
}
|
||||
|
||||
public chartHovered({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
|
||||
console.log(event, active);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<div class="table-container">
|
||||
<div class="table-header">
|
||||
<h3>Cross/Up Selling Scope</h3>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th (click)="sortTable('companyName')" class="sortable">
|
||||
Company Name
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'companyName'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
<th (click)="sortTable('contactPerson')" class="sortable">
|
||||
Contact Person
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'contactPerson'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
<th (click)="sortTable('product')" class="sortable">
|
||||
Product
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'product'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
<th (click)="sortTable('potentialValue')" class="sortable">
|
||||
Potential Value
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'potentialValue'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
<th (click)="sortTable('probability')" class="sortable">
|
||||
Probability
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'probability'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
<th (click)="sortTable('nextAction')" class="sortable">
|
||||
Next Action
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'nextAction'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
<th (click)="sortTable('actionDate')" class="sortable">
|
||||
Action Date
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'actionDate'">
|
||||
<clr-icon *ngIf="sortDirection === 'asc'" shape="arrow up"></clr-icon>
|
||||
<clr-icon *ngIf="sortDirection === 'desc'" shape="arrow down"></clr-icon>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of crossSellingData; let i = index" [class.even]="i % 2 === 0">
|
||||
<td>{{ item.companyName }}</td>
|
||||
<td>{{ item.contactPerson }}</td>
|
||||
<td>{{ item.product }}</td>
|
||||
<td>{{ formatCurrency(item.potentialValue) }}</td>
|
||||
<td>
|
||||
<div class="probability-bar">
|
||||
<div class="probability-fill" [style.width.%]="item.probability" [style.background-color]="item.probability > 70 ? '#0a192f' : item.probability > 50 ? '#ff6b35' : '#64748b'"></div>
|
||||
<span class="probability-text">{{ formatProbability(item.probability) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ item.nextAction }}</td>
|
||||
<td>{{ item.actionDate }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,107 @@
|
||||
.table-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.table-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
|
||||
thead {
|
||||
background: #f1f5f9;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
|
||||
th {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
&.sortable {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.sort-indicator {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #ff6b35;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
|
||||
&.even {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px 15px;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
|
||||
.probability-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
.probability-fill {
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.probability-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { DashboardFilterService } from '../../services/dashboard-filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
interface CrossSellingItem {
|
||||
id: number;
|
||||
companyName: string;
|
||||
contactPerson: string;
|
||||
product: string;
|
||||
potentialValue: number;
|
||||
probability: number;
|
||||
nextAction: string;
|
||||
actionDate: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-data-table',
|
||||
templateUrl: './data-table.component.html',
|
||||
styleUrls: ['./data-table.component.scss']
|
||||
})
|
||||
export class DataTableComponent implements OnInit, OnDestroy {
|
||||
private filterSubscription: Subscription = new Subscription();
|
||||
|
||||
// Mock data for cross/up selling scope
|
||||
originalCrossSellingData: CrossSellingItem[] = [
|
||||
{ id: 1, companyName: 'Tech Solutions Inc', contactPerson: 'John Smith', product: 'Product A', potentialValue: 50000, probability: 75, nextAction: 'Follow-up call', actionDate: '2023-06-15' },
|
||||
{ id: 2, companyName: 'Global Enterprises', contactPerson: 'Sarah Johnson', product: 'Product B', potentialValue: 75000, probability: 60, nextAction: 'Send proposal', actionDate: '2023-06-18' },
|
||||
{ id: 3, companyName: 'Innovative Systems', contactPerson: 'Mike Brown', product: 'Product C', potentialValue: 30000, probability: 85, nextAction: 'Demo scheduled', actionDate: '2023-06-20' },
|
||||
{ id: 4, companyName: 'Future Tech Ltd', contactPerson: 'Emily Davis', product: 'Product A', potentialValue: 45000, probability: 70, nextAction: 'Send quote', actionDate: '2023-06-22' },
|
||||
{ id: 5, companyName: 'Digital Dynamics', contactPerson: 'Robert Wilson', product: 'Product B', potentialValue: 60000, probability: 55, nextAction: 'Meeting scheduled', actionDate: '2023-06-25' },
|
||||
{ id: 6, companyName: 'Alpha Solutions', contactPerson: 'Lisa Miller', product: 'Product C', potentialValue: 40000, probability: 80, nextAction: 'Follow-up email', actionDate: '2023-06-28' },
|
||||
{ id: 7, companyName: 'Beta Innovations', contactPerson: 'David Taylor', product: 'Product A', potentialValue: 55000, probability: 65, nextAction: 'Product demo', actionDate: '2023-06-28' },
|
||||
{ id: 8, companyName: 'Gamma Technologies', contactPerson: 'Jennifer Anderson', product: 'Product B', potentialValue: 70000, probability: 50, nextAction: 'Proposal review', actionDate: '2023-07-05' }
|
||||
];
|
||||
|
||||
crossSellingData: CrossSellingItem[] = [...this.originalCrossSellingData];
|
||||
|
||||
// Sorting properties
|
||||
sortColumn: keyof CrossSellingItem = 'companyName';
|
||||
sortDirection: 'asc' | 'desc' = 'asc';
|
||||
|
||||
constructor(private filterService: DashboardFilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.filterSubscription.add(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
this.updateTableData(filters);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Update table data based on filters
|
||||
updateTableData(filters: any): void {
|
||||
// Simulate data change based on filters
|
||||
// In a real implementation, this would fetch new data from an API
|
||||
|
||||
if (filters.salesRep || filters.partner || filters.tractionChannel || filters.subProductLine) {
|
||||
// Apply filter effects (simplified logic)
|
||||
const filteredData = this.originalCrossSellingData.filter(item => {
|
||||
// Simple filtering logic - in a real app, this would be more sophisticated
|
||||
return Math.random() > 0.3; // Randomly filter out some items
|
||||
});
|
||||
|
||||
this.crossSellingData = filteredData;
|
||||
} else {
|
||||
// No filters applied, show all data
|
||||
this.crossSellingData = [...this.originalCrossSellingData];
|
||||
}
|
||||
|
||||
// Re-apply current sorting
|
||||
this.sortTable(this.sortColumn);
|
||||
}
|
||||
|
||||
// Sort table by column
|
||||
sortTable(column: keyof CrossSellingItem): void {
|
||||
if (this.sortColumn === column) {
|
||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.sortColumn = column;
|
||||
this.sortDirection = 'asc';
|
||||
}
|
||||
|
||||
this.crossSellingData.sort((a, b) => {
|
||||
const aValue = a[column];
|
||||
const bValue = b[column];
|
||||
|
||||
if (aValue < bValue) {
|
||||
return this.sortDirection === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (aValue > bValue) {
|
||||
return this.sortDirection === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Format currency
|
||||
formatCurrency(value: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
// Format probability as percentage
|
||||
formatProbability(probability: number): string {
|
||||
return `${probability}%`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<div class="deal-details-container">
|
||||
<div class="deal-details-header">
|
||||
<h3>Deal Details</h3>
|
||||
</div>
|
||||
<div class="deal-cards">
|
||||
<div class="deal-card" *ngFor="let deal of dealDetails">
|
||||
<div class="deal-header">
|
||||
<div class="company-name">{{ deal.companyName }}</div>
|
||||
<div class="deal-stage" [style.background-color]="getStageColor(deal.stage)">
|
||||
{{ deal.stage }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="deal-description">
|
||||
{{ deal.description }}
|
||||
</div>
|
||||
<div class="deal-info">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Amount</div>
|
||||
<div class="info-value">{{ formatCurrency(deal.amount) }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Stage Days</div>
|
||||
<div class="info-value">{{ deal.stageDays }} days</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Contact</div>
|
||||
<div class="info-value">{{ deal.contactPerson }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Last Contact</div>
|
||||
<div class="info-value">{{ deal.lastContact }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,102 @@
|
||||
.deal-details-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.deal-details-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.deal-cards {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
overflow-y: auto;
|
||||
|
||||
.deal-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.deal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.company-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.deal-stage {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
text-align: center;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.deal-description {
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.deal-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
|
||||
.info-item {
|
||||
.info-label {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #0a192f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.deal-details-container {
|
||||
.deal-cards {
|
||||
.deal-card {
|
||||
.deal-info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { DashboardFilterService } from '../../services/dashboard-filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
interface DealDetail {
|
||||
id: number;
|
||||
companyName: string;
|
||||
stage: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
stageDays: number;
|
||||
contactPerson: string;
|
||||
lastContact: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-deal-details-card',
|
||||
templateUrl: './deal-details-card.component.html',
|
||||
styleUrls: ['./deal-details-card.component.scss']
|
||||
})
|
||||
export class DealDetailsCardComponent implements OnInit, OnDestroy {
|
||||
private filterSubscription: Subscription = new Subscription();
|
||||
|
||||
// Mock deal details data
|
||||
originalDealDetails: DealDetail[] = [
|
||||
{
|
||||
id: 1,
|
||||
companyName: 'Tech Solutions Inc',
|
||||
stage: 'Negotiation',
|
||||
description: 'Enterprise software solution for HR management',
|
||||
amount: 125000,
|
||||
stageDays: 15,
|
||||
contactPerson: 'John Smith',
|
||||
lastContact: '2023-06-10'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
companyName: 'Global Enterprises',
|
||||
stage: 'Decision Making',
|
||||
description: 'Cloud infrastructure services migration',
|
||||
amount: 85000,
|
||||
stageDays: 8,
|
||||
contactPerson: 'Sarah Johnson',
|
||||
lastContact: '2023-06-12'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
companyName: 'Innovative Systems',
|
||||
stage: 'Value Proposition',
|
||||
description: 'Custom AI implementation for logistics',
|
||||
amount: 210000,
|
||||
stageDays: 22,
|
||||
contactPerson: 'Mike Brown',
|
||||
lastContact: '2023-06-05'
|
||||
}
|
||||
];
|
||||
|
||||
dealDetails: DealDetail[] = [...this.originalDealDetails];
|
||||
|
||||
// Stage colors
|
||||
stageColors: { [key: string]: string } = {
|
||||
'Prospecting': '#93c5fd',
|
||||
'Qualification': '#60a5fa',
|
||||
'Needs Analysis': '#3b82f6',
|
||||
'Value Proposition': '#1d4ed8',
|
||||
'Decision Making': '#0a192f',
|
||||
'Negotiation': '#ff6b35'
|
||||
};
|
||||
|
||||
constructor(private filterService: DashboardFilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.filterSubscription.add(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
this.updateDealData(filters);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Update deal data based on filters
|
||||
updateDealData(filters: any): void {
|
||||
// Simulate data change based on filters
|
||||
// In a real implementation, this would fetch new data from an API
|
||||
|
||||
if (filters.salesRep || filters.partner || filters.tractionChannel || filters.subProductLine) {
|
||||
// Apply filter effects (simplified logic)
|
||||
const filteredData = this.originalDealDetails.filter(item => {
|
||||
// Simple filtering logic - in a real app, this would be more sophisticated
|
||||
return Math.random() > 0.2; // Randomly filter out some items
|
||||
});
|
||||
|
||||
this.dealDetails = filteredData;
|
||||
} else {
|
||||
// No filters applied, show all data
|
||||
this.dealDetails = [...this.originalDealDetails];
|
||||
}
|
||||
}
|
||||
|
||||
// Get stage color
|
||||
getStageColor(stage: string): string {
|
||||
return this.stageColors[stage] || '#64748b';
|
||||
}
|
||||
|
||||
// Format currency
|
||||
formatCurrency(value: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>{{ chartTitle }}</h3>
|
||||
</div>
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-content" [class.loading]="isLoading">
|
||||
<canvas
|
||||
baseChart
|
||||
[datasets]="chartData.datasets"
|
||||
[labels]="chartData.labels"
|
||||
[options]="donutChartOptions"
|
||||
[type]="donutChartType"
|
||||
(chartClick)="chartClicked($event)"
|
||||
(chartHover)="chartHovered($event)">
|
||||
</canvas>
|
||||
<div class="loading-overlay" *ngIf="isLoading">
|
||||
<div class="shimmer-donut"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chart-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.chart-content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
|
||||
canvas {
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { Component, Input, OnInit, ViewChild, OnDestroy } from '@angular/core';
|
||||
import { BaseChartDirective } from 'ng2-charts';
|
||||
import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js';
|
||||
import { DashboardFilterService } from '../../services/dashboard-filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-donut-chart',
|
||||
templateUrl: './donut-chart.component.html',
|
||||
styleUrls: ['./donut-chart.component.scss']
|
||||
})
|
||||
export class DonutChartComponent implements OnInit, OnDestroy {
|
||||
@Input() chartType: 'endCustomer' | 'segmentPenetration' = 'endCustomer';
|
||||
|
||||
@ViewChild(BaseChartDirective) chart?: BaseChartDirective;
|
||||
|
||||
private filterSubscription: Subscription = new Subscription();
|
||||
|
||||
// Loading state
|
||||
isLoading: boolean = false;
|
||||
|
||||
public donutChartOptions: ChartConfiguration['options'] = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
color: '#64748b',
|
||||
font: {
|
||||
size: 12
|
||||
},
|
||||
padding: 20
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(10, 25, 47, 0.9)',
|
||||
titleColor: '#ff6b35',
|
||||
bodyColor: '#ffffff',
|
||||
borderColor: 'rgba(255, 107, 53, 0.3)',
|
||||
borderWidth: 1,
|
||||
cornerRadius: 6,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed;
|
||||
return `${label}: ${value}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public donutChartType: ChartType = 'doughnut';
|
||||
public donutChartPlugins = [];
|
||||
|
||||
// Data for End Customer Stage Split
|
||||
public endCustomerData: ChartData<'doughnut'> = {
|
||||
labels: ['Prospecting', 'Qualification', 'Needs Analysis', 'Value Proposition', 'Decision Making'],
|
||||
datasets: [
|
||||
{
|
||||
data: [30, 25, 20, 15, 10],
|
||||
backgroundColor: [
|
||||
'#0a192f',
|
||||
'#1e3a8a',
|
||||
'#3b82f6',
|
||||
'#60a5fa',
|
||||
'#93c5fd'
|
||||
],
|
||||
hoverBackgroundColor: [
|
||||
'#1e3a8a',
|
||||
'#3b82f6',
|
||||
'#60a5fa',
|
||||
'#93c5fd',
|
||||
'#bfdbfe'
|
||||
],
|
||||
borderWidth: 0
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Data for Segment Penetration
|
||||
public segmentPenetrationData: ChartData<'doughnut'> = {
|
||||
labels: ['Enterprise', 'Mid-Market', 'SMB', 'Startup'],
|
||||
datasets: [
|
||||
{
|
||||
data: [40, 30, 20, 10],
|
||||
backgroundColor: [
|
||||
'#ff6b35',
|
||||
'#ff8c66',
|
||||
'#ffb099',
|
||||
'#ffd6cc'
|
||||
],
|
||||
hoverBackgroundColor: [
|
||||
'#ff8c66',
|
||||
'#ffb099',
|
||||
'#ffd6cc',
|
||||
'#ffffff'
|
||||
],
|
||||
borderWidth: 0
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
constructor(private filterService: DashboardFilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.filterSubscription.add(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
this.updateChartData(filters);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Get chart data based on chart type
|
||||
get chartData(): ChartData<'doughnut'> {
|
||||
return this.chartType === 'endCustomer' ? this.endCustomerData : this.segmentPenetrationData;
|
||||
}
|
||||
|
||||
// Get chart title based on chart type
|
||||
get chartTitle(): string {
|
||||
return this.chartType === 'endCustomer'
|
||||
? 'End Customer Stage Split'
|
||||
: 'Segment Penetration';
|
||||
}
|
||||
|
||||
// Update chart data based on filters
|
||||
updateChartData(filters: any): void {
|
||||
// Show loading state
|
||||
this.isLoading = true;
|
||||
|
||||
// Simulate data change based on filters
|
||||
// In a real implementation, this would fetch new data from an API
|
||||
|
||||
// Add a small delay to simulate loading
|
||||
setTimeout(() => {
|
||||
if (this.chartType === 'endCustomer') {
|
||||
const baseData = [30, 25, 20, 15, 10];
|
||||
|
||||
// Apply filter effects (simplified logic)
|
||||
let multiplier = 1;
|
||||
if (filters.salesRep) multiplier *= 0.9;
|
||||
if (filters.partner) multiplier *= 0.85;
|
||||
if (filters.tractionChannel) multiplier *= 0.95;
|
||||
if (filters.subProductLine) multiplier *= 0.8;
|
||||
|
||||
this.endCustomerData.datasets[0].data = baseData.map(value =>
|
||||
Math.floor(value * multiplier)
|
||||
);
|
||||
} else {
|
||||
const baseData = [40, 30, 20, 10];
|
||||
|
||||
// Apply filter effects (simplified logic)
|
||||
let multiplier = 1;
|
||||
if (filters.salesRep) multiplier *= 0.85;
|
||||
if (filters.partner) multiplier *= 0.9;
|
||||
if (filters.tractionChannel) multiplier *= 0.95;
|
||||
if (filters.subProductLine) multiplier *= 0.75;
|
||||
|
||||
this.segmentPenetrationData.datasets[0].data = baseData.map(value =>
|
||||
Math.floor(value * multiplier)
|
||||
);
|
||||
}
|
||||
|
||||
// Update chart
|
||||
this.updateChart();
|
||||
|
||||
// Hide loading state
|
||||
this.isLoading = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Update chart with new data
|
||||
updateChart(): void {
|
||||
if (this.chart) {
|
||||
this.chart.update();
|
||||
}
|
||||
}
|
||||
|
||||
// events
|
||||
public chartClicked({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
|
||||
console.log(event, active);
|
||||
}
|
||||
|
||||
public chartHovered({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
|
||||
console.log(event, active);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
.shimmer-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.shimmer-content {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.shimmer-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
|
||||
.shimmer-animation {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
.shimmer-content {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-loading-shimmer',
|
||||
template: `
|
||||
<div class="shimmer-container" [ngClass]="{ 'active': loading }">
|
||||
<div class="shimmer-content">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="shimmer-overlay" *ngIf="loading">
|
||||
<div class="shimmer-animation"></div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./loading-shimmer.component.scss']
|
||||
})
|
||||
export class LoadingShimmerComponent {
|
||||
@Input() loading: boolean = false;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<div class="map-container">
|
||||
<div class="map-header">
|
||||
<h3>Dealer Locations</h3>
|
||||
</div>
|
||||
<div class="map-wrapper">
|
||||
<!-- Simple map representation with dealer markers -->
|
||||
<div class="map-placeholder">
|
||||
<div class="india-map">
|
||||
<!-- Simplified India map outline -->
|
||||
<div class="map-outline"></div>
|
||||
|
||||
<!-- Dealer markers -->
|
||||
<div
|
||||
*ngFor="let dealer of dealerLocations"
|
||||
class="dealer-marker"
|
||||
[style.left]="dealer.lng * 0.8 + 10 + '%'"
|
||||
[style.top]="100 - (dealer.lat * 1.2) + '%'"
|
||||
[style.background-color]="getStatusColor(dealer.status)"
|
||||
(mouseenter)="onDealerHover(dealer)"
|
||||
(mouseleave)="onDealerLeave()"
|
||||
[attr.data-tooltip]="dealer.name + ' - ' + dealer.city + ' (' + getStatusLabel(dealer.status) + ')'">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="map-legend">
|
||||
<div class="legend-item" *ngFor="let status of ['active', 'inactive', 'training', 'onboarding']">
|
||||
<div class="legend-color" [style.background-color]="getStatusColor(status)"></div>
|
||||
<div class="legend-label">{{ getStatusLabel(status) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dealer info tooltip -->
|
||||
<div class="dealer-tooltip" *ngIf="hoveredDealer" [style.left]="hoveredDealer.lng * 0.8 + 12 + '%'" [style.top]="100 - (hoveredDealer.lat * 1.2) - 5 + '%'">
|
||||
<div class="tooltip-content">
|
||||
<div class="dealer-name">{{ hoveredDealer.name }}</div>
|
||||
<div class="dealer-location">{{ hoveredDealer.city }}, {{ hoveredDealer.state }}</div>
|
||||
<div class="dealer-status" [style.color]="getStatusColor(hoveredDealer.status)">{{ getStatusLabel(hoveredDealer.status) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,135 @@
|
||||
.map-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.map-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.map-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.map-placeholder {
|
||||
height: 100%;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.india-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: #e2e8f0;
|
||||
|
||||
.map-outline {
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
bottom: 10%;
|
||||
background: #cbd5e1;
|
||||
border-radius: 50% 40% 45% 50% / 40% 50% 40% 45%;
|
||||
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dealer-marker {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translate(-50%, -50%) scale(1.3);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-legend {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dealer-tooltip {
|
||||
position: absolute;
|
||||
background: rgba(10, 25, 47, 0.95);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
padding: 10px 15px;
|
||||
transform: translate(-50%, -100%);
|
||||
margin-top: -10px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
min-width: 180px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: rgba(10, 25, 47, 0.95) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
.dealer-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dealer-location {
|
||||
font-size: 13px;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dealer-status {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { DashboardFilterService } from '../../services/dashboard-filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
interface DealerLocation {
|
||||
id: number;
|
||||
name: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
status: 'active' | 'inactive' | 'training' | 'onboarding';
|
||||
city: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-map-chart',
|
||||
templateUrl: './map-chart.component.html',
|
||||
styleUrls: ['./map-chart.component.scss']
|
||||
})
|
||||
export class MapChartComponent implements OnInit, OnDestroy {
|
||||
private filterSubscription: Subscription = new Subscription();
|
||||
|
||||
// Mock dealer location data
|
||||
originalDealerLocations: DealerLocation[] = [
|
||||
{ id: 1, name: 'ABC Motors', lat: 28.6139, lng: 77.2090, status: 'active', city: 'New Delhi', state: 'Delhi' },
|
||||
{ id: 2, name: 'XYZ Auto', lat: 19.0760, lng: 72.8777, status: 'active', city: 'Mumbai', state: 'Maharashtra' },
|
||||
{ id: 3, name: 'PQR Dealers', lat: 13.0827, lng: 80.2707, status: 'training', city: 'Chennai', state: 'Tamil Nadu' },
|
||||
{ id: 4, name: 'LMN Enterprises', lat: 12.9716, lng: 77.5946, status: 'inactive', city: 'Bangalore', state: 'Karnataka' },
|
||||
{ id: 5, name: 'DEF Solutions', lat: 22.5726, lng: 88.3639, status: 'active', city: 'Kolkata', state: 'West Bengal' },
|
||||
{ id: 6, name: 'GHI Services', lat: 25.3176, lng: 82.9739, status: 'onboarding', city: 'Varanasi', state: 'Uttar Pradesh' },
|
||||
{ id: 7, name: 'JKL Group', lat: 23.0225, lng: 72.5714, status: 'active', city: 'Ahmedabad', state: 'Gujarat' },
|
||||
{ id: 8, name: 'MNO Corp', lat: 18.5204, lng: 73.8567, status: 'training', city: 'Pune', state: 'Maharashtra' }
|
||||
];
|
||||
|
||||
dealerLocations: DealerLocation[] = [...this.originalDealerLocations];
|
||||
|
||||
// Status colors
|
||||
statusColors = {
|
||||
active: '#0a192f',
|
||||
inactive: '#64748b',
|
||||
training: '#ff6b35',
|
||||
onboarding: '#f59e0b'
|
||||
};
|
||||
|
||||
// Status labels
|
||||
statusLabels = {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
training: 'Training',
|
||||
onboarding: 'Onboarding'
|
||||
};
|
||||
|
||||
hoveredDealer: DealerLocation | null = null;
|
||||
|
||||
constructor(private filterService: DashboardFilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.filterSubscription.add(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
this.updateMapData(filters);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Update map data based on filters
|
||||
updateMapData(filters: any): void {
|
||||
// Simulate data change based on filters
|
||||
// In a real implementation, this would fetch new data from an API
|
||||
|
||||
if (filters.salesRep || filters.partner || filters.tractionChannel || filters.subProductLine) {
|
||||
// Apply filter effects (simplified logic)
|
||||
const filteredData = this.originalDealerLocations.filter(location => {
|
||||
// Simple filtering logic - in a real app, this would be more sophisticated
|
||||
return Math.random() > 0.25; // Randomly filter out some locations
|
||||
});
|
||||
|
||||
this.dealerLocations = filteredData;
|
||||
} else {
|
||||
// No filters applied, show all data
|
||||
this.dealerLocations = [...this.originalDealerLocations];
|
||||
}
|
||||
}
|
||||
|
||||
// Get status color based on dealer status
|
||||
getStatusColor(status: string): string {
|
||||
return (this.statusColors as any)[status] || '#64748b';
|
||||
}
|
||||
|
||||
// Get status label based on dealer status
|
||||
getStatusLabel(status: string): string {
|
||||
return (this.statusLabels as any)[status] || status;
|
||||
}
|
||||
|
||||
// Handle dealer hover
|
||||
onDealerHover(dealer: DealerLocation): void {
|
||||
this.hoveredDealer = dealer;
|
||||
}
|
||||
|
||||
// Handle dealer leave
|
||||
onDealerLeave(): void {
|
||||
this.hoveredDealer = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="quarterwise-flow-container">
|
||||
<div class="quarterwise-flow-header">
|
||||
<h3>Quarterwise Flow</h3>
|
||||
</div>
|
||||
<div class="quarterwise-flow-content">
|
||||
<div class="placeholder-message">
|
||||
<div class="message-icon">
|
||||
<i class="info-icon">ℹ️</i>
|
||||
</div>
|
||||
<div class="message-text">
|
||||
<h4>Chart Configuration Incomplete</h4>
|
||||
<p>Please configure the quarterwise flow chart settings to visualize the data.</p>
|
||||
</div>
|
||||
<button class="configure-button">
|
||||
<i class="config-icon">⚙️</i>
|
||||
Configure Chart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
.quarterwise-flow-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.quarterwise-flow-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.quarterwise-flow-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.placeholder-message {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
|
||||
.message-icon {
|
||||
color: #94a3b8;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
h4 {
|
||||
color: #0a192f;
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #64748b;
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.configure-button {
|
||||
padding: 10px 20px;
|
||||
background: #0a192f;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #1e3a8a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shield-quarterwise-flow',
|
||||
templateUrl: './quarterwise-flow.component.html',
|
||||
styleUrls: ['./quarterwise-flow.component.scss']
|
||||
})
|
||||
export class QuarterwiseFlowComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="sidebar-filters">
|
||||
<!-- Component Palette Button and List have been moved to the main shield dashboard component -->
|
||||
</div>
|
||||
@@ -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,216 @@
|
||||
.sidebar-filters {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.componentbtn {
|
||||
margin: 10px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
padding: 0;
|
||||
margin: 0 10px;
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 5px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
cursor: move;
|
||||
|
||||
&:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.has-badge {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-header {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.app-name {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 5px 0;
|
||||
color: #ff6b35;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.kpi-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
|
||||
.kpi-title {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.total-leads {
|
||||
border-left: 3px solid #ff6b35;
|
||||
}
|
||||
|
||||
.total-deals {
|
||||
border-left: 3px solid #0a192f;
|
||||
}
|
||||
}
|
||||
|
||||
.component-palette-section {
|
||||
margin: 20px;
|
||||
|
||||
.component-palette-button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: rgba(255, 107, 53, 0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 107, 53, 0.5);
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 107, 53, 0.3);
|
||||
border-color: #ff6b35;
|
||||
}
|
||||
}
|
||||
|
||||
.component-palette {
|
||||
margin-top: 15px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
|
||||
.palette-title {
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
margin: 0 0 15px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.component-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.component-item {
|
||||
padding: 10px 15px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
cursor: move;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 107, 53, 0.2);
|
||||
border-color: #ff6b35;
|
||||
}
|
||||
|
||||
.drag-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filters-section {
|
||||
flex: 1;
|
||||
|
||||
.filters-title {
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
margin: 0 0 20px 0;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 20px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 100%;
|
||||
padding: 10px 15px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #ff6b35;
|
||||
box-shadow: 0 0 0 2px rgba(255, 107, 53, 0.2);
|
||||
}
|
||||
|
||||
option {
|
||||
background: #0a192f;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 107, 53, 0.2);
|
||||
border-color: #ff6b35;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export * from './shield-dashboard.component';
|
||||
|
||||
// Export all components
|
||||
export * from './components/sidebar-filters/sidebar-filters.component';
|
||||
export * from './components/bar-chart/bar-chart.component';
|
||||
export * from './components/donut-chart/donut-chart.component';
|
||||
export * from './components/map-chart/map-chart.component';
|
||||
export * from './components/data-table/data-table.component';
|
||||
export * from './components/deal-details-card/deal-details-card.component';
|
||||
export * from './components/quarterwise-flow/quarterwise-flow.component';
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
// Define the filter state interface
|
||||
export interface FilterState {
|
||||
salesRep: string;
|
||||
partner: string;
|
||||
tractionChannel: string;
|
||||
subProductLine: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DashboardFilterService {
|
||||
// Shared filter state using BehaviorSubject
|
||||
private filterStateSubject = new BehaviorSubject<FilterState>({
|
||||
salesRep: '',
|
||||
partner: '',
|
||||
tractionChannel: '',
|
||||
subProductLine: ''
|
||||
});
|
||||
|
||||
public filterState$ = this.filterStateSubject.asObservable();
|
||||
|
||||
// KPI data
|
||||
private kpiDataSubject = new BehaviorSubject<{ totalLeads: number; totalDeals: number }>({
|
||||
totalLeads: 1248,
|
||||
totalDeals: 842
|
||||
});
|
||||
|
||||
public kpiData$ = this.kpiDataSubject.asObservable();
|
||||
|
||||
constructor() { }
|
||||
|
||||
// Update filter state
|
||||
updateFilter(filterType: keyof FilterState, value: string): void {
|
||||
const currentState = this.filterStateSubject.value;
|
||||
const newState = { ...currentState, [filterType]: value };
|
||||
this.filterStateSubject.next(newState);
|
||||
|
||||
// Simulate KPI data change based on filters
|
||||
this.updateKpiData(newState);
|
||||
}
|
||||
|
||||
// Reset all filters to default values
|
||||
resetFilters(): void {
|
||||
this.filterStateSubject.next({
|
||||
salesRep: '',
|
||||
partner: '',
|
||||
tractionChannel: '',
|
||||
subProductLine: ''
|
||||
});
|
||||
|
||||
// Reset KPI data to default values
|
||||
this.kpiDataSubject.next({
|
||||
totalLeads: 1248,
|
||||
totalDeals: 842
|
||||
});
|
||||
}
|
||||
|
||||
// Update KPI data based on filters (simulated)
|
||||
private updateKpiData(filters: FilterState): void {
|
||||
// This is a simplified simulation - in a real app, this would come from an API
|
||||
let totalLeads = 1248;
|
||||
let totalDeals = 842;
|
||||
|
||||
// Apply filter effects (simplified logic)
|
||||
if (filters.salesRep) totalLeads = Math.floor(totalLeads * 0.8);
|
||||
if (filters.partner) totalDeals = Math.floor(totalDeals * 0.9);
|
||||
if (filters.tractionChannel) totalLeads = Math.floor(totalLeads * 0.85);
|
||||
if (filters.subProductLine) totalDeals = Math.floor(totalDeals * 0.95);
|
||||
|
||||
this.kpiDataSubject.next({
|
||||
totalLeads,
|
||||
totalDeals
|
||||
});
|
||||
}
|
||||
|
||||
// Get current filter values
|
||||
getCurrentFilters(): FilterState {
|
||||
return this.filterStateSubject.value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { ShieldDashboardComponent } from './shield-dashboard.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ShieldDashboardComponent
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ShieldDashboardRoutingModule { }
|
||||
@@ -0,0 +1,493 @@
|
||||
<div class="shield-dashboard">
|
||||
<div class="dashboard-container">
|
||||
<!-- Sidebar Filters -->
|
||||
<div class="sidebar">
|
||||
<button class="btn componentbtn" (click)="toggleComponentPalette()">
|
||||
<clr-icon shape="plus"></clr-icon> Component
|
||||
</button>
|
||||
|
||||
<ul class="nav-list" style="list-style-type: none;" *ngIf="showComponentPalette">
|
||||
<li *ngFor="let widget of WidgetsMock">
|
||||
<!--
|
||||
Draggable widget from store using vanilla javascript event (dragstart)
|
||||
onDrag() is call, it take $event and a widget identifier as parameters
|
||||
-->
|
||||
<a draggable="true" class="nav-link" (dragstart)="onDrag($event, widget.identifier)">
|
||||
<clr-icon shape="drag-handle" style="margin-right: 10px;"></clr-icon>
|
||||
{{ widget.name }}
|
||||
<clr-icon shape="plugin" class="has-badge"></clr-icon>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Main Dashboard Content -->
|
||||
<div class="main-content">
|
||||
<!-- Deleted Items Section -->
|
||||
<div class="deleted-items-section" *ngIf="deletedItems.length > 0">
|
||||
<h3>Deleted Items</h3>
|
||||
<div class="deleted-items-list">
|
||||
<div *ngFor="let item of deletedItems" class="deleted-item">
|
||||
<span>{{ item.name }}</span>
|
||||
<button class="btn btn-sm btn-primary" (click)="restoreItem(item)">
|
||||
<clr-icon shape="undo"></clr-icon> Restore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" (click)="clearDeletedItems()">
|
||||
<clr-icon shape="trash"></clr-icon> Clear All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Grid with Drag and Drop -->
|
||||
<gridster [options]="options" (drop)="onDrop($event)" style="background-color: transparent; min-height: 500px;">
|
||||
<gridster-item [item]="item" *ngFor="let item of dashboard">
|
||||
<!-- Remove Button -->
|
||||
<button class="btn btn-icon btn-danger" style="margin-left: 10px; margin-top: 10px;" (click)="removeItem(item)">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
|
||||
<!-- Edit Button -->
|
||||
<button class="btn btn-icon" style="margin-top: 10px; float: right;" (click)="editGadget(item)">
|
||||
<clr-icon shape="pencil"></clr-icon>
|
||||
</button>
|
||||
|
||||
<!-- Drag Handle -->
|
||||
<button class="btn btn-icon drag-handler" style="margin-left: 10px; margin-top: 10px;">
|
||||
<clr-icon shape="drag-handle"></clr-icon>
|
||||
</button>
|
||||
|
||||
<!-- Chart Components -->
|
||||
<div class="grid-item-content">
|
||||
<h4 style="margin-top: 0px; margin-left: 10px;">{{item.name}}</h4>
|
||||
<div *ngIf="item.chartType === 'bar-chart'">
|
||||
<app-shield-bar-chart></app-shield-bar-chart>
|
||||
</div>
|
||||
<div *ngIf="item.chartType === 'donut-chart' && item.name === 'End Customer Donut'">
|
||||
<app-shield-donut-chart chartType="endCustomer"></app-shield-donut-chart>
|
||||
</div>
|
||||
<div *ngIf="item.chartType === 'donut-chart' && item.name === 'Segment Penetration Donut'">
|
||||
<app-shield-donut-chart chartType="segmentPenetration"></app-shield-donut-chart>
|
||||
</div>
|
||||
<div *ngIf="item.chartType === 'map-chart'">
|
||||
<app-shield-map-chart></app-shield-map-chart>
|
||||
</div>
|
||||
<div *ngIf="item.chartType === 'data-table'">
|
||||
<app-shield-data-table></app-shield-data-table>
|
||||
</div>
|
||||
<div *ngIf="item.chartType === 'deal-details'">
|
||||
<app-shield-deal-details-card></app-shield-deal-details-card>
|
||||
</div>
|
||||
<div *ngIf="item.chartType === 'quarterwise-flow'">
|
||||
<app-shield-quarterwise-flow></app-shield-quarterwise-flow>
|
||||
</div>
|
||||
</div>
|
||||
</gridster-item>
|
||||
</gridster>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Modal -->
|
||||
<clr-modal [(clrModalOpen)]="modeledit" [clrModalStaticBackdrop]="true" clrModalSize="lg">
|
||||
<h3 class="modal-title">Configure Chart</h3>
|
||||
<div class="modal-body" *ngIf="selectedItem">
|
||||
<form [formGroup]="configForm" class="clr-form-horizontal">
|
||||
<!-- Chart Title -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-input-container>
|
||||
<label for="charttitle">Chart Title</label>
|
||||
<input id="charttitle" type="text" formControlName="charttitle" clrInput />
|
||||
<clr-control-helper>Enter a descriptive title for your chart</clr-control-helper>
|
||||
</clr-input-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connection Selection Field -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label for="connection">Connection</label>
|
||||
<select id="connection" formControlName="connection" clrSelect>
|
||||
<option value="">Select Connection</option>
|
||||
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||
{{conn.connection_name || conn.id}}
|
||||
</option>
|
||||
</select>
|
||||
<clr-control-helper>Select a SureConnect connection to use for this chart</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart Options (except for Data Table and Deal Details) -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.name !== 'Data Table' && selectedItem?.name !== 'Deal Details'">
|
||||
<div class="clr-col-12">
|
||||
<div class="clr-form-control clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-checkbox-container>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" id="chartlegend" formControlName="chartlegend" clrCheckbox />
|
||||
<label for="chartlegend">Show Chart Legend</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" id="showlabel" formControlName="showlabel" clrCheckbox />
|
||||
<label for="showlabel">Show Chart Label</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API URL -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-input-container>
|
||||
<label for="table">API URL</label>
|
||||
<div class="clr-input-group">
|
||||
<input type="text" id="table" formControlName="table" clrInput />
|
||||
<button class="btn btn-icon btn-primary" (click)="getColumns(configForm.value.connection, configForm.value.table)" type="button">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
<clr-control-helper>Enter the API endpoint for your data source</clr-control-helper>
|
||||
</clr-input-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- X-Axis (except for Data Table and Deal Details) -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.name !== 'Data Table' && selectedItem?.name !== 'Deal Details'">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label for="xAxis">X-Axis</label>
|
||||
<select id="xAxis" formControlName="xAxis" clrSelect>
|
||||
<option value="">Select Column</option>
|
||||
<option *ngFor="let data of columnData" [value]="data">{{data}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use for the X-axis</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Y-Axis (except for Data Table and Deal Details) -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.name !== 'Data Table' && selectedItem?.name !== 'Deal Details'">
|
||||
<div class="clr-col-12">
|
||||
<clr-combobox-container>
|
||||
<label for="yAxis">Y-Axis (Numeric)</label>
|
||||
<clr-combobox id="yAxis" formControlName="yAxis" clrMulti="true" required>
|
||||
<ng-container *clrOptionSelected="let selected">
|
||||
{{selected}}
|
||||
</ng-container>
|
||||
<clr-options>
|
||||
<clr-option *clrOptionItems="let state of columnData" [clrValue]="state">
|
||||
{{state}}
|
||||
</clr-option>
|
||||
</clr-options>
|
||||
</clr-combobox>
|
||||
<clr-control-helper>Select one or more columns for the Y-axis</clr-control-helper>
|
||||
</clr-combobox-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Base API Filters Section -->
|
||||
<div class="clr-row section-divider">
|
||||
<div class="clr-col-12">
|
||||
<h4>Base API Filters</h4>
|
||||
<p class="clr-subtext">Configure filters for the main API (applied regardless of drilldown settings)</p>
|
||||
|
||||
<!-- Add Base Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addBaseFilter()" type="button">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
<!-- Base Filter Fields List -->
|
||||
<div *ngFor="let filter of selectedItem?.baseFilters; let i = index" class="filter-item">
|
||||
<div class="filter-header">
|
||||
<span>Filter {{i + 1}}</span>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeBaseFilter(i)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clr-row filter-content">
|
||||
<div class="clr-col-5">
|
||||
<clr-input-container>
|
||||
<label [attr.for]="'baseFilterField' + i">Field Name</label>
|
||||
<input type="text" [id]="'baseFilterField' + i" [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
</clr-input-container>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-5">
|
||||
<clr-input-container>
|
||||
<label [attr.for]="'baseFilterValue' + i">Filter Value</label>
|
||||
<input type="text" [id]="'baseFilterValue' + i" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
</clr-input-container>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-2">
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeBaseFilter(i)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Base Drilldown Configuration Section -->
|
||||
<div class="clr-row section-divider">
|
||||
<div class="clr-col-12">
|
||||
<h4>Base Drilldown Configuration</h4>
|
||||
<clr-checkbox-container>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" id="drilldownEnabled" [(ngModel)]="selectedItem.drilldownEnabled" [ngModelOptions]="{standalone: true}" clrCheckbox />
|
||||
<label for="drilldownEnabled">Enable Base Drilldown</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
<p class="clr-subtext">Enable drilldown functionality for this chart</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown API URL -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.drilldownEnabled">
|
||||
<div class="clr-col-12">
|
||||
<clr-input-container>
|
||||
<label for="drilldownApiUrl">Base Drilldown API URL</label>
|
||||
<div class="clr-input-group">
|
||||
<input type="text" id="drilldownApiUrl" [(ngModel)]="selectedItem.drilldownApiUrl" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
<button class="btn btn-icon btn-primary" (click)="refreshDrilldownColumns()" [disabled]="!selectedItem.drilldownApiUrl" type="button">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
<clr-control-helper>Enter the API URL for base drilldown data. Use angle brackets for parameters, e.g., http://api.example.com/data/<country></clr-control-helper>
|
||||
</clr-input-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown X-Axis -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.drilldownEnabled">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label for="drilldownXAxis">Base Drilldown X-Axis</label>
|
||||
<select id="drilldownXAxis" [(ngModel)]="selectedItem.drilldownXAxis" [ngModelOptions]="{standalone: true}" clrSelect>
|
||||
<option value="">Select X-Axis Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use for X-axis in base drilldown view</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Y-Axis (except for Deal Details) -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.drilldownEnabled && selectedItem?.name !== 'Deal Details'">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label for="drilldownYAxis">Base Drilldown Y-Axis</label>
|
||||
<select id="drilldownYAxis" [(ngModel)]="selectedItem.drilldownYAxis" [ngModelOptions]="{standalone: true}" clrSelect>
|
||||
<option value="">Select Y-Axis Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use for Y-axis in base drilldown view</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Parameter -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.drilldownEnabled">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label for="drilldownParameter">Base Drilldown Parameter</label>
|
||||
<select id="drilldownParameter" [(ngModel)]="selectedItem.drilldownParameter" [ngModelOptions]="{standalone: true}" clrSelect>
|
||||
<option value="">Select Parameter Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use as parameter for URL template replacement in base drilldown</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Base Drilldown Filter Configuration -->
|
||||
<div class="clr-row section-divider" *ngIf="selectedItem?.drilldownEnabled">
|
||||
<div class="clr-col-12">
|
||||
<h5>Base Drilldown Filters</h5>
|
||||
<p class="clr-subtext">Configure filters for the base drilldown level</p>
|
||||
|
||||
<!-- Add Drilldown Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addDrilldownFilter()" type="button">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
<!-- Drilldown Filter Fields List -->
|
||||
<div *ngFor="let filter of selectedItem?.drilldownFilters; let i = index" class="filter-item">
|
||||
<div class="filter-header">
|
||||
<span>Filter {{i + 1}}</span>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownFilter(i)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clr-row filter-content">
|
||||
<div class="clr-col-5">
|
||||
<clr-input-container>
|
||||
<label [attr.for]="'drilldownFilterField' + i">Field Name</label>
|
||||
<input type="text" [id]="'drilldownFilterField' + i" [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
</clr-input-container>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-5">
|
||||
<clr-input-container>
|
||||
<label [attr.for]="'drilldownFilterValue' + i">Filter Value</label>
|
||||
<input type="text" [id]="'drilldownFilterValue' + i" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
</clr-input-container>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-2">
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownFilter(i)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Layer Drilldown Configurations -->
|
||||
<div class="clr-row section-divider" *ngIf="selectedItem?.drilldownEnabled">
|
||||
<div class="clr-col-12">
|
||||
<h4>Multi-Layer Drilldown Configurations</h4>
|
||||
<button class="btn btn-sm btn-primary" (click)="addDrilldownLayer()" type="button">
|
||||
<clr-icon shape="plus"></clr-icon> Add Drilldown Layer
|
||||
</button>
|
||||
<p class="clr-subtext">Add additional drilldown layers for multi-level navigation</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Drilldown Layers -->
|
||||
<div class="clr-row" *ngFor="let layer of selectedItem?.drilldownLayers; let i = index">
|
||||
<div class="clr-col-12 drilldown-layer">
|
||||
<div class="layer-header">
|
||||
<h5>Drilldown Layer {{i + 1}}</h5>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownLayer(i)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" [id]="'layerEnabled' + i" [(ngModel)]="layer.enabled" [ngModelOptions]="{standalone: true}" clrCheckbox />
|
||||
<label [for]="'layerEnabled' + i">Enable Layer {{i + 1}} Drilldown</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<!-- Layer API URL -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-input-container>
|
||||
<label [for]="'layerApiUrl' + i">Layer {{i + 1}} API URL</label>
|
||||
<div class="clr-input-group">
|
||||
<input type="text" [id]="'layerApiUrl' + i" [(ngModel)]="layer.apiUrl" clrInput [ngModelOptions]="{standalone: true}" />
|
||||
<button class="btn btn-icon btn-primary" (click)="refreshDrilldownLayerColumns(i)" [disabled]="!layer.apiUrl" type="button">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
<clr-control-helper>Enter the API URL for layer {{i + 1}} drilldown data. Use angle brackets for parameters, e.g., http://api.example.com/data/<state></clr-control-helper>
|
||||
</clr-input-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer X-Axis -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label [for]="'layerXAxis' + i">Layer {{i + 1}} X-Axis</label>
|
||||
<select [id]="'layerXAxis' + i" [(ngModel)]="layer.xAxis" [ngModelOptions]="{standalone: true}" clrSelect>
|
||||
<option value="">Select X-Axis Column</option>
|
||||
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use for X-axis in layer {{i + 1}} drilldown view</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Y-Axis (except for Deal Details) -->
|
||||
<div class="clr-row" *ngIf="selectedItem?.name !== 'Deal Details'">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label [for]="'layerYAxis' + i">Layer {{i + 1}} Y-Axis</label>
|
||||
<select [id]="'layerYAxis' + i" [(ngModel)]="layer.yAxis" [ngModelOptions]="{standalone: true}" clrSelect>
|
||||
<option value="">Select Y-Axis Column</option>
|
||||
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use for Y-axis in layer {{i + 1}} drilldown view</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parameter Selection for Drilldown Layer -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-12">
|
||||
<clr-select-container>
|
||||
<label [for]="'layerParameter' + i">Layer {{i + 1}} Parameter</label>
|
||||
<select [id]="'layerParameter' + i" [(ngModel)]="layer.parameter" [ngModelOptions]="{standalone: true}" clrSelect>
|
||||
<option value="">Select Parameter Column</option>
|
||||
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<clr-control-helper>Select the column to use as parameter for URL template replacement in layer {{i + 1}} drilldown</clr-control-helper>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filter Configuration -->
|
||||
<div class="clr-row section-divider">
|
||||
<div class="clr-col-12">
|
||||
<h5>Layer {{i + 1}} Filters</h5>
|
||||
<p class="clr-subtext">Configure filters for this drilldown layer</p>
|
||||
|
||||
<!-- Add Layer Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addLayerFilter(i)" type="button">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
<!-- Layer Filter Fields List -->
|
||||
<div *ngFor="let filter of layer.filters; let j = index" class="filter-item">
|
||||
<div class="filter-header">
|
||||
<span>Filter {{j + 1}}</span>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeLayerFilter(i, j)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clr-row filter-content">
|
||||
<div class="clr-col-5">
|
||||
<clr-input-container>
|
||||
<label [attr.for]="'layerFilterField' + i + '_' + j">Field Name</label>
|
||||
<input type="text" [id]="'layerFilterField' + i + '_' + j" [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
</clr-input-container>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-5">
|
||||
<clr-input-container>
|
||||
<label [attr.for]="'layerFilterValue' + i + '_' + j">Filter Value</label>
|
||||
<input type="text" [id]="'layerFilterValue' + i + '_' + j" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" clrInput />
|
||||
</clr-input-container>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-2">
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeLayerFilter(i, j)" type="button">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="cancelConfiguration()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="saveConfiguration()">Save</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
@@ -0,0 +1,220 @@
|
||||
.shield-dashboard {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
flex: 0 0 250px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.kpi-section {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kpi-title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.total-leads {
|
||||
border-top: 4px solid #4285f4;
|
||||
}
|
||||
|
||||
.total-deals {
|
||||
border-top: 4px solid #0f9d58;
|
||||
}
|
||||
|
||||
.deleted-items-section {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 20px;
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.deleted-items-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.deleted-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-item-content {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.drag-handler {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.drop-zone-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 500px;
|
||||
background-color: #f8f9fa;
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 4px;
|
||||
color: #6c757d;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Gridster specific styles */
|
||||
gridster {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
gridster-item {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Configuration Modal Styles */
|
||||
.section-divider {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.section-divider:first-child {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.drilldown-layer {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.layer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.layer-header h5 {
|
||||
margin: 0;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.kpi-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item .clr-row > div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { GridsterConfig, GridsterItem } from 'angular-gridster2';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { DatastoreService } from 'src/app/services/fnd/datastore.service';
|
||||
import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
||||
import { SureconnectService } from '../../sureconnect/sureconnect.service';
|
||||
|
||||
interface ShieldDashboardItem extends GridsterItem {
|
||||
chartType: string;
|
||||
name: string;
|
||||
id: number;
|
||||
component?: any;
|
||||
// Configuration properties
|
||||
charttitle?: string;
|
||||
connection?: string;
|
||||
table?: string;
|
||||
xAxis?: string;
|
||||
yAxis?: string[];
|
||||
chartlegend?: boolean;
|
||||
showlabel?: boolean;
|
||||
baseFilters?: any[];
|
||||
drilldownEnabled?: boolean;
|
||||
drilldownApiUrl?: string;
|
||||
drilldownXAxis?: string;
|
||||
drilldownYAxis?: string;
|
||||
drilldownParameter?: string;
|
||||
drilldownFilters?: any[];
|
||||
drilldownLayers?: any[];
|
||||
}
|
||||
|
||||
interface WidgetModel {
|
||||
name: string;
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
@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>;
|
||||
|
||||
// Configuration modal
|
||||
configModalOpen = false;
|
||||
modeledit = false; // Add this to match the main dashboard pattern
|
||||
configForm: FormGroup;
|
||||
selectedItem: ShieldDashboardItem | null = null;
|
||||
|
||||
// Component palette
|
||||
showComponentPalette = false;
|
||||
WidgetsMock: WidgetModel[] = [
|
||||
{
|
||||
name: 'Bar Chart',
|
||||
identifier: 'bar_chart'
|
||||
},
|
||||
{
|
||||
name: 'Doughnut Chart',
|
||||
identifier: 'doughnut_chart'
|
||||
},
|
||||
{
|
||||
name: 'Map Chart',
|
||||
identifier: 'map_chart'
|
||||
},
|
||||
{
|
||||
name: 'Data Table',
|
||||
identifier: 'grid_view'
|
||||
},
|
||||
{
|
||||
name: 'Deal Details',
|
||||
identifier: 'to_do_chart'
|
||||
},
|
||||
{
|
||||
name: 'Quarterwise Flow',
|
||||
identifier: 'line_chart'
|
||||
},
|
||||
{
|
||||
name: 'Compact Filter',
|
||||
identifier: 'compact_filter'
|
||||
}
|
||||
];
|
||||
|
||||
// Services data
|
||||
storedata: any[] = [];
|
||||
columnData: any[] = [];
|
||||
sureconnectData: any[] = [];
|
||||
drilldownColumnData: any[] = [];
|
||||
layerColumnData: { [key: number]: any[] } = {};
|
||||
|
||||
// Keep track of deleted items
|
||||
deletedItems: Array<ShieldDashboardItem> = [];
|
||||
|
||||
constructor(
|
||||
private _fb: FormBuilder,
|
||||
private datastoreService: DatastoreService,
|
||||
private alertService: AlertsService,
|
||||
private sureconnectService: SureconnectService
|
||||
) { }
|
||||
|
||||
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 = [];
|
||||
|
||||
// Initialize form
|
||||
this.configForm = this._fb.group({
|
||||
charttitle: [''],
|
||||
connection: [''],
|
||||
table: [''],
|
||||
xAxis: [''],
|
||||
yAxis: [''],
|
||||
chartlegend: [true],
|
||||
showlabel: [true]
|
||||
});
|
||||
|
||||
// Load service data
|
||||
this.loadServicesData();
|
||||
}
|
||||
|
||||
// Load initial data from services
|
||||
loadServicesData() {
|
||||
// Load sureconnect data
|
||||
this.sureconnectService.getAll().subscribe((data: any[]) => {
|
||||
this.sureconnectData = data;
|
||||
});
|
||||
|
||||
// Load datastore data
|
||||
this.datastoreService.getAll().subscribe((data) => {
|
||||
this.storedata = data as any[];
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle component palette visibility
|
||||
toggleComponentPalette(): void {
|
||||
this.showComponentPalette = !this.showComponentPalette;
|
||||
}
|
||||
|
||||
// Handle drag start event for components - matching the working implementation
|
||||
onDrag(event: DragEvent, identifier: string): void {
|
||||
console.log("on drag", identifier);
|
||||
console.log("on drag ", event);
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('widgetIdentifier', identifier);
|
||||
}
|
||||
}
|
||||
|
||||
onDrop(ev: any) {
|
||||
// Handle dropping new components onto the dashboard
|
||||
console.log('Item dropped:', ev);
|
||||
|
||||
// Get the component identifier from the drag event
|
||||
const componentType = ev.dataTransfer ? ev.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,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
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,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
} else {
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'donut-chart',
|
||||
name: 'Segment Penetration Donut',
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
}
|
||||
break;
|
||||
case "map_chart":
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'map-chart',
|
||||
name: 'Map Chart',
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
break;
|
||||
case "grid_view":
|
||||
newItem = {
|
||||
cols: 10,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'data-table',
|
||||
name: 'Data Table',
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
break;
|
||||
case "to_do_chart":
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'deal-details',
|
||||
name: 'Deal Details',
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
break;
|
||||
case "line_chart":
|
||||
newItem = {
|
||||
cols: 5,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: 'quarterwise-flow',
|
||||
name: 'Quarterwise Flow',
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
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,
|
||||
rows: 6,
|
||||
y: 0,
|
||||
x: 0,
|
||||
chartType: componentType,
|
||||
name: componentType,
|
||||
id: newId,
|
||||
chartlegend: true,
|
||||
showlabel: true,
|
||||
baseFilters: [],
|
||||
drilldownEnabled: false,
|
||||
drilldownLayers: []
|
||||
};
|
||||
}
|
||||
|
||||
// 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,
|
||||
charttitle: item.charttitle,
|
||||
connection: item.connection,
|
||||
table: item.table,
|
||||
xAxis: item.xAxis,
|
||||
yAxis: item.yAxis,
|
||||
chartlegend: item.chartlegend,
|
||||
showlabel: item.showlabel,
|
||||
baseFilters: item.baseFilters || [],
|
||||
drilldownEnabled: item.drilldownEnabled,
|
||||
drilldownApiUrl: item.drilldownApiUrl,
|
||||
drilldownXAxis: item.drilldownXAxis,
|
||||
drilldownYAxis: item.drilldownYAxis,
|
||||
drilldownParameter: item.drilldownParameter,
|
||||
drilldownFilters: item.drilldownFilters || [],
|
||||
drilldownLayers: item.drilldownLayers || []
|
||||
};
|
||||
|
||||
// Remove undefined properties to avoid passing unnecessary data
|
||||
Object.keys(chartInputs).forEach(key => {
|
||||
if (chartInputs[key] === undefined) {
|
||||
delete chartInputs[key];
|
||||
}
|
||||
});
|
||||
|
||||
return chartInputs;
|
||||
}
|
||||
|
||||
// Open configuration modal for a chart
|
||||
editGadget(item: ShieldDashboardItem) {
|
||||
console.log('Opening configuration modal for item:', item);
|
||||
this.selectedItem = item;
|
||||
this.modeledit = true; // Use modeledit instead of configModalOpen
|
||||
|
||||
// Initialize form with item data
|
||||
this.configForm.patchValue({
|
||||
charttitle: item.charttitle || '',
|
||||
connection: item.connection || '',
|
||||
table: item.table || '',
|
||||
xAxis: item.xAxis || '',
|
||||
yAxis: item.yAxis || [],
|
||||
chartlegend: item.chartlegend !== undefined ? item.chartlegend : true,
|
||||
showlabel: item.showlabel !== undefined ? item.showlabel : true
|
||||
});
|
||||
|
||||
console.log('Form values after patch:', this.configForm.value);
|
||||
|
||||
// Load columns if table is set
|
||||
if (item.table) {
|
||||
this.getColumns(item.connection, item.table);
|
||||
}
|
||||
|
||||
// Load drilldown columns if drilldown API URL is set
|
||||
if (item.drilldownApiUrl) {
|
||||
this.refreshDrilldownColumns();
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration changes
|
||||
saveConfiguration() {
|
||||
if (this.selectedItem) {
|
||||
const formData = this.configForm.value;
|
||||
console.log('Saving configuration:', formData);
|
||||
|
||||
// Update the selected item with form data
|
||||
Object.assign(this.selectedItem, formData);
|
||||
|
||||
// Close the modal
|
||||
this.modeledit = false; // Use modeledit instead of configModalOpen
|
||||
this.selectedItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel configuration changes
|
||||
cancelConfiguration() {
|
||||
console.log('Canceling configuration');
|
||||
this.modeledit = false; // Use modeledit instead of configModalOpen
|
||||
this.selectedItem = null;
|
||||
}
|
||||
|
||||
// Get tables from datastore
|
||||
getTables(id: string) {
|
||||
this.alertService.getTablefromstore(parseInt(id, 10)).subscribe(gateway => {
|
||||
console.log(gateway);
|
||||
// Handle table data
|
||||
}, (error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
// Get columns from API
|
||||
getColumns(connectionId: string, table: string) {
|
||||
const connId = connectionId ? parseInt(connectionId, 10) : undefined;
|
||||
this.alertService.getColumnfromurl(table, connId).subscribe(data => {
|
||||
console.log('Column data:', data);
|
||||
this.columnData = data;
|
||||
}, (error) => {
|
||||
console.log(error);
|
||||
this.columnData = [];
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh drilldown columns
|
||||
refreshDrilldownColumns() {
|
||||
if (this.selectedItem && this.selectedItem.drilldownApiUrl) {
|
||||
const connId = this.selectedItem.connection ? parseInt(this.selectedItem.connection, 10) : undefined;
|
||||
this.alertService.getColumnfromurl(this.selectedItem.drilldownApiUrl, connId).subscribe(data => {
|
||||
console.log('Drilldown column data:', data);
|
||||
this.drilldownColumnData = data;
|
||||
}, (error) => {
|
||||
console.log('Error fetching drilldown columns:', error);
|
||||
this.drilldownColumnData = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add base filter
|
||||
addBaseFilter() {
|
||||
if (this.selectedItem) {
|
||||
if (!this.selectedItem.baseFilters) {
|
||||
this.selectedItem.baseFilters = [];
|
||||
}
|
||||
this.selectedItem.baseFilters.push({ field: '', value: '' });
|
||||
}
|
||||
}
|
||||
|
||||
// Remove base filter
|
||||
removeBaseFilter(index: number) {
|
||||
if (this.selectedItem && this.selectedItem.baseFilters) {
|
||||
this.selectedItem.baseFilters.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add drilldown filter
|
||||
addDrilldownFilter() {
|
||||
if (this.selectedItem) {
|
||||
if (!this.selectedItem.drilldownFilters) {
|
||||
this.selectedItem.drilldownFilters = [];
|
||||
}
|
||||
this.selectedItem.drilldownFilters.push({ field: '', value: '' });
|
||||
}
|
||||
}
|
||||
|
||||
// Remove drilldown filter
|
||||
removeDrilldownFilter(index: number) {
|
||||
if (this.selectedItem && this.selectedItem.drilldownFilters) {
|
||||
this.selectedItem.drilldownFilters.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add drilldown layer
|
||||
addDrilldownLayer() {
|
||||
if (this.selectedItem) {
|
||||
if (!this.selectedItem.drilldownLayers) {
|
||||
this.selectedItem.drilldownLayers = [];
|
||||
}
|
||||
this.selectedItem.drilldownLayers.push({
|
||||
enabled: false,
|
||||
apiUrl: '',
|
||||
xAxis: '',
|
||||
yAxis: '',
|
||||
parameter: '',
|
||||
filters: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remove drilldown layer
|
||||
removeDrilldownLayer(index: number) {
|
||||
if (this.selectedItem && this.selectedItem.drilldownLayers) {
|
||||
this.selectedItem.drilldownLayers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add layer filter
|
||||
addLayerFilter(layerIndex: number) {
|
||||
if (this.selectedItem && this.selectedItem.drilldownLayers) {
|
||||
const layer = this.selectedItem.drilldownLayers[layerIndex];
|
||||
if (layer) {
|
||||
if (!layer.filters) {
|
||||
layer.filters = [];
|
||||
}
|
||||
layer.filters.push({ field: '', value: '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh drilldown columns for a specific layer
|
||||
refreshDrilldownLayerColumns(layerIndex: number) {
|
||||
if (this.selectedItem && this.selectedItem.drilldownLayers && this.selectedItem.drilldownLayers[layerIndex]) {
|
||||
const layer = this.selectedItem.drilldownLayers[layerIndex];
|
||||
if (layer && layer.apiUrl) {
|
||||
const connId = this.selectedItem.connection ? parseInt(this.selectedItem.connection, 10) : undefined;
|
||||
this.alertService.getColumnfromurl(layer.apiUrl, connId).subscribe(data => {
|
||||
console.log(`Drilldown layer ${layerIndex} column data:`, data);
|
||||
// Store layer column data in the layerColumnData property
|
||||
this.layerColumnData[layerIndex] = data;
|
||||
}, (error) => {
|
||||
console.log(`Error fetching drilldown layer ${layerIndex} columns:`, error);
|
||||
this.layerColumnData[layerIndex] = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove layer filter
|
||||
removeLayerFilter(layerIndex: number, filterIndex: number) {
|
||||
if (this.selectedItem && this.selectedItem.drilldownLayers) {
|
||||
const layer = this.selectedItem.drilldownLayers[layerIndex];
|
||||
if (layer && layer.filters) {
|
||||
layer.filters.splice(filterIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ClarityModule } from '@clr/angular';
|
||||
import { GridsterModule } from 'angular-gridster2';
|
||||
import { NgChartsModule } from 'ng2-charts';
|
||||
import { DynamicModule } from 'ng-dynamic-component';
|
||||
|
||||
import { ShieldDashboardRoutingModule } from './shield-dashboard-routing.module';
|
||||
import { ShieldDashboardComponent } from './shield-dashboard.component';
|
||||
import { SidebarFiltersComponent } from './components/sidebar-filters/sidebar-filters.component';
|
||||
import { BarChartComponent } from './components/bar-chart/bar-chart.component';
|
||||
import { DonutChartComponent } from './components/donut-chart/donut-chart.component';
|
||||
import { MapChartComponent } from './components/map-chart/map-chart.component';
|
||||
import { DataTableComponent } from './components/data-table/data-table.component';
|
||||
import { DealDetailsCardComponent } from './components/deal-details-card/deal-details-card.component';
|
||||
import { QuarterwiseFlowComponent } from './components/quarterwise-flow/quarterwise-flow.component';
|
||||
import { LoadingShimmerComponent } from './components/loading-shimmer/loading-shimmer.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ShieldDashboardComponent,
|
||||
SidebarFiltersComponent,
|
||||
BarChartComponent,
|
||||
DonutChartComponent,
|
||||
MapChartComponent,
|
||||
DataTableComponent,
|
||||
DealDetailsCardComponent,
|
||||
QuarterwiseFlowComponent,
|
||||
LoadingShimmerComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ClarityModule,
|
||||
GridsterModule,
|
||||
NgChartsModule,
|
||||
DynamicModule,
|
||||
ShieldDashboardRoutingModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
|
||||
})
|
||||
export class ShieldDashboardModule { }
|
||||
@@ -11,6 +11,9 @@
|
||||
<h3>{{ 'all_dashboard' | translate }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-4" style="text-align: right;">
|
||||
<button class="btn btn-success" [routerLink]="['/cns-portal/shield-dashboard']">
|
||||
<clr-icon shape="shield"></clr-icon>Shield Dashboard
|
||||
</button>
|
||||
<button id="add" class="btn btn-primary" (click)="gotoadd()">
|
||||
<clr-icon shape="plus"></clr-icon>{{ 'dashboard_builder' | translate }}
|
||||
</button>
|
||||
@@ -112,6 +115,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bar-runner',
|
||||
@@ -24,8 +28,19 @@ export class BarRunnerComponent implements OnInit {
|
||||
JsonData;
|
||||
|
||||
barData;
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private Dashtestservive:DashrunnerService,
|
||||
private route: ActivatedRoute,
|
||||
private dashboardService: Dashboard3Service,
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
barChartLabels: any[] = [];
|
||||
barChartType: string = 'bar';
|
||||
@@ -47,6 +62,13 @@ export class BarRunnerComponent implements OnInit {
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data)=>{
|
||||
console.log(data);
|
||||
@@ -74,22 +96,62 @@ export class BarRunnerComponent implements OnInit {
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.showlabel = ChartObject[i].showlabel;
|
||||
this.barChartLegend = ChartObject[i].chartlegend;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Bar Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.barChartData = this.JsonData.barChartData;
|
||||
this.barChartLabels = this.JsonData.barChartLabels;
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Fetch data with filters
|
||||
this.fetchChartData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch chart data with filter support
|
||||
fetchChartData(): void {
|
||||
if (this.TableName && this.XAxis && this.YAxis) {
|
||||
// Convert YAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
|
||||
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('BarRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Bar Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.barChartData = this.JsonData.barChartData;
|
||||
this.barChartLabels = this.JsonData.barChartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -98,4 +160,16 @@ export class BarRunnerComponent implements OnInit {
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('BarRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('BarRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
import { DashrunnerService } from '../dashrunner.service';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bubble-runner',
|
||||
@@ -25,9 +29,15 @@ export class BubbleRunnerComponent implements OnInit {
|
||||
JsonData;
|
||||
lineChartNoLabels: [] = [];
|
||||
ChartLegend = false;
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
public bubbleChartOptions: ChartConfiguration['options'] = {
|
||||
// scales: {
|
||||
@@ -87,6 +97,13 @@ export class BubbleRunnerComponent implements OnInit {
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data)=>{
|
||||
console.log(data);
|
||||
@@ -112,22 +129,62 @@ export class BubbleRunnerComponent implements OnInit {
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.showlabel = ChartObject[i].showlabel;
|
||||
this.ChartLegend = ChartObject[i].chartlegend;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Bubble Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.bubbleChartData = this.JsonData.bubbleChartData;
|
||||
// this.radarChartLabels = this.JsonData.radarChartLabels;
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Fetch data with filters
|
||||
this.fetchChartData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch chart data with filter support
|
||||
fetchChartData(): void {
|
||||
if (this.TableName && this.XAxis && this.YAxis) {
|
||||
// Convert YAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
|
||||
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('BubbleRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Bubble Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.bubbleChartData = this.JsonData.bubbleChartData;
|
||||
// this.radarChartLabels = this.JsonData.radarChartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -136,5 +193,18 @@ export class BubbleRunnerComponent implements OnInit {
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('BubbleRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('BubbleRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<div class="compact-filter">
|
||||
<div class="filter-header" (click)="toggleFilter()">
|
||||
<span class="filter-label" *ngIf="filterLabel">{{ filterLabel }}</span>
|
||||
<span class="filter-key" *ngIf="!filterLabel && filterKey">{{ filterKey }}</span>
|
||||
<span class="filter-type">({{ filterType }})</span>
|
||||
<clr-icon shape="caret down" class="expand-icon" *ngIf="!isExpanded"></clr-icon>
|
||||
<clr-icon shape="caret up" class="expand-icon" *ngIf="isExpanded"></clr-icon>
|
||||
</div>
|
||||
|
||||
<div class="filter-content" *ngIf="isExpanded">
|
||||
<!-- 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; let i = index" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter -->
|
||||
<div class="filter-control" *ngIf="filterType === 'multiselect'">
|
||||
<div class="multiselect-container">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of filterOptions; let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(option)"
|
||||
(change)="onMultiSelectChange(option, $event)"
|
||||
[id]="'checkbox-' + filterKey + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'checkbox-' + filterKey + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div class="filter-control date-range" *ngIf="filterType === 'date-range'">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filterValue.start"
|
||||
(ngModelChange)="onDateRangeChange({ start: $event, end: filterValue.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input compact-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filterValue.end"
|
||||
(ngModelChange)="onDateRangeChange({ start: filterValue.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input compact-date">
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
@@ -0,0 +1,149 @@
|
||||
.compact-filter {
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
margin: 8px;
|
||||
padding: 0;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d7d7d7;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 15px;
|
||||
cursor: pointer;
|
||||
background: #f8f8f8;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.filter-label, .filter-key {
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.filter-type {
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
margin: 0 8px;
|
||||
background: #eaeaea;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
padding: 15px;
|
||||
|
||||
.filter-control {
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.date-separator {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.compact-input,
|
||||
.compact-select,
|
||||
.compact-date {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #d7d7d7;
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #0072ce;
|
||||
box-shadow: 0 0 0 1px #0072ce;
|
||||
}
|
||||
}
|
||||
|
||||
.compact-select {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #d7d7d7;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.clr-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.clr-toggle {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Host styling
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { FilterService, Filter } from '../../../dashboardnew/common-filter/filter.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-compact-filter-runner',
|
||||
templateUrl: './compact-filter-runner.component.html',
|
||||
styleUrls: ['./compact-filter-runner.component.scss']
|
||||
})
|
||||
export class CompactFilterRunnerComponent implements OnInit, OnChanges {
|
||||
@Input() filterKey: string = '';
|
||||
@Input() filterType: string = 'text';
|
||||
@Input() filterOptions: string[] = [];
|
||||
@Input() filterLabel: string = '';
|
||||
@Input() apiUrl: string = '';
|
||||
@Input() connection: number | undefined;
|
||||
@Output() filterChange = new EventEmitter<any>();
|
||||
|
||||
selectedFilter: Filter | null = null;
|
||||
filterValue: any = '';
|
||||
availableFilters: Filter[] = [];
|
||||
availableKeys: string[] = [];
|
||||
availableValues: string[] = [];
|
||||
isExpanded: boolean = false; // Add expansion state
|
||||
|
||||
constructor(
|
||||
private filterService: FilterService
|
||||
) {
|
||||
console.log('=== COMPACT FILTER RUNNER CONSTRUCTOR CALLED ===');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
console.log('=== COMPACT FILTER RUNNER DEBUG INFO ===');
|
||||
console.log('Component initialized with inputs:');
|
||||
console.log('- filterKey:', this.filterKey);
|
||||
console.log('- filterType:', this.filterType);
|
||||
console.log('- filterOptions:', this.filterOptions);
|
||||
console.log('- filterLabel:', this.filterLabel);
|
||||
console.log('- apiUrl:', this.apiUrl);
|
||||
console.log('- connection:', this.connection);
|
||||
console.log('========================================');
|
||||
|
||||
// Register this filter with the filter service
|
||||
this.registerFilter();
|
||||
|
||||
// Subscribe to filter definitions to get available filters
|
||||
this.filterService.filters$.subscribe(filters => {
|
||||
this.availableFilters = filters;
|
||||
console.log('Available filters updated:', filters);
|
||||
this.updateSelectedFilter();
|
||||
});
|
||||
|
||||
// Subscribe to filter state changes
|
||||
this.filterService.filterState$.subscribe(state => {
|
||||
console.log('Filter state updated:', state);
|
||||
if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) {
|
||||
this.filterValue = state[this.selectedFilter.id];
|
||||
console.log('Filter value updated for', this.selectedFilter.id, ':', this.filterValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('=== COMPACT FILTER RUNNER CHANGES DEBUG ===');
|
||||
console.log('Component inputs changed:', changes);
|
||||
|
||||
// If filterKey or filterType changes, re-register the filter
|
||||
if (changes.filterKey || changes.filterType || changes.filterOptions) {
|
||||
console.log('Re-registering filter due to input changes');
|
||||
this.registerFilter();
|
||||
}
|
||||
console.log('==========================================');
|
||||
}
|
||||
|
||||
// Toggle filter expansion
|
||||
toggleFilter(): void {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
}
|
||||
|
||||
// Register this filter with the filter service
|
||||
registerFilter(): void {
|
||||
console.log('Registering filter with key:', this.filterKey, 'type:', this.filterType);
|
||||
|
||||
if (this.filterKey) {
|
||||
// Get current filter values from the service
|
||||
const currentFilterValues = this.filterService.getFilterValues();
|
||||
console.log('Current filter values from service:', currentFilterValues);
|
||||
|
||||
// Create a filter definition for this compact filter
|
||||
const filterDef: Filter = {
|
||||
id: `${this.filterKey}`,
|
||||
field: this.filterKey,
|
||||
label: this.filterLabel || this.filterKey,
|
||||
type: this.filterType as any,
|
||||
options: this.filterOptions,
|
||||
value: this.filterValue // Use the current filter value
|
||||
};
|
||||
|
||||
console.log('Created filter definition:', filterDef);
|
||||
|
||||
// Get current filters
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
console.log('Current filters from service:', currentFilters);
|
||||
|
||||
// Check if this filter is already registered
|
||||
const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id);
|
||||
console.log('Existing filter index:', existingFilterIndex);
|
||||
|
||||
if (existingFilterIndex >= 0) {
|
||||
// Preserve the existing filter configuration
|
||||
const existingFilter = currentFilters[existingFilterIndex];
|
||||
console.log('Found existing filter:', existingFilter);
|
||||
|
||||
// Preserve the existing filter value if it exists in the service
|
||||
if (currentFilterValues.hasOwnProperty(existingFilter.id)) {
|
||||
filterDef.value = currentFilterValues[existingFilter.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
console.log('Using value from service:', filterDef.value);
|
||||
} else if (existingFilter.value !== undefined) {
|
||||
// Fallback to existing filter's value if no service value
|
||||
filterDef.value = existingFilter.value;
|
||||
this.filterValue = filterDef.value;
|
||||
console.log('Using value from existing filter:', filterDef.value);
|
||||
}
|
||||
|
||||
// Preserve other configuration properties
|
||||
filterDef.label = existingFilter.label;
|
||||
filterDef.options = existingFilter.options || this.filterOptions;
|
||||
|
||||
// Update existing filter
|
||||
currentFilters[existingFilterIndex] = filterDef;
|
||||
console.log('Updated existing filter:', filterDef);
|
||||
} else {
|
||||
// For new filters, check if there's already a value in the service
|
||||
if (currentFilterValues.hasOwnProperty(filterDef.id)) {
|
||||
filterDef.value = currentFilterValues[filterDef.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
console.log('Using value from service for new filter:', filterDef.value);
|
||||
}
|
||||
|
||||
// Add new filter
|
||||
currentFilters.push(filterDef);
|
||||
console.log('Added new filter:', filterDef);
|
||||
}
|
||||
|
||||
// Update the filter service with the new filter list
|
||||
this.filterService.setFilters(currentFilters);
|
||||
|
||||
// Update the selected filter reference
|
||||
this.selectedFilter = filterDef;
|
||||
console.log('Selected filter set to:', this.selectedFilter);
|
||||
} else {
|
||||
console.log('No filterKey provided, skipping filter registration');
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedFilter(): void {
|
||||
console.log('Updating selected filter. Filter key:', this.filterKey, 'Available filters:', this.availableFilters);
|
||||
if (this.filterKey && this.availableFilters.length > 0) {
|
||||
this.selectedFilter = this.availableFilters.find(f => f.field === this.filterKey) || null;
|
||||
console.log('Found selected filter:', this.selectedFilter);
|
||||
if (this.selectedFilter) {
|
||||
// Get current value for this filter from the service
|
||||
const currentState = this.filterService.getFilterValues();
|
||||
console.log('Current state from service:', currentState);
|
||||
const filterValue = currentState[this.selectedFilter.id];
|
||||
if (filterValue !== undefined) {
|
||||
this.filterValue = filterValue;
|
||||
} else if (this.selectedFilter.value !== undefined) {
|
||||
// Use the filter's default value if no service value
|
||||
this.filterValue = this.selectedFilter.value;
|
||||
} else {
|
||||
// Use the current filter value as fallback
|
||||
this.filterValue = this.filterValue || '';
|
||||
}
|
||||
|
||||
console.log('Updated selected filter value:', this.filterValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onFilterValueChange(value: any): void {
|
||||
console.log('Filter value changed:', value);
|
||||
|
||||
if (this.selectedFilter) {
|
||||
this.filterValue = value;
|
||||
this.filterService.updateFilterValue(this.selectedFilter.id, value);
|
||||
this.filterChange.emit({ filterId: this.selectedFilter.id, value: value });
|
||||
|
||||
// Update the filter definition in the service to reflect the new value
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
const filterIndex = currentFilters.findIndex(f => f.id === this.selectedFilter.id);
|
||||
if (filterIndex >= 0) {
|
||||
currentFilters[filterIndex].value = value;
|
||||
this.filterService.setFilters(currentFilters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onToggleChange(checked: boolean): void {
|
||||
this.onFilterValueChange(checked);
|
||||
}
|
||||
|
||||
onDateRangeChange(dateRange: { start: string | null, end: string | null }): void {
|
||||
this.onFilterValueChange(dateRange);
|
||||
}
|
||||
|
||||
// Handle multi-select changes
|
||||
onMultiSelectChange(option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filterValue as array if it's not already
|
||||
if (!Array.isArray(this.filterValue)) {
|
||||
this.filterValue = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!this.filterValue.includes(option)) {
|
||||
this.filterValue.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
this.filterValue = this.filterValue.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Emit the change
|
||||
this.onFilterValueChange(this.filterValue);
|
||||
}
|
||||
|
||||
// Add method to check if an option is selected for checkboxes (needed for proper UI rendering)
|
||||
isOptionSelected(option: string): boolean {
|
||||
console.log('Checking if option is selected:', option, 'Current filter value:', this.filterValue);
|
||||
if (!this.filterValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure filterValue is an array for multiselect
|
||||
if (!Array.isArray(this.filterValue)) {
|
||||
this.filterValue = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.filterValue.includes(option);
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,27 @@ getlinechart(): any[] {
|
||||
return this._http.get(url);
|
||||
}
|
||||
|
||||
// New method to support filters
|
||||
public getChartDataWithFilters(tableName: string, jobType: string, xAxis:any, yAxes:any, sureId: number | undefined, parameterField: string, parameterValue: string, filterParams: string): Observable<any> {
|
||||
let url = `${baseUrl}/chart/getdashjson/${jobType}?tableName=${tableName}&xAxis=${xAxis}&yAxes=${yAxes}`;
|
||||
|
||||
// Add sureId if provided
|
||||
if (sureId) {
|
||||
url += `&sureId=${sureId}`;
|
||||
}
|
||||
|
||||
// Add parameter field and value if provided
|
||||
if (parameterField && parameterValue) {
|
||||
url += `¶meter=${encodeURIComponent(parameterField)}¶meterValue=${encodeURIComponent(parameterValue)}`;
|
||||
}
|
||||
|
||||
// Add filter parameters if provided
|
||||
if (filterParams) {
|
||||
url += `&filters=${encodeURIComponent(filterParams)}`;
|
||||
}
|
||||
|
||||
return this._http.get(url);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -26,7 +26,14 @@
|
||||
<!-- <span><button class="btn btn-primary" (click)="Export(item.name)">Export</button></span> -->
|
||||
<!-- <span><app-line-runner (buttonClicked)="generatePDFFile()"></app-line-runner></span> -->
|
||||
<!-- <h4 style="margin-top: 10px; margin-left: 10px;">{{ item.charttitle }}</h4> -->
|
||||
<ndc-dynamic class="no-drag" [ndcDynamicComponent]="item.component" (moduleInfo)="display($event)"></ndc-dynamic>
|
||||
|
||||
|
||||
|
||||
<ndc-dynamic class="no-drag"
|
||||
[ndcDynamicComponent]="item.component"
|
||||
[ndcDynamicInputs]="getComponentInputs(item)"
|
||||
(moduleInfo)="display($event)">
|
||||
</ndc-dynamic>
|
||||
</gridster-item>
|
||||
</gridster>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,10 @@ import { BubbleRunnerComponent } from './bubble-runner/bubble-runner.component';
|
||||
import { ScatterRunnerComponent } from './scatter-runner/scatter-runner.component';
|
||||
import { PolarRunnerComponent } from './polar-runner/polar-runner.component';
|
||||
import { RadarRunnerComponent } from './radar-runner/radar-runner.component';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../dashboardnew/common-filter/filter.service';
|
||||
// Add CompactFilterRunnerComponent import
|
||||
import { CompactFilterRunnerComponent } from './compact-filter-runner/compact-filter-runner.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashrunnerline',
|
||||
@@ -44,10 +48,13 @@ export class DashrunnerlineComponent implements OnInit {
|
||||
{ name: "Radar Chart", componentInstance: RadarRunnerComponent },
|
||||
{ name: "Grid View", componentInstance: GridRunnerComponent },
|
||||
{ name: "To Do Chart", componentInstance: TodoRunnerComponent },
|
||||
{ name: "Compact Filter", componentInstance: CompactFilterRunnerComponent }, // Add Compact Filter Runner
|
||||
];
|
||||
|
||||
constructor(private Dashtestservive:DashrunnerService, private dashboardService: Dashboard3Service,private route: ActivatedRoute,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@@ -288,4 +295,54 @@ dashboard_name = "Dashtest";
|
||||
console.log('Button clicked in SomeComponent');
|
||||
// Add your custom logic here when the button is clicked in SomeComponent
|
||||
}
|
||||
|
||||
// Method to provide inputs for dynamic components based on their type
|
||||
getComponentInputs(item: any): any {
|
||||
const inputs: any = {};
|
||||
|
||||
// Common inputs for all components
|
||||
if (item.table !== undefined) inputs.table = item.table;
|
||||
if (item.xAxis !== undefined) inputs.xAxis = item.xAxis;
|
||||
if (item.yAxis !== undefined) inputs.yAxis = item.yAxis;
|
||||
if (item.connection !== undefined) inputs.connection = item.connection;
|
||||
if (item.charttitle !== undefined) inputs.charttitle = item.charttitle;
|
||||
if (item.chartlegend !== undefined) inputs.chartlegend = item.chartlegend;
|
||||
if (item.showlabel !== undefined) inputs.showlabel = item.showlabel;
|
||||
|
||||
// Compact Filter specific inputs
|
||||
if (item.name === 'Compact Filter') {
|
||||
console.log('=== COMPACT FILTER INPUTS DEBUG ===');
|
||||
console.log('Item data for compact filter:', item);
|
||||
|
||||
if (item.filterKey !== undefined) inputs.filterKey = item.filterKey;
|
||||
if (item.filterType !== undefined) inputs.filterType = item.filterType;
|
||||
if (item.filterLabel !== undefined) inputs.filterLabel = item.filterLabel;
|
||||
if (item.filterOptions !== undefined) inputs.filterOptions = item.filterOptions;
|
||||
if (item.table !== undefined) inputs.apiUrl = item.table; // Use table as API URL for compact filter
|
||||
if (item.connection !== undefined) inputs.connection = item.connection ? parseInt(item.connection, 10) : undefined;
|
||||
|
||||
console.log('Final inputs for compact filter:', inputs);
|
||||
console.log('==============================');
|
||||
}
|
||||
|
||||
// Grid View specific inputs
|
||||
if (item.name === 'Grid View') {
|
||||
if (item.baseFilters !== undefined) inputs.baseFilters = item.baseFilters;
|
||||
}
|
||||
|
||||
// Chart specific inputs
|
||||
if (item.name.includes('Chart') && item.name !== 'Compact Filter') {
|
||||
if (item.baseFilters !== undefined) inputs.baseFilters = item.baseFilters;
|
||||
if (item.drilldownEnabled !== undefined) inputs.drilldownEnabled = item.drilldownEnabled;
|
||||
if (item.drilldownApiUrl !== undefined) inputs.drilldownApiUrl = item.drilldownApiUrl;
|
||||
if (item.drilldownXAxis !== undefined) inputs.drilldownXAxis = item.drilldownXAxis;
|
||||
if (item.drilldownYAxis !== undefined) inputs.drilldownYAxis = item.drilldownYAxis;
|
||||
if (item.drilldownParameter !== undefined) inputs.drilldownParameter = item.drilldownParameter;
|
||||
if (item.drilldownFilters !== undefined) inputs.drilldownFilters = item.drilldownFilters;
|
||||
if (item.drilldownLayers !== undefined) inputs.drilldownLayers = item.drilldownLayers;
|
||||
}
|
||||
|
||||
console.log('Component inputs for', item.name, ':', inputs);
|
||||
return inputs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import { ChartDataset, ChartType, } from 'chart.js';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-doughnut-runner',
|
||||
@@ -33,10 +37,16 @@ export class DoughnutRunnerComponent implements OnInit {
|
||||
"chartLabels": ["Project", "Repository", "Wireframe"]
|
||||
}
|
||||
doughnutChartType: ChartType = 'doughnut';
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
ngOnInit(): void {
|
||||
this.doughnutChartData = this.doughnutData.chartData;
|
||||
this.doughnutChartLabels = this.doughnutData.chartLabels;
|
||||
@@ -44,6 +54,14 @@ export class DoughnutRunnerComponent implements OnInit {
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data)=>{
|
||||
console.log(data);
|
||||
this.workflowLine = data.dashbord1_Line[0].model;
|
||||
@@ -70,22 +88,62 @@ export class DoughnutRunnerComponent implements OnInit {
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.showlabel = ChartObject[i].showlabel;
|
||||
this.doughnutChartLegend = ChartObject[i].chartlegend;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Doughnut Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.doughnutChartData = this.JsonData.chartData;
|
||||
this.doughnutChartLabels = this.JsonData.chartLabels;
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Fetch data with filters
|
||||
this.fetchChartData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch chart data with filter support
|
||||
fetchChartData(): void {
|
||||
if (this.TableName && this.XAxis && this.YAxis) {
|
||||
// Convert YAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
|
||||
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('DoughnutRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Doughnut Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.doughnutChartData = this.JsonData.chartData;
|
||||
this.doughnutChartLabels = this.JsonData.chartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
// this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -94,6 +152,19 @@ export class DoughnutRunnerComponent implements OnInit {
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('DoughnutRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('DoughnutRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -39,16 +39,35 @@
|
||||
</div> -->
|
||||
<div><button class="btn btn-primary" (click)="generatePDFFile()">Export</button></div>
|
||||
<div style="max-height: 400px; overflow: auto; padding: 10px;">
|
||||
<table class="table">
|
||||
<!-- Debug information -->
|
||||
<div *ngIf="false" style="background-color: #f0f0f0; padding: 10px; margin-bottom: 10px;">
|
||||
<h4>Debug Information</h4>
|
||||
<p><strong>TableName:</strong> {{ TableName }}</p>
|
||||
<p><strong>XAxis:</strong> {{ XAxis }}</p>
|
||||
<p><strong>YAxis:</strong> {{ YAxis }}</p>
|
||||
<p><strong>Rows:</strong> {{ rows?.length }} items</p>
|
||||
<p><strong>Headers:</strong> {{ getHeaders() | json }}</p>
|
||||
<div *ngIf="error"><strong>Error:</strong> {{ error }}</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="error" class="error_mess">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<table class="table" *ngIf="rows && rows.length > 0; else noData">
|
||||
<thead>
|
||||
<tr>
|
||||
<th *ngFor="let co of getHeaders();let i=index">{{co}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of rows?.slice()?.reverse()">
|
||||
<tr *ngFor="let item of rows">
|
||||
<td *ngFor="let key of getHeaders()">{{item[key]}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ng-template #noData>
|
||||
<p *ngIf="!error">No data available</p>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-grid-runner',
|
||||
@@ -26,57 +30,150 @@ export class GridRunnerComponent implements OnInit {
|
||||
public DashtestboardArray: DashboardContentModel[] = [];
|
||||
workflowLine;
|
||||
TableName;
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router
|
||||
private Dashtestservive:DashrunnerService,
|
||||
private route: ActivatedRoute,
|
||||
private dashboardService: Dashboard3Service,
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
console.log('GridRunner: Component initialized with editId:', this.editId);
|
||||
// this.getbyId();
|
||||
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
console.log('GridRunner: Filter state changed:', filters);
|
||||
// When filters change, refresh the grid data
|
||||
this.fetchGridData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data) => {
|
||||
console.log(data);
|
||||
console.log('GridRunner: Received dashboard data:', data);
|
||||
this.workflowLine = data.dashbord1_Line[0].model;
|
||||
const dash = JSON.parse(this.workflowLine);
|
||||
// this.DashtestboardArray = dash.dashboard;
|
||||
// console.log(this.DashtestboardArray);
|
||||
|
||||
const ChartObject = dash.dashboard.filter(obj => obj.name === "Grid View");
|
||||
console.log(ChartObject);
|
||||
console.log('GridRunner: ChartObject for Grid View:', ChartObject);
|
||||
for (let i = 0; i < ChartObject.length; i++) {
|
||||
const ids = this.Dashtestservive.getgridview();
|
||||
console.log('GridRunner: Current gridview ids:', ids);
|
||||
console.log('GridRunner: Checking chartid:', ChartObject[i].chartid);
|
||||
// console.log(ids);
|
||||
if (ids.includes(ChartObject[i].chartid)) {
|
||||
// If the chartid is already in the ids array, continue to the next iteration
|
||||
console.log('GridRunner: Skipping chartid as it already exists:', ChartObject[i].chartid);
|
||||
continue;
|
||||
}
|
||||
console.log('GridRunner: Adding new chartid:', ChartObject[i].chartid);
|
||||
this.Dashtestservive.setgridview(ChartObject[i].chartid);
|
||||
const id = ids[i];
|
||||
console.log(id);
|
||||
|
||||
if (ChartObject[i].chartid === id) {
|
||||
this.TableName = ChartObject[i].table;
|
||||
this.XAxis = ChartObject[i].xAxis;
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Grid View",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.rows = Ldata;
|
||||
this.rowdata = this.rows
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Add connection ID if available
|
||||
this.ConnectionId = ChartObject[i].connection;
|
||||
console.log('GridRunner: TableName:', this.TableName);
|
||||
console.log('GridRunner: XAxis:', this.XAxis);
|
||||
console.log('GridRunner: YAxis:', this.YAxis);
|
||||
console.log('GridRunner: ConnectionId:', this.ConnectionId);
|
||||
// Fetch data with filters
|
||||
this.fetchGridData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
}, (error) => {
|
||||
console.log('GridRunner: Error fetching dashboard data:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch grid data with filter support
|
||||
fetchGridData(): void {
|
||||
console.log('fetching grid data ...')
|
||||
if (this.TableName) {
|
||||
console.log('GridRunner: Fetching data for TableName:', this.TableName, 'XAxis:', this.XAxis, 'YAxis:', this.YAxis);
|
||||
// Convert YAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
|
||||
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('GridRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "grid", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log('GridRunner: Received data from API:', Ldata);
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (Ldata && Ldata.chartData) {
|
||||
this.rows = Ldata.chartData;
|
||||
this.rowdata = this.rows;
|
||||
} else if (Ldata && Ldata.data) {
|
||||
// Handle the original expected format as fallback
|
||||
this.rows = Ldata.data;
|
||||
this.rowdata = this.rows;
|
||||
} else if (Array.isArray(Ldata)) {
|
||||
// Handle case where data is directly an array
|
||||
this.rows = Ldata;
|
||||
this.rowdata = this.rows;
|
||||
} else {
|
||||
console.warn('GridRunner: Received data does not have expected structure', Ldata);
|
||||
this.rows = [];
|
||||
this.rowdata = [];
|
||||
}
|
||||
|
||||
// Log the structure of the received data
|
||||
if (this.rows) {
|
||||
console.log('GridRunner: Rows length:', this.rows.length);
|
||||
if (this.rows.length > 0) {
|
||||
console.log('GridRunner: First row structure:', this.rows[0]);
|
||||
}
|
||||
} else {
|
||||
console.log('GridRunner: No data received');
|
||||
}
|
||||
}, (error) => {
|
||||
console.log('GridRunner: Error fetching data:', error);
|
||||
this.error = error;
|
||||
});
|
||||
} else {
|
||||
console.log('GridRunner: Missing TableName or XAxis');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//dynamic table
|
||||
@@ -87,16 +184,16 @@ getTableData(id){
|
||||
getHeaders() {
|
||||
let headers: string[] = [];
|
||||
if (this.rows) {
|
||||
console.log('GridRunner: Getting headers from rows:', this.rows);
|
||||
this.rows.forEach((value) => {
|
||||
Object.keys(value).forEach((key) => {
|
||||
if (!headers.find((header) => header == key)) {
|
||||
headers.push(key)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
console.log('GridRunner: Computed headers:', headers);
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -107,5 +204,17 @@ generatePDFFile(){
|
||||
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('GridRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('GridRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
import { jsPDF } from 'jspdf';
|
||||
import domtoimage from 'dom-to-image';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
@Component({
|
||||
selector: 'app-line-runner',
|
||||
templateUrl: './line-runner.component.html',
|
||||
@@ -54,8 +58,14 @@ export class LineRunnerComponent implements OnInit {
|
||||
lineChartLegend = false;
|
||||
lineChartPlugins = [];
|
||||
lineChartType = 'line';
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@@ -65,6 +75,13 @@ export class LineRunnerComponent implements OnInit {
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data)=>{
|
||||
console.log(data);
|
||||
@@ -92,16 +109,10 @@ export class LineRunnerComponent implements OnInit {
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.showlabel = ChartObject[i].showlabel;
|
||||
this.lineChartLegend = ChartObject[i].chartlegend;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Line Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.lineChartData = this.JsonData.chartData;
|
||||
this.lineChartLabels = this.JsonData.chartLabels;
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Fetch data with filters
|
||||
this.fetchChartData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
@@ -128,6 +139,52 @@ export class LineRunnerComponent implements OnInit {
|
||||
// }
|
||||
}
|
||||
|
||||
// Fetch chart data with filter support
|
||||
fetchChartData(): void {
|
||||
if (this.TableName && this.XAxis && this.YAxis) {
|
||||
// Convert YAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
|
||||
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('LineRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Line Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.lineChartData = this.JsonData.chartData;
|
||||
this.lineChartLabels = this.JsonData.chartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -166,4 +223,17 @@ export class LineRunnerComponent implements OnInit {
|
||||
// }
|
||||
// }
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('LineRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('LineRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -23,9 +27,15 @@ export class PieRunnerComponent implements OnInit {
|
||||
showlabel;
|
||||
JsonData;
|
||||
lineChartNoLabels: any[] = [];
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
public pieChartLabels: string[] = ['SciFi', 'Drama', 'Comedy'];
|
||||
public pieChartData: number[] = [30, 50, 20];
|
||||
@@ -39,6 +49,13 @@ export class PieRunnerComponent implements OnInit {
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data)=>{
|
||||
console.log(data);
|
||||
@@ -66,22 +83,62 @@ export class PieRunnerComponent implements OnInit {
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.showlabel = ChartObject[i].showlabel;
|
||||
this.ChartLegend = ChartObject[i].chartlegend;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Pie Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.pieChartData = this.JsonData.pieChartData;
|
||||
this.pieChartLabels = this.JsonData.pieChartLabels;
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Fetch data with filters
|
||||
this.fetchChartData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch chart data with filter support
|
||||
fetchChartData(): void {
|
||||
if (this.TableName && this.XAxis && this.YAxis) {
|
||||
// Convert YAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
|
||||
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('PieRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Pie Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.pieChartData = this.JsonData.pieChartData;
|
||||
this.pieChartLabels = this.JsonData.pieChartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -89,4 +146,17 @@ export class PieRunnerComponent implements OnInit {
|
||||
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('PieRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('PieRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
// import { Label } from 'ng2-charts';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-polar-runner',
|
||||
@@ -23,9 +27,15 @@ export class PolarRunnerComponent implements OnInit {
|
||||
showlabel;
|
||||
JsonData;
|
||||
lineChartNoLabels: any[] = [];
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
public polarAreaChartLabels: string[] = [ 'Download Sales', 'In-Store Sales', 'Mail Sales', 'Telesales', 'Corporate Sales' ];
|
||||
public polarAreaChartData: any = [
|
||||
@@ -41,6 +51,13 @@ export class PolarRunnerComponent implements OnInit {
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data)=>{
|
||||
console.log(data);
|
||||
@@ -66,22 +83,62 @@ export class PolarRunnerComponent implements OnInit {
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.showlabel = ChartObject[i].showlabel;
|
||||
this.ChartLegend = ChartObject[i].chartlegend;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"PolarArea Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.polarAreaChartData = this.JsonData.polarAreaChartData;
|
||||
this.polarAreaChartLabels = this.JsonData.polarAreaChartLabels;
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Fetch data with filters
|
||||
this.fetchChartData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch chart data with filter support
|
||||
fetchChartData(): void {
|
||||
if (this.TableName && this.XAxis && this.YAxis) {
|
||||
// Convert YAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
|
||||
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('PolarRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "PolarArea Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.polarAreaChartData = this.JsonData.polarAreaChartData;
|
||||
this.polarAreaChartLabels = this.JsonData.polarAreaChartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -90,4 +147,17 @@ export class PolarRunnerComponent implements OnInit {
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('PolarRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('PolarRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
// import { Label } from 'ng2-charts';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-radar-runner',
|
||||
@@ -24,9 +28,15 @@ export class RadarRunnerComponent implements OnInit {
|
||||
JsonData;
|
||||
lineChartNoLabels: any[] = [];
|
||||
ChartLegend = false;
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
public radarChartLabels: string[] = [
|
||||
"Eating",
|
||||
@@ -50,6 +60,13 @@ export class RadarRunnerComponent implements OnInit {
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data)=>{
|
||||
console.log(data);
|
||||
@@ -75,22 +92,62 @@ export class RadarRunnerComponent implements OnInit {
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.showlabel = ChartObject[i].showlabel;
|
||||
this.ChartLegend = ChartObject[i].chartlegend;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Radar Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.radarChartData = this.JsonData.radarChartData;
|
||||
this.radarChartLabels = this.JsonData.radarChartLabels;
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Fetch data with filters
|
||||
this.fetchChartData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch chart data with filter support
|
||||
fetchChartData(): void {
|
||||
if (this.TableName && this.XAxis && this.YAxis) {
|
||||
// Convert YAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
|
||||
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('RadarRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Radar Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.radarChartData = this.JsonData.radarChartData;
|
||||
this.radarChartLabels = this.JsonData.radarChartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -99,4 +156,17 @@ export class RadarRunnerComponent implements OnInit {
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('RadarRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('RadarRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
// import { Label } from 'ng2-charts';
|
||||
import { ChartDataset } from 'chart.js';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-scatter-runner',
|
||||
@@ -25,9 +29,15 @@ export class ScatterRunnerComponent implements OnInit {
|
||||
JsonData;
|
||||
lineChartNoLabels: any[] = [];
|
||||
ChartLegend = false;
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,) { }
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
public scatterChartLabels: string[] = [ 'Eating', 'Drinking', 'Sleeping', 'Designing', 'Coding', 'Cycling', 'Running' ];
|
||||
|
||||
@@ -69,6 +79,13 @@ export class ScatterRunnerComponent implements OnInit {
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data)=>{
|
||||
console.log(data);
|
||||
@@ -94,22 +111,62 @@ export class ScatterRunnerComponent implements OnInit {
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.showlabel = ChartObject[i].showlabel;
|
||||
this.ChartLegend = ChartObject[i].chartlegend;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Scatter Chart",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.scatterChartData = this.JsonData.scatterChartData;
|
||||
this.scatterChartLabels = this.JsonData.scatterChartLabels;
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Fetch data with filters
|
||||
this.fetchChartData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch chart data with filter support
|
||||
fetchChartData(): void {
|
||||
if (this.TableName && this.XAxis && this.YAxis) {
|
||||
// Convert YAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
|
||||
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('ScatterRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Scatter Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.JsonData = Ldata;
|
||||
this.scatterChartData = this.JsonData.scatterChartData;
|
||||
this.scatterChartLabels = this.JsonData.scatterChartLabels;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generatePDFFile(){
|
||||
this.buttonClicked.emit();
|
||||
const content = this.contentContainerRef.nativeElement;
|
||||
@@ -118,4 +175,17 @@ export class ScatterRunnerComponent implements OnInit {
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('ScatterRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('ScatterRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
|
||||
import { DashboardContentModel } from 'src/app/models/builder/dashboard';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-todo-runner',
|
||||
@@ -12,8 +16,9 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
export class TodoRunnerComponent implements OnInit {
|
||||
@ViewChild('contentContainer') contentContainerRef!: ElementRef;
|
||||
@Output() buttonClicked = new EventEmitter<void>();
|
||||
constructor( private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router) { }
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
loading = false;
|
||||
givendata;
|
||||
@@ -25,6 +30,7 @@ export class TodoRunnerComponent implements OnInit {
|
||||
public DashtestboardArray: DashboardContentModel[] = [];
|
||||
workflowLine;
|
||||
TableName;
|
||||
ConnectionId: number; // Add ConnectionId property
|
||||
|
||||
list;
|
||||
data: any;
|
||||
@@ -34,11 +40,25 @@ export class TodoRunnerComponent implements OnInit {
|
||||
listName: "title123",
|
||||
List:['todo 1','todo 2'],
|
||||
}
|
||||
|
||||
constructor( private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
|
||||
private router : Router,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.editId = this.route.snapshot.params.id;
|
||||
console.log(this.editId);
|
||||
// this.getbyId();
|
||||
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the todo data
|
||||
this.fetchTodoData();
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardService.getById(this.editId).subscribe((data)=>{
|
||||
console.log(data);
|
||||
this.workflowLine = data.dashbord1_Line[0].model;
|
||||
@@ -63,15 +83,10 @@ export class TodoRunnerComponent implements OnInit {
|
||||
this.TableName = ChartObject[i].table;
|
||||
this.XAxis = ChartObject[i].xAxis;
|
||||
this.YAxis = ChartObject[i].yAxis;
|
||||
this.ConnectionId = ChartObject[i].connection; // Add connection ID
|
||||
console.log(this.TableName);
|
||||
this.Dashtestservive.getChartData(this.TableName,"Todo List",this.XAxis,this.YAxis).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.todoList.listName = Ldata.listName;
|
||||
this.todoList.List = Ldata.List;
|
||||
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
// Fetch data with filters
|
||||
this.fetchTodoData();
|
||||
break; // No need to continue the loop once the correct placeholder is found
|
||||
}
|
||||
}
|
||||
@@ -100,4 +115,58 @@ generatePDFFile(){
|
||||
|
||||
this.Dashtestservive.generatePDF(content, filename);
|
||||
}
|
||||
|
||||
// Fetch todo data with filter support
|
||||
fetchTodoData(): void {
|
||||
if (this.TableName && this.XAxis && this.YAxis) {
|
||||
// Get filter parameters from common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
|
||||
// Build filter object using field names as keys
|
||||
const filterObj = {};
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('TodoRunner: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service with filters
|
||||
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Todo List", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
|
||||
console.log(Ldata);
|
||||
this.todoList.listName = Ldata.listName;
|
||||
this.todoList.List = Ldata.List;
|
||||
},(error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('TodoRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
console.log('TodoRunnerComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user