31 Commits

Author SHA1 Message Date
Gaurav Kumar
3e70f85644 dashboard 2025-10-31 17:19:18 +05:30
Gaurav Kumar
482805b5cf Update editnewdash.component.ts 2025-10-31 17:17:01 +05:30
Gaurav Kumar
ffda17e6b1 unified 2025-10-31 16:35:15 +05:30
Gaurav Kumar
7396843bc6 Update report-build2all.component.html 2025-10-31 16:03:37 +05:30
Gaurav Kumar
4f75ecb3e0 builder 2025-10-31 15:51:28 +05:30
Gaurav Kumar
7c1a487114 dashboard 2025-10-31 10:47:43 +05:30
Gaurav Kumar
47e9fb92e3 Update compact-filter.component.ts 2025-10-30 12:46:39 +05:30
Gaurav Kumar
7f735dcada Update editnewdash.component.ts 2025-10-30 12:29:22 +05:30
Gaurav Kumar
bd315f42a3 compact 2025-10-29 17:55:05 +05:30
Gaurav Kumar
e8c1f46430 Update editnewdash.component.html 2025-10-29 17:30:13 +05:30
Gaurav Kumar
fa96ca81bd runner 2025-10-29 17:18:26 +05:30
Gaurav Kumar
c384f44c0c doughnut 2025-10-29 17:01:50 +05:30
Gaurav Kumar
50df914ca9 pie 2025-10-29 16:52:45 +05:30
Gaurav Kumar
e0bd888c45 polr 2025-10-29 16:14:45 +05:30
Gaurav Kumar
02b82fcaf8 Update polar-chart.component.ts 2025-10-29 16:07:59 +05:30
Gaurav Kumar
557afc348f bubble 2025-10-29 15:55:01 +05:30
Gaurav Kumar
1dec787062 Update bubble-chart.component.ts 2025-10-29 15:50:08 +05:30
Gaurav Kumar
8853cf75cf Update bubble-chart.component.ts 2025-10-29 15:42:21 +05:30
Gaurav Kumar
ad57f11f8a bubble 2025-10-29 15:34:10 +05:30
Gaurav Kumar
9b775a8c63 fin 2025-10-29 12:59:20 +05:30
Gaurav Kumar
cf4fc1be93 todo 2025-10-29 12:21:24 +05:30
Gaurav Kumar
4c135c4901 to do 2025-10-29 12:16:54 +05:30
Gaurav Kumar
0b738ca7ca scatter 2025-10-29 11:35:09 +05:30
Gaurav Kumar
1b17bb706d scatter 2025-10-29 11:11:36 +05:30
Gaurav Kumar
f740076d60 bar chart 2025-10-28 17:04:02 +05:30
Gaurav Kumar
bedcc0822d chart 2025-10-28 16:31:45 +05:30
Gaurav Kumar
87810acc9e line 2025-10-28 15:41:36 +05:30
Gaurav Kumar
96b90e5dbd barchart 2025-10-28 15:32:37 +05:30
Gaurav Kumar
ced99e0940 grid view 2025-10-28 15:11:40 +05:30
Gaurav Kumar
4f82ae8698 grid view 2025-10-28 12:40:01 +05:30
Gaurav Kumar
e6779e8f34 filter 2025-10-28 12:29:10 +05:30
61 changed files with 16150 additions and 1923 deletions

View File

@@ -4,5 +4,6 @@ public report_name:string;
public description: string; public description: string;
public report_tags: string; public report_tags: string;
public servicename: string; public servicename: string;
// Add SureConnect reference
public sureConnectId: number | null;
} }

View File

@@ -23,8 +23,22 @@ export interface DashboardContentModel {
component?: any; component?: any;
name: string; name: string;
type?:string; type?:string;
// Common properties // Chart properties
xAxis?: string;
yAxis?: string | string[];
chartType?: string;
charttitle?: string;
chartlegend?: boolean;
showlabel?: boolean;
chartcolor?: boolean;
slices?: boolean;
donut?: boolean;
charturl?: string;
chartparameter?: string;
datastore?: string;
table?: string; table?: string;
datasource?: string;
fieldName?: string;
connection?: string; connection?: string;
baseFilters?: any[]; baseFilters?: any[];
// Common filter properties // Common filter properties
@@ -37,6 +51,11 @@ export interface DashboardContentModel {
drilldownParameter?: string; drilldownParameter?: string;
drilldownFilters?: any[]; drilldownFilters?: any[];
drilldownLayers?: any[]; drilldownLayers?: any[];
// Compact filter properties
filterKey?: string;
filterType?: string;
filterLabel?: string;
filterOptions?: string[];
} }
export interface DashboardModel { export interface DashboardModel {

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>gaurav</title>
</head>
<body>
<h1>this is h1</h1>
<h2>this is h1</h2>
<h3>this is h1</h3>
<h4>this is h1</h4>
<p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsa fuga, asperiores mollitia iste vitae repellendus adipisci atque eum corrupti ad placeat unde voluptatum quia perferendis neque expedita, sequi iure quo. Ut error adipisci ex cum sint, suscipit, voluptatem repellat nemo dolorum unde dolores quasi aut. A earum quo mollitia voluptatibus!</p>
</body>
</html>

View File

@@ -79,7 +79,12 @@
<!-- Multi-Select Filter --> <!-- Multi-Select Filter -->
<div class="filter-control" *ngIf="filterType === 'multiselect'"> <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 class="compact-multiselect-display" (click)="toggleMultiselectDropdown()" style="padding: 5px; border: 1px solid #ddd; cursor: pointer; background-color: #f8f8f8;">
<span *ngIf="filterValue && filterValue.length > 0">{{ filterValue.length }} selected</span>
<span *ngIf="!filterValue || filterValue.length === 0">{{ filterLabel || filterKey || 'Select options' }}</span>
<clr-icon shape="caret down" style="float: right; margin-top: 3px;"></clr-icon>
</div>
<div class="compact-multiselect-dropdown" *ngIf="showMultiselectDropdown" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; border-top: none; padding: 10px; background-color: white; position: absolute; z-index: 1000; width: calc(100% - 2px); box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<div *ngFor="let option of filterOptions" class="clr-checkbox-wrapper" style="margin-bottom: 5px;"> <div *ngFor="let option of filterOptions" class="clr-checkbox-wrapper" style="margin-bottom: 5px;">
<input type="checkbox" <input type="checkbox"
[id]="'multiselect-' + option" [id]="'multiselect-' + option"

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnDestroy } from '@angular/core';
import { FilterService, Filter } from './filter.service'; import { FilterService, Filter } from './filter.service';
import { AlertsService } from 'src/app/services/fnd/alerts.service'; import { AlertsService } from 'src/app/services/fnd/alerts.service';
@@ -7,7 +7,7 @@ import { AlertsService } from 'src/app/services/fnd/alerts.service';
templateUrl: './compact-filter.component.html', templateUrl: './compact-filter.component.html',
styleUrls: ['./compact-filter.component.scss'] styleUrls: ['./compact-filter.component.scss']
}) })
export class CompactFilterComponent implements OnInit, OnChanges { export class CompactFilterComponent implements OnInit, OnChanges, OnDestroy {
@Input() filterKey: string = ''; @Input() filterKey: string = '';
@Input() filterType: string = 'text'; @Input() filterType: string = 'text';
@Input() filterOptions: string[] = []; @Input() filterOptions: string[] = [];
@@ -23,6 +23,9 @@ export class CompactFilterComponent implements OnInit, OnChanges {
availableKeys: string[] = []; availableKeys: string[] = [];
availableValues: string[] = []; availableValues: string[] = [];
// Multiselect dropdown state
showMultiselectDropdown: boolean = false;
// Configuration properties // Configuration properties
isConfigMode: boolean = false; isConfigMode: boolean = false;
configFilterKey: string = ''; configFilterKey: string = '';
@@ -73,6 +76,24 @@ export class CompactFilterComponent implements OnInit, OnChanges {
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
// If filterKey changes, clear the previous filter value and remove old filter from service
if (changes.filterKey) {
// Clear the previous filter value
this.filterValue = '';
// Clear filter options
this.filterOptions = [];
// Clear available values
this.availableValues = [];
// If we had a previous selected filter, clear its value in the service
if (this.selectedFilter && changes.filterKey.previousValue) {
const oldFilterId = changes.filterKey.previousValue;
this.filterService.updateFilterValue(oldFilterId, '');
}
}
// If filterKey or filterType changes, re-register the filter // If filterKey or filterType changes, re-register the filter
if (changes.filterKey || changes.filterType) { if (changes.filterKey || changes.filterType) {
// Load available values for the current filter key if it's a dropdown or multiselect // Load available values for the current filter key if it's a dropdown or multiselect
@@ -201,6 +222,14 @@ export class CompactFilterComponent implements OnInit, OnChanges {
this.onFilterValueChange(dateRange); this.onFilterValueChange(dateRange);
} }
ngOnDestroy(): void {
// Component cleanup - remove this filter from the filter service
if (this.selectedFilter) {
// Use the proper removeFilter method which handles both filter definition and state
this.filterService.removeFilter(this.selectedFilter.id);
}
}
// Load available keys from API // Load available keys from API
loadAvailableKeys(): void { loadAvailableKeys(): void {
if (this.apiUrl) { if (this.apiUrl) {
@@ -278,6 +307,9 @@ export class CompactFilterComponent implements OnInit, OnChanges {
this.apiUrl = config.apiUrl; this.apiUrl = config.apiUrl;
this.connectionId = config.connectionId; this.connectionId = config.connectionId;
// Clear filter value when changing configuration
this.filterValue = '';
// Load available keys if API URL is provided // Load available keys if API URL is provided
if (this.apiUrl) { if (this.apiUrl) {
this.loadAvailableKeys(); this.loadAvailableKeys();
@@ -304,11 +336,23 @@ export class CompactFilterComponent implements OnInit, OnChanges {
// Handle filter key change in configuration // Handle filter key change in configuration
onFilterKeyChange(key: string): void { onFilterKeyChange(key: string): void {
// Clear the previous filter value when changing keys
this.filterValue = '';
// Clear filter options until new values are loaded
this.filterOptions = [];
this.configFilterKey = key; this.configFilterKey = key;
// Load available values for the selected key if it's a dropdown or multiselect // Load available values for the selected key if it's a dropdown or multiselect
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && key) { if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && key) {
this.loadAvailableValues(key); this.loadAvailableValues(key);
} }
// Clear the filter service value for the previous filter key
if (this.selectedFilter) {
this.filterService.updateFilterValue(this.selectedFilter.id, '');
}
} }
// Handle API URL change in configuration // Handle API URL change in configuration
@@ -375,4 +419,23 @@ export class CompactFilterComponent implements OnInit, OnChanges {
// Emit the change event // Emit the change event
this.onFilterValueChange(this.filterValue); this.onFilterValueChange(this.filterValue);
} }
// Add method to toggle multiselect dropdown visibility
toggleMultiselectDropdown(): void {
this.showMultiselectDropdown = !this.showMultiselectDropdown;
// Add document click handler to close dropdown when clicking outside
if (this.showMultiselectDropdown) {
setTimeout(() => {
const handleClick = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest('.compact-multiselect-display') && !target.closest('.compact-multiselect-dropdown')) {
this.showMultiselectDropdown = false;
document.removeEventListener('click', handleClick);
}
};
document.addEventListener('click', handleClick);
}, 0);
}
}
} }

View File

@@ -7,8 +7,8 @@
</ol> --> </ol> -->
<div style="display: inline;"> <div style="display: inline;">
<button class="btn componentbtn" (click)="toggleMenu()"><clr-icon shape="plus"></clr-icon>component</button> <button class="btn componentbtn" (click)="toggleMenu()" *ngIf="!fromRunner"><clr-icon shape="plus"></clr-icon>component</button>
<button class="btn btn-primary" (click)="openCommonFilterModal()" style="margin-left: 10px;"> <button class="btn btn-primary" (click)="openCommonFilterModal()" style="margin-left: 10px;" *ngIf="!fromRunner">
<clr-icon shape="filter"></clr-icon> Common Filter <clr-icon shape="filter"></clr-icon> Common Filter
</button> </button>
<div style="display: inline;"> <div style="display: inline;">
@@ -22,7 +22,7 @@
</div> </div>
<div class="content-container"> <div class="content-container">
<nav class="sidenav" *ngIf="toggle" style="width: 16%;"> <nav class="sidenav" *ngIf="toggle && !fromRunner" style="width: 16%;">
<ul class="nav-list" style="list-style-type: none;"> <ul class="nav-list" style="list-style-type: none;">
<li *ngFor="let widget of WidgetsMock"> <li *ngFor="let widget of WidgetsMock">
@@ -42,14 +42,14 @@
<gridster [options]="options" (drop)="onDrop($event)" style="background-color: transparent;"> <gridster [options]="options" (drop)="onDrop($event)" style="background-color: transparent;">
<gridster-item [item]="item" *ngFor="let item of dashboardArray"> <gridster-item [item]="item" *ngFor="let item of dashboardArray">
<!-- <ng-container *ngIf="addToDashboard && item.addToDashboard"> --> <!-- <ng-container *ngIf="addToDashboard && item.addToDashboard"> -->
<button class="btn btn-icon btn-danger" style="margin-left: 10px; margin-top: 10px;" (click)="removeItem(item)"> <button class="btn btn-icon btn-danger" style="margin-left: 10px; margin-top: 10px;" (click)="removeItem(item)" *ngIf="!fromRunner">
<clr-icon shape="trash"></clr-icon> <clr-icon shape="trash"></clr-icon>
</button> </button>
<button class="btn btn-icon drag-handler" style="margin-left: 10px; margin-top: 10px;"> <button class="btn btn-icon drag-handler" style="margin-left: 10px; margin-top: 10px;" *ngIf="!fromRunner">
<clr-icon shape="drag-handle"></clr-icon> <clr-icon shape="drag-handle"></clr-icon>
</button> </button>
<button class="btn btn-icon" style="margin-top: 10px; float: right;"> <button class="btn btn-icon" style="margin-top: 10px; float: right;" *ngIf="!fromRunner">
<input type="checkbox" clrToggle [(ngModel)]="item.addToDashboard" name="addToDashboardSwitch" <input type="checkbox" clrToggle [(ngModel)]="item.addToDashboard" name="addToDashboardSwitch"
(change)="toggleAddToDashboard(item)" /> (change)="toggleAddToDashboard(item)" />
</button> </button>
@@ -57,7 +57,7 @@
<!-- <label for="workflow_name">Add to Dasboard</label> <!-- <label for="workflow_name">Add to Dasboard</label>
<input class="btn btn-icon" style="margin-top: 10px;float: right;" type="checkbox" clrToggle value="billable" name="billable" /> <input class="btn btn-icon" style="margin-top: 10px;float: right;" type="checkbox" clrToggle value="billable" name="billable" />
--> -->
<button class="btn btn-icon" style="margin-top: 10px;float: right;" (click)="editGadget(item)"> <button class="btn btn-icon" style="margin-top: 10px;float: right;" (click)="editGadget(item)" *ngIf="!fromRunner">
<clr-icon shape="pencil"></clr-icon> <clr-icon shape="pencil"></clr-icon>
</button> </button>
@@ -72,7 +72,7 @@
</div> </div>
<div style="text-align: center;"> <div style="text-align: center;">
<button class="btn btn-outline" (click)="goBack()">Back</button> <button class="btn btn-outline" (click)="goBack()">Back</button>
<button type="submit" class="btn btn-primary btn-adddata " (click)="UpdateLine()"> <button type="submit" class="btn btn-primary btn-adddata " (click)="UpdateLine()" *ngIf="!fromRunner">
<b>Update</b> <b>Update</b>
</button> </button>
</div> </div>
@@ -335,8 +335,8 @@
</div> </div>
<div class="clr-row" style="margin-top: 8px;"> <div class="clr-row" style="margin-top: 8px;">
<div class="clr-col-sm-5"> <div class="clr-col-sm-4">
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select" <select [(ngModel)]="filter.field" (ngModelChange)="onBaseFilterFieldChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="gadgetsEditdata.commonFilterEnabled"> [disabled]="gadgetsEditdata.commonFilterEnabled">
<option value="">Select Field</option> <option value="">Select Field</option>
<!-- Base API filters should always use columnData, not drilldownColumnData --> <!-- Base API filters should always use columnData, not drilldownColumnData -->
@@ -345,7 +345,26 @@
</select> </select>
</div> </div>
<div class="clr-col-sm-5"> <div class="clr-col-sm-3">
<select [(ngModel)]="filter.type" (ngModelChange)="onBaseFilterTypeChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="gadgetsEditdata.commonFilterEnabled">
<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>
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Option1,Option2,Option3" [disabled]="gadgetsEditdata.commonFilterEnabled" />
<div class="clr-subtext" *ngIf="filter.availableValues">
Available: {{ filter.availableValues }}
</div>
</div>
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input" <input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabled" /> placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabled" />
</div> </div>
@@ -476,8 +495,8 @@
</div> </div>
<div class="clr-row" style="margin-top: 8px;"> <div class="clr-row" style="margin-top: 8px;">
<div class="clr-col-sm-5"> <div class="clr-col-sm-4">
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select" <select [(ngModel)]="filter.field" (ngModelChange)="onDrilldownFilterFieldChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown"> [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
<option value="">Select Field</option> <option value="">Select Field</option>
<option <option
@@ -486,7 +505,26 @@
</select> </select>
</div> </div>
<div class="clr-col-sm-5"> <div class="clr-col-sm-3">
<select [(ngModel)]="filter.type" (ngModelChange)="onDrilldownFilterTypeChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
<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>
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Option1,Option2,Option3" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
<div class="clr-subtext" *ngIf="filter.availableValues">
Available: {{ filter.availableValues }}
</div>
</div>
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input" <input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" /> placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
</div> </div>
@@ -629,8 +667,8 @@
</div> </div>
<div class="clr-row" style="margin-top: 8px;"> <div class="clr-row" style="margin-top: 8px;">
<div class="clr-col-sm-5"> <div class="clr-col-sm-4">
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select" <select [(ngModel)]="filter.field" (ngModelChange)="onLayerFilterFieldChange(i, j, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="layer.commonFilterEnabled"> [disabled]="layer.commonFilterEnabled">
<option value="">Select Field</option> <option value="">Select Field</option>
<option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])" <option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])"
@@ -638,9 +676,28 @@
</select> </select>
</div> </div>
<div class="clr-col-sm-5"> <div class="clr-col-sm-3">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" <select [(ngModel)]="filter.type" (ngModelChange)="onLayerFilterTypeChange(i, j, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
class="clr-input" placeholder="Filter Value" [disabled]="layer.commonFilterEnabled" /> [disabled]="layer.commonFilterEnabled">
<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>
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Option1,Option2,Option3" [disabled]="layer.commonFilterEnabled" />
<div class="clr-subtext" *ngIf="filter.availableValues">
Available: {{ filter.availableValues }}
</div>
</div>
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Filter Value" [disabled]="layer.commonFilterEnabled" />
</div> </div>
<div class="clr-col-sm-2"> <div class="clr-col-sm-2">

View File

@@ -26,6 +26,10 @@ import { SureconnectService } from '../sureconnect/sureconnect.service';
import { CommonFilterComponent } from '../common-filter/common-filter.component'; import { CommonFilterComponent } from '../common-filter/common-filter.component';
// Add the CompactFilterComponent import // Add the CompactFilterComponent import
import { CompactFilterComponent } from '../common-filter'; import { CompactFilterComponent } from '../common-filter';
// Add the FilterService import
import { FilterService } from '../common-filter/filter.service';
// Add the UnifiedChartComponent import
import { UnifiedChartComponent } from '../gadgets/unified-chart';
function isNullArray(arr) { function isNullArray(arr) {
return !Array.isArray(arr) || arr.length === 0; return !Array.isArray(arr) || arr.length === 0;
@@ -91,14 +95,14 @@ export class EditnewdashComponent implements OnInit {
name: 'Scatter Chart', name: 'Scatter Chart',
identifier: 'scatter_chart' identifier: 'scatter_chart'
}, },
// { {
// name: 'Dynamic Chart', name: 'Dynamic Chart',
// identifier: 'dynamic_chart' identifier: 'dynamic_chart'
// }, },
// { {
// name: 'Financial Chart', name: 'Financial Chart',
// identifier: 'financial_chart' identifier: 'financial_chart'
// }, },
{ {
name: 'To Do', name: 'To Do',
identifier: 'to_do_chart' identifier: 'to_do_chart'
@@ -110,6 +114,10 @@ export class EditnewdashComponent implements OnInit {
{ {
name: 'Compact Filter', name: 'Compact Filter',
identifier: 'compact_filter' identifier: 'compact_filter'
},
{
name: 'Unified Chart',
identifier: 'unified_chart'
} }
] ]
@@ -135,7 +143,8 @@ 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 { name: "Compact Filter", componentInstance: CompactFilterComponent },
{ name: "Unified Chart", componentInstance: UnifiedChartComponent },
]; ];
model: any; model: any;
linesdata: any; linesdata: any;
@@ -168,6 +177,7 @@ export class EditnewdashComponent implements OnInit {
yAxis: '', yAxis: '',
xAxis: '', xAxis: '',
connection: '', // Add connection field connection: '', // Add connection field
chartType: '', // Add chartType field
// Drilldown configuration properties (base level) // Drilldown configuration properties (base level)
drilldownEnabled: false, drilldownEnabled: false,
drilldownApiUrl: '', drilldownApiUrl: '',
@@ -203,9 +213,15 @@ export class EditnewdashComponent implements OnInit {
private _fb: FormBuilder, private _fb: FormBuilder,
private datastoreService: DatastoreService, private datastoreService: DatastoreService,
private alertService: AlertsService, private alertService: AlertsService,
private sureconnectService: SureconnectService) { } // Add SureconnectService to constructor private sureconnectService: SureconnectService,
private filterService: FilterService) { } // Add SureconnectService and FilterService to constructor
// Add property to track if coming from dashboard runner
fromRunner: boolean = false;
ngOnInit(): void { ngOnInit(): void {
// Reset the filter service when the component is initialized
this.filterService.resetFilters();
// Grid options // Grid options
this.options = { this.options = {
@@ -231,6 +247,13 @@ export class EditnewdashComponent implements OnInit {
itemResizeCallback: this.itemResize.bind(this) itemResizeCallback: this.itemResize.bind(this)
}; };
// Check if coming from dashboard runner
this.route.queryParams.subscribe(params => {
if (params['fromRunner'] === 'true') {
this.fromRunner = true;
}
});
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
this.dashboardService.getById(this.editId).subscribe((data) => { this.dashboardService.getById(this.editId).subscribe((data) => {
@@ -317,6 +340,9 @@ export class EditnewdashComponent implements OnInit {
dashboardLine: any; dashboardLine: any;
dashboardName: any; dashboardName: any;
getData() { getData() {
// Reset the filter service when switching between dashboard records
this.filterService.resetFilters();
// We get the id in get current router dashboard/:id // We get the id in get current router dashboard/:id
this.route.params.subscribe(params => { this.route.params.subscribe(params => {
// + is used to cast string to int // + is used to cast string to int
@@ -435,7 +461,17 @@ export class EditnewdashComponent implements OnInit {
onDrop(ev) { onDrop(ev) {
const componentType = ev.dataTransfer.getData("widgetIdentifier"); const componentType = ev.dataTransfer.getData("widgetIdentifier");
let maxChartId = this.dashboardArray?.reduce((maxId, item) => Math.max(maxId, item.chartid), 0); // Safely calculate maxChartId, handling cases where chartid might be NaN or missing
let maxChartId = 0;
if (this.dashboardArray && this.dashboardArray.length > 0) {
const validChartIds = this.dashboardArray
.map(item => item.chartid)
.filter(chartid => typeof chartid === 'number' && !isNaN(chartid));
if (validChartIds.length > 0) {
maxChartId = Math.max(...validChartIds);
}
}
switch (componentType) { switch (componentType) {
case "radar_chart": case "radar_chart":
return this.dashboardArray.push({ return this.dashboardArray.push({
@@ -582,6 +618,22 @@ export class EditnewdashComponent implements OnInit {
component: GridViewComponent, component: GridViewComponent,
name: "Grid View" name: "Grid View"
}); });
case "unified_chart":
return this.dashboardArray.push({
cols: 5,
rows: 6,
x: 0,
y: 0,
chartid: maxChartId + 1,
component: UnifiedChartComponent,
name: "Unified Chart",
// Add default configuration for unified chart
chartType: 'bar',
xAxis: '',
yAxis: '',
table: '',
connection: undefined
});
} }
} }
removeItem(item) { removeItem(item) {
@@ -597,7 +649,14 @@ export class EditnewdashComponent implements OnInit {
} }
modelid: number; modelid: number;
// Update the editGadget method to initialize filter properties
editGadget(item) { editGadget(item) {
// If coming from dashboard runner, skip showing the config modal
if (this.fromRunner) {
console.log('Coming from dashboard runner, skipping config modal');
return;
}
this.modeledit = true; this.modeledit = true;
this.modelid = item.chartid; this.modelid = item.chartid;
console.log(this.modelid); console.log(this.modelid);
@@ -627,6 +686,10 @@ export class EditnewdashComponent implements OnInit {
if (item['filterOptions'] === undefined) { if (item['filterOptions'] === undefined) {
this.gadgetsEditdata['filterOptions'] = []; this.gadgetsEditdata['filterOptions'] = [];
} }
// Initialize chartType property if not present (for unified chart)
if (item['chartType'] === undefined) {
this.gadgetsEditdata['chartType'] = 'bar';
}
// Initialize filterOptionsString for compact filter // Initialize filterOptionsString for compact filter
if (item.name === 'Compact Filter') { if (item.name === 'Compact Filter') {
@@ -639,6 +702,65 @@ export class EditnewdashComponent implements OnInit {
this.filterOptionsString = ''; this.filterOptionsString = '';
} }
// Initialize base filters with type and options if not present
if (item['baseFilters'] === undefined) {
this.gadgetsEditdata['baseFilters'] = [];
} else {
// Ensure each base filter has type and options properties
this.gadgetsEditdata['baseFilters'] = this.gadgetsEditdata['baseFilters'].map(filter => ({
field: filter.field || '',
value: filter.value || '',
type: filter.type || 'text',
options: filter.options || '',
availableValues: filter.availableValues || ''
}));
}
// Initialize drilldown filters with type and options if not present
if (item['drilldownFilters'] === undefined) {
this.gadgetsEditdata['drilldownFilters'] = [];
} else {
// Ensure each drilldown filter has type and options properties
this.gadgetsEditdata['drilldownFilters'] = this.gadgetsEditdata['drilldownFilters'].map(filter => ({
field: filter.field || '',
value: filter.value || '',
type: filter.type || 'text',
options: filter.options || '',
availableValues: filter.availableValues || ''
}));
}
// Initialize drilldown layers with proper filter structure if not present
if (item['drilldownLayers'] === undefined) {
this.gadgetsEditdata['drilldownLayers'] = [];
} else {
// Ensure each layer has proper filter structure
this.gadgetsEditdata['drilldownLayers'] = this.gadgetsEditdata['drilldownLayers'].map(layer => {
// Initialize parameter if not present
if (layer['parameter'] === undefined) {
layer['parameter'] = '';
}
// Initialize filters if not present
if (layer['filters'] === undefined) {
layer['filters'] = [];
} else {
// Ensure each layer filter has type and options properties
layer['filters'] = layer['filters'].map(filter => ({
field: filter.field || '',
value: filter.value || '',
type: filter.type || 'text',
options: filter.options || '',
availableValues: filter.availableValues || ''
}));
}
// Initialize common filter property for layer if not present
if (layer['commonFilterEnabled'] === undefined) {
layer['commonFilterEnabled'] = false;
}
return layer;
});
}
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
@@ -666,37 +788,6 @@ export class EditnewdashComponent implements OnInit {
this.gadgetsEditdata['drilldownParameter'] = ''; this.gadgetsEditdata['drilldownParameter'] = '';
} }
// Initialize base filters if not present
if (item['baseFilters'] === undefined) {
this.gadgetsEditdata['baseFilters'] = [];
}
// Initialize drilldown filters if not present
if (item['drilldownFilters'] === undefined) {
this.gadgetsEditdata['drilldownFilters'] = [];
}
// Initialize drilldown layers if not present
if (item['drilldownLayers'] === undefined) {
this.gadgetsEditdata['drilldownLayers'] = [];
} else {
// Ensure each layer has proper structure (removed parameterKey, added parameter)
this.gadgetsEditdata['drilldownLayers'].forEach((layer, index) => {
// Initialize parameter if not present
if (layer['parameter'] === undefined) {
layer['parameter'] = '';
}
// Initialize filters if not present
if (layer['filters'] === undefined) {
layer['filters'] = [];
}
// Initialize common filter property for layer if not present
if (layer['commonFilterEnabled'] === undefined) {
layer['commonFilterEnabled'] = false;
}
});
}
// Reset drilldown column data // Reset drilldown column data
this.drilldownColumnData = []; this.drilldownColumnData = [];
@@ -705,15 +796,34 @@ export class EditnewdashComponent implements OnInit {
this.refreshBaseDrilldownColumns(); this.refreshBaseDrilldownColumns();
} }
if (item.datastore !== undefined || '' || null) { // Check if we have either datastore or table to fetch columns
if ((item.datastore !== undefined && item.datastore !== '' && item.datastore !== null) ||
(item.table !== undefined && item.table !== '' && item.table !== null)) {
const datastore = item.datastore; const datastore = item.datastore;
this.getTables(datastore);
const table = item.table; const table = item.table;
// Fetch tables if datastore is available
if (datastore) {
this.getTables(datastore);
}
// Fetch columns if table is available
if (table) {
this.getColumns(datastore, table); this.getColumns(datastore, table);
}
console.log(item.yAxis); console.log(item.yAxis);
// Set selectedyAxis regardless of whether it's an array or string
if (item.yAxis !== undefined && item.yAxis !== '' && item.yAxis !== null) {
if (isArray(item.yAxis)) { if (isArray(item.yAxis)) {
this.selectedyAxis = item.yAxis; this.selectedyAxis = item.yAxis;
} else {
// For single yAxis values, convert to array
this.selectedyAxis = [item.yAxis];
}
console.log(this.selectedyAxis); console.log(this.selectedyAxis);
} else {
this.selectedyAxis = [];
} }
} else { } else {
this.selectedyAxis = []; this.selectedyAxis = [];
@@ -784,14 +894,32 @@ export class EditnewdashComponent implements OnInit {
// } // }
} }
// Update the onSubmit method to properly save filter data
onSubmit(id) { onSubmit(id) {
console.log(id); console.log(id);
if (!isNullArray(this.selectedyAxis)) {
console.log("get y-axis array", this.selectedyAxis); // Check if ID is valid, including handling NaN
if (id === null || id === undefined || isNaN(id)) {
console.warn('Chart ID is null, undefined, or NaN, using modelid instead:', this.modelid);
id = this.modelid;
}
// Ensure we have a valid numeric ID
const numId = typeof id === 'number' ? id : parseInt(id, 10);
if (isNaN(numId)) {
console.error('Unable to determine valid chart ID, aborting onSubmit');
return;
}
// Handle both array and string yAxis values
if (this.selectedyAxis !== undefined && this.selectedyAxis !== null &&
((Array.isArray(this.selectedyAxis) && this.selectedyAxis.length > 0) ||
(typeof this.selectedyAxis === 'string' && this.selectedyAxis !== ''))) {
console.log("get y-axis", this.selectedyAxis);
this.entryForm.patchValue({ yAxis: this.selectedyAxis }); this.entryForm.patchValue({ yAxis: this.selectedyAxis });
} }
let formdata = this.entryForm.value; let formdata = this.entryForm.value;
let num = id; let num = numId;
console.log(this.entryForm.value); console.log(this.entryForm.value);
this.dashboardCollection.dashboard = this.dashboardCollection.dashboard.map(item => { this.dashboardCollection.dashboard = this.dashboardCollection.dashboard.map(item => {
if (item.chartid == num) { if (item.chartid == num) {
@@ -830,6 +958,11 @@ export class EditnewdashComponent implements OnInit {
xyz.connection = this.gadgetsEditdata.connection || undefined; xyz.connection = this.gadgetsEditdata.connection || undefined;
} }
// For unified chart, preserve chart configuration properties
if (item.name === 'Unified Chart') {
xyz.chartType = this.gadgetsEditdata.chartType || 'bar';
}
console.log(xyz); console.log(xyz);
return xyz; return xyz;
} }
@@ -913,6 +1046,48 @@ export class EditnewdashComponent implements OnInit {
return commonFilterInputs; return commonFilterInputs;
} }
// For UnifiedChartComponent, pass chart properties with chartType
if (item.name === 'Unified Chart') {
const unifiedChartInputs = {
chartType: item.chartType || 'bar',
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(unifiedChartInputs).forEach(key => {
if (unifiedChartInputs[key] === undefined) {
delete unifiedChartInputs[key];
}
});
return unifiedChartInputs;
}
// For GridViewComponent, pass chart properties with drilldown support // For GridViewComponent, pass chart properties with drilldown support
if (item.component && item.component.name === 'GridViewComponent') { if (item.component && item.component.name === 'GridViewComponent') {
const gridInputs = { const gridInputs = {
@@ -978,8 +1153,8 @@ export class EditnewdashComponent implements OnInit {
drilldownXAxis: item['drilldownXAxis'], drilldownXAxis: item['drilldownXAxis'],
drilldownYAxis: item['drilldownYAxis'], drilldownYAxis: item['drilldownYAxis'],
drilldownParameter: item['drilldownParameter'], // Add drilldown parameter drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
baseFilters: item['baseFilters'] || [], // Add base filters baseFilters: item['baseFilters'] || [], // Add base filters with type information
drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters with type information
// Multi-layer drilldown configurations // Multi-layer drilldown configurations
drilldownLayers: item['drilldownLayers'] || [] drilldownLayers: item['drilldownLayers'] || []
}; };
@@ -994,18 +1169,29 @@ export class EditnewdashComponent implements OnInit {
return chartInputs; return chartInputs;
} }
// Update the applyChanges method to properly save filter data
applyChanges(id) { applyChanges(id) {
console.log('Apply changes for chart ID:', id); console.log('Apply changes for chart ID:', id);
// Check if ID is valid // Check if ID is valid, including handling NaN
if (id === null || id === undefined) { if (id === null || id === undefined || isNaN(id)) {
console.warn('Chart ID is null or undefined, using modelid instead:', this.modelid); console.warn('Chart ID is null, undefined, or NaN, using modelid instead:', this.modelid);
id = this.modelid; id = this.modelid;
} }
// Update the form with selected Y-axis values if it's an array // Ensure we have a valid numeric ID
if (!isNullArray(this.selectedyAxis)) { const numId = typeof id === 'number' ? id : parseInt(id, 10);
console.log("get y-axis array", this.selectedyAxis); if (isNaN(numId)) {
console.error('Unable to determine valid chart ID, aborting applyChanges');
return;
}
// Update the form with selected Y-axis values
// Handle both array and string yAxis values
if (this.selectedyAxis !== undefined && this.selectedyAxis !== null &&
((Array.isArray(this.selectedyAxis) && this.selectedyAxis.length > 0) ||
(typeof this.selectedyAxis === 'string' && this.selectedyAxis !== ''))) {
console.log("get y-axis", this.selectedyAxis);
this.entryForm.patchValue({ yAxis: this.selectedyAxis }); this.entryForm.patchValue({ yAxis: this.selectedyAxis });
} }
@@ -1096,6 +1282,9 @@ export class EditnewdashComponent implements OnInit {
// Note: We don't close the modal here, allowing the user to make additional changes // Note: We don't close the modal here, allowing the user to make additional changes
// The user can click "Save" when they're done with all changes // The user can click "Save" when they're done with all changes
// Reset the filter service to ensure clean state
this.filterService.resetFilters();
} }
goBack() { goBack() {
@@ -1293,46 +1482,239 @@ export class EditnewdashComponent implements OnInit {
// We're now using removeBaseFilter and removeLayerFilter methods instead // We're now using removeBaseFilter and removeLayerFilter methods instead
} }
// Add method to add a base filter // Add method to handle base filter field change
onBaseFilterFieldChange(index: number, field: string) {
const filter = this.gadgetsEditdata.baseFilters[index];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and table URL, load available values
if (field && this.gadgetsEditdata.table) {
this.loadFilterValuesForField(
this.gadgetsEditdata.table,
this.gadgetsEditdata.connection,
field,
index,
'base'
);
}
}
}
// Add method to handle base filter type change
onBaseFilterTypeChange(index: number, type: string) {
const filter = this.gadgetsEditdata.baseFilters[index];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.gadgetsEditdata.table) {
this.loadFilterValuesForField(
this.gadgetsEditdata.table,
this.gadgetsEditdata.connection,
filter.field,
index,
'base'
);
}
}
}
// Add method to handle drilldown filter field change
onDrilldownFilterFieldChange(index: number, field: string) {
const filter = this.gadgetsEditdata.drilldownFilters[index];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and drilldown API URL, load available values
if (field && this.gadgetsEditdata.drilldownApiUrl) {
this.loadFilterValuesForField(
this.gadgetsEditdata.drilldownApiUrl,
this.gadgetsEditdata.connection,
field,
index,
'drilldown'
);
}
}
}
// Add method to handle drilldown filter type change
onDrilldownFilterTypeChange(index: number, type: string) {
const filter = this.gadgetsEditdata.drilldownFilters[index];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.gadgetsEditdata.drilldownApiUrl) {
this.loadFilterValuesForField(
this.gadgetsEditdata.drilldownApiUrl,
this.gadgetsEditdata.connection,
filter.field,
index,
'drilldown'
);
}
}
}
// Add method to handle layer filter field change
onLayerFilterFieldChange(layerIndex: number, filterIndex: number, field: string) {
const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and layer API URL, load available values
if (field && layer.apiUrl) {
this.loadFilterValuesForField(
layer.apiUrl,
this.gadgetsEditdata.connection,
field,
filterIndex,
'layer',
layerIndex
);
}
}
}
}
// Add method to handle layer filter type change
onLayerFilterTypeChange(layerIndex: number, filterIndex: number, type: string) {
const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && layer.apiUrl) {
this.loadFilterValuesForField(
layer.apiUrl,
this.gadgetsEditdata.connection,
filter.field,
filterIndex,
'layer',
layerIndex
);
}
}
}
}
// Add method to load filter values for a specific field
loadFilterValuesForField(
apiUrl: string,
connectionId: string | undefined,
field: string,
filterIndex: number,
filterType: 'base' | 'drilldown' | 'layer',
layerIndex?: number
) {
if (apiUrl && field) {
const connectionIdNum = connectionId ? parseInt(connectionId, 10) : undefined;
this.alertService.getValuesFromUrl(apiUrl, connectionIdNum, field).subscribe(
(values: string[]) => {
// Update the filter with available values
if (filterType === 'base') {
const filter = this.gadgetsEditdata.baseFilters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
} else if (filterType === 'drilldown') {
const filter = this.gadgetsEditdata.drilldownFilters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
} else if (filterType === 'layer' && layerIndex !== undefined) {
const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
}
}
},
(error) => {
console.error('Error loading available values for field:', field, error);
}
);
}
}
// Add method to add a base filter with default properties
addBaseFilter() { addBaseFilter() {
const newFilter = { const newFilter = {
field: '', field: '',
value: '' value: '',
type: 'text',
options: '',
availableValues: ''
}; };
this.gadgetsEditdata.baseFilters.push(newFilter); this.gadgetsEditdata.baseFilters.push(newFilter);
} }
// Add method to add a drilldown filter with default properties
addDrilldownFilter() {
const newFilter = {
field: '',
value: '',
type: 'text',
options: '',
availableValues: ''
};
this.gadgetsEditdata.drilldownFilters.push(newFilter);
}
// Add method to add a layer filter with default properties
addLayerFilter(layerIndex: number) {
const newFilter = {
field: '',
value: '',
type: 'text',
options: '',
availableValues: ''
};
if (!this.gadgetsEditdata.drilldownLayers[layerIndex].filters) {
this.gadgetsEditdata.drilldownLayers[layerIndex].filters = [];
}
this.gadgetsEditdata.drilldownLayers[layerIndex].filters.push(newFilter);
}
// Add method to remove a base filter // Add method to remove a base filter
removeBaseFilter(index: number) { removeBaseFilter(index: number) {
this.gadgetsEditdata.baseFilters.splice(index, 1); this.gadgetsEditdata.baseFilters.splice(index, 1);
} }
// Add method to add a drilldown filter
addDrilldownFilter() {
const newFilter = {
field: '',
value: ''
};
this.gadgetsEditdata.drilldownFilters.push(newFilter);
}
// Add method to remove a drilldown filter // Add method to remove a drilldown filter
removeDrilldownFilter(index: number) { removeDrilldownFilter(index: number) {
this.gadgetsEditdata.drilldownFilters.splice(index, 1); this.gadgetsEditdata.drilldownFilters.splice(index, 1);
} }
// Add method to add a layer filter
addLayerFilter(layerIndex: number) {
const newFilter = {
field: '',
value: ''
};
if (!this.gadgetsEditdata.drilldownLayers[layerIndex].filters) {
this.gadgetsEditdata.drilldownLayers[layerIndex].filters = [];
}
this.gadgetsEditdata.drilldownLayers[layerIndex].filters.push(newFilter);
}
// Add method to remove a layer filter // Add method to remove a layer filter
removeLayerFilter(layerIndex: number, filterIndex: number) { removeLayerFilter(layerIndex: number, filterIndex: number) {
this.gadgetsEditdata.drilldownLayers[layerIndex].filters.splice(filterIndex, 1); this.gadgetsEditdata.drilldownLayers[layerIndex].filters.splice(filterIndex, 1);

View File

@@ -1,25 +1,295 @@
<div style="display: block; height: 100%; width: 100%;"> <div class="chart-container">
<!-- No filter controls needed with the new simplified approach --> <!-- Filter Controls Section -->
<!-- Filters are now configured at the drilldown level --> <div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Drilldown mode indicator --> <!-- Text Filter -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <input type="text"
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> [(ngModel)]="filter.value"
Back to Level {{currentDrilldownLevel - 1}} (ngModelChange)="onBaseFilterChange(filter)"
</button> [placeholder]="filter.field"
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> class="clr-input filter-text-input">
Back to Main View
</button>
</div> </div>
<!-- No data message --> <!-- Dropdown Filter -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;"> <div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="chart-header">
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Bar Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</div>
</div>
<!-- Show current drilldown level -->
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
<div class="clr-col-12">
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
<div class="alert-items">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
</div>
<span class="alert-text">
Drilldown Level: {{currentDrilldownLevel}}
<span *ngIf="drilldownStack.length > 0">
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="chart-wrapper">
<div class="chart-content" [class.loading]="isLoading">
<div *ngIf="noDataAvailable" class="no-data-message">
No data available No data available
</div> </div>
<!-- Chart display -->
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);"> <div *ngIf="!noDataAvailable" class="chart-display">
<canvas baseChart <canvas baseChart
[datasets]="barChartData" [datasets]="barChartData"
[labels]="barChartLabels" [labels]="barChartLabels"
@@ -29,4 +299,36 @@
(chartClick)="chartClicked($event)"> (chartClick)="chartClicked($event)">
</canvas> </canvas>
</div> </div>
<div class="loading-overlay" *ngIf="isLoading">
<div class="shimmer-bar"></div>
</div>
</div>
</div>
<!-- sheield dashboard -->
<!--
<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> -->
</div> </div>

View File

@@ -1,31 +1,278 @@
// Bar Chart Component Styles // Chart container structure
:host { .chart-container {
display: block;
height: 100%; height: 100%;
width: 100%; display: flex;
flex-direction: column;
// Filter section styling
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
} }
.bar-chart-container { .filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// Chart header styling
.chart-header {
margin-bottom: 20px;
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
}
// Chart wrapper and content
.chart-wrapper {
flex: 1;
position: relative;
.chart-content {
position: relative;
height: 100%;
min-height: 300px; // Ensure minimum height for chart
&.loading {
opacity: 0.7;
.chart-display {
filter: blur(2px);
}
}
.no-data-message {
text-align: center;
padding: 20px;
color: #666;
font-style: italic;
}
.chart-display {
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
}
canvas {
display: block;
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
transition: filter 0.3s ease;
} }
// Responsive design for chart container .loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.8);
.shimmer-bar {
width: 80%;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
}
}
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
// Responsive design
@media (max-width: 768px) { @media (max-width: 768px) {
.bar-chart-container { .chart-container {
height: 300px; .filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.chart-header {
.header-row {
.chart-title {
font-size: 16px;
}
} }
} }
@media (max-width: 480px) { .chart-content {
.bar-chart-container { min-height: 250px; // Adjust for mobile
height: 250px; }
} }
} }

View File

@@ -57,21 +57,46 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
ticks: { ticks: {
autoSkip: false, autoSkip: false,
maxRotation: 45, maxRotation: 45,
minRotation: 45 minRotation: 45,
padding: 15,
font: {
size: 12
}
},
grid: {
display: false
} }
}, },
y: { y: {
beginAtZero: true beginAtZero: true,
ticks: {
font: {
size: 12
}
}
} }
}, },
plugins: { plugins: {
legend: { legend: {
display: true, display: true,
position: 'top', position: 'top',
labels: {
font: {
size: 12
}
}
}, },
tooltip: { tooltip: {
enabled: true enabled: true
} }
},
layout: {
padding: {
bottom: 60,
left: 15,
right: 15,
top: 15
}
} }
}; };
@@ -84,12 +109,20 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// No data state // No data state
noDataAvailable: boolean = false; noDataAvailable: boolean = false;
// Loading state
isLoading: boolean = false;
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
// Subscriptions to unsubscribe on destroy // Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor( constructor(
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
private filterService: FilterService private filterService: FilterService
@@ -111,6 +144,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('BarChartComponent input changes:', changes); console.log('BarChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
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;
@@ -141,15 +180,329 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
} }
} }
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
fetchChartData(): void { fetchChartData(): void {
// Set loading state
this.isLoading = true;
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
// If we're in drilldown mode, fetch the appropriate drilldown data // If we're in drilldown mode, fetch the appropriate drilldown data
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) { if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
this.fetchDrilldownData(); this.fetchDrilldownData();
// Reset flag after fetching // Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
return; return;
} }
@@ -219,8 +572,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true; this.noDataAvailable = true;
this.barChartLabels = []; this.barChartLabels = [];
this.barChartData = []; this.barChartData = [];
// Reset flag after fetching // Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
return; return;
} }
@@ -249,8 +603,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.barChartLabels = []; this.barChartLabels = [];
this.barChartData = []; this.barChartData = [];
} }
// Reset flag after fetching // Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
}, },
(error) => { (error) => {
console.error('=== BAR CHART ERROR ==='); console.error('=== BAR CHART ERROR ===');
@@ -258,8 +613,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true; this.noDataAvailable = true;
this.barChartLabels = []; this.barChartLabels = [];
this.barChartData = []; this.barChartData = [];
// Reset flag after fetching // Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
// Keep default data in case of error // Keep default data in case of error
} }
); );
@@ -271,8 +627,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true; this.noDataAvailable = true;
this.barChartLabels = []; this.barChartLabels = [];
this.barChartData = []; this.barChartData = [];
// Reset flag after fetching // Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
} }
} }
@@ -434,17 +791,23 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Trigger change detection // Trigger change detection
// this.barChartData = [...this.barChartData]; // this.barChartData = [...this.barChartData];
console.log('Updated bar chart with drilldown data:', { labels: this.barChartLabels, data: this.barChartData }); console.log('Updated bar chart with drilldown data:', { labels: this.barChartLabels, data: this.barChartData });
// Set loading state to false
this.isLoading = false;
} else if (data && data.labels && data.datasets) { } else if (data && data.labels && data.datasets) {
// Backend has already filtered the data, just display it // Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.barChartLabels = data.labels; this.barChartLabels = data.labels;
this.barChartData = data.datasets; this.barChartData = data.datasets;
console.log('Updated bar chart with drilldown legacy data format:', { labels: this.barChartLabels, data: this.barChartData }); console.log('Updated bar chart with drilldown legacy data format:', { labels: this.barChartLabels, data: this.barChartData });
// Set loading state to false
this.isLoading = false;
} else { } else {
console.warn('Drilldown received data does not have expected structure', data); console.warn('Drilldown received data does not have expected structure', data);
this.noDataAvailable = true; this.noDataAvailable = true;
this.barChartLabels = []; this.barChartLabels = [];
this.barChartData = []; this.barChartData = [];
// Set loading state to false
this.isLoading = false;
} }
}, },
(error) => { (error) => {
@@ -452,12 +815,17 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true; this.noDataAvailable = true;
this.barChartLabels = []; this.barChartLabels = [];
this.barChartData = []; this.barChartData = [];
// Set loading state to false
this.isLoading = false;
// Keep current data in case of error // Keep current data in case of error
} }
); );
// Add subscription to array for cleanup // Add subscription to array for cleanup
this.subscriptions.push(subscription); this.subscriptions.push(subscription);
// Set loading state
this.isLoading = true;
} }
// Reset to original data (go back to base level) // Reset to original data (go back to base level)
@@ -661,6 +1029,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.originalBarChartLabels = []; this.originalBarChartLabels = [];
this.originalBarChartData = []; this.originalBarChartData = [];
// Clear multiselect tracking
this.openMultiselects.clear();
// Remove document click handler
this.removeDocumentClickHandler();
console.log('BarChartComponent destroyed and cleaned up'); console.log('BarChartComponent destroyed and cleaned up');
} }
} }

