12 Commits

Author SHA1 Message Date
Gaurav Kumar
82425d5377 Update editnewdash.component.html 2025-10-28 12:13:15 +05:30
Gaurav Kumar
2995328ec1 compact filter 2025-10-27 20:36:22 +05:30
Gaurav Kumar
afc2c1f8a1 filter with runner 2025-10-27 20:12:22 +05:30
Gaurav Kumar
418b02acd7 checkbox multislect filter, in edit new dash 2025-10-27 19:02:47 +05:30
Gaurav Kumar
cdd752469c builder 2025-10-27 18:48:16 +05:30
Gaurav Kumar
cdcf1e07c7 Update compact-filter.component.ts 2025-10-25 21:57:10 +05:30
Gaurav Kumar
c6ad8b5c2f filter 2025-10-25 21:46:54 +05:30
Gaurav Kumar
98e0908920 sheild 2025-10-25 20:15:06 +05:30
Gaurav Kumar
8266bfdc01 dashboard 2025-10-25 20:08:04 +05:30
Gaurav Kumar
f2b6a4d145 dashboard 2025-10-25 19:28:03 +05:30
Gaurav Kumar
02f37a1bc5 shield 2025-10-25 17:55:46 +05:30
Gaurav Kumar
f24138cfbd shield dashboard 2025-10-25 16:15:16 +05:30
67 changed files with 7453 additions and 814 deletions

View File

@@ -11,6 +11,9 @@
<h3>{{ 'Dashboard_builder' | translate }}</h3> <h3>{{ 'Dashboard_builder' | translate }}</h3>
</div> </div>
<div class="clr-col-4" style="text-align: right;"> <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()"> <button id="add" class="btn btn-primary" (click)="gotorunner()">
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }} <clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
</button> </button>
@@ -139,7 +142,4 @@
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary" >{{ 'Delete' | translate }}</button> <button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary" >{{ 'Delete' | translate }}</button>
</div> </div>
</div> </div>
</clr-modal> </clr-modal>

View File

@@ -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>

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -1,3 +1,4 @@
export * from './filter.service'; export * from './filter.service';
export * from './common-filter.component'; export * from './common-filter.component';
export * from './chart-wrapper.component'; export * from './chart-wrapper.component';
export * from './compact-filter.component';

View File