View File

@@ -1,28 +1,310 @@
<div style="display:block"> <div style="display:block; height: 100%; width: 100%;">
<!-- Drilldown mode indicator --> <!-- Filter Controls Section -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div class="filter-section" *ngIf="hasActiveFilters()">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <!-- Base Filters -->
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
Back to Level {{currentDrilldownLevel - 1}} <h4>Base Filters</h4>
</button> <div class="filter-controls">
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div *ngFor="let filter of baseFilters" class="filter-item">
Back to Main View <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Bubble Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button> </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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Chart container -->
<div style="position: relative; height: calc(100% - 80px); width: 100%; padding: 0 10px 30px 10px;">
<!-- Loading indicator -->
<div *ngIf="!dataLoaded" style="text-align: center; padding: 20px; color: #666; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; width: 100%;">
Loading data...
</div>
<!-- No data message --> <!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;"> <div *ngIf="dataLoaded && (noDataAvailable || !isChartDataValid())" style="text-align: center; padding: 20px; color: #666; font-style: italic; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; width: 100%;">
No data available No data available
</div> </div>
<!-- Chart display --> <!-- Chart display - Always render the canvas but conditionally show/hide with CSS -->
<div *ngIf="!noDataAvailable">
<canvas baseChart <canvas baseChart
[datasets]="bubbleChartData" [datasets]="bubbleChartData"
[type]="bubbleChartType" [type]="bubbleChartType"
[options]="bubbleChartOptions" [options]="bubbleChartOptions"
(chartHover)="chartHovered($event)" (chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)"> (chartClick)="chartClicked($event)"
[style.visibility]="dataLoaded && !noDataAvailable && isChartDataValid() ? 'visible' : 'hidden'"
[style.position]="'absolute'"
[style.top]="'0'"
[style.left]="'0'"
[style.height]="'100%'"
[style.width]="'100%'"
[style.padding]="'0 10px 20px 10px'">
</canvas> </canvas>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,192 @@
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
}

View File

@@ -1,39 +1,296 @@
<div class="doughnut-chart-container"> <div class="chart-container">
<!-- Compact Filters --> <!-- Filter Controls Section -->
<div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0"> <div class="filter-section" *ngIf="hasActiveFilters()">
<app-compact-filter <!-- Base Filters -->
*ngFor="let filter of baseFilters" <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
[filterKey]="filter.field" <h4>Base Filters</h4>
(filterChange)="onFilterChange($event)"> <div class="filter-controls">
</app-compact-filter> <div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div> </div>
<!-- Drilldown mode indicator --> <!-- Dropdown Filter -->
<div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator"> <div *ngIf="filter.type === 'dropdown'" class="filter-input">
<span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span> <select [(ngModel)]="filter.value"
<button class="btn btn-secondary btn-sm" (click)="navigateBack()"> (ngModelChange)="onBaseFilterChange(filter)"
Back to Level {{currentDrilldownLevel - 1}} class="clr-select filter-select">
</button> <option value="">Select {{ filter.field }}</option>
<button class="btn btn-danger btn-sm" (click)="resetToOriginalData()"> <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
Back to Main View </select>
</button>
</div> </div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="chart-header"> <div class="chart-header">
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3> <h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
</div> </div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</div>
</div>
<!-- Show current drilldown level -->
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
<div class="clr-col-12">
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
<div class="alert-items">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
</div>
<span class="alert-text">
Drilldown Level: {{currentDrilldownLevel}}
<span *ngIf="drilldownStack.length > 0">
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="chart-wrapper"> <div class="chart-wrapper">
<div class="chart-content" [class.loading]="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable"> <div class="chart-content" [class.loading]="isLoading">
<!-- Show no data message --> <!-- Show no data message -->
<div class="no-data-message" *ngIf="noDataAvailable"> <div class="no-data-message" *ngIf="noDataAvailable && doughnutChartLabels.length === 0">
<p>No chart data available</p> <p>No chart data available</p>
</div> </div>
<!-- Show chart when data is available --> <!-- Show chart when data is available -->
<canvas baseChart <canvas baseChart
*ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0" [datasets]="doughnutChartData"
[data]="doughnutChartData"
[labels]="doughnutChartLabels" [labels]="doughnutChartLabels"
[type]="doughnutChartType" [type]="doughnutChartType"
[options]="doughnutChartOptions" [options]="doughnutChartOptions"
@@ -42,17 +299,17 @@
</canvas> </canvas>
<!-- Loading overlay --> <!-- Loading overlay -->
<div class="loading-overlay" *ngIf="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable"> <div class="loading-overlay" *ngIf="isLoading">
<div class="shimmer-donut"></div> <div class="shimmer-donut"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="chart-legend" *ngIf="!noDataAvailable && showlabel && doughnutChartLabels && doughnutChartLabels.length > 0"> <div class="chart-legend" *ngIf="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>
<span class="legend-label">{{ label }}</span> <span class="legend-label">{{ label }}</span>
<span class="legend-value">{{ doughnutChartData && doughnutChartData[i] !== undefined ? doughnutChartData[i] : 0 }}</span> <span class="legend-value">{{ doughnutChartData && doughnutChartData[0] && doughnutChartData[0].data && doughnutChartData[0].data[i] !== undefined ? doughnutChartData[0].data[i] : 0 }}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,129 +1,200 @@
.doughnut-chart-container { // Chart container structure - simplified to match shield dashboard
.chart-container {
height: 100%;
min-height: 400px; // Ensure minimum height
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 400px;
min-height: 400px; // Filter section styling
padding: 20px; .filter-section {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); margin-bottom: 20px;
border-radius: 12px; padding: 15px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); border: 1px solid #ddd;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; border-radius: 4px;
transition: all 0.3s ease; background-color: #f9f9f9;
border: 1px solid #eaeaea;
} }
.doughnut-chart-container:hover { .filter-group {
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2); margin-bottom: 15px;
transform: translateY(-2px);
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
} }
.compact-filters-container { .filter-controls {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 5px; gap: 15px;
margin-bottom: 10px;
padding: 5px;
background: #ffffff;
border: 1px solid #e9ecef;
border-radius: 6px;
min-height: 40px;
} }
.drilldown-indicator { .filter-item {
background-color: #e0e0e0; flex: 1 1 300px;
min-width: 250px;
padding: 10px; padding: 10px;
margin-bottom: 15px; background: white;
border-radius: 8px; border: 1px solid #e0e0e0;
text-align: center; border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.drilldown-text { .filter-label {
font-weight: bold; font-weight: 500;
color: #333; margin-bottom: 8px;
font-size: 16px; color: #555;
font-size: 14px;
} }
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn { .btn {
padding: 6px 12px; font-size: 13px;
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 styling
.chart-header { .chart-header {
margin-bottom: 20px; margin-bottom: 20px;
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title { .chart-title {
font-size: 22px; margin: 0;
font-size: 18px;
font-weight: 600; font-weight: 600;
color: #0a192f; color: #0a192f;
margin: 0; }
text-align: center;
padding-bottom: 10px;
border-bottom: 2px solid #3498db;
} }
} }
// Chart wrapper and content - simplified to match shield dashboard
.chart-wrapper { .chart-wrapper {
position: relative;
flex: 1; flex: 1;
min-height: 250px; position: relative;
margin: 15px 0;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-wrapper canvas {
max-width: 100%;
max-height: 100%;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
}
.chart-wrapper canvas:hover {
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
transform: scale(1.02);
transition: all 0.3s ease;
}
.chart-content { .chart-content {
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
&.loading { &.loading {
opacity: 0.7; opacity: 0.7;
@@ -135,20 +206,15 @@
.no-data-message { .no-data-message {
text-align: center; text-align: center;
padding: 30px; padding: 20px;
color: #666; color: #666;
font-size: 18px;
font-style: italic; font-style: italic;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
} }
.no-data-message p { canvas {
margin: 0; max-width: 100%;
max-height: calc(100% - 40px); // Leave space for legend
transition: filter 0.3s ease;
} }
.loading-overlay { .loading-overlay {
@@ -172,68 +238,55 @@
} }
} }
} }
}
// Chart legend - simplified
.chart-legend { .chart-legend {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 15px; gap: 15px;
margin-top: 20px; margin: 20px 0;
padding: 20px; padding: 15px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 8px;
border: 1px solid #dee2e6;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
}
.legend-item { .legend-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 12px 20px; padding: 8px 15px;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); background: #f8f9fa;
border-radius: 25px; border-radius: 20px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); border: 1px solid #e9ecef;
transition: all 0.3s ease; min-width: 120px;
border: 1px solid #eaeaea; justify-content: space-between;
cursor: pointer;
}
.legend-item:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
border-color: #3498db;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
} }
.legend-color { .legend-color {
width: 20px; width: 15px;
height: 20px; height: 15px;
border-radius: 50%; border-radius: 50%;
margin-right: 12px; margin-right: 8px;
display: inline-block; display: inline-block;
border: 2px solid white; flex-shrink: 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
} }
.legend-label { .legend-label {
font-size: 16px; font-size: 14px;
font-weight: 600; font-weight: 500;
color: #2c3e50; color: #2c3e50;
margin-right: 15px; margin-right: 10px;
white-space: nowrap; white-space: nowrap;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); overflow: hidden;
text-overflow: ellipsis;
} }
.legend-value { .legend-value {
font-size: 16px; font-size: 14px;
font-weight: 700; font-weight: 600;
color: #3498db; color: #3498db;
background: linear-gradient(135deg, #e9ecef 0%, #dde1e5 100%); min-width: 30px;
padding: 6px 12px; text-align: right;
border-radius: 12px; }
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); }
min-width: 40px;
text-align: center;
} }
@keyframes shimmer { @keyframes shimmer {
@@ -245,46 +298,46 @@
} }
} }
/* Responsive design */ // Responsive design
@media (max-width: 768px) { @media (max-width: 768px) {
.doughnut-chart-container { .chart-container {
padding: 15px; .filter-controls {
}
.chart-header .chart-title {
font-size: 18px;
}
.drilldown-indicator {
flex-direction: column; flex-direction: column;
gap: 5px;
} }
.drilldown-text { .filter-item {
font-size: 14px; min-width: 100%;
} }
.chart-wrapper { .chart-header {
min-height: 200px; .header-row {
.chart-title {
font-size: 16px;
}
}
}
.chart-content {
min-height: 250px;
} }
.chart-legend { .chart-legend {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
}
.legend-item { .legend-item {
width: 100%; width: 100%;
max-width: 300px; max-width: 300px;
justify-content: space-between; justify-content: space-between;
} }
.no-data-message {
font-size: 16px;
padding: 20px;
} }
.compact-filters-container { .chart-content {
flex-wrap: wrap; min-height: 250px;
canvas {
max-height: calc(100% - 60px); // More space for legend on mobile
}
}
} }
} }

View File

@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
templateUrl: './doughnut-chart.component.html', templateUrl: './doughnut-chart.component.html',
styleUrls: ['./doughnut-chart.component.scss'] styleUrls: ['./doughnut-chart.component.scss']
}) })
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked { export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
@Input() xAxis: string; @Input() xAxis: string;
@Input() yAxis: string | string[]; @Input() yAxis: string | string[];
@Input() table: string; @Input() table: string;
@@ -36,7 +36,21 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
public doughnutChartLabels: string[] = ["Category A", "Category B", "Category C"]; public doughnutChartLabels: string[] = ["Category A", "Category B", "Category C"];
public doughnutChartData: number[] = [30, 50, 20]; public doughnutChartData: any[] = [
{
data: [30, 50, 20],
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
],
hoverBackgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
]
}
];
public doughnutChartType: string = "doughnut"; public doughnutChartType: string = "doughnut";
public doughnutChartOptions: any = { public doughnutChartOptions: any = {
responsive: true, responsive: true,
@@ -72,6 +86,14 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
borderWidth: 2, borderWidth: 2,
borderColor: '#fff' borderColor: '#fff'
} }
},
layout: {
padding: {
top: 20,
bottom: 20,
left: 20,
right: 20
}
} }
}; };
@@ -96,12 +118,20 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
// No data state // No data state
noDataAvailable: boolean = false; noDataAvailable: boolean = false;
// Loading state
isLoading: boolean = false;
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
// Subscriptions to unsubscribe on destroy // Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor( constructor(
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
private filterService: FilterService private filterService: FilterService
@@ -118,8 +148,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
// Validate initial data // Validate initial data
this.validateChartData(); this.validateChartData();
// Only fetch data if we have the required inputs, otherwise show default data
if (this.table && this.xAxis && this.yAxis) {
this.fetchChartData(); this.fetchChartData();
} }
}
/** /**
* Validate and sanitize chart data * Validate and sanitize chart data
@@ -138,17 +171,33 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) { if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
// Add default data to ensure chart visibility // Add default data to ensure chart visibility
this.doughnutChartLabels = ["Category A", "Category B", "Category C"]; this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
this.doughnutChartData = [30, 50, 20]; this.doughnutChartData = [
{
data: [30, 50, 20],
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
],
hoverBackgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
]
}
];
} }
// Ensure we have matching arrays // Ensure we have matching arrays
if (this.doughnutChartLabels.length !== this.doughnutChartData.length) { if (this.doughnutChartLabels.length !== (this.doughnutChartData[0]?.data?.length || 0)) {
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length); const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData[0]?.data?.length || 0);
while (this.doughnutChartLabels.length < maxLength) { while (this.doughnutChartLabels.length < maxLength) {
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`); this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
} }
while (this.doughnutChartData.length < maxLength) { if (this.doughnutChartData[0]) {
this.doughnutChartData.push(0); while (this.doughnutChartData[0].data.length < maxLength) {
this.doughnutChartData[0].data.push(0);
}
} }
} }
} }
@@ -164,6 +213,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('DoughnutChartComponent input changes:', changes); console.log('DoughnutChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
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;
@@ -185,6 +240,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
console.log('Chart configuration changed, fetching new data'); console.log('Chart configuration changed, fetching new data');
this.fetchChartData(); this.fetchChartData();
} }
// If we have the required inputs and haven't fetched data yet, fetch it
if ((xAxisChanged || yAxisChanged || tableChanged) && this.table && this.xAxis && this.yAxis && !this.isFetchingData) {
console.log('Required inputs available, fetching data');
this.fetchChartData();
}
} }
ngAfterViewChecked() { ngAfterViewChecked() {
@@ -198,12 +259,318 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
ngOnDestroy(): void { ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
} }
// Handle filter changes from compact filters // Initialize filter values with proper default values based on type
onFilterChange(event: { filterId: string, value: any }): void { private initializeFilterValues(): void {
console.log('Compact filter changed:', event); console.log('Initializing filter values');
// The filter service will automatically trigger chart updates through the subscription
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
} }
// Public method to refresh data when filters change // Public method to refresh data when filters change
@@ -212,14 +579,18 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
} }
fetchChartData(): void { fetchChartData(): void {
// Set loading state
this.isLoading = true;
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
// If we're in drilldown mode, fetch the appropriate drilldown data // If we're in drilldown mode, fetch the appropriate drilldown data
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) { if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
this.fetchDrilldownData(); this.fetchDrilldownData();
// Reset flag after fetching // Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
return; return;
} }
@@ -289,7 +660,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Doughnut chart data URL:', url); console.log('Chart data URL:', url);
// 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
@@ -297,89 +668,113 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
(data: any) => { (data: any) => {
console.log('Received doughnut chart data:', data); console.log('Received doughnut chart data:', data);
if (data === null) { if (data === null) {
console.warn('Doughnut chart API returned null data. Check if the API endpoint is working correctly.'); console.warn('API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true; this.noDataAvailable = true;
this.doughnutChartLabels = []; this.doughnutChartLabels = [];
this.doughnutChartData = []; this.doughnutChartData = [];
// Validate and sanitize data to show default data // Reset flags after fetching
this.validateChartData();
// Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
return; return;
} }
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) { if (data && data.chartLabels && data.chartData) {
// For doughnut charts, we need to extract the data differently // Backend has already filtered the data, just display it
// The first dataset's data array contains the values for the doughnut chart
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.doughnutChartLabels = data.chartLabels || []; this.doughnutChartLabels = data.chartLabels;
if (data.chartData && data.chartData.length > 0) {
this.doughnutChartData = data.chartData[0].data.map(value => { // Handle different data structures
// Convert to number if it's not already let chartDataValues;
const numValue = Number(value); if (Array.isArray(data.chartData)) {
return isNaN(numValue) ? 0 : numValue; // If chartData is already an array of values
}); if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
} else { chartDataValues = data.chartData;
this.doughnutChartData = [];
} }
// Ensure labels and data arrays have the same length // If chartData is an array with one object containing the data
this.syncLabelAndDataArrays(); else if (data.chartData.length > 0 && data.chartData[0].data) {
// Validate and sanitize data chartDataValues = data.chartData[0].data;
this.validateChartData(); }
// Trigger change detection // Default case
this.doughnutChartData = [...this.doughnutChartData]; else {
chartDataValues = data.chartData;
}
} else {
chartDataValues = [data.chartData];
}
this.doughnutChartData = [
{
data: chartDataValues,
backgroundColor: this.chartColors.slice(0, chartDataValues.length),
hoverBackgroundColor: this.chartColors.slice(0, chartDataValues.length)
}
];
console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
} else if (data && data.labels && data.data) { } else if (data && data.labels && data.datasets) {
// Handle the original expected format as fallback // Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.doughnutChartLabels = data.labels || []; this.doughnutChartLabels = data.labels;
this.doughnutChartData = data.data.map(value => { this.doughnutChartData = data.datasets;
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.doughnutChartData = [...this.doughnutChartData];
console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
} else { } else {
console.warn('Doughnut chart received data does not have expected structure', data); console.warn('Received data does not have expected structure', data);
// Reset to default data
this.noDataAvailable = true; this.noDataAvailable = true;
this.doughnutChartLabels = []; // Keep default data instead of empty arrays
this.doughnutChartData = []; this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
// Validate and sanitize data to show default data this.doughnutChartData = [
this.validateChartData(); {
data: [30, 50, 20],
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
],
hoverBackgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
]
} }
// Reset flag after fetching ];
}
// Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
}, },
(error) => { (error) => {
console.error('Error fetching doughnut chart data:', error); console.error('Error fetching doughnut chart data:', error);
this.noDataAvailable = true; this.noDataAvailable = true;
this.doughnutChartLabels = []; // Keep default data in case of error
this.doughnutChartData = []; this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
// Validate and sanitize data to show default data this.doughnutChartData = [
this.validateChartData(); {
// Reset flag after fetching data: [30, 50, 20],
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
],
hoverBackgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
]
}
];
// Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
} }
); );
} else { } else {
console.log('Missing required data for doughnut chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
// Don't set noDataAvailable to true when there's no required data this.noDataAvailable = true;
// This allows static data to be displayed this.doughnutChartLabels = [];
this.noDataAvailable = false; this.doughnutChartData = [];
// Validate the chart data to ensure we have some data to display // Reset flags after fetching
this.validateChartData();
// Force a redraw to ensure the chart displays
this.doughnutChartData = [...this.doughnutChartData];
// Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
} }
} }
@@ -475,6 +870,35 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
} }
} }
// Add common filters to drilldown filter parameters
const commonFilters = this.filterService.getFilterValues();
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with drilldown filters
const mergedFilterObj = {};
// Add drilldown filters first
if (filterParams) {
try {
const drilldownFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
}
// Add common filters
Object.keys(commonFilters).forEach(key => {
const value = commonFilters[key];
if (value !== undefined && value !== null && value !== '') {
mergedFilterObj[key] = value;
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url); console.log('Drilldown data URL:', url);
@@ -494,59 +918,66 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) { if (data && data.chartLabels && data.chartData) {
// For doughnut charts, we need to extract the data differently // Backend has already filtered the data, just display it
// The first dataset's data array contains the values for the doughnut chart
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.doughnutChartLabels = data.chartLabels || []; this.doughnutChartLabels = data.chartLabels;
if (data.chartData && data.chartData.length > 0) {
this.doughnutChartData = data.chartData[0].data.map(value => { // Handle different data structures
// Convert to number if it's not already let chartDataValues;
const numValue = Number(value); if (Array.isArray(data.chartData)) {
return isNaN(numValue) ? 0 : numValue; // If chartData is already an array of values
}); if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
} else { chartDataValues = data.chartData;
this.doughnutChartData = [];
} }
// Ensure labels and data arrays have the same length // If chartData is an array with one object containing the data
this.syncLabelAndDataArrays(); else if (data.chartData.length > 0 && data.chartData[0].data) {
// Validate and sanitize data chartDataValues = data.chartData[0].data;
this.validateChartData(); }
// Trigger change detection // Default case
this.doughnutChartData = [...this.doughnutChartData]; else {
chartDataValues = data.chartData;
}
} else {
chartDataValues = [data.chartData];
}
this.doughnutChartData = [
{
data: chartDataValues,
backgroundColor: this.chartColors.slice(0, chartDataValues.length),
hoverBackgroundColor: this.chartColors.slice(0, chartDataValues.length)
}
];
console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
} else if (data && data.labels && data.data) { // Set loading state to false
// Handle the original expected format as fallback this.isLoading = false;
} else if (data && data.labels && data.datasets) {
// Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.doughnutChartLabels = data.labels || []; this.doughnutChartLabels = data.labels;
this.doughnutChartData = data.data.map(value => { this.doughnutChartData = data.datasets;
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.doughnutChartData = [...this.doughnutChartData];
console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
// Set loading state to false
this.isLoading = false;
} else { } else {
console.warn('Drilldown received data does not have expected structure', data); console.warn('Drilldown received data does not have expected structure', data);
this.noDataAvailable = true; this.noDataAvailable = true;
this.doughnutChartLabels = []; // Keep current data instead of empty arrays
this.doughnutChartData = []; // Set loading state to false
// Validate and sanitize data this.isLoading = false;
this.validateChartData();
} }
}, },
(error) => { (error) => {
console.error('Error fetching drilldown data:', error); console.error('Error fetching drilldown data:', error);
this.noDataAvailable = true; this.noDataAvailable = true;
this.doughnutChartLabels = [];
this.doughnutChartData = [];
// Keep current data in case of error // Keep current data in case of error
// Set loading state to false
this.isLoading = false;
} }
); );
// Set loading state
this.isLoading = true;
} }
// Reset to original data (go back to base level) // Reset to original data (go back to base level)
@@ -563,7 +994,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
console.log('Restored original labels'); console.log('Restored original labels');
} }
if (this.originalDoughnutChartData.length > 0) { if (this.originalDoughnutChartData.length > 0) {
this.doughnutChartData = [...this.originalDoughnutChartData]; this.doughnutChartData = JSON.parse(JSON.stringify(this.originalDoughnutChartData));
console.log('Restored original data'); console.log('Restored original data');
} }
@@ -605,44 +1036,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
} }
} }
/** // Get legend color for a specific index
* Get color for legend item getLegendColor(index: number): string {
* @param index Index of the legend item
*/
public getLegendColor(index: number): string {
return this.chartColors[index % this.chartColors.length]; return this.chartColors[index % this.chartColors.length];
} }
/**
* Ensure labels and data arrays have the same length
*/
private syncLabelAndDataArrays(): void {
// Handle empty arrays
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
return;
}
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length);
// Pad the shorter array with default values
while (this.doughnutChartLabels.length < maxLength) {
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
}
while (this.doughnutChartData.length < maxLength) {
this.doughnutChartData.push(0);
}
// Truncate the longer array if needed
if (this.doughnutChartLabels.length > maxLength) {
this.doughnutChartLabels = this.doughnutChartLabels.slice(0, maxLength);
}
if (this.doughnutChartData.length > maxLength) {
this.doughnutChartData = this.doughnutChartData.slice(0, maxLength);
}
}
// events // events
public chartClicked(e: any): void { public chartClicked(e: any): void {
console.log('Doughnut chart clicked:', e); console.log('Doughnut chart clicked:', e);
@@ -729,6 +1127,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
} }
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log(e); console.log('Doughnut chart hovered:', e);
} }
} }

View File

@@ -1,4 +1,285 @@
<div class="dynamic-chart-container"> <div class="dynamic-chart-container">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Dynamic Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</div>
</div>
<!-- Show current drilldown level -->
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
<div class="clr-col-12">
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
<div class="alert-items">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
</div>
<span class="alert-text">
Drilldown Level: {{currentDrilldownLevel}}
<span *ngIf="drilldownStack.length > 0">
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Existing content -->
<!-- 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" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>

View File

@@ -0,0 +1,192 @@
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
}

View File

@@ -2,6 +2,8 @@ import { Component, OnInit, ViewChild, Input, OnChanges, SimpleChanges } from '@
import { ChartConfiguration, ChartData, ChartDataset } from 'chart.js'; import { ChartConfiguration, ChartData, ChartDataset } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts'; import { BaseChartDirective } from 'ng2-charts';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-dynamic-chart', selector: 'app-dynamic-chart',
@@ -37,9 +39,20 @@ export class DynamicChartComponent implements OnInit, OnChanges {
@ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined;
constructor(private dashboardService: Dashboard3Service) { } constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
// Initialize with default data // Initialize with default data
this.fetchChartData(); this.fetchChartData();
} }
@@ -47,6 +60,12 @@ export class DynamicChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('DynamicChartComponent input changes:', changes); console.log('DynamicChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
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;
@@ -107,6 +126,14 @@ export class DynamicChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
fetchChartData(): void { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -139,7 +166,49 @@ export class DynamicChartComponent implements OnInit, OnChanges {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/dynamic?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/dynamic?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -504,4 +573,322 @@ export class DynamicChartComponent implements OnInit, OnChanges {
} }
this.dynamicChartData = _dynamicChartData; this.dynamicChartData = _dynamicChartData;
} }
ngOnDestroy(): void {
// Unsubscribe from all subscriptions to prevent memory leaks
this.subscriptions.forEach(subscription => subscription.unsubscribe());
// Remove document click handler if it exists
this.removeDocumentClickHandler();
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
} }

View File

@@ -1,4 +1,285 @@
<div class="financial-chart-container"> <div class="financial-chart-container">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Financial Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</div>
</div>
<!-- Show current drilldown level -->
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
<div class="clr-col-12">
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
<div class="alert-items">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
</div>
<span class="alert-text">
Drilldown Level: {{currentDrilldownLevel}}
<span *ngIf="drilldownStack.length > 0">
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Existing content -->
<!-- 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" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>

View File

@@ -1,108 +1,192 @@
.financial-chart-container { .filter-section {
display: flex;
flex-direction: column;
height: 400px;
min-height: 400px;
padding: 20px;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
transition: all 0.3s ease;
border: 1px solid #eaeaea;
}
.financial-chart-container:hover {
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
transform: translateY(-2px);
}
.chart-title {
font-size: 26px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 20px; margin-bottom: 20px;
text-align: center;
padding-bottom: 15px;
border-bottom: 2px solid #3498db;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.chart-wrapper {
position: relative;
flex: 1;
min-height: 250px;
margin: 15px 0;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-wrapper canvas {
max-width: 100%;
max-height: 100%;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
}
.chart-wrapper canvas:hover {
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
transform: scale(1.02);
transition: all 0.3s ease;
}
.loading-indicator, .no-data-message {
text-align: center;
padding: 30px;
color: #666;
font-size: 18px;
font-style: italic;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.loading-indicator p, .no-data-message p {
margin: 10px 0 0 0;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive design */
@media (max-width: 768px) {
.financial-chart-container {
padding: 15px; padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
} }
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title { .chart-title {
font-size: 20px; margin: 0;
margin-bottom: 15px; font-size: 18px;
font-weight: 600;
color: #333;
}
} }
.chart-wrapper { // Responsive design
min-height: 200px; @media (max-width: 768px) {
.filter-controls {
flex-direction: column;
} }
.no-data-message { .filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px; font-size: 16px;
padding: 20px; }
} }
} }

View File

@@ -1,5 +1,8 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
import { AlertsService } from 'src/app/services/fnd/alerts.service';
@Component({ @Component({
selector: 'app-financial-chart', selector: 'app-financial-chart',
@@ -33,9 +36,21 @@ export class FinancialChartComponent implements OnInit, OnChanges {
// Multi-layer drilldown configuration inputs // Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
constructor(private dashboardService: Dashboard3Service) { } constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService,
private alertService: AlertsService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
// Initialize with default data // Initialize with default data
this.fetchChartData(); this.fetchChartData();
} }
@@ -43,6 +58,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('FinancialChartComponent input changes:', changes); console.log('FinancialChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
// Load filter options for dropdown/multiselect filters
this.loadFilterOptions();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
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;
@@ -86,6 +109,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
fetchChartData(): void { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -118,7 +149,49 @@ export class FinancialChartComponent implements OnInit, OnChanges {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/financial?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/financial?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -496,4 +569,520 @@ export class FinancialChartComponent implements OnInit, OnChanges {
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log(e); console.log(e);
} }
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
// Ensure filter has required properties
if (!filter.type) filter.type = 'text';
if (!filter.options) filter.options = '';
if (!filter.availableValues) filter.availableValues = '';
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
// Ensure filter has required properties
if (!filter.type) filter.type = 'text';
if (!filter.options) filter.options = '';
if (!filter.availableValues) filter.availableValues = '';
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
// Ensure filter has required properties
if (!filter.type) filter.type = 'text';
if (!filter.options) filter.options = '';
if (!filter.availableValues) filter.availableValues = '';
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
ngOnDestroy(): void {
// Unsubscribe from all subscriptions to prevent memory leaks
this.subscriptions.forEach(subscription => subscription.unsubscribe());
// Remove document click handler if it exists
this.removeDocumentClickHandler();
}
// Load filter options for dropdown and multiselect filters
private loadFilterOptions(): void {
console.log('Loading filter options');
// Load options for base filters
if (this.baseFilters && this.table) {
this.baseFilters.forEach((filter, index) => {
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
}
});
}
// Load options for drilldown filters
if (this.drilldownFilters && this.drilldownApiUrl) {
this.drilldownFilters.forEach((filter, index) => {
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
}
});
}
// Load options for layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach((layer, layerIndex) => {
if (layer.filters && layer.apiUrl) {
layer.filters.forEach((filter, filterIndex) => {
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
}
});
}
});
}
}
// Load filter values for a specific field
private loadFilterValuesForField(
apiUrl: string,
connectionId: number | undefined,
field: string,
filterIndex: number,
filterType: 'base' | 'drilldown' | 'layer',
layerIndex?: number
): void {
if (apiUrl && field) {
this.alertService.getValuesFromUrl(apiUrl, connectionId, field).subscribe(
(values: string[]) => {
console.log(`Loaded filter values for ${filterType} filter ${field}:`, values);
// Update the filter with available values
if (filterType === 'base') {
const filter = this.baseFilters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
} else if (filterType === 'drilldown') {
const filter = this.drilldownFilters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
} else if (filterType === 'layer' && layerIndex !== undefined) {
const layer = this.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
}
}
},
(error) => {
console.error('Error loading available values for field:', field, error);
}
);
}
}
// Handle base filter field change
onBaseFilterFieldChange(index: number, field: string): void {
const filter = this.baseFilters[index];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and table URL, load available values
if (field && this.table && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
this.loadFilterValuesForField(this.table, this.connection, field, index, 'base');
}
}
}
// Handle base filter type change
onBaseFilterTypeChange(index: number, type: string): void {
const filter = this.baseFilters[index];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.table) {
this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
}
}
}
// Handle drilldown filter field change
onDrilldownFilterFieldChange(index: number, field: string): void {
const filter = this.drilldownFilters[index];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and drilldown API URL, load available values
if (field && this.drilldownApiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, field, index, 'drilldown');
}
}
}
// Handle drilldown filter type change
onDrilldownFilterTypeChange(index: number, type: string): void {
const filter = this.drilldownFilters[index];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.drilldownApiUrl) {
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
}
}
}
// Handle layer filter field change
onLayerFilterFieldChange(layerIndex: number, filterIndex: number, field: string): void {
const layer = this.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and layer API URL, load available values
if (field && layer.apiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
this.loadFilterValuesForField(layer.apiUrl, this.connection, field, filterIndex, 'layer', layerIndex);
}
}
}
}
// Handle layer filter type change
onLayerFilterTypeChange(layerIndex: number, filterIndex: number, type: string): void {
const layer = this.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && layer.apiUrl) {
this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
}
}
}
}
} }