@@ -24,6 +24,8 @@ import { isArray } from 'highcharts';
import { SureconnectService } from '../sureconnect/sureconnect.service'; import { SureconnectService } from '../sureconnect/sureconnect.service';
// Add the CommonFilterComponent import // Add the CommonFilterComponent import
import { CommonFilterComponent } from '../common-filter/common-filter.component'; import { CommonFilterComponent } from '../common-filter/common-filter.component';
// Add the CompactFilterComponent import
import { CompactFilterComponent } from '../common-filter';
function isNullArray(arr) { function isNullArray(arr) {
return !Array.isArray(arr) || arr.length === 0; return !Array.isArray(arr) || arr.length === 0;
@@ -45,6 +47,12 @@ export class EditnewdashComponent implements OnInit {
commonFilterModalOpen: boolean = false; // Add common filter modal state commonFilterModalOpen: boolean = false; // Add common filter modal state
public entryForm: FormGroup; public entryForm: FormGroup;
public commonFilterForm: FormGroup; // Add common filter form 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[] = [ WidgetsMock: WidgetModel[] = [
{ {
@@ -98,6 +106,10 @@ export class EditnewdashComponent implements OnInit {
{ {
name: 'Grid View', name: 'Grid View',
identifier: 'grid_view' identifier: 'grid_view'
},
{
name: 'Compact Filter',
identifier: 'compact_filter'
} }
] ]
@@ -123,6 +135,7 @@ export class EditnewdashComponent implements OnInit {
{ name: "Financial Chart", componentInstance: FinancialChartComponent }, { name: "Financial Chart", componentInstance: FinancialChartComponent },
{ name: "To Do Chart", componentInstance: ToDoChartComponent }, { name: "To Do Chart", componentInstance: ToDoChartComponent },
{ name: "Grid View", componentInstance: GridViewComponent }, { name: "Grid View", componentInstance: GridViewComponent },
{ name: "Compact Filter", componentInstance: CompactFilterComponent }, // Add this line
]; ];
model: any; model: any;
linesdata: any; linesdata: any;
@@ -168,7 +181,12 @@ export class EditnewdashComponent implements OnInit {
drilldownLayers: [] as any[], drilldownLayers: [] as any[],
// Common filter properties // Common filter properties
commonFilterEnabled: false, commonFilterEnabled: false,
commonFilterEnabledDrilldown: false commonFilterEnabledDrilldown: false,
// Compact filter properties
filterKey: '',
filterType: 'text',
filterLabel: '',
filterOptions: [] as string[]
}; };
// Add sureconnect data property // Add sureconnect data property
@@ -348,6 +366,16 @@ export class EditnewdashComponent implements OnInit {
dashboard.component = component.componentInstance; 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; 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) // 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, component: CommonFilterComponent,
name: "Common Filter" name: "Common Filter"
}); });
case "compact_filter":
return this.dashboardArray.push({
cols: 3,
rows: 2,
x: 0,
y: 0,
chartid: maxChartId + 1,
component: CompactFilterComponent,
name: "Compact Filter",
// Add default configuration for compact filter
filterKey: '',
filterType: 'text',
filterLabel: '',
filterOptions: []
});
case "grid_view": case "grid_view":
return this.dashboardArray.push({ return this.dashboardArray.push({
cols: 5, cols: 5,
@@ -561,6 +614,31 @@ export class EditnewdashComponent implements OnInit {
if (item['commonFilterEnabledDrilldown'] === undefined) { if (item['commonFilterEnabledDrilldown'] === undefined) {
this.gadgetsEditdata['commonFilterEnabledDrilldown'] = false; this.gadgetsEditdata['commonFilterEnabledDrilldown'] = false;
} }
// Initialize compact filter properties if not present
if (item['filterKey'] === undefined) {
this.gadgetsEditdata['filterKey'] = '';
}
if (item['filterType'] === undefined) {
this.gadgetsEditdata['filterType'] = 'text';
}
if (item['filterLabel'] === undefined) {
this.gadgetsEditdata['filterLabel'] = '';
}
if (item['filterOptions'] === undefined) {
this.gadgetsEditdata['filterOptions'] = [];
}
// 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(); this.getStores();
// Set default connection if none is set and we have connections // 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 //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 => { let cmp = this.dashboardCollection.dashboard.forEach(dashboard => {
this.componentCollection.forEach(component => { this.componentCollection.forEach(component => {
if (dashboard.name === component.name) { if (dashboard.name === component.name) {
@@ -679,8 +760,6 @@ export class EditnewdashComponent implements OnInit {
//console.log(merged); //console.log(merged);
console.log("temp data", typeof tmp); console.log("temp data", typeof tmp);
console.log(tmp); console.log(tmp);
let parsed = JSON.parse(tmp);
this.serialize(parsed.dashboard);
this.dashbord1_Line.model = tmp; this.dashbord1_Line.model = tmp;
// let obj = this.dashboardCollection; // let obj = this.dashboardCollection;
@@ -736,6 +815,21 @@ export class EditnewdashComponent implements OnInit {
xyz.drilldownLayers = this.gadgetsEditdata.drilldownLayers; xyz.drilldownLayers = this.gadgetsEditdata.drilldownLayers;
xyz.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property xyz.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
// For compact filter, preserve filter configuration properties
if (item.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); console.log(xyz);
return xyz; return xyz;
} }
@@ -770,7 +864,97 @@ export class EditnewdashComponent implements OnInit {
* This prevents errors when trying to set properties that don't exist on the components * This prevents errors when trying to set properties that don't exist on the components
*/ */
getChartInputs(item: any): any { getChartInputs(item: any): any {
// Only pass properties that are relevant to chart components // 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,
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(gridInputs).forEach(key => {
if (gridInputs[key] === undefined) {
delete gridInputs[key];
}
});
return gridInputs;
}
// For all other chart components, pass chart-specific properties
const chartInputs = { const chartInputs = {
xAxis: item.xAxis, xAxis: item.xAxis,
yAxis: item.yAxis, yAxis: item.yAxis,
@@ -800,15 +984,6 @@ export class EditnewdashComponent implements OnInit {
drilldownLayers: item['drilldownLayers'] || [] 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 // Remove undefined properties to avoid passing unnecessary data
Object.keys(chartInputs).forEach(key => { Object.keys(chartInputs).forEach(key => {
if (chartInputs[key] === undefined) { if (chartInputs[key] === undefined) {
@@ -863,6 +1038,27 @@ export class EditnewdashComponent implements OnInit {
updatedItem.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property updatedItem.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
updatedItem.commonFilterEnabledDrilldown = this.gadgetsEditdata.commonFilterEnabledDrilldown; // Add drilldown common filter property updatedItem.commonFilterEnabledDrilldown = this.gadgetsEditdata.commonFilterEnabledDrilldown; // Add drilldown common filter property
// For compact filter, preserve filter configuration properties
if (item.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); console.log('Updated item:', updatedItem);
return updatedItem; return updatedItem;
} }
@@ -1288,4 +1484,80 @@ export class EditnewdashComponent implements OnInit {
// This would require the chart component to have a public resize method // 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);
}
}
} }

View File

@@ -173,73 +173,41 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
console.log('Bar chart data URL:', url); console.log('Bar chart data URL:', url);
// Convert baseFilters to filter parameters // Convert baseFilters to filter parameters
let filterParams = ''; const filterObj = {};
// Add base filters
if (this.baseFilters && this.baseFilters.length > 0) { if (this.baseFilters && this.baseFilters.length > 0) {
const filterObj = {};
this.baseFilters.forEach(filter => { this.baseFilters.forEach(filter => {
if (filter.field && filter.value) { if (filter.field && filter.value) {
filterObj[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 commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters(); const filterDefinitions = this.filterService.getFilters();
console.log('Common filters from service:', commonFilters); Object.keys(commonFilters).forEach(filterId => {
console.log('Filter definitions:', filterDefinitions); 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(commonFilters).length > 0) { // Convert to JSON string for API call
// Merge common filters with base filters let filterParams = '';
const mergedFilterObj = {}; if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
console.log(`Processing filter ID: ${filterId}, Value:`, filterValue);
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
console.log(`Filter definition for ${filterId}:`, filterDef);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
console.log(`Mapping filter ID ${filterId} to field name: ${fieldName}`);
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
console.log(`Added to merged filters: ${fieldName} =`, filterValue);
}
} else {
// Fallback to using filterId as field name if no field is defined
console.log(`No field name found for filter ID ${filterId}, using ID as field name`);
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
console.log(`Added to merged filters: ${filterId} =`, filterValue);
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
} }
console.log('Final merged filter object:', filterParams); console.log('Final filter object:', filterObj);
// Fetch data from the dashboard service with parameter field and value // 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 // 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( 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); console.log('Drilldown data URL:', url);
// Convert drilldown layer filters to filter parameters (if applicable) // Convert drilldown layer filters to filter parameters (if applicable)
let filterParams = ''; const filterObj = {};
// Add drilldown layer filters
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) { if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
const filterObj = {};
drilldownConfig.filters.forEach((filter: any) => { drilldownConfig.filters.forEach((filter: any) => {
if (filter.field && filter.value) { if (filter.field && filter.value) {
filterObj[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 // Add drilldownFilters
let drilldownFilterParams = '';
if (this.drilldownFilters && this.drilldownFilters.length > 0) { if (this.drilldownFilters && this.drilldownFilters.length > 0) {
const filterObj = {};
this.drilldownFilters.forEach(filter => { this.drilldownFilters.forEach(filter => {
if (filter.field && filter.value) { if (filter.field && filter.value) {
filterObj[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 // Add common filters
const commonFilters = this.filterService.getFilterValues(); const commonFilters = this.filterService.getFilterValues();
if (Object.keys(commonFilters).length > 0) { const filterDefinitions = this.filterService.getFilters();
// Merge common filters with drilldown filters Object.keys(commonFilters).forEach(filterId => {
const mergedFilterObj = {}; const filterValue = commonFilters[filterId];
// Add drilldown filters first // Find the filter definition to get the field name
if (drilldownFilterParams) { const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
try {
const drilldownFilterObj = JSON.parse(drilldownFilterParams); if (filterDef && filterDef.field) {
Object.assign(mergedFilterObj, drilldownFilterObj); const fieldName = filterDef.field;
} catch (e) { if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
console.warn('Failed to parse drilldown filter parameters:', e); filterObj[fieldName] = filterValue;
} }
} }
});
// Add common filters
Object.keys(commonFilters).forEach(key => { // Convert to JSON string for API call
const value = commonFilters[key]; let drilldownFilterParams = '';
if (value !== undefined && value !== null && value !== '') { if (Object.keys(filterObj).length > 0) {
mergedFilterObj[key] = value; drilldownFilterParams = JSON.stringify(filterObj);
}
});
if (Object.keys(mergedFilterObj).length > 0) {
drilldownFilterParams = JSON.stringify(mergedFilterObj);
}
} }
console.log('Drilldown filter parameters:', drilldownFilterParams); console.log('Drilldown filter parameters:', drilldownFilterParams);

View File

@@ -1,39 +1,53 @@
<div class="doughnut-chart-container"> <div class="doughnut-chart-container">
<!-- Compact Filters -->
<div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0">
<app-compact-filter
*ngFor="let filter of baseFilters"
[filterKey]="filter.field"
(filterChange)="onFilterChange($event)">
</app-compact-filter>
</div>
<!-- Drilldown mode indicator --> <!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span>
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> <button class="btn btn-secondary btn-sm" (click)="navigateBack()">
Back to Level {{currentDrilldownLevel - 1}} Back to Level {{currentDrilldownLevel - 1}}
</button> </button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> <button class="btn btn-danger btn-sm" (click)="resetToOriginalData()">
Back to Main View Back to Main View
</button> </button>
</div> </div>
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3> <div class="chart-header">
<div class="chart-wrapper"> <h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
<!-- Show loading indicator -->
<div class="loading-indicator" *ngIf="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
<div class="spinner"></div>
<p>Loading chart data...</p>
</div>
<!-- Show no data message -->
<div class="no-data-message" *ngIf="noDataAvailable">
<p>No chart data available</p>
</div>
<!-- Show chart when data is available -->
<canvas baseChart
*ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0"
[data]="doughnutChartData"
[labels]="doughnutChartLabels"
[type]="doughnutChartType"
[options]="doughnutChartOptions"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
</canvas>
</div> </div>
<div class="chart-wrapper">
<div class="chart-content" [class.loading]="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
<!-- Show no data message -->
<div class="no-data-message" *ngIf="noDataAvailable">
<p>No chart data available</p>
</div>
<!-- Show chart when data is available -->
<canvas baseChart
*ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0"
[data]="doughnutChartData"
[labels]="doughnutChartLabels"
[type]="doughnutChartType"
[options]="doughnutChartOptions"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
</canvas>
<!-- Loading overlay -->
<div class="loading-overlay" *ngIf="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
<div class="shimmer-donut"></div>
</div>
</div>
</div>
<div class="chart-legend" *ngIf="!noDataAvailable && showlabel && doughnutChartLabels && doughnutChartLabels.length > 0"> <div class="chart-legend" *ngIf="!noDataAvailable && showlabel && doughnutChartLabels && doughnutChartLabels.length > 0">
<div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index"> <div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index">
<span class="legend-color" [style.background-color]="getLegendColor(i)"></span> <span class="legend-color" [style.background-color]="getLegendColor(i)"></span>

View File

@@ -17,17 +17,78 @@
transform: translateY(-2px); transform: translateY(-2px);
} }
.compact-filters-container {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 10px;
padding: 5px;
background: #ffffff;
border: 1px solid #e9ecef;
border-radius: 6px;
min-height: 40px;
}
.drilldown-indicator {
.chart-title { background-color: #e0e0e0;
font-size: 26px; padding: 10px;
font-weight: 700; margin-bottom: 15px;
color: #2c3e50; border-radius: 8px;
margin-bottom: 20px;
text-align: center; text-align: center;
padding-bottom: 15px; display: flex;
border-bottom: 2px solid #3498db; justify-content: center;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); align-items: center;
gap: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.drilldown-text {
font-weight: bold;
color: #333;
font-size: 16px;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn-secondary {
background-color: #007cba;
color: white;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.chart-header {
margin-bottom: 20px;
.chart-title {
font-size: 22px;
font-weight: 600;
color: #0a192f;
margin: 0;
text-align: center;
padding-bottom: 10px;
border-bottom: 2px solid #3498db;
}
} }
.chart-wrapper { .chart-wrapper {
@@ -56,6 +117,62 @@
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.chart-content {
position: relative;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
&.loading {
opacity: 0.7;
canvas {
filter: blur(2px);
}
}
.no-data-message {
text-align: center;
padding: 30px;
color: #666;
font-size: 18px;
font-style: italic;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
.no-data-message p {
margin: 0;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.8);
.shimmer-donut {
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
}
}
.chart-legend { .chart-legend {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -119,36 +236,13 @@
text-align: center; text-align: center;
} }
.loading-indicator, .no-data-message { @keyframes shimmer {
text-align: center; 0% {
padding: 30px; background-position: -200% 0;
color: #666; }
font-size: 18px; 100% {
font-style: italic; background-position: 200% 0;
display: flex; }
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.loading-indicator p, .no-data-message p {
margin: 10px 0 0 0;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
} }
/* Responsive design */ /* Responsive design */
@@ -157,9 +251,17 @@
padding: 15px; padding: 15px;
} }
.chart-title { .chart-header .chart-title {
font-size: 20px; font-size: 18px;
margin-bottom: 15px; }
.drilldown-indicator {
flex-direction: column;
gap: 5px;
}
.drilldown-text {
font-size: 14px;
} }
.chart-wrapper { .chart-wrapper {
@@ -181,4 +283,8 @@
font-size: 16px; font-size: 16px;
padding: 20px; padding: 20px;
} }
.compact-filters-container {
flex-wrap: wrap;
}
} }

View File

@@ -200,6 +200,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions.forEach(sub => sub.unsubscribe());
} }
// Handle filter changes from compact filters
onFilterChange(event: { filterId: string, value: any }): void {
console.log('Compact filter changed:', event);
// The filter service will automatically trigger chart updates through the subscription
}
// Public method to refresh data when filters change // Public method to refresh data when filters change
refreshData(): void { refreshData(): void {
this.fetchChartData(); this.fetchChartData();

View File

@@ -4,9 +4,41 @@
<div class="clr-col-8"> <div class="clr-col-8">
<h3>{{charttitle || 'Data Grid'}}</h3> <h3>{{charttitle || 'Data Grid'}}</h3>
</div> </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> </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-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> <div *ngIf="error;else loadingSpinner">{{error}}</div>
</clr-dg-placeholder> </clr-dg-placeholder>
@@ -19,7 +51,7 @@
<clr-dg-row *clrDgItems="let item of givendata" [clrDgItem]="item"> <clr-dg-row *clrDgItems="let item of givendata" [clrDgItem]="item">
<!-- Dynamic cells based on response keys --> <!-- 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]}} {{item[header.key]}}
</clr-dg-cell> </clr-dg-cell>
</clr-dg-row> </clr-dg-row>

View File

@@ -1,12 +1,28 @@
@import '../../../../../../../styles1.scss'; // Add styles for drilldown navigation
input.ng-invalid.ng-touched { .alert-info {
border-color: red; background-color: #dcedf7;
border-color: #a3d4f5;
color: #21333b;
} }
.error_mess { .alert-info .alert-icon {
color: red; color: #0072a3;
} }
clr-datagrid{
height: 400px; /* Adjust the height as needed */ .btn-link {
overflow-y: auto; color: #0072a3;
text-decoration: none;
}
.btn-link:hover {
color: #00567a;
text-decoration: underline;
}
.dg-wrapper {
padding: 12px;
}
.clr-row {
margin-bottom: 12px;
} }

View File

@@ -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 { UsergrpmaintainceService } from 'src/app/services/admin/usergrpmaintaince.service';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.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({ @Component({
selector: 'app-grid-view', selector: 'app-grid-view',
templateUrl: './grid-view.component.html', templateUrl: './grid-view.component.html',
styleUrls: ['./grid-view.component.scss'] styleUrls: ['./grid-view.component.scss']
}) })
export class GridViewComponent implements OnInit, OnChanges { export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
@Input() xAxis: string; @Input() xAxis: string;
@Input() yAxis: string | string[]; @Input() yAxis: string | string[];
@Input() table: string; @Input() table: string;
@@ -23,6 +27,16 @@ export class GridViewComponent implements OnInit, OnChanges {
@Input() datasource: string; @Input() datasource: string;
@Input() fieldName: string; @Input() fieldName: string;
@Input() connection: number; // Add connection input @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; loading = false;
givendata: any[] = []; givendata: any[] = [];
@@ -38,13 +52,37 @@ export class GridViewComponent implements OnInit, OnChanges {
submitted = false; submitted = false;
dynamicHeaders: any[] = []; 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 mainservice: UsergrpmaintainceService,
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
// Add FilterService to constructor
private filterService: FilterService
) { } ) { }
ngOnInit(): void { 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(); this.fetchGridData();
} }
@@ -55,87 +93,501 @@ export class GridViewComponent implements OnInit, OnChanges {
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
const tableChanged = changes.table && !changes.table.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 // Respond to input changes
if (xAxisChanged || yAxisChanged || tableChanged || connectionChanged) { if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
console.log('X or Y axis or table or connection changed, fetching new data'); drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
// Only fetch data if xAxis, yAxis, table, or connection has changed (and it's not the first change) 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(); this.fetchGridData();
} }
} }
// Dynamic headers for the grid // Dynamic headers for the grid
fetchGridData(): void { 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 we have the necessary data, fetch grid data from the service
if (this.table && this.xAxis) { // 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) {
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 // Convert yAxis to string if it's an array
const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis; 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 // 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) => { (data: any) => {
console.log('=== GRID VIEW DATA RESPONSE ===');
console.log('Received grid data:', data); console.log('Received grid data:', data);
if (data === null) { if (data === null) {
console.warn('Grid API returned null data. Check if the API endpoint is working correctly.'); console.warn('Grid API returned null data. Check if the API endpoint is working correctly.');
this.error = "No data Available"; this.error = "No data Available";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
return; return;
} }
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartData) { if (data && data.chartData) {
this.givendata = data.chartData; this.givendata = data.chartData;
this.extractDynamicHeaders(data.chartData); this.extractDynamicHeaders(data.chartData);
this.error = this.givendata.length === 0 ? "No data Available" : undefined; this.error = this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata.length === 0;
console.log('Updated grid with data:', this.givendata); console.log('Updated grid with data:', this.givendata);
} else if (data && data.data) { } else if (data && data.data) {
// Handle the original expected format as fallback // Handle the original expected format as fallback
this.givendata = data.data; this.givendata = data.data;
this.extractDynamicHeaders(data.data); this.extractDynamicHeaders(data.data);
this.error = this.givendata.length === 0 ? "No data Available" : undefined; 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); console.log('Updated grid with legacy data format:', this.givendata);
} else if (Array.isArray(data)) { } else if (Array.isArray(data)) {
// Handle case where data is directly an array // Handle case where data is directly an array
this.givendata = data; this.givendata = data;
this.extractDynamicHeaders(data); this.extractDynamicHeaders(data);
this.error = this.givendata.length === 0 ? "No data Available" : undefined; 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); console.log('Updated grid with array data:', this.givendata);
} else { } else {
console.warn('Grid received data does not have expected structure', data); console.warn('Grid received data does not have expected structure', data);
this.error = "No valid data received"; this.error = "No valid data received";
this.givendata = []; this.givendata = [];
this.noDataAvailable = true;
} }
// Reset flag after fetching
this.isFetchingData = false;
}, (error) => { }, (error) => {
console.log('Error fetching grid data:', error); console.log('Error fetching grid data:', error);
this.error = "Server Error"; this.error = "Server Error";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
}); });
} else if (this.table) { } else if (this.table) {
console.log('Missing xAxis, falling back to default data fetching'); console.log('Missing xAxis, falling back to default data fetching');
// Fallback to default data fetching when only table is provided // Fallback to default data fetching when only table is provided
// Convert yAxis to string if it's an array // Convert yAxis to string if it's an array
const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis; const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
// Fetch data from the dashboard service, similar to other chart components // 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).subscribe(
(data: any) => { (data: any) => {
// this.mainservice.getAll().subscribe((data: any) => { // this.mainservice.getAll().subscribe((data: any) => {
console.log('recv data ', data); console.log('recv data ', data);
this.givendata = Array.isArray(data) ? data : []; this.givendata = Array.isArray(data) ? data : [];
this.extractDynamicHeaders(data); this.extractDynamicHeaders(data);
this.error = this.givendata && this.givendata.length === 0 ? "No data Available" : undefined; this.error = this.givendata && this.givendata.length === 0 ? "No data Available" : undefined;
}, (error) => { this.noDataAvailable = this.givendata && this.givendata.length === 0;
console.log(error); // Reset flag after fetching
this.error = "Server Error"; this.isFetchingData = false;
}); }, (error) => {
console.log(error);
this.error = "Server Error";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
});
} else { } else {
console.log('Missing required data for grid:', { table: this.table }); console.log('Missing required data for grid:', { table: this.table });
this.error = "Table name is required"; 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 += `&parameter=${encodeURIComponent(parameterField)}&parameterValue=${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');
}
}
/** /**
* Extract dynamic headers from the data * Extract dynamic headers from the data
* @param data Array of data objects * @param data Array of data objects
@@ -143,7 +595,7 @@ export class GridViewComponent implements OnInit, OnChanges {
private extractDynamicHeaders(data: any): void { private extractDynamicHeaders(data: any): void {
// Ensure data is an array // Ensure data is an array
const dataArray = Array.isArray(data) ? data : []; const dataArray = Array.isArray(data) ? data : [];
if (dataArray && dataArray.length > 0) { if (dataArray && dataArray.length > 0) {
// Get all unique keys from the data objects // Get all unique keys from the data objects
const allKeys = new Set<string>(); const allKeys = new Set<string>();
@@ -152,19 +604,19 @@ export class GridViewComponent implements OnInit, OnChanges {
Object.keys(item).forEach(key => allKeys.add(key)); Object.keys(item).forEach(key => allKeys.add(key));
} }
}); });
// Convert to array of header objects with key and display name // Convert to array of header objects with key and display name
this.dynamicHeaders = Array.from(allKeys).map(key => ({ this.dynamicHeaders = Array.from(allKeys).map(key => ({
key: key, key: key,
displayName: this.formatHeader(key) displayName: this.formatHeader(key)
})); }));
console.log('Dynamic headers extracted:', this.dynamicHeaders); console.log('Dynamic headers extracted:', this.dynamicHeaders);
} else { } else {
this.dynamicHeaders = []; this.dynamicHeaders = [];
} }
} }
/** /**
* Format header name for better display * Format header name for better display
* @param key The key to format * @param key The key to format
@@ -175,4 +627,23 @@ export class GridViewComponent implements OnInit, OnChanges {
.replace(/([A-Z])/g, ' $1') .replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase()); .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');
}
} }

View File

@@ -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.

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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);
}
}
}
}
}
}
}
}

View File

@@ -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}%`;
}
}

View File

@@ -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>

View File

@@ -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;
}
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;
}
}
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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;
}
}
}
}
}

View File

@@ -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 {
}
}

View File

@@ -0,0 +1,3 @@
<div class="sidebar-filters">
<!-- Component Palette Button and List have been moved to the main shield dashboard component -->
</div>

View File

@@ -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>

View File

@@ -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;
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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';

View File

@@ -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;
}
}

View File

@@ -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 { }

View File

@@ -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/&lt;country&gt;</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/&lt;state&gt;</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>

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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 { }

View File

@@ -11,6 +11,9 @@
<h3>{{ 'all_dashboard' | translate }}</h3> <h3>{{ 'all_dashboard' | translate }}</h3>
</div> </div>
<div class="clr-col-4" style="text-align: right;"> <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()"> <button id="add" class="btn btn-primary" (click)="gotoadd()">
<clr-icon shape="plus"></clr-icon>{{ 'dashboard_builder' | translate }} <clr-icon shape="plus"></clr-icon>{{ 'dashboard_builder' | translate }}
</button> </button>
@@ -111,7 +114,4 @@
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary" >{{'DELETE' |translate}}</button> <button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary" >{{'DELETE' |translate}}</button>
</div> </div>
</div> </div>
</clr-modal> </clr-modal>

View File

@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; 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({ @Component({
selector: 'app-bar-runner', selector: 'app-bar-runner',
@@ -24,8 +28,19 @@ export class BarRunnerComponent implements OnInit {
JsonData; JsonData;
barData; barData;
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, ConnectionId: number; // Add ConnectionId property
private router : Router,) { }
// 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[] = []; barChartLabels: any[] = [];
barChartType: string = 'bar'; barChartType: string = 'bar';
@@ -47,6 +62,13 @@ export class BarRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); 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)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -74,22 +96,62 @@ export class BarRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.barChartLegend = ChartObject[i].chartlegend; this.barChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Bar Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.barChartData = this.JsonData.barChartData;
this.barChartLabels = this.JsonData.barChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found 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(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -97,5 +159,17 @@ export class BarRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); 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');
}
}

View File

@@ -5,6 +5,10 @@ import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
import { DashrunnerService } from '../dashrunner.service'; import { DashrunnerService } from '../dashrunner.service';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.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({ @Component({
selector: 'app-bubble-runner', selector: 'app-bubble-runner',
@@ -25,9 +29,15 @@ export class BubbleRunnerComponent implements OnInit {
JsonData; JsonData;
lineChartNoLabels: [] = []; lineChartNoLabels: [] = [];
ChartLegend = false; 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, 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'] = { public bubbleChartOptions: ChartConfiguration['options'] = {
// scales: { // scales: {
@@ -87,6 +97,13 @@ export class BubbleRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); 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)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -112,22 +129,62 @@ export class BubbleRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Bubble Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.bubbleChartData = this.JsonData.bubbleChartData;
// this.radarChartLabels = this.JsonData.radarChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found 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(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -135,6 +192,19 @@ export class BubbleRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); 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');
}
} }

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -160,7 +160,27 @@ getlinechart(): any[] {
return this._http.get(url); 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 += `&parameter=${encodeURIComponent(parameterField)}&parameterValue=${encodeURIComponent(parameterValue)}`;
}
// Add filter parameters if provided
if (filterParams) {
url += `&filters=${encodeURIComponent(filterParams)}`;
}
return this._http.get(url);
}
////////////////////////////////////////////// //////////////////////////////////////////////

View File

@@ -26,7 +26,14 @@
<!-- <span><button class="btn btn-primary" (click)="Export(item.name)">Export</button></span> --> <!-- <span><button class="btn btn-primary" (click)="Export(item.name)">Export</button></span> -->
<!-- <span><app-line-runner (buttonClicked)="generatePDFFile()"></app-line-runner></span> --> <!-- <span><app-line-runner (buttonClicked)="generatePDFFile()"></app-line-runner></span> -->
<!-- <h4 style="margin-top: 10px; margin-left: 10px;">{{ item.charttitle }}</h4> --> <!-- <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-item>
</gridster> </gridster>
</div> </div>

View File

@@ -17,6 +17,10 @@ import { BubbleRunnerComponent } from './bubble-runner/bubble-runner.component';
import { ScatterRunnerComponent } from './scatter-runner/scatter-runner.component'; import { ScatterRunnerComponent } from './scatter-runner/scatter-runner.component';
import { PolarRunnerComponent } from './polar-runner/polar-runner.component'; import { PolarRunnerComponent } from './polar-runner/polar-runner.component';
import { RadarRunnerComponent } from './radar-runner/radar-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({ @Component({
selector: 'app-dashrunnerline', selector: 'app-dashrunnerline',
@@ -44,10 +48,13 @@ export class DashrunnerlineComponent implements OnInit {
{ name: "Radar Chart", componentInstance: RadarRunnerComponent }, { name: "Radar Chart", componentInstance: RadarRunnerComponent },
{ name: "Grid View", componentInstance: GridRunnerComponent }, { name: "Grid View", componentInstance: GridRunnerComponent },
{ name: "To Do Chart", componentInstance: TodoRunnerComponent }, { 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, 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 { ngOnInit(): void {
@@ -288,4 +295,54 @@ dashboard_name = "Dashtest";
console.log('Button clicked in SomeComponent'); console.log('Button clicked in SomeComponent');
// Add your custom logic here when the button is 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;
}
} }

View File

@@ -5,6 +5,10 @@ import { ChartDataset, ChartType, } from 'chart.js';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; 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({ @Component({
selector: 'app-doughnut-runner', selector: 'app-doughnut-runner',
@@ -33,10 +37,16 @@ export class DoughnutRunnerComponent implements OnInit {
"chartLabels": ["Project", "Repository", "Wireframe"] "chartLabels": ["Project", "Repository", "Wireframe"]
} }
doughnutChartType: ChartType = 'doughnut'; 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, 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 { ngOnInit(): void {
this.doughnutChartData = this.doughnutData.chartData; this.doughnutChartData = this.doughnutData.chartData;
this.doughnutChartLabels = this.doughnutData.chartLabels; this.doughnutChartLabels = this.doughnutData.chartLabels;
@@ -44,6 +54,14 @@ export class DoughnutRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); 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)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
this.workflowLine = data.dashbord1_Line[0].model; this.workflowLine = data.dashbord1_Line[0].model;
@@ -70,22 +88,62 @@ export class DoughnutRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.doughnutChartLegend = ChartObject[i].chartlegend; this.doughnutChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Doughnut Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.doughnutChartData = this.JsonData.chartData;
this.doughnutChartLabels = this.JsonData.chartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found 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(){ generatePDFFile(){
// this.buttonClicked.emit(); // this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -93,6 +151,19 @@ export class DoughnutRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); 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');
}

View File

@@ -39,16 +39,35 @@
</div> --> </div> -->
<div><button class="btn btn-primary" (click)="generatePDFFile()">Export</button></div> <div><button class="btn btn-primary" (click)="generatePDFFile()">Export</button></div>
<div style="max-height: 400px; overflow: auto; padding: 10px;"> <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> <thead>
<tr> <tr>
<th *ngFor="let co of getHeaders();let i=index">{{co}}</th> <th *ngFor="let co of getHeaders();let i=index">{{co}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let item of rows?.slice()?.reverse()"> <tr *ngFor="let item of rows">
<td *ngFor="let key of getHeaders()">{{item[key]}}</td> <td *ngFor="let key of getHeaders()">{{item[key]}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<ng-template #noData>
<p *ngIf="!error">No data available</p>
</ng-template>
</div> </div>

View File

@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.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({ @Component({
selector: 'app-grid-runner', selector: 'app-grid-runner',
@@ -26,86 +30,191 @@ export class GridRunnerComponent implements OnInit {
public DashtestboardArray: DashboardContentModel[] = []; public DashtestboardArray: DashboardContentModel[] = [];
workflowLine; workflowLine;
TableName; TableName;
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor( constructor(
private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, private Dashtestservive:DashrunnerService,
private router : Router private route: ActivatedRoute,
private dashboardService: Dashboard3Service,
private router : Router,
// Add FilterService to constructor
private filterService: FilterService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log('GridRunner: Component initialized with editId:', this.editId);
// this.getbyId(); // this.getbyId();
this.dashboardService.getById(this.editId).subscribe((data)=>{ // Subscribe to filter changes
console.log(data); 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('GridRunner: Received dashboard data:', data);
this.workflowLine = data.dashbord1_Line[0].model; this.workflowLine = data.dashbord1_Line[0].model;
const dash = JSON.parse(this.workflowLine) ; const dash = JSON.parse(this.workflowLine);
// this.DashtestboardArray = dash.dashboard; // this.DashtestboardArray = dash.dashboard;
// console.log(this.DashtestboardArray); // console.log(this.DashtestboardArray);
const ChartObject = dash.dashboard.filter(obj => obj.name === "Grid View"); 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++) { for (let i = 0; i < ChartObject.length; i++) {
const ids = this.Dashtestservive.getgridview(); const ids = this.Dashtestservive.getgridview();
console.log('GridRunner: Current gridview ids:', ids);
console.log('GridRunner: Checking chartid:', ChartObject[i].chartid);
// console.log(ids); // console.log(ids);
if (ids.includes(ChartObject[i].chartid)) { if (ids.includes(ChartObject[i].chartid)) {
// If the chartid is already in the ids array, continue to the next iteration // 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; continue;
} }
console.log('GridRunner: Adding new chartid:', ChartObject[i].chartid);
this.Dashtestservive.setgridview(ChartObject[i].chartid); this.Dashtestservive.setgridview(ChartObject[i].chartid);
const id = ids[i];
console.log(id); this.TableName = ChartObject[i].table;
this.XAxis = ChartObject[i].xAxis;
if (ChartObject[i].chartid === id) { this.YAxis = ChartObject[i].yAxis;
this.TableName = ChartObject[i].table; // Add connection ID if available
this.XAxis = ChartObject[i].xAxis; this.ConnectionId = ChartObject[i].connection;
this.YAxis = ChartObject[i].yAxis; console.log('GridRunner: TableName:', this.TableName);
console.log(this.TableName); console.log('GridRunner: XAxis:', this.XAxis);
this.Dashtestservive.getChartData(this.TableName,"Grid View",this.XAxis,this.YAxis).subscribe((Ldata) => { console.log('GridRunner: YAxis:', this.YAxis);
console.log(Ldata); console.log('GridRunner: ConnectionId:', this.ConnectionId);
this.rows = Ldata; // Fetch data with filters
this.rowdata = this.rows this.fetchGridData();
break; // No need to continue the loop once the correct placeholder is found
},(error) => {
console.log(error);
});
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
//dynamic table 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];
getTableData(id){ // Find the filter definition to get the field name
} const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
getHeaders() {
let headers: string[] = []; if (filterDef && filterDef.field) {
if(this.rows) { const fieldName = filterDef.field;
this.rows.forEach((value) => { if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
Object.keys(value).forEach((key) => { filterObj[fieldName] = filterValue;
if(!headers.find((header) => header == key)){ }
headers.push(key) }
});
// 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) => {
return headers; console.log('GridRunner: Received data from API:', Ldata);
}
generatePDFFile(){ // Handle the actual data structure returned by the API
this.buttonClicked.emit(); if (Ldata && Ldata.chartData) {
const content = this.contentContainerRef.nativeElement; this.rows = Ldata.chartData;
const filename = 'gridview.pdf'; // You can provide any desired filename here 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 = [];
}
this.Dashtestservive.generatePDF(content, filename); // 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
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;
}
generatePDFFile() {
this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement;
const filename = 'gridview.pdf'; // You can provide any desired filename here
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');
}
}

View File

@@ -7,6 +7,10 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
import { jsPDF } from 'jspdf'; import { jsPDF } from 'jspdf';
import domtoimage from 'dom-to-image'; 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({ @Component({
selector: 'app-line-runner', selector: 'app-line-runner',
templateUrl: './line-runner.component.html', templateUrl: './line-runner.component.html',
@@ -54,8 +58,14 @@ export class LineRunnerComponent implements OnInit {
lineChartLegend = false; lineChartLegend = false;
lineChartPlugins = []; lineChartPlugins = [];
lineChartType = 'line'; 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, 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 { ngOnInit(): void {
@@ -65,6 +75,13 @@ export class LineRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); 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)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -92,16 +109,10 @@ export class LineRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.lineChartLegend = ChartObject[i].chartlegend; this.lineChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Line Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.lineChartData = this.JsonData.chartData;
this.lineChartLabels = this.JsonData.chartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found 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(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -165,5 +222,18 @@ export class LineRunnerComponent implements OnInit {
// console.error('Error generating PDF:', error); // console.error('Error generating PDF:', error);
// } // }
// } // }
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');
}
} }

View File

@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; 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({ @Component({
@@ -23,9 +27,15 @@ export class PieRunnerComponent implements OnInit {
showlabel; showlabel;
JsonData; JsonData;
lineChartNoLabels: any[] = []; 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, 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 pieChartLabels: string[] = ['SciFi', 'Drama', 'Comedy'];
public pieChartData: number[] = [30, 50, 20]; public pieChartData: number[] = [30, 50, 20];
@@ -39,6 +49,13 @@ export class PieRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); 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)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -66,22 +83,62 @@ export class PieRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Pie Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.pieChartData = this.JsonData.pieChartData;
this.pieChartLabels = this.JsonData.pieChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found 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(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -89,4 +146,17 @@ export class PieRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); 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');
}
} }

View File

@@ -4,6 +4,10 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// import { Label } from 'ng2-charts'; // import { Label } from 'ng2-charts';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-polar-runner', selector: 'app-polar-runner',
@@ -23,9 +27,15 @@ export class PolarRunnerComponent implements OnInit {
showlabel; showlabel;
JsonData; JsonData;
lineChartNoLabels: any[] = []; 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, 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 polarAreaChartLabels: string[] = [ 'Download Sales', 'In-Store Sales', 'Mail Sales', 'Telesales', 'Corporate Sales' ];
public polarAreaChartData: any = [ public polarAreaChartData: any = [
@@ -41,6 +51,13 @@ export class PolarRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); 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)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -66,22 +83,62 @@ export class PolarRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"PolarArea Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.polarAreaChartData = this.JsonData.polarAreaChartData;
this.polarAreaChartLabels = this.JsonData.polarAreaChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found 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(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -89,5 +146,18 @@ export class PolarRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); 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');
}
} }

View File

@@ -4,6 +4,10 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// import { Label } from 'ng2-charts'; // import { Label } from 'ng2-charts';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-radar-runner', selector: 'app-radar-runner',
@@ -24,9 +28,15 @@ export class RadarRunnerComponent implements OnInit {
JsonData; JsonData;
lineChartNoLabels: any[] = []; lineChartNoLabels: any[] = [];
ChartLegend = false; 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, 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[] = [ public radarChartLabels: string[] = [
"Eating", "Eating",
@@ -50,6 +60,13 @@ export class RadarRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); 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)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -75,22 +92,62 @@ export class RadarRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Radar Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.radarChartData = this.JsonData.radarChartData;
this.radarChartLabels = this.JsonData.radarChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found 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(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -98,5 +155,18 @@ export class RadarRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); 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');
}
} }

View File

@@ -5,6 +5,10 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// import { Label } from 'ng2-charts'; // import { Label } from 'ng2-charts';
import { ChartDataset } from 'chart.js'; import { ChartDataset } from 'chart.js';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-scatter-runner', selector: 'app-scatter-runner',
@@ -25,9 +29,15 @@ export class ScatterRunnerComponent implements OnInit {
JsonData; JsonData;
lineChartNoLabels: any[] = []; lineChartNoLabels: any[] = [];
ChartLegend = false; 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, 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' ]; 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; this.editId = this.route.snapshot.params.id;
console.log(this.editId); 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)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -94,22 +111,62 @@ export class ScatterRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Scatter Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.scatterChartData = this.JsonData.scatterChartData;
this.scatterChartLabels = this.JsonData.scatterChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found 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(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -117,5 +174,18 @@ export class ScatterRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); 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');
}
} }

View File

@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.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({ @Component({
selector: 'app-todo-runner', selector: 'app-todo-runner',
@@ -12,19 +16,21 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
export class TodoRunnerComponent implements OnInit { export class TodoRunnerComponent implements OnInit {
@ViewChild('contentContainer') contentContainerRef!: ElementRef; @ViewChild('contentContainer') contentContainerRef!: ElementRef;
@Output() buttonClicked = new EventEmitter<void>(); @Output() buttonClicked = new EventEmitter<void>();
constructor( private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router) { }
loading = false;
givendata;
error;
XAxis;
YAxis;
editId; // Add subscriptions to unsubscribe on destroy
public DashtestboardArray: DashboardContentModel[] = []; private subscriptions: Subscription[] = [];
workflowLine;
TableName; loading = false;
givendata;
error;
XAxis;
YAxis;
editId;
public DashtestboardArray: DashboardContentModel[] = [];
workflowLine;
TableName;
ConnectionId: number; // Add ConnectionId property
list; list;
data: any; data: any;
@@ -34,11 +40,25 @@ export class TodoRunnerComponent implements OnInit {
listName: "title123", listName: "title123",
List:['todo 1','todo 2'], 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 { ngOnInit(): void {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// this.getbyId(); // 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)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
this.workflowLine = data.dashbord1_Line[0].model; this.workflowLine = data.dashbord1_Line[0].model;
@@ -63,15 +83,10 @@ export class TodoRunnerComponent implements OnInit {
this.TableName = ChartObject[i].table; this.TableName = ChartObject[i].table;
this.XAxis = ChartObject[i].xAxis; this.XAxis = ChartObject[i].xAxis;
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Todo List",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchTodoData();
this.todoList.listName = Ldata.listName;
this.todoList.List = Ldata.List;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
@@ -100,4 +115,58 @@ generatePDFFile(){
this.Dashtestservive.generatePDF(content, filename); 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');
}
}