View File

@@ -1,9 +1,254 @@
<div style="display: block;"> <div style="display: block;">
<div class="dg-wrapper"> <div class="dg-wrapper">
<div class="clr-row"> <!-- Filter Controls Section -->
<div class="clr-col-8"> <div class="filter-section" *ngIf="hasActiveFilters()">
<h3>{{charttitle || 'Data Grid'}}</h3> <!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div> </div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<div class="clr-row">
<!-- <div class="clr-col-8">
<h3>{{charttitle || 'Data Grid'}}</h3>
</div> -->
<!-- Add drilldown navigation controls --> <!-- Add drilldown navigation controls -->
<div class="clr-col-4" *ngIf="drilldownEnabled && drilldownStack.length > 0" style="text-align: right;"> <div class="clr-col-4" *ngIf="drilldownEnabled && drilldownStack.length > 0" style="text-align: right;">
<button class="btn btn-sm btn-link" (click)="navigateBack()"> <button class="btn btn-sm btn-link" (click)="navigateBack()">

View File

@@ -1,28 +1,180 @@
// Add styles for drilldown navigation .filter-section {
.alert-info { margin-bottom: 20px;
background-color: #dcedf7; padding: 15px;
border-color: #a3d4f5; border: 1px solid #ddd;
color: #21333b; border-radius: 4px;
background-color: #f9f9f9;
} }
.alert-info .alert-icon { .filter-group {
color: #0072a3; margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
} }
.btn-link { .filter-controls {
color: #0072a3; display: flex;
text-decoration: none; flex-wrap: wrap;
gap: 15px;
} }
.btn-link:hover { .filter-item {
color: #00567a; flex: 1 1 300px;
text-decoration: underline; min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
} }
.dg-wrapper { .dg-wrapper {
padding: 12px; padding: 15px;
} }
.clr-row { clr-datagrid {
margin-bottom: 12px; margin-top: 10px;
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
} }

View File

@@ -66,6 +66,15 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
// Add subscriptions to unsubscribe on destroy // Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
// Add a flag to track if filters have been initialized
private filtersInitialized: boolean = false;
// Add properties to track open multiselect dropdowns
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
// Add property to track document click handler
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
constructor( constructor(
private mainservice: UsergrpmaintainceService, private mainservice: UsergrpmaintainceService,
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
@@ -102,6 +111,12 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange; const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange;
const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.firstChange; const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.firstChange;
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Respond to input changes // Respond to input changes
if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged || drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
@@ -112,6 +127,84 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
} }
} }
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Dynamic headers for the grid // Dynamic headers for the grid
fetchGridData(): void { fetchGridData(): void {
@@ -628,6 +721,238 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
.replace(/^./, str => str.toUpperCase()); .replace(/^./, str => str.toUpperCase());
} }
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchGridData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchGridData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchGridData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchGridData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchGridData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchGridData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchGridData();
}
ngOnDestroy() { ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks // Unsubscribe from all subscriptions to prevent memory leaks
console.log('GridViewComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions'); console.log('GridViewComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
@@ -644,6 +969,12 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
this.drilldownStack = []; this.drilldownStack = [];
this.originalGridData = []; this.originalGridData = [];
// Clear multiselect tracking
this.openMultiselects.clear();
// Remove document click handler
this.removeDocumentClickHandler();
console.log('GridViewComponent destroyed and cleaned up'); console.log('GridViewComponent destroyed and cleaned up');
} }
} }

View File

@@ -1,14 +1,283 @@
<div style="display: block"> <div style="display: block; height: 100%; width: 100%;">
<!-- Drilldown mode indicator --> <!-- Filter Controls Section -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div class="filter-section" *ngIf="hasActiveFilters()">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <!-- Base Filters -->
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
Back to Level {{currentDrilldownLevel - 1}} <h4>Base Filters</h4>
</button> <div class="filter-controls">
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div *ngFor="let filter of baseFilters" class="filter-item">
Back to Main View <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Line Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button> </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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- No data message --> <!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;"> <div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
@@ -16,12 +285,11 @@
</div> </div>
<!-- Chart display --> <!-- Chart display -->
<div *ngIf="!noDataAvailable"> <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
<canvas baseChart <canvas baseChart
[datasets]="lineChartData" [datasets]="lineChartData"
[labels]="lineChartLabels" [labels]="lineChartLabels"
[options]="lineChartOptions" [options]="lineChartOptions"
[legend]="lineChartLegend" [legend]="lineChartLegend"
[type]="lineChartType" [type]="lineChartType"
(chartHover)="chartHovered($event)" (chartHover)="chartHovered($event)"

View File

@@ -0,0 +1,192 @@
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
}

View File

@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
templateUrl: './line-chart.component.html', templateUrl: './line-chart.component.html',
styleUrls: ['./line-chart.component.scss'] styleUrls: ['./line-chart.component.scss']
}) })
export class LineChartComponent implements OnInit, OnChanges { export class LineChartComponent implements OnInit, OnChanges, OnDestroy {
@Input() xAxis: string; @Input() xAxis: string;
@Input() yAxis: string | string[]; @Input() yAxis: string | string[];
@Input() table: string; @Input() table: string;
@@ -88,6 +88,11 @@ export class LineChartComponent implements OnInit, OnChanges {
// Subscriptions to unsubscribe on destroy // Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor( constructor(
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
private filterService: FilterService private filterService: FilterService
@@ -109,6 +114,12 @@ export class LineChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('LineChartComponent input changes:', changes); console.log('LineChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
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;
@@ -140,6 +151,318 @@ export class LineChartComponent implements OnInit, OnChanges {
ngOnDestroy(): void { ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
} }
// Public method to refresh data when filters change // Public method to refresh data when filters change

View File

@@ -1,19 +1,287 @@
<div class="pie-chart-container"> <div class="pie-chart-container">
<!-- Drilldown mode indicator --> <!-- Filter Controls Section -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div class="filter-section" *ngIf="hasActiveFilters()">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <!-- Base Filters -->
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
Back to Level {{currentDrilldownLevel - 1}} <h4>Base Filters</h4>
</button> <div class="filter-controls">
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div *ngFor="let filter of baseFilters" class="filter-item">
Back to Main View <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
</button>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div> </div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3> <h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</div>
</div>
<!-- Show current drilldown level -->
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
<div class="clr-col-12">
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
<div class="alert-items">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
</div>
<span class="alert-text">
Drilldown Level: {{currentDrilldownLevel}}
<span *ngIf="drilldownStack.length > 0">
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="chart-wrapper"> <div class="chart-wrapper">
<!-- Show loading indicator --> <!-- Show loading indicator -->
<div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable"> <div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable && !isFetchingData">
<div class="spinner"></div> <div class="spinner"></div>
<p>Loading chart data...</p> <p>Loading chart data...</p>
</div> </div>
@@ -23,15 +291,15 @@
<p>No chart data available</p> <p>No chart data available</p>
</div> </div>
<!-- Show chart when data is available --> <!-- Show chart when data is available or show default data -->
<canvas baseChart <canvas baseChart
*ngIf="pieChartLabels.length > 0 && pieChartData.length > 0" [datasets]="pieChartDatasets"
[data]="pieChartData"
[labels]="pieChartLabels" [labels]="pieChartLabels"
[type]="pieChartType" [type]="pieChartType"
[options]="pieChartOptions" [options]="pieChartOptions"
(chartHover)="chartHovered($event)" (chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)"> (chartClick)="chartClicked($event)"
[style.display]="shouldShowChart() ? 'block' : 'none'">
</canvas> </canvas>
</div> </div>
<div class="chart-legend" *ngIf="showlabel && pieChartLabels && pieChartLabels.length > 0"> <div class="chart-legend" *ngIf="showlabel && pieChartLabels && pieChartLabels.length > 0">

View File

@@ -149,10 +149,192 @@
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
/* Responsive design */ // Filter section styles
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
text-align: left;
padding-bottom: 0;
border-bottom: none;
}
}
// Responsive design
@media (max-width: 768px) { @media (max-width: 768px) {
.pie-chart-container { .pie-chart-container {
padding: 15px; padding: 15px;
height: auto;
min-height: 300px;
} }
.chart-title { .chart-title {
@@ -179,4 +361,18 @@
font-size: 16px; font-size: 16px;
padding: 20px; padding: 20px;
} }
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
} }

View File

@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
templateUrl: './pie-chart.component.html', templateUrl: './pie-chart.component.html',
styleUrls: ['./pie-chart.component.scss'] styleUrls: ['./pie-chart.component.scss']
}) })
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
@Input() xAxis: string; @Input() xAxis: string;
@Input() yAxis: string | string[]; @Input() yAxis: string | string[];
@Input() table: string; @Input() table: string;
@@ -37,6 +37,12 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
public pieChartLabels: string[] = ['Category A', 'Category B', 'Category C']; public pieChartLabels: string[] = ['Category A', 'Category B', 'Category C'];
public pieChartData: number[] = [30, 50, 20]; public pieChartData: number[] = [30, 50, 20];
public pieChartDatasets: any[] = [
{
data: [30, 50, 20],
label: 'Dataset 1'
}
];
public pieChartType: string = 'pie'; public pieChartType: string = 'pie';
public pieChartOptions: any = { public pieChartOptions: any = {
responsive: true, responsive: true,
@@ -96,11 +102,16 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
noDataAvailable: boolean = false; noDataAvailable: boolean = false;
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; isFetchingData: boolean = false;
// Subscriptions to unsubscribe on destroy // Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor( constructor(
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
private filterService: FilterService private filterService: FilterService
@@ -127,12 +138,28 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData }); console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData });
// Validate initial data // Validate initial data
this.validateChartData(); this.validateChartData();
// Initialize datasets with default data
this.pieChartDatasets = [
{
data: this.pieChartData,
label: 'Dataset 1'
}
];
// Only fetch data if we have the required inputs, otherwise show default data
if (this.table && this.xAxis && this.yAxis) {
this.fetchChartData(); this.fetchChartData();
} }
}
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('PieChartComponent input changes:', changes); console.log('PieChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
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;
@@ -154,10 +181,328 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
console.log('Chart configuration changed, fetching new data'); console.log('Chart configuration changed, fetching new data');
this.fetchChartData(); this.fetchChartData();
} }
// If we have the required inputs and haven't fetched data yet, fetch it
if ((xAxisChanged || yAxisChanged || tableChanged) && this.table && this.xAxis && this.yAxis && !this.isFetchingData) {
console.log('Required inputs available, fetching data');
this.fetchChartData();
}
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
} }
// Public method to refresh data when filters change // Public method to refresh data when filters change
@@ -243,7 +588,7 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Pie chart data URL:', url); console.log('Chart data URL:', url);
// 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
@@ -251,12 +596,8 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
(data: any) => { (data: any) => {
console.log('Received pie chart data:', data); console.log('Received pie chart data:', data);
if (data === null) { if (data === null) {
console.warn('Pie chart API returned null data. Check if the API endpoint is working correctly.'); console.warn('API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true; this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Reset flag after fetching // Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
return; return;
@@ -264,50 +605,57 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) { if (data && data.chartLabels && data.chartData) {
// For pie charts, we need to extract the data differently // Backend has already filtered the data, just display it
// The first dataset's data array contains the values for the pie chart
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.pieChartLabels = data.chartLabels || []; this.pieChartLabels = data.chartLabels;
if (data.chartData && data.chartData.length > 0) {
this.pieChartData = data.chartData[0].data.map(value => { // Extract the actual data values from the chartData array
// Convert to number if it's not already // chartData is an array with one object containing the data
const numValue = Number(value); if (data.chartData.length > 0 && data.chartData[0].data) {
return isNaN(numValue) ? 0 : numValue; this.pieChartData = data.chartData[0].data;
});
} else { } else {
this.pieChartData = []; this.pieChartData = [];
} }
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection // Trigger change detection
this.pieChartLabels = [...this.pieChartLabels];
this.pieChartData = [...this.pieChartData]; this.pieChartData = [...this.pieChartData];
this.pieChartDatasets = [
{
data: this.pieChartData,
label: 'Dataset 1'
}
];
console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData }); console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData });
} else if (data && data.labels && data.data) { } else if (data && data.labels && data.datasets) {
// Handle the original expected format as fallback // Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.pieChartLabels = data.labels || []; this.pieChartLabels = data.labels;
this.pieChartData = data.data.map(value => { this.pieChartData = data.datasets[0]?.data || [];
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection // Trigger change detection
this.pieChartLabels = [...this.pieChartLabels];
this.pieChartData = [...this.pieChartData]; this.pieChartData = [...this.pieChartData];
this.pieChartDatasets = [
{
data: this.pieChartData,
label: 'Dataset 1'
}
];
console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData }); console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
} else { } else {
console.warn('Pie chart received data does not have expected structure', data); console.warn('Received data does not have expected structure', data);
// Reset to default data
this.noDataAvailable = true; this.noDataAvailable = true;
this.pieChartLabels = []; // Keep default data if no data is available
this.pieChartData = []; if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
// Validate and sanitize data to show default data this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
this.validateChartData(); this.pieChartData = [30, 50, 20];
this.pieChartDatasets = [
{
data: this.pieChartData,
label: 'Dataset 1'
}
];
}
} }
// Reset flag after fetching // Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
@@ -315,23 +663,13 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
(error) => { (error) => {
console.error('Error fetching pie chart data:', error); console.error('Error fetching pie chart data:', error);
this.noDataAvailable = true; this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Reset flag after fetching // Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
} }
); );
} else { } else {
console.log('Missing required data for pie chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
// Don't set noDataAvailable to true when there's no required data this.noDataAvailable = true;
// This allows static data to be displayed
this.noDataAvailable = false;
// Validate the chart data to ensure we have some data to display
this.validateChartData();
// Force a redraw to ensure the chart displays
this.pieChartData = [...this.pieChartData];
// Reset flag after fetching // Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
} }
@@ -360,8 +698,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
} else { } else {
console.warn('Invalid drilldown layer index:', layerIndex); console.warn('Invalid drilldown layer index:', layerIndex);
this.noDataAvailable = true; this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
return; return;
} }
} }
@@ -372,8 +708,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) { if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel); console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
this.noDataAvailable = true; this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
return; return;
} }
@@ -470,64 +804,57 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
if (data === null) { if (data === null) {
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.'); console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true; this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
return; return;
} }
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) { if (data && data.chartLabels && data.chartData) {
// For pie charts, we need to extract the data differently // Backend has already filtered the data, just display it
// The first dataset's data array contains the values for the pie chart
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.pieChartLabels = data.chartLabels || []; this.pieChartLabels = data.chartLabels;
if (data.chartData && data.chartData.length > 0) {
this.pieChartData = data.chartData[0].data.map(value => { // Extract the actual data values from the chartData array
// Convert to number if it's not already // chartData is an array with one object containing the data
const numValue = Number(value); if (data.chartData.length > 0 && data.chartData[0].data) {
return isNaN(numValue) ? 0 : numValue; this.pieChartData = data.chartData[0].data;
});
} else { } else {
this.pieChartData = []; this.pieChartData = [];
} }
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection // Trigger change detection
this.pieChartLabels = [...this.pieChartLabels];
this.pieChartData = [...this.pieChartData]; this.pieChartData = [...this.pieChartData];
this.pieChartDatasets = [
{
data: this.pieChartData,
label: 'Dataset 1'
}
];
console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData }); console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData });
} else if (data && data.labels && data.data) { } else if (data && data.labels && data.datasets) {
// Handle the original expected format as fallback // Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.pieChartLabels = data.labels || []; this.pieChartLabels = data.labels;
this.pieChartData = data.data.map(value => { this.pieChartData = data.datasets[0]?.data || [];
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection // Trigger change detection
this.pieChartLabels = [...this.pieChartLabels];
this.pieChartData = [...this.pieChartData]; this.pieChartData = [...this.pieChartData];
this.pieChartDatasets = [
{
data: this.pieChartData,
label: 'Dataset 1'
}
];
console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData }); console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
} else { } else {
console.warn('Drilldown received data does not have expected structure', data); console.warn('Drilldown received data does not have expected structure', data);
this.noDataAvailable = true; this.noDataAvailable = true;
this.pieChartLabels = []; // Keep current data if no data is available
this.pieChartData = [];
// Validate and sanitize data
this.validateChartData();
} }
}, },
(error) => { (error) => {
console.error('Error fetching drilldown data:', error); console.error('Error fetching drilldown data:', error);
this.noDataAvailable = true; this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
// Keep current data in case of error
} }
); );
} }
@@ -588,82 +915,52 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
} }
} }
/** // Validate chart data to ensure labels and data arrays have the same length
* Get color for legend item private validateChartData(): void {
* @param index Index of the legend item if (this.pieChartLabels && this.pieChartData) {
*/ // For pie charts, we need to ensure labels and data arrays have the same length
public getLegendColor(index: number): string { const labelCount = this.pieChartLabels.length;
const dataCount = this.pieChartData.length;
if (labelCount !== dataCount) {
console.warn('Pie chart labels and data arrays have different lengths:', { labels: labelCount, data: dataCount });
// Pad or truncate data array to match label count
if (dataCount < labelCount) {
// Pad with zeros
while (this.pieChartData.length < labelCount) {
this.pieChartData.push(0);
}
} else if (dataCount > labelCount) {
// Truncate data array
this.pieChartData = this.pieChartData.slice(0, labelCount);
}
}
}
}
// Get legend color for a specific index
getLegendColor(index: number): string {
return this.chartColors[index % this.chartColors.length]; return this.chartColors[index % this.chartColors.length];
} }
/** // Method to determine if chart should be displayed
* Ensure labels and data arrays have the same length shouldShowChart(): boolean {
*/ // Show chart if we have data
private syncLabelAndDataArrays(): void { if (this.pieChartLabels.length > 0 && this.pieChartData.length > 0) {
// Ensure we have matching arrays return true;
if (this.pieChartLabels.length !== this.pieChartData.length) {
const maxLength = Math.max(this.pieChartLabels.length, this.pieChartData.length);
while (this.pieChartLabels.length < maxLength) {
this.pieChartLabels.push(`Label ${this.pieChartLabels.length + 1}`);
}
while (this.pieChartData.length < maxLength) {
this.pieChartData.push(0);
}
}
} }
/** // Show chart if we're still fetching data
* Validate and sanitize chart data if (this.isFetchingData) {
*/ return true;
private validateChartData(): void {
console.log('Validating chart data:', { labels: this.pieChartLabels, data: this.pieChartData });
// Ensure we have valid arrays
if (!Array.isArray(this.pieChartLabels)) {
this.pieChartLabels = [];
} }
if (!Array.isArray(this.pieChartData)) { // Show chart if we have default data
this.pieChartData = []; if (this.pieChartLabels.length > 0 && this.originalPieChartLabels.length > 0) {
return true;
} }
// Ensure we have some data to display return false;
if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
// Add default data to ensure chart visibility
this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
this.pieChartData = [30, 50, 20];
console.log('Added default data for chart display');
}
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Ensure all data values are numbers
this.pieChartData = this.pieChartData.map(value => {
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
console.log('After validation:', { labels: this.pieChartLabels, data: this.pieChartData });
}
ngAfterViewChecked() {
// Debugging: Log component state after view checks
console.log('PieChartComponent state:', {
labels: this.pieChartLabels,
data: this.pieChartData,
hasData: this.pieChartLabels.length > 0 && this.pieChartData.length > 0
});
}
/**
* Check if chart data is valid and ready to display
*/
public isChartDataValid(): boolean {
return this.pieChartLabels && this.pieChartData &&
Array.isArray(this.pieChartLabels) && Array.isArray(this.pieChartData) &&
this.pieChartLabels.length > 0 && this.pieChartData.length > 0 &&
this.pieChartLabels.length === this.pieChartData.length;
} }
// events // events
@@ -752,6 +1049,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
} }
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log(e); console.log('Pie chart hovered:', e);
}
ngAfterViewChecked(): void {
// This lifecycle hook can be used if needed for post-render operations
} }
} }

View File

@@ -1,10 +1,292 @@
<div style="display: block; height: 100%; width: 100%;">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<div style="display: block"> <!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Polar Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</div>
</div>
<!-- Show current drilldown level -->
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
<div class="clr-col-12">
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
<div class="alert-items">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
</div>
<span class="alert-text">
Drilldown Level: {{currentDrilldownLevel}}
<span *ngIf="drilldownStack.length > 0">
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<div style="position: relative; height: calc(100% - 80px); padding: 0 10px 30px 10px;">
<canvas baseChart <canvas baseChart
[datasets]="polarAreaChartData" [datasets]="polarAreaChartData"
[labels]="polarAreaChartLabels" [labels]="polarAreaChartLabels"
[options]="polarAreaChartOptions"
[type]="polarAreaChartType" [type]="polarAreaChartType"
(chartHover)="chartHovered($event)" (chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)"> (chartClick)="chartClicked($event)">
</canvas> </canvas>
</div> </div>
</div>

View File

@@ -1,18 +1,192 @@
// Polar Chart Component Styles .filter-section {
div[style*="display: block"] { margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative; position: relative;
width: 100%;
height: 100%;
} }
canvas { .multiselect-display {
max-width: 100%; display: flex;
max-height: 100%; align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
} }
// Ensure the chart container has proper sizing .multiselect-value {
:host { color: #666;
display: block; font-size: 12px;
width: 100%; margin-right: 8px;
height: 100%; }
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
} }

View File

@@ -1,5 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-polar-chart', selector: 'app-polar-chart',
@@ -33,9 +35,20 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Multi-layer drilldown configuration inputs // Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
constructor(private dashboardService: Dashboard3Service) { } constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
// Initialize with default data // Initialize with default data
this.fetchChartData(); this.fetchChartData();
} }
@@ -43,6 +56,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('PolarChartComponent input changes:', changes); console.log('PolarChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
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;
@@ -71,6 +90,32 @@ export class PolarChartComponent implements OnInit, OnChanges {
{ data: [ 300, 500, 100, 40, 120 ], label: 'Series 1'} { data: [ 300, 500, 100, 40, 120 ], label: 'Series 1'}
]; ];
public polarAreaChartOptions: any = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
left: 10,
right: 10,
top: 10,
bottom: 30
}
},
plugins: {
legend: {
display: true,
position: 'top'
}
},
scales: {
r: {
ticks: {
backdropColor: 'rgba(0, 0, 0, 0)'
}
}
}
};
public polarAreaChartType: string = 'polarArea'; public polarAreaChartType: string = 'polarArea';
// Multi-layer drilldown state tracking // Multi-layer drilldown state tracking
@@ -85,6 +130,324 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
fetchChartData(): void { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -117,7 +480,49 @@ export class PolarChartComponent implements OnInit, OnChanges {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -132,7 +537,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.warn('Polar chart API returned null data. Check if the API endpoint is working correctly.'); console.warn('Polar chart API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true; this.noDataAvailable = true;
this.polarAreaChartLabels = []; this.polarAreaChartLabels = [];
this.polarAreaChartData = []; this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
// Reset flag after fetching // Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
return; return;
@@ -145,32 +555,54 @@ export class PolarChartComponent implements OnInit, OnChanges {
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.polarAreaChartLabels = data.chartLabels; this.polarAreaChartLabels = data.chartLabels;
if (data.chartData && data.chartData.length > 0) { if (data.chartData && data.chartData.length > 0) {
this.polarAreaChartData = data.chartData[0].data.map(value => { // Convert the data to the expected format for polar area charts
const chartValues = data.chartData[0].data.map(value => {
// Convert to number if it's not already // Convert to number if it's not already
return isNaN(Number(value)) ? 0 : Number(value); return isNaN(Number(value)) ? 0 : Number(value);
}); });
} else { // Assign data in the correct format (array of objects with data property)
this.polarAreaChartData = []; this.polarAreaChartData = [
{
data: chartValues,
label: data.chartData[0].label || 'Dataset 1'
}
];
} else {
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
} }
// Trigger change detection
this.polarAreaChartData = [...this.polarAreaChartData];
console.log('Updated polar chart with data:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData }); console.log('Updated polar chart with data:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
} else if (data && data.labels && data.data) { } else if (data && data.labels && data.data) {
// Handle the original expected format as fallback // Handle the original expected format as fallback
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.polarAreaChartLabels = data.labels; this.polarAreaChartLabels = data.labels;
this.polarAreaChartData = data.data.map(value => { // Convert the data to the expected format for polar area charts
const chartValues = data.data.map(value => {
// Convert to number if it's not already // Convert to number if it's not already
return isNaN(Number(value)) ? 0 : Number(value); return isNaN(Number(value)) ? 0 : Number(value);
}); });
// Trigger change detection // Assign data in the correct format (array of objects with data property)
this.polarAreaChartData = [...this.polarAreaChartData]; this.polarAreaChartData = [
{
data: chartValues,
label: 'Dataset 1'
}
];
console.log('Updated polar chart with legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData }); console.log('Updated polar chart with legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
} else { } else {
console.warn('Polar chart received data does not have expected structure', data); console.warn('Polar chart received data does not have expected structure', data);
this.noDataAvailable = true; this.noDataAvailable = true;
this.polarAreaChartLabels = []; this.polarAreaChartLabels = [];
this.polarAreaChartData = []; this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
} }
// Reset flag after fetching // Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
@@ -179,7 +611,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.error('Error fetching polar chart data:', error); console.error('Error fetching polar chart data:', error);
this.noDataAvailable = true; this.noDataAvailable = true;
this.polarAreaChartLabels = []; this.polarAreaChartLabels = [];
this.polarAreaChartData = []; this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
// Reset flag after fetching // Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
// Keep default data in case of error // Keep default data in case of error
@@ -189,7 +626,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.log('Missing required data for polar chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); console.log('Missing required data for polar chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
this.noDataAvailable = true; this.noDataAvailable = true;
this.polarAreaChartLabels = []; this.polarAreaChartLabels = [];
this.polarAreaChartData = []; this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
// Reset flag after fetching // Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
} }
@@ -219,7 +661,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.warn('Invalid drilldown layer index:', layerIndex); console.warn('Invalid drilldown layer index:', layerIndex);
this.noDataAvailable = true; this.noDataAvailable = true;
this.polarAreaChartLabels = []; this.polarAreaChartLabels = [];
this.polarAreaChartData = []; this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
return; return;
} }
} }
@@ -231,7 +678,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel); console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
this.noDataAvailable = true; this.noDataAvailable = true;
this.polarAreaChartLabels = []; this.polarAreaChartLabels = [];
this.polarAreaChartData = []; this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
return; return;
} }
@@ -287,6 +739,35 @@ export class PolarChartComponent implements OnInit, OnChanges {
} }
} }
// Add common filters to drilldown filter parameters
const commonFilters = this.filterService.getFilterValues();
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with drilldown filters
const mergedFilterObj = {};
// Add drilldown filters first
if (filterParams) {
try {
const drilldownFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
}
// Add common filters
Object.keys(commonFilters).forEach(key => {
const value = commonFilters[key];
if (value !== undefined && value !== null && value !== '') {
mergedFilterObj[key] = value;
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url); console.log('Drilldown data URL:', url);
@@ -300,23 +781,40 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.'); console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true; this.noDataAvailable = true;
this.polarAreaChartLabels = []; this.polarAreaChartLabels = [];
this.polarAreaChartData = []; this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
return; return;
} }
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) { if (data && data.chartLabels && data.chartData) {
// For polar charts, we need to extract the data differently // For polar charts, we need to extract the data differently
// The first dataset's data array contains the values for the polar chart
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.polarAreaChartLabels = data.chartLabels; this.polarAreaChartLabels = data.chartLabels;
if (data.chartData && data.chartData.length > 0) { if (data.chartData && data.chartData.length > 0) {
this.polarAreaChartData = data.chartData[0].data.map(value => { // Convert the data to the expected format for polar area charts
const chartValues = data.chartData[0].data.map(value => {
// Convert to number if it's not already // Convert to number if it's not already
return isNaN(Number(value)) ? 0 : Number(value); return isNaN(Number(value)) ? 0 : Number(value);
}); });
// Assign data in the correct format (array of objects with data property)
this.polarAreaChartData = [
{
data: chartValues,
label: data.chartData[0].label || 'Dataset 1'
}
];
} else { } else {
this.polarAreaChartData = []; this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
} }
// Trigger change detection // Trigger change detection
this.polarAreaChartData = [...this.polarAreaChartData]; this.polarAreaChartData = [...this.polarAreaChartData];
@@ -325,10 +823,18 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Handle the original expected format as fallback // Handle the original expected format as fallback
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.polarAreaChartLabels = data.labels; this.polarAreaChartLabels = data.labels;
this.polarAreaChartData = data.data.map(value => { // Convert the data to the expected format for polar area charts
const chartValues = data.data.map(value => {
// Convert to number if it's not already // Convert to number if it's not already
return isNaN(Number(value)) ? 0 : Number(value); return isNaN(Number(value)) ? 0 : Number(value);
}); });
// Assign data in the correct format (array of objects with data property)
this.polarAreaChartData = [
{
data: chartValues,
label: 'Dataset 1'
}
];
// Trigger change detection // Trigger change detection
this.polarAreaChartData = [...this.polarAreaChartData]; this.polarAreaChartData = [...this.polarAreaChartData];
console.log('Updated polar chart with drilldown legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData }); console.log('Updated polar chart with drilldown legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
@@ -336,14 +842,24 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.warn('Drilldown received data does not have expected structure', data); console.warn('Drilldown received data does not have expected structure', data);
this.noDataAvailable = true; this.noDataAvailable = true;
this.polarAreaChartLabels = []; this.polarAreaChartLabels = [];
this.polarAreaChartData = []; this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
} }
}, },
(error) => { (error) => {
console.error('Error fetching drilldown data:', error); console.error('Error fetching drilldown data:', error);
this.noDataAvailable = true; this.noDataAvailable = true;
this.polarAreaChartLabels = []; this.polarAreaChartLabels = [];
this.polarAreaChartData = []; this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
// Keep current data in case of error // Keep current data in case of error
} }
); );
@@ -417,13 +933,13 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Get the label of the clicked element // Get the label of the clicked element
const clickedLabel = this.polarAreaChartLabels[clickedIndex]; const clickedLabel = this.polarAreaChartLabels[clickedIndex];
console.log('Clicked on polar area:', { index: clickedIndex, label: clickedLabel }); console.log('Clicked on polar point:', { index: clickedIndex, label: clickedLabel });
// If we're not at the base level, store original data // If we're not at the base level, store original data
if (this.currentDrilldownLevel === 0) { if (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode // Store original data before entering drilldown mode
this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels]; this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels];
this.originalPolarAreaChartData = [...this.polarAreaChartData]; this.originalPolarAreaChartData = JSON.parse(JSON.stringify(this.polarAreaChartData));
console.log('Stored original data for drilldown'); console.log('Stored original data for drilldown');
} }
@@ -491,6 +1007,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
} }
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log(e); console.log('Polar chart hovered:', e);
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
} }
} }

View File

@@ -1,14 +1,283 @@
<div style="display: block"> <div style="display: block; height: 100%; width: 100%;">
<!-- Drilldown mode indicator --> <!-- Filter Controls Section -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div class="filter-section" *ngIf="hasActiveFilters()">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <!-- Base Filters -->
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
Back to Level {{currentDrilldownLevel - 1}} <h4>Base Filters</h4>
</button> <div class="filter-controls">
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div *ngFor="let filter of baseFilters" class="filter-item">
Back to Main View <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Radar Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button> </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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- No data message --> <!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;"> <div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
@@ -16,7 +285,7 @@
</div> </div>
<!-- Chart display --> <!-- Chart display -->
<div *ngIf="!noDataAvailable"> <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
<canvas baseChart <canvas baseChart
[datasets]="radarChartData" [datasets]="radarChartData"
[labels]="radarChartLabels" [labels]="radarChartLabels"

View File

@@ -0,0 +1,192 @@
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
}

View File

@@ -1,5 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-radar-chart', selector: 'app-radar-chart',
@@ -62,15 +64,40 @@ export class RadarChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
constructor(private dashboardService: Dashboard3Service) { } // Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.fetchChartData(); this.fetchChartData();
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('RadarChartComponent input changes:', changes); console.log('RadarChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
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;
@@ -94,6 +121,316 @@ export class RadarChartComponent implements OnInit, OnChanges {
} }
} }
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
fetchChartData(): void { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -126,7 +463,49 @@ export class RadarChartComponent implements OnInit, OnChanges {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/radar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/radar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -298,6 +677,35 @@ export class RadarChartComponent implements OnInit, OnChanges {
} }
} }
// Add common filters to drilldown filter parameters
const commonFilters = this.filterService.getFilterValues();
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with drilldown filters
const mergedFilterObj = {};
// Add drilldown filters first
if (filterParams) {
try {
const drilldownFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
}
// Add common filters
Object.keys(commonFilters).forEach(key => {
const value = commonFilters[key];
if (value !== undefined && value !== null && value !== '') {
mergedFilterObj[key] = value;
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url); console.log('Drilldown data URL:', url);
@@ -321,7 +729,6 @@ export class RadarChartComponent implements OnInit, OnChanges {
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.radarChartLabels = data.chartLabels; this.radarChartLabels = data.chartLabels;
// For radar charts, we need to ensure the data is properly formatted // For radar charts, we need to ensure the data is properly formatted
// Each dataset should have a data array with numeric values
this.radarChartData = data.chartData.map(dataset => ({ this.radarChartData = data.chartData.map(dataset => ({
...dataset, ...dataset,
data: dataset.data ? dataset.data.map(value => { data: dataset.data ? dataset.data.map(value => {
@@ -358,6 +765,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
this.noDataAvailable = true; this.noDataAvailable = true;
this.radarChartLabels = []; this.radarChartLabels = [];
this.radarChartData = []; this.radarChartData = [];
// Keep current data in case of error
} }
); );
} }
@@ -436,7 +844,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
if (this.currentDrilldownLevel === 0) { if (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode // Store original data before entering drilldown mode
this.originalRadarChartLabels = [...this.radarChartLabels]; this.originalRadarChartLabels = [...this.radarChartLabels];
this.originalRadarChartData = [...this.radarChartData]; this.originalRadarChartData = JSON.parse(JSON.stringify(this.radarChartData));
console.log('Stored original data for drilldown'); console.log('Stored original data for drilldown');
} }
@@ -504,6 +912,12 @@ export class RadarChartComponent implements OnInit, OnChanges {
} }
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log(e); console.log('Radar chart hovered:', e);
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
} }
} }

View File

@@ -1,14 +1,283 @@
<div style="display: block"> <div style="display: block; height: 100%; width: 100%;">
<!-- Drilldown mode indicator --> <!-- Filter Controls Section -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div class="filter-section" *ngIf="hasActiveFilters()">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <!-- Base Filters -->
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
Back to Level {{currentDrilldownLevel - 1}} <h4>Base Filters</h4>
</button> <div class="filter-controls">
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div *ngFor="let filter of baseFilters" class="filter-item">
Back to Main View <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Scatter Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button> </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].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- No data message --> <!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;"> <div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
@@ -16,9 +285,10 @@
</div> </div>
<!-- Chart display --> <!-- Chart display -->
<div *ngIf="!noDataAvailable"> <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 70px); min-height: 300px;">
<canvas baseChart <canvas baseChart
[datasets]="scatterChartData" [datasets]="scatterChartData"
[options]="scatterChartOptions"
[type]="scatterChartType" [type]="scatterChartType"
(chartHover)="chartHovered($event)" (chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)"> (chartClick)="chartClicked($event)">

View File

@@ -0,0 +1,192 @@
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
}

View File

@@ -1,6 +1,8 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ChartData,ChartDataset } from 'chart.js'; import { ChartData,ChartDataset,ChartOptions } from 'chart.js';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-scatter-chart', selector: 'app-scatter-chart',
@@ -34,9 +36,20 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Multi-layer drilldown configuration inputs // Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
constructor(private dashboardService: Dashboard3Service) { } constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
// Initialize with default data // Initialize with default data
this.fetchChartData(); this.fetchChartData();
} }
@@ -44,6 +57,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('ScatterChartComponent input changes:', changes); console.log('ScatterChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
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;
@@ -94,6 +113,89 @@ export class ScatterChartComponent implements OnInit, OnChanges {
], ],
}, },
]; ];
public scatterChartOptions: any = {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
position: 'bottom',
title: {
display: true,
text: 'X Axis'
},
ticks: {
autoSkip: true,
maxTicksLimit: 10,
callback: function(value: any) {
if (typeof value === 'number') {
// Format large numbers for better readability
if (Math.abs(value) >= 1000000) {
return (value / 1000000).toFixed(1) + 'M';
} else if (Math.abs(value) >= 1000) {
return (value / 1000).toFixed(1) + 'K';
}
return value.toString();
}
return value;
}
},
grid: {
display: true,
color: 'rgba(0, 0, 0, 0.1)'
}
},
y: {
title: {
display: true,
text: 'Y Axis'
},
ticks: {
autoSkip: true,
maxTicksLimit: 10,
callback: function(value: any) {
if (typeof value === 'number') {
// Format large numbers for better readability
if (Math.abs(value) >= 1000000) {
return (value / 1000000).toFixed(1) + 'M';
} else if (Math.abs(value) >= 1000) {
return (value / 1000).toFixed(1) + 'K';
}
return value.toString();
}
return value;
}
},
grid: {
display: true,
color: 'rgba(0, 0, 0, 0.1)'
}
}
},
plugins: {
legend: {
display: true,
position: 'top',
},
tooltip: {
callbacks: {
label: function(context: any) {
return `(${context.parsed.x}, ${context.parsed.y})`;
}
}
}
},
layout: {
padding: {
left: 15,
right: 15,
top: 15,
bottom: 60 // Add padding at the bottom to ensure X-axis visibility
}
}
};
public scatterChartType: string = 'scatter'; public scatterChartType: string = 'scatter';
// Multi-layer drilldown state tracking // Multi-layer drilldown state tracking
@@ -107,6 +209,417 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
// Transform data to scatter chart format
private transformToScatterData(labels: any[], data: any[]): ChartDataset[] {
// For scatter charts, we need to transform the data into scatter format
// Scatter charts expect data in the format: {x: number, y: number}
console.log('Transforming data to scatter format:', { labels, data });
// If we have the expected scatter data format, return it as is
if (data && data.length > 0 && data[0].data && data[0].data.length > 0 &&
typeof data[0].data[0] === 'object' && data[0].data[0].hasOwnProperty('x') &&
data[0].data[0].hasOwnProperty('y')) {
return data;
}
// Transform the data properly for scatter chart
// Assuming labels are x-values and data[0].data are y-values
if (labels && data && data.length > 0 && data[0].data) {
const yValues = data[0].data;
const label = data[0].label || 'Dataset 1';
// Create scatter points from labels (x) and data (y)
const scatterPoints = [];
const minLength = Math.min(labels.length, yValues.length);
for (let i = 0; i < minLength; i++) {
// Convert to numbers if they're strings
const x = typeof labels[i] === 'string' ? parseFloat(labels[i]) : labels[i];
const y = typeof yValues[i] === 'string' ? parseFloat(yValues[i]) : yValues[i];
// Only add valid points
if (!isNaN(x) && !isNaN(y)) {
scatterPoints.push({ x, y });
}
}
// Generate different colors for each point to avoid all points showing the same color
const backgroundColors = [];
const borderColors = [];
for (let i = 0; i < scatterPoints.length; i++) {
// Generate a color based on the point index for variety
const hue = (i * 137.508) % 360; // Use golden angle to spread colors
backgroundColors.push(`hsla(${hue}, 70%, 50%, 0.6)`);
borderColors.push(`hsla(${hue}, 70%, 40%, 1)`);
}
// Create a single dataset with all scatter points
const scatterDatasets: ChartDataset[] = [
{
data: scatterPoints,
label: label,
pointRadius: 8,
pointHoverRadius: 10,
backgroundColor: backgroundColors,
borderColor: borderColors,
borderWidth: 1,
pointHoverBackgroundColor: 'rgba(255, 99, 132, 1)',
}
];
console.log('Transformed scatter data:', scatterDatasets);
return scatterDatasets;
}
// Otherwise, create a default scatter dataset
const scatterDatasets: ChartDataset[] = [
{
data: [
{ x: 1, y: 1 },
{ x: 2, y: 3 },
{ x: 3, y: -2 },
{ x: 4, y: 4 },
{ x: 5, y: -3 },
],
label: 'Dataset 1',
pointRadius: 10,
backgroundColor: [
'red',
'green',
'blue',
'purple',
'yellow',
'brown',
'magenta',
'cyan',
'orange',
'pink'
],
}
];
return scatterDatasets;
}
fetchChartData(): void { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -139,7 +652,49 @@ export class ScatterChartComponent implements OnInit, OnChanges {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/scatter?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/scatter?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -165,11 +720,19 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Scatter charts expect data in the format: {x: number, y: number} // Scatter charts expect data in the format: {x: number, y: number}
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData); this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
// Update chart options with axis titles
this.updateChartOptionsWithAxisTitles();
console.log('Updated scatter chart with data:', this.scatterChartData); console.log('Updated scatter chart with data:', this.scatterChartData);
} else if (data && data.labels && data.datasets) { } else if (data && data.labels && data.datasets) {
// Handle the original expected format as fallback // Handle the original expected format as fallback
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.scatterChartData = data.datasets; this.scatterChartData = data.datasets;
// Update chart options with axis titles
this.updateChartOptionsWithAxisTitles();
console.log('Updated scatter chart with legacy data format:', this.scatterChartData); console.log('Updated scatter chart with legacy data format:', this.scatterChartData);
} else { } else {
console.warn('Scatter chart received data does not have expected structure', data); console.warn('Scatter chart received data does not have expected structure', data);
@@ -197,6 +760,20 @@ export class ScatterChartComponent implements OnInit, OnChanges {
} }
} }
// Update chart options with axis titles
private updateChartOptionsWithAxisTitles(): void {
// Update X axis title
if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.x) {
this.scatterChartOptions.scales.x.title.text = this.xAxis || 'X Axis';
}
// Update Y axis title
if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.y) {
const yAxisLabel = Array.isArray(this.yAxis) ? this.yAxis[0] : this.yAxis;
this.scatterChartOptions.scales.y.title.text = yAxisLabel || 'Y Axis';
}
}
// Fetch drilldown data based on current drilldown level // Fetch drilldown data based on current drilldown level
fetchDrilldownData(): void { fetchDrilldownData(): void {
console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel); console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
@@ -287,24 +864,34 @@ export class ScatterChartComponent implements OnInit, OnChanges {
} }
} }
// Convert drilldownFilters to filter parameters for drilldown level // Add common filters to drilldown filter parameters
let drilldownFilterParams = ''; const commonFilters = this.filterService.getFilterValues();
if (this.drilldownFilters && this.drilldownFilters.length > 0) { if (Object.keys(commonFilters).length > 0) {
const filterObj = {}; // Merge common filters with drilldown filters
this.drilldownFilters.forEach(filter => { const mergedFilterObj = {};
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value; // Add drilldown filters first
if (filterParams) {
try {
const drilldownFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
}
// Add common filters
Object.keys(commonFilters).forEach(key => {
const value = commonFilters[key];
if (value !== undefined && value !== null && value !== '') {
mergedFilterObj[key] = value;
} }
}); });
if (Object.keys(filterObj).length > 0) {
drilldownFilterParams = JSON.stringify(filterObj);
}
}
console.log('Drilldown filter parameters:', drilldownFilterParams);
// Use drilldown filters if available, otherwise use layer filters if (Object.keys(mergedFilterObj).length > 0) {
const finalFilterParams = drilldownFilterParams || filterParams; filterParams = JSON.stringify(mergedFilterObj);
console.log('Final filter parameters:', finalFilterParams); }
}
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -312,7 +899,7 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Fetch data from the dashboard service with parameter field and value // Fetch data from the dashboard service with parameter field and value
// Backend handles filtering, we just pass the parameter field and value // Backend handles filtering, we just pass the parameter field and value
this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, finalFilterParams).subscribe( this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
(data: any) => { (data: any) => {
console.log('Received drilldown data:', data); console.log('Received drilldown data:', data);
if (data === null) { if (data === null) {
@@ -325,7 +912,6 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) { if (data && data.chartLabels && data.chartData) {
// For scatter charts, we need to transform the data into scatter format // For scatter charts, we need to transform the data into scatter format
// Scatter charts expect data in the format: {x: number, y: number}
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData); this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
console.log('Updated scatter chart with drilldown data:', this.scatterChartData); console.log('Updated scatter chart with drilldown data:', this.scatterChartData);
@@ -349,33 +935,6 @@ export class ScatterChartComponent implements OnInit, OnChanges {
); );
} }
// Transform chart data to scatter chart format
private transformToScatterData(labels: string[], datasets: any[]): ChartDataset[] {
// For scatter charts, we need to transform the data into scatter format
// Scatter charts expect data in the format: {x: number, y: number}
// This is a simple transformation - in a real implementation, you might want to
// create a more sophisticated mapping based on your data structure
return datasets.map((dataset, index) => {
// Create scatter data points
const scatterData = labels.map((label, i) => {
// Use x-axis data as x coordinate, y-axis data as y coordinate
const xValue = dataset.data[i] || 0;
const yValue = i < datasets.length ? (datasets[i].data[index] || 0) : 0;
return { x: xValue, y: yValue };
});
return {
data: scatterData,
label: dataset.label || `Dataset ${index + 1}`,
backgroundColor: dataset.backgroundColor || `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.6)`,
borderColor: dataset.borderColor || 'rgba(0, 0, 0, 1)',
pointRadius: dataset.pointRadius || 5
};
});
}
// Reset to original data (go back to base level) // Reset to original data (go back to base level)
resetToOriginalData(): void { resetToOriginalData(): void {
console.log('Resetting to original data'); console.log('Resetting to original data');
@@ -436,16 +995,18 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Get the index of the clicked element // Get the index of the clicked element
const clickedIndex = e.active[0].index; const clickedIndex = e.active[0].index;
// Get the label of the clicked element // Get the dataset index
// For scatter charts, we might not have labels in the same way as other charts const datasetIndex = e.active[0].datasetIndex;
const clickedLabel = `Point ${clickedIndex}`;
console.log('Clicked on scatter point:', { index: clickedIndex, label: clickedLabel }); // Get the data point
const dataPoint = this.scatterChartData[datasetIndex].data[clickedIndex];
console.log('Clicked on scatter point:', { datasetIndex, clickedIndex, dataPoint });
// If we're not at the base level, store original data // If we're not at the base level, store original data
if (this.currentDrilldownLevel === 0) { if (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode // Store original data before entering drilldown mode
this.originalScatterChartData = [...this.scatterChartData]; this.originalScatterChartData = JSON.parse(JSON.stringify(this.scatterChartData));
console.log('Stored original data for drilldown'); console.log('Stored original data for drilldown');
} }
@@ -487,9 +1048,10 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Add this click to the drilldown stack // Add this click to the drilldown stack
const stackEntry = { const stackEntry = {
level: nextDrilldownLevel, level: nextDrilldownLevel,
datasetIndex: datasetIndex,
clickedIndex: clickedIndex, clickedIndex: clickedIndex,
clickedLabel: clickedLabel, dataPoint: dataPoint,
clickedValue: clickedLabel // Using label as value for now clickedValue: dataPoint // Using data point as value for now
}; };
this.drilldownStack.push(stackEntry); this.drilldownStack.push(stackEntry);
@@ -513,6 +1075,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
} }
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log(e); console.log('Scatter chart hovered:', e);
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
} }
} }

View File

@@ -1,4 +1,287 @@
<table class="table"> <div class="to-do-chart-container">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'To Do Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</div>
</div>
<!-- Show current drilldown level -->
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
<div class="clr-col-12">
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
<div class="alert-items">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
</div>
<span class="alert-text">
Drilldown Level: {{currentDrilldownLevel}}
<span *ngIf="drilldownStack.length > 0">
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Existing content -->
<div class="todo-table-container">
<table class="table todo-table">
<thead> <thead>
<th class="c-col">#</th> <th class="c-col">#</th>
<th>Item</th> <th>Item</th>
@@ -8,20 +291,17 @@
<td class="c-col">{{i + 1}}</td> <td class="c-col">{{i + 1}}</td>
<td>{{todo}}</td> <td>{{todo}}</td>
<td style="text-align:right"> <td style="text-align:right">
<a routerLink="." (click)="removeTodo(i)"> <a (click)="removeTodo(i)" class="remove-button">
<clr-icon shape="times"></clr-icon> <clr-icon shape="times"></clr-icon>
</a> </a>
</td> </td>
</tr> </tr>
<tr>
<td></td>
<td>
<input [(ngModel)]="todo" placeholder="Add Todo" class="clr-input">
</td>
<td style="text-align:right">
<a routerLink="." color='primary' (click)="addTodo(todo)">
<clr-icon shape="plus"></clr-icon>
</a>
</td>
</tr>
</table> </table>
</div>
<div class="add-todo-section">
<input [(ngModel)]="todo" (keyup.enter)="addTodo(todo)" placeholder="Add Todo" class="clr-input todo-input">
<button (click)="addTodo(todo)" class="btn btn-primary add-button">
<clr-icon shape="plus"></clr-icon> Add
</button>
</div>
</div>

View File

@@ -0,0 +1,289 @@
// To Do Chart specific styles
.to-do-chart-container {
padding: 20px;
height: 100%;
display: flex;
flex-direction: column;
}
.todo-table-container {
flex: 1;
overflow-y: auto;
max-height: 400px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 20px; // Add margin at the bottom for spacing
.todo-table {
width: 100%;
border-collapse: collapse;
margin: 0;
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
font-weight: bold;
position: sticky;
top: 0;
z-index: 10;
}
tr:hover {
background-color: #f5f5f5;
}
.c-col {
width: 50px;
}
// Add padding at the bottom of the table body
tbody {
tr:last-child td {
padding-bottom: 20px; // Extra padding for the last row
}
}
}
}
.add-todo-section {
display: flex;
gap: 10px;
margin-top: 15px;
padding: 20px; // Increased padding all around
border-top: 1px solid #eee;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
.todo-input {
flex: 1;
padding: 12px; // Increased padding for better touch targets
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.add-button {
white-space: nowrap;
padding: 12px 20px; // Increased padding for better touch targets
}
}
.remove-button {
background: none;
border: none;
cursor: pointer;
padding: 8px; // Increased padding for better touch targets
border-radius: 3px;
color: #dc3545;
&:hover {
background-color: #e0e0e0;
}
}
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
.add-todo-section {
flex-direction: column;
}
.to-do-chart-container {
padding: 15px; // Adjust padding for mobile
}
}

View File

@@ -1,4 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-to-do-chart', selector: 'app-to-do-chart',
@@ -21,15 +24,49 @@ export class ToDoChartComponent 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
constructor() { } // Multi-layer drilldown state tracking
drilldownStack: any[] = []; // Stack to track drilldown navigation history
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
originalTodoList: string[] = [];
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchToDoData();
})
);
// Initialize with default data
this.fetchToDoData();
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('ToDoChartComponent input changes:', changes); console.log('ToDoChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
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;
@@ -46,26 +83,119 @@ export class ToDoChartComponent implements OnInit, OnChanges {
data: any; data: any;
todo: string; todo: string;
todoList = ['todo 1']; todoList: string[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
fetchToDoData(): void { fetchToDoData(): void {
// If we have the necessary data, fetch to-do data from the service // If we have the necessary data, fetch to-do data from the service
if (this.table) { if (this.table && this.xAxis) {
console.log('Fetching to-do data for:', { table: this.table }); console.log('Fetching to-do data for:', { table: this.table, xAxis: this.xAxis, connection: this.connection });
// For to-do chart, we might want to fetch data differently // Convert baseFilters to filter parameters
// This is a placeholder implementation - you may need to adjust based on your API let filterParams = '';
console.log('To-do chart would fetch data from table:', this.table); if (this.baseFilters && this.baseFilters.length > 0) {
const filterObj = {};
this.baseFilters.forEach(filter => {
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value;
}
});
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
}
// In a real implementation, you would connect to your service here // Add common filters to filter parameters
// For now, we'll just keep the default to-do list const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else { } else {
console.log('Missing required data for to-do chart:', { table: this.table }); // Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Fetch data from the dashboard service
this.dashboardService.getChartData(this.table, 'todo', this.xAxis, '', this.connection, '', '', filterParams).subscribe(
(data: any) => {
console.log('Received to-do chart data:', data);
if (data === null) {
console.warn('To-do chart API returned null data. Check if the API endpoint is working correctly.');
this.todoList = [];
return;
}
// Handle the actual data structure returned by the API
if (data && data.chartLabels) {
// Use chartLabels as the todo items
this.todoList = data.chartLabels;
} else if (data && data.labels) {
// Fallback to labels if chartLabels is not available
this.todoList = data.labels;
} else {
console.warn('To-do chart received data does not have expected structure', data);
this.todoList = [];
}
},
(error) => {
console.error('Error fetching to-do chart data:', error);
this.todoList = [];
}
);
} else {
console.log('Missing required data for to-do chart:', { table: this.table, xAxis: this.xAxis });
// Initialize with default data if no table is specified
if (this.todoList.length === 0) {
this.todoList = ['Sample Task 1', 'Sample Task 2', 'Sample Task 3'];
}
} }
} }
public addTodo(todo: string) { public addTodo(todo: string) {
this.todoList.push(todo); if (todo && todo.trim() !== '') {
this.todoList.push(todo.trim());
this.todo = ''; // Clear the input field
}
} }
public removeTodo(todoIx: number) { public removeTodo(todoIx: number) {
@@ -73,4 +203,353 @@ export class ToDoChartComponent implements OnInit, OnChanges {
this.todoList.splice(todoIx, 1); this.todoList.splice(todoIx, 1);
} }
} }
// 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.fetchToDoData();
} else {
// Back to base level
console.log('Back to base level, resetting to original data');
this.todoList = [...this.originalTodoList];
}
} else {
// Already at base level, reset to original data
console.log('Already at base level, resetting to original data');
this.todoList = [...this.originalTodoList];
}
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchToDoData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchToDoData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchToDoData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchToDoData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchToDoData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchToDoData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchToDoData();
}
ngOnDestroy(): void {
// Unsubscribe from all subscriptions to prevent memory leaks
this.subscriptions.forEach(subscription => subscription.unsubscribe());
// Remove document click handler if it exists
this.removeDocumentClickHandler();
}
} }

View File

@@ -0,0 +1 @@
export * from './unified-chart.component';

View File

@@ -0,0 +1,365 @@
<div class="chart-container" *ngIf="!noDataAvailable && !isLoading">
<!-- Back button for drilldown navigation -->
<div class="drilldown-back" *ngIf="currentDrilldownLevel > 0">
<button class="btn btn-sm btn-secondary" (click)="navigateBack()">
<clr-icon shape="arrow" dir="left"></clr-icon>
Back
</button>
<span class="drilldown-level">Level: {{ currentDrilldownLevel }}</span>
</div>
<!-- Chart title -->
<div class="chart-title" *ngIf="charttitle">
<h4>{{ charttitle }}</h4>
</div>
<!-- Render different chart types based on chartType input -->
<div class="chart-wrapper">
<!-- Bar Chart -->
<div *ngIf="chartType === 'bar'">
<canvas baseChart
[data]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'bar'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Line Chart -->
<div *ngIf="chartType === 'line'">
<canvas baseChart
[data]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'line'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Pie Chart -->
<div *ngIf="chartType === 'pie'">
<canvas baseChart
[data]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'pie'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Doughnut Chart -->
<div *ngIf="chartType === 'doughnut'">
<canvas baseChart
[data]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'doughnut'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Bubble Chart -->
<div *ngIf="chartType === 'bubble'">
<canvas baseChart
[datasets]="bubbleChartData"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'bubble'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Radar Chart -->
<div *ngIf="chartType === 'radar'">
<canvas baseChart
[data]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'radar'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Polar Area Chart -->
<div *ngIf="chartType === 'polar'">
<canvas baseChart
[data]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'polarArea'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Scatter Chart -->
<div *ngIf="chartType === 'scatter'">
<canvas baseChart
[datasets]="chartData"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'scatter'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Default/Unknown Chart Type -->
<div *ngIf="!['bar', 'line', 'pie', 'doughnut', 'bubble', 'radar', 'polar', 'scatter'].includes(chartType)">
<canvas baseChart
[data]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'bar'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
</div>
<!-- Base Filters -->
<div class="filters-section" *ngIf="baseFilters && baseFilters.length > 0">
<h5>Filters</h5>
<div class="filters-container">
<div class="filter-item" *ngFor="let filter of baseFilters; let i = index">
<!-- Text Filter -->
<div *ngIf="filter.type === 'text'" class="filter-text">
<label>{{ filter.field }}</label>
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)"
class="form-control" placeholder="Enter {{ filter.field }}">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
<label>{{ filter.field }}</label>
<select [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)" class="form-control">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">
{{ option }}
</option>
</select>
</div>
<!-- Multiselect Filter -->
<div *ngIf="filter.type === 'multiselect'" class="filter-multiselect">
<label>{{ filter.field }}</label>
<div class="multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span *ngIf="getSelectedOptionsCount(filter) === 0">Select {{ filter.field }}</span>
<span *ngIf="getSelectedOptionsCount(filter) > 0">
{{ getSelectedOptionsCount(filter) }} selected
</span>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="multiselect-option" *ngFor="let option of getFilterOptions(filter)">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
id="base-{{ filter.field }}-{{ option }}">
<label [for]="'base-' + filter.field + '-' + option">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-date-range">
<label>{{ filter.field }}</label>
<div class="date-range-inputs">
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeChange(filter, filter.value)"
class="form-control" placeholder="Start Date">
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeChange(filter, filter.value)"
class="form-control" placeholder="End Date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-toggle">
<label>{{ filter.field }}</label>
<div class="toggle-switch">
<input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"
id="toggle-{{ filter.field }}">
<label [for]="'toggle-' + filter.field" class="toggle-label">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filters-section" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h5>Drilldown Filters</h5>
<div class="filters-container">
<div class="filter-item" *ngFor="let filter of drilldownFilters; let i = index">
<!-- Text Filter -->
<div *ngIf="filter.type === 'text'" class="filter-text">
<label>{{ filter.field }}</label>
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)"
class="form-control" placeholder="Enter {{ filter.field }}">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
<label>{{ filter.field }}</label>
<select [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)" class="form-control">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">
{{ option }}
</option>
</select>
</div>
<!-- Multiselect Filter -->
<div *ngIf="filter.type === 'multiselect'" class="filter-multiselect">
<label>{{ filter.field }}</label>
<div class="multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span *ngIf="getSelectedOptionsCount(filter) === 0">Select {{ filter.field }}</span>
<span *ngIf="getSelectedOptionsCount(filter) > 0">
{{ getSelectedOptionsCount(filter) }} selected
</span>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="multiselect-option" *ngFor="let option of getFilterOptions(filter)">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
id="drilldown-{{ filter.field }}-{{ option }}">
<label [for]="'drilldown-' + filter.field + '-' + option">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-date-range">
<label>{{ filter.field }}</label>
<div class="date-range-inputs">
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeChange(filter, filter.value)"
class="form-control" placeholder="Start Date">
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeChange(filter, filter.value)"
class="form-control" placeholder="End Date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-toggle">
<label>{{ filter.field }}</label>
<div class="toggle-switch">
<input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"
id="drilldown-toggle-{{ filter.field }}">
<label [for]="'drilldown-toggle-' + filter.field" class="toggle-label">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filters-section" *ngIf="hasActiveLayerFilters()">
<h5>Layer Filters</h5>
<div class="filters-container">
<div class="filter-item" *ngFor="let filter of getActiveLayerFilters(); let i = index">
<!-- Text Filter -->
<div *ngIf="filter.type === 'text'" class="filter-text">
<label>{{ filter.field }}</label>
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)"
class="form-control" placeholder="Enter {{ filter.field }}">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
<label>{{ filter.field }}</label>
<select [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)" class="form-control">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">
{{ option }}
</option>
</select>
</div>
<!-- Multiselect Filter -->
<div *ngIf="filter.type === 'multiselect'" class="filter-multiselect">
<label>{{ filter.field }}</label>
<div class="multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span *ngIf="getSelectedOptionsCount(filter) === 0">Select {{ filter.field }}</span>
<span *ngIf="getSelectedOptionsCount(filter) > 0">
{{ getSelectedOptionsCount(filter) }} selected
</span>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="multiselect-option" *ngFor="let option of getFilterOptions(filter)">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
id="layer-{{ filter.field }}-{{ option }}">
<label [for]="'layer-' + filter.field + '-' + option">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-date-range">
<label>{{ filter.field }}</label>
<div class="date-range-inputs">
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeChange(filter, filter.value)"
class="form-control" placeholder="Start Date">
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeChange(filter, filter.value)"
class="form-control" placeholder="End Date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-toggle">
<label>{{ filter.field }}</label>
<div class="toggle-switch">
<input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"
id="layer-toggle-{{ filter.field }}">
<label [for]="'layer-toggle-' + filter.field" class="toggle-label">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="clear-filters" *ngIf="hasActiveFilters()">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">
Clear All Filters
</button>
</div>
</div>
<!-- No Data Available Message -->
<div class="no-data-message" *ngIf="noDataAvailable && !isLoading">
<p>No data available for the selected filters.</p>
<button class="btn btn-sm btn-primary" (click)="fetchChartData()">Retry</button>
</div>
<!-- Loading Indicator -->
<div class="loading-indicator" *ngIf="isLoading">
<div class="spinner"></div>
<p>Loading chart data...</p>
</div>

View File

@@ -0,0 +1,262 @@
.chart-container {
position: relative;
height: 100%;
width: 100%;
padding: 10px;
}
.drilldown-back {
display: flex;
align-items: center;
margin-bottom: 10px;
.drilldown-level {
margin-left: 10px;
font-size: 14px;
color: #666;
}
}
.chart-title {
text-align: center;
margin-bottom: 15px;
h4 {
margin: 0;
color: #333;
}
}
.chart-wrapper {
position: relative;
height: calc(100% - 100px);
min-height: 300px;
}
.filters-section {
margin-top: 20px;
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #f9f9f9;
h5 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
}
}
.filters-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 200px;
min-width: 150px;
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #555;
}
.form-control {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
}
.filter-text,
.filter-dropdown,
.filter-date-range {
.form-control {
height: 36px;
}
}
.date-range-inputs {
display: flex;
gap: 10px;
.form-control {
flex: 1;
}
}
.filter-multiselect {
position: relative;
.multiselect-container {
position: relative;
}
.multiselect-display {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: white;
cursor: pointer;
min-height: 36px;
display: flex;
align-items: center;
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1000;
max-height: 200px;
overflow-y: auto;
}
.multiselect-option {
padding: 8px 12px;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
&:hover {
background-color: #f5f5f5;
}
input[type="checkbox"] {
margin-right: 8px;
}
label {
margin: 0;
font-weight: normal;
cursor: pointer;
}
}
}
.filter-toggle {
display: flex;
align-items: center;
gap: 10px;
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-label {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.toggle-slider {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-label {
background-color: #2196F3;
}
input:checked + .toggle-label .toggle-slider {
transform: translateX(26px);
}
}
.clear-filters {
margin-top: 15px;
text-align: center;
}
.no-data-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
text-align: center;
color: #666;
p {
margin-bottom: 15px;
font-size: 16px;
}
}
.loading-indicator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
p {
margin: 0;
color: #666;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// Responsive adjustments
@media (max-width: 768px) {
.filters-container {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.chart-wrapper {
min-height: 250px;
}
}

View File

@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UnifiedChartComponent } from './unified-chart.component';
describe('UnifiedChartComponent', () => {
let component: UnifiedChartComponent;
let fixture: ComponentFixture<UnifiedChartComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [UnifiedChartComponent]
});
fixture = TestBed.createComponent(UnifiedChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -51,8 +51,7 @@ export class DashrunnerallComponent implements OnInit {
} }
getdashboard() getdashboard() {
{
this.dashboardService.getAllDash().subscribe((data) => { this.dashboardService.getAllDash().subscribe((data) => {
this.data = data; this.data = data;
this.rows = this.data; this.rows = this.data;
@@ -62,17 +61,23 @@ export class DashrunnerallComponent implements OnInit {
}); });
} }
openModal() openModal() {
{
this.addModall = true; this.addModall = true;
} }
gotoadd() gotoadd() {
{
this.router.navigate(['../../dashboardbuilder'], { relativeTo: this.route }); this.router.navigate(['../../dashboardbuilder'], { relativeTo: this.route });
} }
goToEdit(id:number) // for runner line navigation
{ // goToEditData(id: number){
this.router.navigate(['../dashrunner/'+id],{relativeTo:this.route}); // this.router.navigate(['../editdata/'+id],{relativeTo:this.route});
// }
goToEdit(id: number) {
// Navigate to editnewdash component instead of dashrunnerline
// Pass a query parameter to indicate this is from dashboard runner
this.router.navigate(['../../dashboardbuilder/editdashn/' + id], {
relativeTo: this.route,
queryParams: { fromRunner: true }
});
} }
goToEditData(id: number) { goToEditData(id: number) {
@@ -96,8 +101,7 @@ export class DashrunnerallComponent implements OnInit {
console.log(this.rowSelected); console.log(this.rowSelected);
this.modalDelete = true; this.modalDelete = true;
} }
delete(id) delete(id) {
{
this.modalDelete = false; this.modalDelete = false;
console.log("in delete " + id); console.log("in delete " + id);
this.dashboardService.deleteField(id).subscribe((data) => { this.dashboardService.deleteField(id).subscribe((data) => {

View File

@@ -254,7 +254,7 @@ export class ReportbuildqueryComponent implements OnInit {
name; name;
databaseName; databaseName;
databasename(val) { databasename(val) {
console.log(val); console.log('connection ', val);
this.databaseName = val.name; this.databaseName = val.name;
this.selecteddatabase = val.conn_string; this.selecteddatabase = val.conn_string;
console.log(this.selecteddatabase); console.log(this.selecteddatabase);

View File

@@ -23,6 +23,18 @@
<label for="workflow_name">{{'ACTIVE'| translate}}</label> <label for="workflow_name">{{'ACTIVE'| translate}}</label>
<input type="checkbox" formControlName="active" clrToggle value="billable" name="billable" /> <input type="checkbox" formControlName="active" clrToggle value="billable" name="billable" />
</div> </div>
<!-- SureConnect Dropdown -->
<div class="clr-col-md-4 clr-col-sm-12">
<label for="sureConnectId">SureConnect Connection</label>
<select formControlName="sureConnectId" class="clr-select">
<option value="">-- Select SureConnect --</option>
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
{{conn.connection_name || conn.id}}
</option>
</select>
</div>
<!-- <!--
<div class="clr-col-md-4 clr-col-sm-12"> <div class="clr-col-md-4 clr-col-sm-12">
<label for="url">Get URL</label> <label for="url">Get URL</label>

View File

@@ -4,6 +4,8 @@ import { FormBuilder, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { ReportBuilderService } from 'src/app/services/api/report-builder.service'; import { ReportBuilderService } from 'src/app/services/api/report-builder.service';
import { SureconnectService } from '../../dashboardnew/sureconnect/sureconnect.service';
@Component({ @Component({
selector: 'app-report-build2add', selector: 'app-report-build2add',
templateUrl: './report-build2add.component.html', templateUrl: './report-build2add.component.html',
@@ -11,8 +13,12 @@ import { ReportBuilderService } from 'src/app/services/api/report-builder.servic
}) })
export class ReportBuild2addComponent implements OnInit { export class ReportBuild2addComponent implements OnInit {
public entryForm: FormGroup; public entryForm: FormGroup;
// Add sureconnect data property
sureconnectData: any[] = [];
constructor(private _fb: FormBuilder, private router: Router,private toastr: ToastrService, constructor(private _fb: FormBuilder, private router: Router,private toastr: ToastrService,
private route: ActivatedRoute,private reportBuilderService: ReportBuilderService) { } private route: ActivatedRoute,private reportBuilderService: ReportBuilderService,
private sureconnectService: SureconnectService) { }
ngOnInit(): void { ngOnInit(): void {
this.entryForm = this._fb.group({ this.entryForm = this._fb.group({
@@ -20,9 +26,23 @@ export class ReportBuild2addComponent implements OnInit {
description:[null], description:[null],
active:[null], active:[null],
isSql:[false], isSql:[false],
// Add sureConnectId field to the form
sureConnectId: [null],
Rpt_builder2_lines: this._fb.array([this.initLinesFormReport()]), Rpt_builder2_lines: this._fb.array([this.initLinesFormReport()]),
}); });
// Load sureconnect data
this.loadSureconnectData();
}
// Add method to load sureconnect data
loadSureconnectData() {
this.sureconnectService.getAll().subscribe((data: any[]) => {
this.sureconnectData = data;
console.log('Sureconnect data loaded:', this.sureconnectData);
}, (error) => {
console.log('Error loading sureconnect data:', error);
});
} }
initLinesFormReport() { initLinesFormReport() {

View File

@@ -22,37 +22,54 @@
<clr-datagrid [clrDgLoading]="loading"> <clr-datagrid [clrDgLoading]="loading">
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{'LOADING' | translate}}</clr-spinner></ng-template> <clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{'LOADING' | translate}}</clr-spinner></ng-template>
<div *ngIf="error;else loadingSpinner">{{error}}</div></clr-dg-placeholder> <div *ngIf="error;else loadingSpinner">{{error}}</div>
</clr-dg-placeholder>
<clr-dg-column [clrDgField]="''"><ng-container *clrDgHideableColumn="{hidden: false}"> <clr-dg-column [clrDgField]="''"><ng-container *clrDgHideableColumn="{hidden: false}">
{{'GO_TO' | translate}} {{'GO_TO' | translate}}
</ng-container></clr-dg-column> </ng-container></clr-dg-column>
<clr-dg-column [clrDgField]="'name'"><ng-container *clrDgHideableColumn="{hidden: false}"> <clr-dg-column [clrDgField]="'name'"><ng-container *clrDgHideableColumn="{hidden: false}">
{{'REPORT_NAME' | translate}} {{'REPORT_NAME' | translate}}
</ng-container></clr-dg-column> </ng-container></clr-dg-column>
<clr-dg-column [clrDgField]="'description'"><ng-container *clrDgHideableColumn="{hidden: false}"> <clr-dg-column [clrDgField]="'description'"><ng-container *clrDgHideableColumn="{hidden: false}">
{{'REPORT_DESCRIPTION' | translate}} {{'REPORT_DESCRIPTION' | translate}}
</ng-container></clr-dg-column> </ng-container></clr-dg-column>
<clr-dg-column [clrDgField]="''"><ng-container *clrDgHideableColumn="{hidden: false}">
Sureconnect
</ng-container></clr-dg-column>
<clr-dg-column [clrDgField]="'active'"><ng-container *clrDgHideableColumn="{hidden: false}"> <clr-dg-column [clrDgField]="'active'"><ng-container *clrDgHideableColumn="{hidden: false}">
{{'ACTIVE' | translate}} {{'ACTIVE' | translate}}
</ng-container></clr-dg-column> </ng-container></clr-dg-column>
<clr-dg-column><ng-container *clrDgHideableColumn="{hidden: false}"> <clr-dg-column><ng-container *clrDgHideableColumn="{hidden: false}">
<clr-icon shape="bars"></clr-icon>{{'ACTION' | translate}} <clr-icon shape="bars"></clr-icon>{{'ACTION' | translate}}
</ng-container></clr-dg-column> </ng-container></clr-dg-column>
<clr-dg-row *clrDgItems="let user of gridData?.slice()?.reverse();" [clrDgItem]="user"> <clr-dg-row *clrDgItems="let user of gridData?.slice()?.reverse();" [clrDgItem]="user">
<clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;" (click)="goToLines(user)"> {{'SET_UP' | translate}}</span></clr-dg-cell> <clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;"
(click)="goToLines(user)"> {{'SET_UP' | translate}}</span></clr-dg-cell>
<clr-dg-cell id="word">{{user.reportName}}</clr-dg-cell> <clr-dg-cell id="word">{{user.reportName}}</clr-dg-cell>
<clr-dg-cell id="word">{{user.description}}</clr-dg-cell> <clr-dg-cell id="word">{{user.description}}</clr-dg-cell>
<clr-dg-cell id="word">{{user.sureconnect_name}}</clr-dg-cell>
<clr-dg-cell id="word">{{user.active}}</clr-dg-cell> <clr-dg-cell id="word">{{user.active}}</clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true" class="tooltip tooltip-sm tooltip-top-left"> <a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true"
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error" style="color: red;"></clr-icon></span> class="tooltip tooltip-sm tooltip-top-left">
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error"
style="color: red;"></clr-icon></span>
<span class="tooltip-content"> {{'DELETE' | translate}}</span> <span class="tooltip-content"> {{'DELETE' | translate}}</span>
</a> </a>
<clr-signpost> <clr-signpost>
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span> <span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success"
style="color: rgb(0, 130, 236);"></clr-icon></span>
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen> <clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
<h5 style="margin-top: 0">{{'WHO_COLUMN' | translate}}</h5> <h5 style="margin-top: 0">{{'WHO_COLUMN' | translate}}</h5>
<div>{{'ACCOUNT_ID' | translate}}: <code class="clr-code">{{user.accountId}}</code></div> <div>{{'ACCOUNT_ID' | translate}}: <code class="clr-code">{{user.accountId}}</code></div>

View File

@@ -26,20 +26,35 @@
</div> --> </div> -->
<div class="clr-col-md-4 clr-col-sm-12"> <div class="clr-col-md-4 clr-col-sm-12">
<label for="url">Get URL</label> <label for="url">Get URL</label>
<div> <input type="text" class="clr-input" formControlName="url" name="url" [(ngModel)]="nodeEditProperties.url" placeholder="Enter Url" style="width: 76%">&nbsp;<span><button class="btn btn-icon btn-primary" (click)="getkeys()"> <div> <input type="text" class="clr-input" formControlName="url" name="url"
[(ngModel)]="nodeEditProperties.url" placeholder="Enter Url" style="width: 76%">&nbsp;<span><button
class="btn btn-icon btn-primary" (click)="getkeys()">
<clr-icon shape="view-list"></clr-icon> <clr-icon shape="view-list"></clr-icon>
</button></span></div> </button></span></div>
</div> </div>
<div class="clr-col-md-4 clr-col-sm-12"> <div class="clr-col-md-4 clr-col-sm-12">
<label for="workflow_name">Include Date filter</label> <label for="workflow_name">Include Date filter</label>
<input type="checkbox" formControlName="date_param_req" name="date_param_req" [(ngModel)]="nodeEditProperties.date_param_req" clrToggle /> <input type="checkbox" formControlName="date_param_req" name="date_param_req"
[(ngModel)]="nodeEditProperties.date_param_req" clrToggle />
</div> </div>
<!-- SureConnect Dropdown -->
<div class="clr-col-md-4 clr-col-sm-12">
<label for="sureConnectId">SureConnect Connection</label>
<select formControlName="sureConnectId" class="clr-select">
<option value="">-- Select SureConnect --</option>
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
{{conn.name || conn.id}}
</option>
</select>
</div>
<div class="clr-col-md-4 clr-col-sm-12"> <div class="clr-col-md-4 clr-col-sm-12">
<label>Standard Parameters</label> <label>Standard Parameters</label>
<clr-combobox-container style="margin-top: 0; padding-top: 0;"> <clr-combobox-container style="margin-top: 0; padding-top: 0;">
<!-- <label style="padding-bottom: 5px; padding-top:0px; font-weight: lighter;" class="p1">Select Left Side Filter</label> --> <!-- <label style="padding-bottom: 5px; padding-top:0px; font-weight: lighter;" class="p1">Select Left Side Filter</label> -->
<clr-combobox formControlName="std_param_html" name="std_param_html" [(ngModel)]="nodeEditProperties.std_param_html" clrMulti="true" <clr-combobox formControlName="std_param_html" name="std_param_html"
required> [(ngModel)]="nodeEditProperties.std_param_html" clrMulti="true" required>
<ng-container *clrOptionSelected="let selected"> <ng-container *clrOptionSelected="let selected">
{{selected}} {{selected}}
</ng-container> </ng-container>
@@ -92,4 +107,3 @@
</div> </div>
</form> </form>
</div> </div>

View File

@@ -3,6 +3,7 @@ import { FormBuilder, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { ReportBuilderService } from 'src/app/services/api/report-builder.service'; import { ReportBuilderService } from 'src/app/services/api/report-builder.service';
import { SureconnectService } from '../../dashboardnew/sureconnect/sureconnect.service';
@Component({ @Component({
@@ -23,11 +24,17 @@ export class ReportBuild2editComponent implements OnInit {
date_param_req: '', date_param_req: '',
// folderName:'', // folderName:'',
url: '', url: '',
// Add sureConnectId property
sureConnectId: null,
}; };
// Add sureconnect data property
sureconnectData: any[] = [];
constructor(private router: Router, constructor(private router: Router,
private route: ActivatedRoute, private reportBuilderService: ReportBuilderService, private route: ActivatedRoute, private reportBuilderService: ReportBuilderService,
private toastr: ToastrService, private _fb: FormBuilder) { } private toastr: ToastrService, private _fb: FormBuilder,
private sureconnectService: SureconnectService) { }
ngOnInit(): void { ngOnInit(): void {
this.id = this.route.snapshot.params["id"]; this.id = this.route.snapshot.params["id"];
@@ -41,11 +48,26 @@ export class ReportBuild2editComponent implements OnInit {
date_param_req: [null], date_param_req: [null],
// folderName:[null], // folderName:[null],
url: [null], url: [null],
// Add sureConnectId to form
sureConnectId: [null],
}); });
// Load sureconnect data first, then load report data
this.loadSureconnectData();
this.getById(this.id); this.getById(this.id);
this.listoddatabase(); this.listoddatabase();
} }
// Add method to load sureconnect data
loadSureconnectData() {
this.sureconnectService.getAll().subscribe((data: any[]) => {
this.sureconnectData = data;
console.log('Sureconnect data loaded:', this.sureconnectData);
}, (error) => {
console.log('Error loading sureconnect data:', error);
});
}
databaselist; databaselist;
listoddatabase() { listoddatabase() {
this.reportBuilderService.getdatabse().subscribe((data) => { this.reportBuilderService.getdatabse().subscribe((data) => {
@@ -71,8 +93,7 @@ export class ReportBuild2editComponent implements OnInit {
this.builderLine = this.ReportData.rpt_builder2_lines; this.builderLine = this.ReportData.rpt_builder2_lines;
this.lineId = this.builderLine[0].id this.lineId = this.builderLine[0].id
console.log("line data ", this.lineId, this.builderLine); console.log("line data ", this.lineId, this.builderLine);
if(this.builderLine[0].model != '') if (this.builderLine[0].model != '') {
{
this.builderLineData = JSON.parse(this.builderLine[0].model); this.builderLineData = JSON.parse(this.builderLine[0].model);
console.log(this.builderLineData); console.log(this.builderLineData);
@@ -82,6 +103,11 @@ export class ReportBuild2editComponent implements OnInit {
// this.nodeEditProperties.conn_name = this.builderLineData.conn_name; // this.nodeEditProperties.conn_name = this.builderLineData.conn_name;
this.nodeEditProperties.date_param_req = this.builderLineData[0].date_param_req; this.nodeEditProperties.date_param_req = this.builderLineData[0].date_param_req;
this.nodeEditProperties.url = this.builderLineData[0].url; this.nodeEditProperties.url = this.builderLineData[0].url;
// Set sureConnectId if it exists in the data
this.nodeEditProperties.sureConnectId = this.builderLineData[0].sureConnectId || null;
// Update form with loaded data
this.entryForm.patchValue(this.nodeEditProperties);
} }
}, },
(err) => { (err) => {
@@ -95,7 +121,7 @@ export class ReportBuild2editComponent implements OnInit {
getkeys() { getkeys() {
if (this.nodeEditProperties.url !== null) { if (this.nodeEditProperties.url !== null) {
this.reportBuilderService.getcolumnDetailsByurl(this.nodeEditProperties.url).subscribe(data => { this.reportBuilderService.getcolumnDetailsByurl(this.nodeEditProperties.url).subscribe(data => {
console.log(data); console.log('coloum list data ', data);
this.keysfromurl = data; this.keysfromurl = data;
this.nodeEditProperties.adhoc_param_html = this.keysfromurl; this.nodeEditProperties.adhoc_param_html = this.keysfromurl;
}) })
@@ -116,6 +142,8 @@ export class ReportBuild2editComponent implements OnInit {
adhoc_param_html: this.nodeEditProperties.adhoc_param_html, adhoc_param_html: this.nodeEditProperties.adhoc_param_html,
date_param_req: this.nodeEditProperties.date_param_req, date_param_req: this.nodeEditProperties.date_param_req,
url: this.nodeEditProperties.url, url: this.nodeEditProperties.url,
// Add sureConnectId to the data
sureConnectId: this.nodeEditProperties.sureConnectId,
}; };
this.builderLineData[0].std_param_html = this.nodeEditProperties.std_param_html; this.builderLineData[0].std_param_html = this.nodeEditProperties.std_param_html;
@@ -124,6 +152,9 @@ export class ReportBuild2editComponent implements OnInit {
// this.builderLineData.conn_name = this.nodeEditProperties.conn_name ; // this.builderLineData.conn_name = this.nodeEditProperties.conn_name ;
this.builderLineData[0].date_param_req = this.nodeEditProperties.date_param_req; this.builderLineData[0].date_param_req = this.nodeEditProperties.date_param_req;
this.builderLineData[0].url = this.nodeEditProperties.url; this.builderLineData[0].url = this.nodeEditProperties.url;
// Add sureConnectId to the data
this.builderLineData[0].sureConnectId = this.nodeEditProperties.sureConnectId;
console.log(this.builderLineData); console.log(this.builderLineData);
// this.builderLineData.splice(1); // this.builderLineData.splice(1);
console.log(this.builderLineData); console.log(this.builderLineData);
@@ -149,6 +180,8 @@ console.log(this.listBuilder_Lines);
onSubmit() { onSubmit() {
this.updated = true; this.updated = true;
// Update nodeEditProperties with form values including sureConnectId
Object.assign(this.nodeEditProperties, this.entryForm.value);
this.update(); this.update();
} }

View File

@@ -104,8 +104,11 @@ todayDate;
adhocList:any[]; adhocList:any[];
SQLQuery; SQLQuery;
getUrl; getUrl;
// Add sureConnectId property
sureConnectId: number | null = null;
stdParamfields; stdParamfields;
DateParam; DateParam;
getById(id: number) { getById(id: number) {
this.reportBuilderService.getrbDetailsById(id).subscribe( this.reportBuilderService.getrbDetailsById(id).subscribe(
(data) => { (data) => {
@@ -120,6 +123,8 @@ todayDate;
// this.adhocList = JSON.parse(adhocList); // this.adhocList = JSON.parse(adhocList);
this.DateParam = this.builderLineData.date_param_req; this.DateParam = this.builderLineData.date_param_req;
this.getUrl = this.builderLineData.url; this.getUrl = this.builderLineData.url;
// Get sureConnectId if it exists
this.sureConnectId = this.builderLineData.sureConnectId || null;
console.log(this.adhocList,this.DateParam,this.getUrl) console.log(this.adhocList,this.DateParam,this.getUrl)
this.getStdParam(this.header_id); this.getStdParam(this.header_id);
this.featchData(); this.featchData();
@@ -127,14 +132,50 @@ todayDate;
} }
/**
* Fetch data using the URL and SureConnect connection if available
*/
featchData(){ featchData(){
// If sureConnectId is available, we might want to modify the request
// For now, we'll use the existing implementation but with better error handling
this.reportBuilderService.getAllDetailsByurl(this.getUrl).subscribe(data =>{ this.reportBuilderService.getAllDetailsByurl(this.getUrl).subscribe(data =>{
console.log(data); console.log(data);
if(data.body){ if(data.body){
// Check if the response is XML (starts with <) or JSON
if (typeof data.body === 'string' && data.body.trim().startsWith('<')) {
// Handle XML response
console.log('Received XML response, parsing to JSON');
try {
this.rows = this.parseXMLToJSON(data.body);
this.filterRows = [...this.rows]; // Create a copy
console.log('Parsed XML data:', this.rows);
} catch (error) {
console.error('Error parsing XML:', error);
this.rows = [];
this.filterRows = [];
}
} else {
// Handle JSON response
try {
console.log(JSON.parse(data.body)); console.log(JSON.parse(data.body));
this.rows = JSON.parse(data.body); this.rows = JSON.parse(data.body);
this.filterRows = JSON.parse(data.body); this.filterRows = JSON.parse(data.body);
} catch (error) {
console.error('Error parsing JSON:', error);
this.rows = [];
this.filterRows = [];
} }
}
} else {
// If no body, initialize with empty arrays
this.rows = [];
this.filterRows = [];
}
}, error => {
// Handle HTTP errors
console.error('Error fetching data:', error);
this.rows = [];
this.filterRows = [];
}); });
} }
@@ -595,13 +636,10 @@ this.rowdata= [this.rows];
var j; var j;
var cart = []; var cart = [];
for(var i = 0; i < data.length; i++) for (var i = 0; i < data.length; i++) {
{
var columnsIn = data[i]; var columnsIn = data[i];
if(i==1) if (i == 1) {
{ for (var key in columnsIn) {
for(var key in columnsIn)
{
j = { prop: key, name: key }; j = { prop: key, name: key };
cart.push(j) cart.push(j)
@@ -702,6 +740,14 @@ selectcolumn(data: any) {
filtered = false; filtered = false;
filterRowsBySelectedValues() { filterRowsBySelectedValues() {
// Check if rows is defined and iterable
if (!this.rows || !Array.isArray(this.rows)) {
console.warn('Rows is not defined or not an array');
this.filterRows = [];
this.filtered = false;
return;
}
// Create a filteredRows array to store the filtered data // Create a filteredRows array to store the filtered data
const filteredRows = []; const filteredRows = [];
@@ -828,4 +874,81 @@ isDate(value: any): boolean {
); );
} }
/**
* Simple XML parser to convert XML to JSON array
* Handles the specific format: <User><id>101</id><name>John Doe</name><email>john.doe@example.com</email></User>
* @param xmlString The XML string to parse
* @returns Array of objects representing the XML data
*/
private parseXMLToJSON(xmlString: string): any[] {
// Remove any XML declaration
xmlString = xmlString.replace(/<\?xml[^>]*\?>/g, '');
// Extract the root element name (e.g., "User" from <User>...</User>)
const rootMatch = xmlString.match(/<(\w+)(?:\s[^>]*)?>/);
if (!rootMatch) {
console.warn('Could not identify root element in XML');
return [];
}
const rootElement = rootMatch[1];
const results: any[] = [];
// Create a regex to match all instances of the root element
const regex = new RegExp(`<${rootElement}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${rootElement}>`, 'g');
let match;
while ((match = regex.exec(xmlString)) !== null) {
const elementContent = match[1];
const item: any = {};
// Extract all tags and their values
const tagRegex = /<(\w+)>(.*?)<\/\1>/g;
let tagMatch;
while ((tagMatch = tagRegex.exec(elementContent)) !== null) {
const tagName = tagMatch[1];
const tagValue = tagMatch[2];
// Try to convert to appropriate type
item[tagName] = this.convertValueType(tagValue);
}
results.push(item);
}
return results;
}
/**
* Convert string values to appropriate types (number, boolean, etc.)
* @param value The string value to convert
* @returns The value converted to the appropriate type
*/
private convertValueType(value: string): any {
// Check for empty value
if (value === '') {
return value;
}
// Check for boolean values
if (value.toLowerCase() === 'true') {
return true;
}
if (value.toLowerCase() === 'false') {
return false;
}
// Check for numeric values
if (!isNaN(Number(value)) && !isNaN(parseFloat(value))) {
// Check if it's an integer or float
if (Number.isInteger(parseFloat(value))) {
return parseInt(value, 10);
} else {
return parseFloat(value);
}
}
// Return as string if no other type matches
return value;
}
} }

View File

@@ -39,6 +39,14 @@
</div> </div>
</a> </a>
<a href="javascript://" class="nav-link nav-icon modern-nav-icon" routerLinkActive="active"
routerLink="/cns-portal/shield-dashboard">
<div class="nav-icon-wrapper">
<clr-icon shape="shield" solid></clr-icon>
<span class="nav-tooltip">Shield Dashboard</span>
</div>
</a>
<a href="javascript://" class="nav-link nav-icon modern-nav-icon" routerLinkActive="active" <a href="javascript://" class="nav-link nav-icon modern-nav-icon" routerLinkActive="active"
routerLink="/cns-portal/rerunner/all" (click)="getName()"> routerLink="/cns-portal/rerunner/all" (click)="getName()">
<div class="nav-icon-wrapper"> <div class="nav-icon-wrapper">
@@ -122,27 +130,27 @@
<a href="javascript://" clrDropdownItem (click)="switchLanguage('en')" class="modern-lang-item"> <a href="javascript://" clrDropdownItem (click)="switchLanguage('en')" class="modern-lang-item">
<clr-icon shape="globe" class="lang-icon"></clr-icon> <clr-icon shape="globe" class="lang-icon"></clr-icon>
<span>English</span> <span>English</span>
<div class="lang-flag">🇺🇸</div> <div class="lang-flag">🇺🇸</div>
</a> </a>
<a href="javascript://" clrDropdownItem (click)="switchLanguage('hi')" class="modern-lang-item"> <a href="javascript://" clrDropdownItem (click)="switchLanguage('hi')" class="modern-lang-item">
<clr-icon shape="globe" class="lang-icon"></clr-icon> <clr-icon shape="globe" class="lang-icon"></clr-icon>
<span>हिन्दी</span> <span>हिन्दी</span>
<div class="lang-flag">🇮🇳</div> <div class="lang-flag">🇮🇳</div>
</a> </a>
<a href="javascript://" clrDropdownItem (click)="switchLanguage('ta')" class="modern-lang-item"> <a href="javascript://" clrDropdownItem (click)="switchLanguage('ta')" class="modern-lang-item">
<clr-icon shape="globe" class="lang-icon"></clr-icon> <clr-icon shape="globe" class="lang-icon"></clr-icon>
<span>தமிழ்</span> <span>தமிழ்</span>
<div class="lang-flag">🇮🇳</div> <div class="lang-flag">🇮🇳</div>
</a> </a>
<a href="javascript://" clrDropdownItem (click)="switchLanguage('pa')" class="modern-lang-item"> <a href="javascript://" clrDropdownItem (click)="switchLanguage('pa')" class="modern-lang-item">
<clr-icon shape="globe" class="lang-icon"></clr-icon> <clr-icon shape="globe" class="lang-icon"></clr-icon>
<span>ਪੰਜਾਬੀ</span> <span>ਪੰਜਾਬੀ</span>
<div class="lang-flag">🇮🇳</div> <div class="lang-flag">🇮🇳</div>
</a> </a>
<a href="javascript://" clrDropdownItem (click)="switchLanguage('ml')" class="modern-lang-item"> <a href="javascript://" clrDropdownItem (click)="switchLanguage('ml')" class="modern-lang-item">
<clr-icon shape="globe" class="lang-icon"></clr-icon> <clr-icon shape="globe" class="lang-icon"></clr-icon>
<span>മലയാളം</span> <span>മലയാളം</span>
<div class="lang-flag">🇮🇳</div> <div class="lang-flag">🇮🇳</div>
</a> </a>
</clr-dropdown-menu> </clr-dropdown-menu>
</clr-dropdown> </clr-dropdown>

View File

@@ -1,3 +1,26 @@
import { Ad10Component } from './BuilderComponents/angulardatatype/Ad10/Ad10.component';
import { DefatestComponent } from './BuilderComponents/defu/Defatest/Defatest.component';
import { ChildformComponent } from './BuilderComponents/stpkg/Childform/Childform.component';
import { DistrictComponent } from './BuilderComponents/testdata/District/District.component';
import { StateComponent } from './BuilderComponents/testdata/State/State.component';
import { CountryComponent } from './BuilderComponents/testdata/Country/Country.component';
import { Ad9Component } from './BuilderComponents/angulardatatype/Ad9/Ad9.component';
import { Ad8Component } from './BuilderComponents/angulardatatype/Ad8/Ad8.component';
import { Ad7Component } from './BuilderComponents/angulardatatype/Ad7/Ad7.component';
import { Ad6Component } from './BuilderComponents/angulardatatype/Ad6/Ad6.component';
import { Adv5Component } from './BuilderComponents/angulardatatype/Adv5/Adv5.component';
import { Adv4Component } from './BuilderComponents/angulardatatype/Adv4/Adv4.component';
import { SupportComponent } from './BuilderComponents/angulardatatype/Support/Support.component';
import { Adv3Component } from './BuilderComponents/angulardatatype/Adv3/Adv3.component';
import { Dv2Component } from './BuilderComponents/angulardatatype/Dv2/Dv2.component';
import { Adv1Component } from './BuilderComponents/angulardatatype/Adv1/Adv1.component';
import { Basicp3Component } from './BuilderComponents/angulardatatype/Basicp3/Basicp3.component';
import { Basicp2Component } from './BuilderComponents/angulardatatype/Basicp2/Basicp2.component';
import { Basicp1Component } from './BuilderComponents/angulardatatype/Basicp1/Basicp1.component';
import { SequencegenaratorComponent } from './fnd/sequencegenarator/sequencegenarator.component'; import { SequencegenaratorComponent } from './fnd/sequencegenarator/sequencegenarator.component';
import { Component, NgModule } from '@angular/core'; import { Component, NgModule } from '@angular/core';
@@ -82,6 +105,17 @@ import { MappingruleallComponent } from './datamanagement/mappingrule/mappingrul
import { MappingruleaddComponent } from './datamanagement/mappingrule/mappingruleadd/mappingruleadd.component'; import { MappingruleaddComponent } from './datamanagement/mappingrule/mappingruleadd/mappingruleadd.component';
import { MappingruleeditComponent } from './datamanagement/mappingrule/mappingruleedit/mappingruleedit.component'; import { MappingruleeditComponent } from './datamanagement/mappingrule/mappingruleedit/mappingruleedit.component';
import { Stepper_workflowComponent } from './BuilderComponents/stepperworkflow/Stepper_workflow/Stepper_workflow.component'; import { Stepper_workflowComponent } from './BuilderComponents/stepperworkflow/Stepper_workflow/Stepper_workflow.component';
import { AllapiregisteryComponent } from './fnd/apiregistery/allapiregistery/allapiregistery.component';
import { AddapiregisteryComponent } from './fnd/apiregistery/addapiregistery/addapiregistery.component';
import { EditapiregisteryComponent } from './fnd/apiregistery/editapiregistery/editapiregistery.component';
import { ApiregisterylineComponent } from './fnd/apiregistery/Apiregisteryline/Apiregisteryline.component';
import { Customer_informationComponent } from './BuilderComponents/angulardatatype/Customer_information/Customer_information.component';
import { Deployment_typeComponent } from './BuilderComponents/angulardatatype/Deployment_type/Deployment_type.component';
import { ManufacturerComponent } from './BuilderComponents/angulardatatype/Manufacturer/Manufacturer.component';
import { Order_summaryComponent } from './BuilderComponents/angulardatatype/Order_summary/Order_summary.component';
import { ProductComponent } from './BuilderComponents/angulardatatype/Product/Product.component';
import { TypesComponent } from './BuilderComponents/angulardatatype/Types/Types.component';
import { Test2Component } from './BuilderComponents/testdata/Test2/Test2.component';
import { Token_registeryComponent } from './fnd/Token_registery/Token_registery.component'; import { Token_registeryComponent } from './fnd/Token_registery/Token_registery.component';
import { MyworkspaceComponent } from './admin/myworkspace/myworkspace.component'; import { MyworkspaceComponent } from './admin/myworkspace/myworkspace.component';
import { ThemeCustomizationComponent } from './theme-customization/theme-customization.component'; import { ThemeCustomizationComponent } from './theme-customization/theme-customization.component';
@@ -89,9 +123,9 @@ import { Data_lakeComponent } from './builder/dashboardnew/Data_lake/Data_lake.c
import { SureconnectComponent } from './builder/dashboardnew/sureconnect/sureconnect.component'; import { SureconnectComponent } from './builder/dashboardnew/sureconnect/sureconnect.component';
import { EditsureconnectComponent } from './builder/dashboardnew/sureconnect/editsureconnect/editsureconnect.component'; import { EditsureconnectComponent } from './builder/dashboardnew/sureconnect/editsureconnect/editsureconnect.component';
import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.component'; import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.component';
// import { QueryComponent } from './superadmin/query/query.component'; import { QueryComponent } from './superadmin/query/query.component';
// import { QueryaddComponent } from './superadmin/queryadd/queryadd.component'; import { QueryaddComponent } from './superadmin/queryadd/queryadd.component';
// import { QueryeditComponent } from './superadmin/queryedit/queryedit.component'; import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
@@ -132,6 +166,8 @@ const routes: Routes = [
{ path: 'editconnect/:id', component: EditsureconnectComponent }, { path: 'editconnect/:id', component: EditsureconnectComponent },
{ {
path: 'reportbuild', component: ReportBuildComponent, path: 'reportbuild', component: ReportBuildComponent,
children: [ children: [
@@ -143,9 +179,9 @@ const routes: Routes = [
//SUPER ADMIN //SUPER ADMIN
// { path: 'query', component: QueryComponent, canActivate: [AuthGuard], data: { roles: [Role.Admin] } }, { path: 'query', component: QueryComponent, canActivate: [AuthGuard], data: { roles: [Role.Admin] } },
// { path: 'reportQuery/:id/queryadd', component: QueryaddComponent, canActivate: [AuthGuard], data: { roles: [Role.Admin] } }, { path: 'reportQuery/:id/queryadd', component: QueryaddComponent, canActivate: [AuthGuard], data: { roles: [Role.Admin] } },
// { path: 'reportQuery/queryedit/:id', component: QueryeditComponent, canActivate: [AuthGuard], data: { roles: [Role.Admin] } }, { path: 'reportQuery/queryedit/:id', component: QueryeditComponent, canActivate: [AuthGuard], data: { roles: [Role.Admin] } },
@@ -189,6 +225,12 @@ const routes: Routes = [
] ]
}, },
// Shield Dashboard
{
path: 'shield-dashboard',
loadChildren: () => import('./builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard-routing.module').then(m => m.ShieldDashboardRoutingModule)
},
{ {
path: 'dashboardrunner', component: DashboardrunnerComponent, path: 'dashboardrunner', component: DashboardrunnerComponent,
children: [ children: [
@@ -236,8 +278,20 @@ const routes: Routes = [
], ],
}, },
{ path: 'SequenceGenerator', component: SequencegenaratorComponent }, { path: 'SequenceGenerator', component: SequencegenaratorComponent },
{ path: 'apiregistery', component: ApiregisteryComponent },
// Api registery
{
path: 'apiregistery', component: ApiregisteryComponent,
children: [
{ path: '', redirectTo: 'all', pathMatch: 'full' },
{ path: 'all', component: AllapiregisteryComponent },
{ path: 'add', component: AddapiregisteryComponent },
{ path: 'edit/:id', component: EditapiregisteryComponent },
{ path: 'line/:id', component: ApiregisterylineComponent },
],
},
// DATA MANAGEMENT // DATA MANAGEMENT
@@ -270,18 +324,186 @@ const routes: Routes = [
// buildercomponents // buildercomponents
{ path: 'Country', component: CountryComponent },
{ path: 'Adv3', component: Adv3Component },
{ path: 'Ad10', component: Ad10Component },
{ path: 'Childform', component: ChildformComponent },
{ path: 'District', component: DistrictComponent },
{ path: 'State', component: StateComponent },
{ path: 'Country', component: CountryComponent },
{ path: 'Ad9', component: Ad9Component },
{ path: 'Ad8', component: Ad8Component },
{ path: 'Ad7', component: Ad7Component },
{ path: 'Ad6', component: Ad6Component },
{ path: 'Adv5', component: Adv5Component },
{ path: 'Support', component: SupportComponent },
{ path: 'Adv3', component: Adv3Component },
{ path: 'tokenregistery', component: Token_registeryComponent }, { path: 'tokenregistery', component: Token_registeryComponent },
{ path: 'Defatest', component: DefatestComponent },
{ path: 'Country', component: CountryComponent },
{ path: 'Defatest', component: DefatestComponent },
{ path: 'Test2', component: Test2Component },
{ path: 'Country', component: CountryComponent },
{ path: 'Test2', component: Test2Component },
{ path: 'Childform', component: ChildformComponent },
{ path: 'District', component: DistrictComponent },
{ path: 'State', component: StateComponent },
{ path: 'Country', component: CountryComponent },
{ path: 'Ad9', component: Ad9Component },
{ path: 'Ad8', component: Ad8Component },
{ path: 'Ad7', component: Ad7Component },
{ path: 'Ad6', component: Ad6Component },
{ path: 'Adv5', component: Adv5Component },
{ path: 'Adv4', component: Adv4Component },
{ path: 'Support', component: SupportComponent },
{ path: 'Adv3', component: Adv3Component },
{ path: 'Dv2', component: Dv2Component },
{ path: 'Adv1', component: Adv1Component },
{ path: 'Basicp3', component: Basicp3Component },
{ path: 'Basicp2', component: Basicp2Component },
{ path: 'Basicp1', component: Basicp1Component },
{ path: 'cust', component: Customer_informationComponent },
{ path: 'Order_summary', component: Order_summaryComponent },
{ path: 'Types', component: TypesComponent },
{ path: 'Product', component: ProductComponent },
{ path: 'Manufacturer', component: ManufacturerComponent },
{ path: 'Deployment_type', component: Deployment_typeComponent },
{ path: 'Stepper_workflow', component: Stepper_workflowComponent }, { path: 'Stepper_workflow', component: Stepper_workflowComponent },
{ path: '**', component: PageNotFoundComponent }, { path: '**', component: PageNotFoundComponent },
] ]

View File

@@ -1,3 +1,25 @@
import { Ad10Component } from './BuilderComponents/angulardatatype/Ad10/Ad10.component';
import { DefatestComponent } from './BuilderComponents/defu/Defatest/Defatest.component';
import { ChildformComponent } from './BuilderComponents/stpkg/Childform/Childform.component';
import { DistrictComponent } from './BuilderComponents/testdata/District/District.component';
import { StateComponent } from './BuilderComponents/testdata/State/State.component';
import { CountryComponent } from './BuilderComponents/testdata/Country/Country.component';
import { Ad9Component } from './BuilderComponents/angulardatatype/Ad9/Ad9.component';
import { Ad8Component } from './BuilderComponents/angulardatatype/Ad8/Ad8.component';
import { Ad7Component } from './BuilderComponents/angulardatatype/Ad7/Ad7.component';
import { Ad6Component } from './BuilderComponents/angulardatatype/Ad6/Ad6.component';
import { Adv5Component } from './BuilderComponents/angulardatatype/Adv5/Adv5.component';
import { Adv4Component } from './BuilderComponents/angulardatatype/Adv4/Adv4.component';
import { SupportComponent } from './BuilderComponents/angulardatatype/Support/Support.component';
import { Adv3Component } from './BuilderComponents/angulardatatype/Adv3/Adv3.component';
import { Dv2Component } from './BuilderComponents/angulardatatype/Dv2/Dv2.component';
import { Adv1Component } from './BuilderComponents/angulardatatype/Adv1/Adv1.component';
import { Basicp3Component } from './BuilderComponents/angulardatatype/Basicp3/Basicp3.component';
import { Basicp2Component } from './BuilderComponents/angulardatatype/Basicp2/Basicp2.component';
import { Basicp1Component } from './BuilderComponents/angulardatatype/Basicp1/Basicp1.component';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@@ -74,6 +96,9 @@ import { RadarChartComponent } from './builder/dashboardnew/gadgets/radar-chart/
import { ScatterChartComponent } from './builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component'; import { ScatterChartComponent } from './builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component';
import { ToDoChartComponent } from './builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component'; import { ToDoChartComponent } from './builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component';
import { ScheduleComponent } from './builder/dashboardnew/schedule/schedule.component'; import { ScheduleComponent } from './builder/dashboardnew/schedule/schedule.component';
import { CommonFilterComponent } from './builder/dashboardnew/common-filter/common-filter.component';
import { ChartWrapperComponent } from './builder/dashboardnew/common-filter/chart-wrapper.component';
import { CompactFilterComponent } from './builder/dashboardnew/common-filter/compact-filter.component';
import { AddextensionComponent } from './fnd/extension/addextension/addextension.component'; import { AddextensionComponent } from './fnd/extension/addextension/addextension.component';
import { AllextensionComponent } from './fnd/extension/allextension/allextension.component'; import { AllextensionComponent } from './fnd/extension/allextension/allextension.component';
import { EditextensionComponent } from './fnd/extension/editextension/editextension.component'; import { EditextensionComponent } from './fnd/extension/editextension/editextension.component';
@@ -91,6 +116,9 @@ import { RadarRunnerComponent } from './builder/dashboardrunner/dashrunnerline/r
import { ScatterRunnerComponent } from './builder/dashboardrunner/dashrunnerline/scatter-runner/scatter-runner.component'; import { ScatterRunnerComponent } from './builder/dashboardrunner/dashrunnerline/scatter-runner/scatter-runner.component';
import { TodoRunnerComponent } from './builder/dashboardrunner/dashrunnerline/todo-runner/todo-runner.component'; import { TodoRunnerComponent } from './builder/dashboardrunner/dashrunnerline/todo-runner/todo-runner.component';
// Import CompactFilterRunnerComponent
import { CompactFilterRunnerComponent } from './builder/dashboardrunner/dashrunnerline/compact-filter-runner/compact-filter-runner.component';
import { ApiregisteryComponent } from './fnd/apiregistery/apiregistery.component'; import { ApiregisteryComponent } from './fnd/apiregistery/apiregistery.component';
import { BulkimportComponent } from './datamanagement/bulkimport/bulkimport.component'; import { BulkimportComponent } from './datamanagement/bulkimport/bulkimport.component';
@@ -106,18 +134,34 @@ import { MappingruleaddComponent } from './datamanagement/mappingrule/mappingrul
import { MappingruleallComponent } from './datamanagement/mappingrule/mappingruleall/mappingruleall.component'; import { MappingruleallComponent } from './datamanagement/mappingrule/mappingruleall/mappingruleall.component';
import { MappingruleeditComponent } from './datamanagement/mappingrule/mappingruleedit/mappingruleedit.component'; import { MappingruleeditComponent } from './datamanagement/mappingrule/mappingruleedit/mappingruleedit.component';
import { Stepper_workflowComponent } from './BuilderComponents/stepperworkflow/Stepper_workflow/Stepper_workflow.component'; import { Stepper_workflowComponent } from './BuilderComponents/stepperworkflow/Stepper_workflow/Stepper_workflow.component';
import { AllapiregisteryComponent } from './fnd/apiregistery/allapiregistery/allapiregistery.component';
import { AddapiregisteryComponent } from './fnd/apiregistery/addapiregistery/addapiregistery.component';
import { EditapiregisteryComponent } from './fnd/apiregistery/editapiregistery/editapiregistery.component';
import { ApiregisterylineComponent } from './fnd/apiregistery/Apiregisteryline/Apiregisteryline.component';
import { Customer_informationComponent } from './BuilderComponents/angulardatatype/Customer_information/Customer_information.component';
import { Deployment_typeComponent } from './BuilderComponents/angulardatatype/Deployment_type/Deployment_type.component';
import { ManufacturerComponent } from './BuilderComponents/angulardatatype/Manufacturer/Manufacturer.component';
import { Order_summaryComponent } from './BuilderComponents/angulardatatype/Order_summary/Order_summary.component';
import { ProductComponent } from './BuilderComponents/angulardatatype/Product/Product.component';
import { TypesComponent } from './BuilderComponents/angulardatatype/Types/Types.component';
import { Test2Component } from './BuilderComponents/testdata/Test2/Test2.component';
import { Token_registeryComponent } from './fnd/Token_registery/Token_registery.component'; import { Token_registeryComponent } from './fnd/Token_registery/Token_registery.component';
import { MyworkspaceComponent } from './admin/myworkspace/myworkspace.component'; import { MyworkspaceComponent } from './admin/myworkspace/myworkspace.component';
import { ThemeCustomizationComponent } from './theme-customization/theme-customization.component'; import { ThemeCustomizationComponent } from './theme-customization/theme-customization.component';
import { QueryComponent } from './superadmin/query/query.component';
import { QueryaddComponent } from './superadmin/queryadd/queryadd.component';
import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
import { FieldTypesModule } from '../../shared/components/field-types/field-types.module';
import { SharedModule } from '../../shared/shared.module';
import { Data_lakeComponent } from './builder/dashboardnew/Data_lake/Data_lake.component'; import { Data_lakeComponent } from './builder/dashboardnew/Data_lake/Data_lake.component';
import { CronJobBuilderComponent } from './builder/dashboardnew/Data_lake/cron-job-builder/cron-job-builder.component'; import { CronJobBuilderComponent } from './builder/dashboardnew/Data_lake/cron-job-builder/cron-job-builder.component';
import { SureconnectComponent } from './builder/dashboardnew/sureconnect/sureconnect.component'; import { SureconnectComponent } from './builder/dashboardnew/sureconnect/sureconnect.component';
import { EditsureconnectComponent } from './builder/dashboardnew/sureconnect/editsureconnect/editsureconnect.component'; import { EditsureconnectComponent } from './builder/dashboardnew/sureconnect/editsureconnect/editsureconnect.component';
import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.component'; import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.component';
// import { QueryComponent } from './superadmin/query/query.component'; // Import Shield Dashboard Module
// import { QueryaddComponent } from './superadmin/queryadd/queryadd.component'; import { ShieldDashboardModule } from './builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.module';
// import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -128,71 +172,58 @@ import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.c
UsermaintanceaddComponent, UsermaintanceeditComponent, UsermaintanceaddComponent, UsermaintanceeditComponent,
SubmenuComponent, ModulesComponent, SessionloggerComponent, SubmenuComponent, ModulesComponent, SessionloggerComponent,
DashboardnewComponent, EditformnewdashComponent, EditnewdashComponent, ScheduleComponent, DashboardnewComponent, EditformnewdashComponent, EditnewdashComponent, ScheduleComponent,
DoughnutChartComponent, LineChartComponent, RadarChartComponent, BarChartComponent, BubbleChartComponent, DynamicChartComponent, ScatterChartComponent, PolarChartComponent, PieChartComponent, FinancialChartComponent, ToDoChartComponent, GridViewComponent, CommonFilterComponent, ChartWrapperComponent, CompactFilterComponent, DoughnutChartComponent, LineChartComponent, RadarChartComponent, BarChartComponent, BubbleChartComponent, DynamicChartComponent, ScatterChartComponent, PolarChartComponent, PieChartComponent, FinancialChartComponent, ToDoChartComponent, GridViewComponent,
DashrunnerlineComponent, BarRunnerComponent, LineRunnerComponent, DoughnutRunnerComponent, GridRunnerComponent, PieRunnerComponent, PolarRunnerComponent, RadarRunnerComponent, ScatterRunnerComponent, TodoRunnerComponent, BubbleRunnerComponent, DashrunnerlineComponent, BarRunnerComponent, LineRunnerComponent, DoughnutRunnerComponent, GridRunnerComponent, PieRunnerComponent, PolarRunnerComponent, RadarRunnerComponent, ScatterRunnerComponent, TodoRunnerComponent, BubbleRunnerComponent,
// Add CompactFilterRunnerComponent to declarations
CompactFilterRunnerComponent,
ReportBuildComponent, ReportbuildeditComponent, ReportbuildqueryComponent, ReportBuild2Component, ReportBuild2editComponent, ReportBuildComponent, ReportbuildeditComponent, ReportbuildqueryComponent, ReportBuild2Component, ReportBuild2editComponent,
// QueryComponent, QueryaddComponent, QueryeditComponent, QueryComponent, QueryaddComponent, QueryeditComponent,
ExtensionComponent, ExtensionComponent,
AllextensionComponent, AllextensionComponent,
AddextensionComponent, EditextensionComponent, ApiregisteryComponent, AddextensionComponent, EditextensionComponent, ApiregisteryComponent, AllapiregisteryComponent, AddapiregisteryComponent, EditapiregisteryComponent,
DatamanagementComponent, DatamananementworkflowComponent, BulkimportComponent, BulkimportallComponent, BulkimportaddComponent, BulkimporteditComponent, BulkimportlineComponent, BulkimporteditlineComponent, MappingruleComponent,
MappingruleallComponent, MappingruleaddComponent, MappingruleeditComponent, ApiregisterylineComponent,
ThemeCustomizationComponent, DatamanagementComponent, DatamananementworkflowComponent, BulkimportComponent, BulkimportallComponent, BulkimportaddComponent, BulkimporteditComponent, BulkimportlineComponent, BulkimporteditlineComponent, MappingruleComponent, MappingruleallComponent,
MappingruleaddComponent,
MappingruleeditComponent, Stepper_workflowComponent, Customer_informationComponent,
Data_lakeComponent, Data_lakeComponent,
SureconnectComponent, SureconnectComponent,
EditsureconnectComponent, EditsureconnectComponent,
OauthComponent, OauthComponent,
CronJobBuilderComponent, CronJobBuilderComponent,
// FileUploadListComponent,
// buildercomponents // buildercomponents
ThemeCustomizationComponent,
Ad10Component,
Token_registeryComponent, Token_registeryComponent,
DefatestComponent,
Test2Component,
Order_summaryComponent,
TypesComponent,
ProductComponent,
ManufacturerComponent,
Deployment_typeComponent,
ChildformComponent,
DistrictComponent,
StateComponent,
CountryComponent,
Ad9Component,
Ad8Component,
Ad7Component,
Ad6Component,
Adv5Component,
Adv4Component,
SupportComponent,
Adv3Component,
Dv2Component,
Adv1Component,
Basicp3Component,
Basicp2Component,
Basicp1Component,
Stepper_workflowComponent,
], ],
imports: [ imports: [
QRCodeModule, QRCodeModule,
@@ -212,6 +243,9 @@ import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.c
NgChartsModule, NgChartsModule,
NgxChartsModule, NgxChartsModule,
DynamicModule, DynamicModule,
FieldTypesModule,
SharedModule,
ShieldDashboardModule,
], ],
providers: [ providers: [
CookieService, CookieService,

View File

@@ -0,0 +1,252 @@
import { Ad10Component } from './BuilderComponents/angulardatatype/Ad10/Ad10.component';
import { DefatestComponent } from './BuilderComponents/defu/Defatest/Defatest.component';
import { ChildformComponent } from './BuilderComponents/stpkg/Childform/Childform.component';
import { DistrictComponent } from './BuilderComponents/testdata/District/District.component';
import { StateComponent } from './BuilderComponents/testdata/State/State.component';
import { CountryComponent } from './BuilderComponents/testdata/Country/Country.component';
import { Ad9Component } from './BuilderComponents/angulardatatype/Ad9/Ad9.component';
import { Ad8Component } from './BuilderComponents/angulardatatype/Ad8/Ad8.component';
import { Ad7Component } from './BuilderComponents/angulardatatype/Ad7/Ad7.component';
import { Ad6Component } from './BuilderComponents/angulardatatype/Ad6/Ad6.component';
import { Adv5Component } from './BuilderComponents/angulardatatype/Adv5/Adv5.component';
import { Adv4Component } from './BuilderComponents/angulardatatype/Adv4/Adv4.component';
import { SupportComponent } from './BuilderComponents/angulardatatype/Support/Support.component';
import { Adv3Component } from './BuilderComponents/angulardatatype/Adv3/Adv3.component';
import { Dv2Component } from './BuilderComponents/angulardatatype/Dv2/Dv2.component';
import { Adv1Component } from './BuilderComponents/angulardatatype/Adv1/Adv1.component';
import { Basicp3Component } from './BuilderComponents/angulardatatype/Basicp3/Basicp3.component';
import { Basicp2Component } from './BuilderComponents/angulardatatype/Basicp2/Basicp2.component';
import { Basicp1Component } from './BuilderComponents/angulardatatype/Basicp1/Basicp1.component';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ClarityModule } from '@clr/angular';
import { MainPageComponent } from '../main/fnd/main-page/main-page.component';
import { MainRoutingModule } from './main-routing.module';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
// import { AboutComponent } from '../main/admin/about/about.component';
// import { LayoutComponent } from './layout/layout.component';
import { HelperModule } from 'src/app/pipes/helpers.module';
import { PasswordResetComponent } from '../main/admin/password-reset/password-reset.component';
import { UserComponent } from '../main/admin/user/user.component';
import { AllMenuGroupComponent } from '../main/admin/menu-group/all/all-menu-group.component';
import { EditMenuGroupComponent } from '../main/admin/menu-group/edit/edit-menu-group.component';
import { MenuGroupComponent } from '../main/admin/menu-group/menu-group.component';
import { ReadOnlyMenuGroupComponent } from '../main/admin/menu-group/read-only/readonly-menu-group.component';
import { AddMenurComponent } from '../main/admin/menu-register/add-menur/add-menur.component';
import { AllMenurComponent } from '../main/admin/menu-register/all-menur/all-menur.component';
import { EditMenurComponent } from '../main/admin/menu-register/edit-menur/edit-menur.component';
import { MenuRegisterComponent } from '../main/admin/menu-register/menu-register.component';
import { ReadonlyMenurComponent } from '../main/admin/menu-register/readonly-menur/readonly-menur.component';
import { ProfileSettingComponent } from '../main/admin/profile-setting/profile-setting.component';
import { UsermaintanceaddComponent } from '../main/admin/usermaintanceadd/usermaintanceadd.component';
import { UsermaintanceeditComponent } from '../main/admin/usermaintanceedit/usermaintanceedit.component';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { HttpClientModule } from '@angular/common/http';
import { CodemirrorModule } from "@ctrl/ngx-codemirror";
import { NgxChartsModule } from '@swimlane/ngx-charts';
import { GridsterModule } from 'angular-gridster2';
import { DynamicModule } from 'ng-dynamic-component';
import { NgChartsModule } from 'ng2-charts';
import { CKEditorModule } from 'ng2-ckeditor';
import { UserRegistrationComponent } from '../main/admin/user-registration/user-registration.component';
import { QRCodeModule } from 'angularx-qrcode';
import { TagInputModule } from 'ngx-chips';
import { CookieService } from 'ngx-cookie-service';
import { ImageCropperModule } from 'ngx-image-cropper';
import { ModulesComponent } from './admin/modules/modules.component';
import { SessionloggerComponent } from './admin/sessionlogger/sessionlogger.component';
import { SubmenuComponent } from './admin/submenu/submenu.component';
import { WireframeService } from 'src/app/services/builder/wireframe.service';
import { ReportBuildComponent } from './builder/report-build/report-build.component';
import { ReportbuildeditComponent } from './builder/report-build/reportbuildedit/reportbuildedit.component';
import { ReportbuildqueryComponent } from './builder/report-build/reportbuildquery/reportbuildquery.component';
import { ReportBuild2Component } from './builder/report-build2/report-build2.component';
import { ReportBuild2editComponent } from './builder/report-build2/report-build2edit/report-build2edit.component';
import { ReportRunnerComponent } from './builder/report-runner/report-runner.component';
import { ReportrunnereditComponent } from './builder/report-runner/reportrunneredit/reportrunneredit.component';
import { Reportrunneredit2Component } from './builder/report-runner/reportrunneredit2/reportrunneredit2.component';
import { DashboardnewComponent } from './builder/dashboardnew/dashboardnew.component';
import { EditformnewdashComponent } from './builder/dashboardnew/editformnewdash/editformnewdash.component';
import { EditnewdashComponent } from './builder/dashboardnew/editnewdash/editnewdash.component';
import { BarChartComponent } from './builder/dashboardnew/gadgets/bar-chart/bar-chart.component';
import { BubbleChartComponent } from './builder/dashboardnew/gadgets/bubble-chart/bubble-chart.component';
import { DoughnutChartComponent } from './builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component';
import { DynamicChartComponent } from './builder/dashboardnew/gadgets/dynamic-chart/dynamic-chart.component';
import { FinancialChartComponent } from './builder/dashboardnew/gadgets/financial-chart/financial-chart.component';
import { GridViewComponent } from './builder/dashboardnew/gadgets/grid-view/grid-view.component';
import { LineChartComponent } from './builder/dashboardnew/gadgets/line-chart/line-chart.component';
import { PieChartComponent } from './builder/dashboardnew/gadgets/pie-chart/pie-chart.component';
import { PolarChartComponent } from './builder/dashboardnew/gadgets/polar-chart/polar-chart.component';
import { RadarChartComponent } from './builder/dashboardnew/gadgets/radar-chart/radar-chart.component';
import { ScatterChartComponent } from './builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component';
import { ToDoChartComponent } from './builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component';
import { ScheduleComponent } from './builder/dashboardnew/schedule/schedule.component';
import { CommonFilterComponent } from './builder/dashboardnew/common-filter/common-filter.component';
import { ChartWrapperComponent } from './builder/dashboardnew/common-filter/chart-wrapper.component';
import { AddextensionComponent } from './fnd/extension/addextension/addextension.component';
import { AllextensionComponent } from './fnd/extension/allextension/allextension.component';
import { EditextensionComponent } from './fnd/extension/editextension/editextension.component';
import { ExtensionComponent } from './fnd/extension/extension.component';
import { BarRunnerComponent } from './builder/dashboardrunner/dashrunnerline/bar-runner/bar-runner.component';
import { BubbleRunnerComponent } from './builder/dashboardrunner/dashrunnerline/bubble-runner/bubble-runner.component';
import { DashrunnerlineComponent } from './builder/dashboardrunner/dashrunnerline/dashrunnerline.component';
import { DoughnutRunnerComponent } from './builder/dashboardrunner/dashrunnerline/doughnut-runner/doughnut-runner.component';
import { GridRunnerComponent } from './builder/dashboardrunner/dashrunnerline/grid-runner/grid-runner.component';
import { LineRunnerComponent } from './builder/dashboardrunner/dashrunnerline/line-runner/line-runner.component';
import { PieRunnerComponent } from './builder/dashboardrunner/dashrunnerline/pie-runner/pie-runner.component';
import { PolarRunnerComponent } from './builder/dashboardrunner/dashrunnerline/polar-runner/polar-runner.component';
import { RadarRunnerComponent } from './builder/dashboardrunner/dashrunnerline/radar-runner/radar-runner.component';
import { ScatterRunnerComponent } from './builder/dashboardrunner/dashrunnerline/scatter-runner/scatter-runner.component';
import { TodoRunnerComponent } from './builder/dashboardrunner/dashrunnerline/todo-runner/todo-runner.component';
import { ApiregisteryComponent } from './fnd/apiregistery/apiregistery.component';
import { BulkimportComponent } from './datamanagement/bulkimport/bulkimport.component';
import { BulkimportaddComponent } from './datamanagement/bulkimport/bulkimportadd/bulkimportadd.component';
import { BulkimportallComponent } from './datamanagement/bulkimport/bulkimportall/bulkimportall.component';
import { BulkimporteditComponent } from './datamanagement/bulkimport/bulkimportedit/bulkimportedit.component';
import { BulkimporteditlineComponent } from './datamanagement/bulkimport/bulkimporteditline/bulkimporteditline.component';
import { BulkimportlineComponent } from './datamanagement/bulkimport/bulkimportline/bulkimportline.component';
import { DatamanagementComponent } from './datamanagement/datamanagement/datamanagement.component';
import { DatamananementworkflowComponent } from './datamanagement/datamananementworkflow/datamananementworkflow.component';
import { MappingruleComponent } from './datamanagement/mappingrule/mappingrule.component';
import { MappingruleaddComponent } from './datamanagement/mappingrule/mappingruleadd/mappingruleadd.component';
import { MappingruleallComponent } from './datamanagement/mappingrule/mappingruleall/mappingruleall.component';
import { MappingruleeditComponent } from './datamanagement/mappingrule/mappingruleedit/mappingruleedit.component';
import { Stepper_workflowComponent } from './BuilderComponents/stepperworkflow/Stepper_workflow/Stepper_workflow.component';
import { AllapiregisteryComponent } from './fnd/apiregistery/allapiregistery/allapiregistery.component';
import { AddapiregisteryComponent } from './fnd/apiregistery/addapiregistery/addapiregistery.component';
import { EditapiregisteryComponent } from './fnd/apiregistery/editapiregistery/editapiregistery.component';
import { ApiregisterylineComponent } from './fnd/apiregistery/Apiregisteryline/Apiregisteryline.component';
import { Customer_informationComponent } from './BuilderComponents/angulardatatype/Customer_information/Customer_information.component';
import { Deployment_typeComponent } from './BuilderComponents/angulardatatype/Deployment_type/Deployment_type.component';
import { ManufacturerComponent } from './BuilderComponents/angulardatatype/Manufacturer/Manufacturer.component';
import { Order_summaryComponent } from './BuilderComponents/angulardatatype/Order_summary/Order_summary.component';
import { ProductComponent } from './BuilderComponents/angulardatatype/Product/Product.component';
import { TypesComponent } from './BuilderComponents/angulardatatype/Types/Types.component';
import { Test2Component } from './BuilderComponents/testdata/Test2/Test2.component';
import { Token_registeryComponent } from './fnd/Token_registery/Token_registery.component';
import { MyworkspaceComponent } from './admin/myworkspace/myworkspace.component';
import { ThemeCustomizationComponent } from './theme-customization/theme-customization.component';
// import { QueryComponent } from './superadmin/query/query.component';
// import { QueryaddComponent } from './superadmin/queryadd/queryadd.component';
// import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
import { FieldTypesModule } from '../../shared/components/field-types/field-types.module';
import { SharedModule } from '../../shared/shared.module';
import { Data_lakeComponent } from './builder/dashboardnew/Data_lake/Data_lake.component';
import { CronJobBuilderComponent } from './builder/dashboardnew/Data_lake/cron-job-builder/cron-job-builder.component';
import { SureconnectComponent } from './builder/dashboardnew/sureconnect/sureconnect.component';
import { EditsureconnectComponent } from './builder/dashboardnew/sureconnect/editsureconnect/editsureconnect.component';
import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.component';
// Import Shield Dashboard Module
import { ShieldDashboardModule } from './builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.module';
@NgModule({
declarations: [
MainPageComponent, PageNotFoundComponent, UserComponent, PasswordResetComponent,
MyworkspaceComponent,
ReportRunnerComponent, ReportrunnereditComponent, Reportrunneredit2Component, MenuGroupComponent, AllMenuGroupComponent, EditMenuGroupComponent, ReadOnlyMenuGroupComponent, UserRegistrationComponent,
MenuRegisterComponent, AddMenurComponent, EditMenurComponent, AllMenurComponent, ReadonlyMenurComponent, ProfileSettingComponent,
UsermaintanceaddComponent, UsermaintanceeditComponent,
SubmenuComponent, ModulesComponent, SessionloggerComponent,
DashboardnewComponent, EditformnewdashComponent, EditnewdashComponent, ScheduleComponent,
CommonFilterComponent, ChartWrapperComponent, DoughnutChartComponent, LineChartComponent, RadarChartComponent, BarChartComponent, BubbleChartComponent, DynamicChartComponent, ScatterChartComponent, PolarChartComponent, PieChartComponent, FinancialChartComponent, ToDoChartComponent, GridViewComponent,
DashrunnerlineComponent, BarRunnerComponent, LineRunnerComponent, DoughnutRunnerComponent, GridRunnerComponent, PieRunnerComponent, PolarRunnerComponent, RadarRunnerComponent, ScatterRunnerComponent, TodoRunnerComponent, BubbleRunnerComponent,
ReportBuildComponent, ReportbuildeditComponent, ReportbuildqueryComponent, ReportBuild2Component, ReportBuild2editComponent,
// QueryComponent, QueryaddComponent, QueryeditComponent,
ExtensionComponent,
AllextensionComponent,
AddextensionComponent, EditextensionComponent, ApiregisteryComponent, AllapiregisteryComponent, AddapiregisteryComponent, EditapiregisteryComponent,
ApiregisterylineComponent,
DatamanagementComponent, DatamananementworkflowComponent, BulkimportComponent, BulkimportallComponent, BulkimportaddComponent, BulkimporteditComponent, BulkimportlineComponent, BulkimporteditlineComponent, MappingruleComponent, MappingruleallComponent,
MappingruleaddComponent,
MappingruleeditComponent, Stepper_workflowComponent, Customer_informationComponent,
Data_lakeComponent,
SureconnectComponent,
EditsureconnectComponent,
OauthComponent,
CronJobBuilderComponent,
// FileUploadListComponent,
// buildercomponents
ThemeCustomizationComponent,
Ad10Component,
Token_registeryComponent,
DefatestComponent,
Test2Component,
Order_summaryComponent,
TypesComponent,
ProductComponent,
ManufacturerComponent,
Deployment_typeComponent,
ChildformComponent,
DistrictComponent,
StateComponent,
CountryComponent,
Ad9Component,
Ad8Component,
Ad7Component,
Ad6Component,
Adv5Component,
Adv4Component,
SupportComponent,
Adv3Component,
Dv2Component,
Adv1Component,
Basicp3Component,
Basicp2Component,
Basicp1Component,
],
imports: [
QRCodeModule,
CommonModule,
FormsModule,
ReactiveFormsModule,
ClarityModule,
HelperModule,
MainRoutingModule,
DragDropModule,
HttpClientModule,
ImageCropperModule,
TagInputModule,
CodemirrorModule,
CKEditorModule,
GridsterModule,
NgChartsModule,
NgxChartsModule,
DynamicModule,
FieldTypesModule,
SharedModule,
ShieldDashboardModule,
],
providers: [
CookieService,
WireframeService,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class MainModule { }

View File

@@ -69,4 +69,13 @@ export class AlertsService {
} }
return this.apiRequest.get(apiUrl); return this.apiRequest.get(apiUrl);
} }
// Get values for a specific key from API
public getValuesFromUrl(url: string, sureId: number | undefined, key: string): Observable<any> {
let apiUrl = `chart/getValue?apiUrl=${url}&key=${key}`;
if (sureId) {
apiUrl += `&sureId=${sureId}`;
}
return this.apiRequest.get(apiUrl);
}
} }