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

@@ -1,8 +1,9 @@
export class ReportBuilder {
public report_id: number;
public report_name:string;
public description: string;
public report_tags: string;
public servicename:string;
public report_name: string;
public description: string;
public report_tags: string;
public servicename: string;
// Add SureConnect reference
public sureConnectId: number | null;
}

View File

@@ -23,8 +23,22 @@ export interface DashboardContentModel {
component?: any;
name: 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;
datasource?: string;
fieldName?: string;
connection?: string;
baseFilters?: any[];
// Common filter properties
@@ -37,6 +51,11 @@ export interface DashboardContentModel {
drilldownParameter?: string;
drilldownFilters?: any[];
drilldownLayers?: any[];
// Compact filter properties
filterKey?: string;
filterType?: string;
filterLabel?: string;
filterOptions?: string[];
}
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 -->
<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;">
<input type="checkbox"
[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 { 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',
styleUrls: ['./compact-filter.component.scss']
})
export class CompactFilterComponent implements OnInit, OnChanges {
export class CompactFilterComponent implements OnInit, OnChanges, OnDestroy {
@Input() filterKey: string = '';
@Input() filterType: string = 'text';
@Input() filterOptions: string[] = [];
@@ -23,6 +23,9 @@ export class CompactFilterComponent implements OnInit, OnChanges {
availableKeys: string[] = [];
availableValues: string[] = [];
// Multiselect dropdown state
showMultiselectDropdown: boolean = false;
// Configuration properties
isConfigMode: boolean = false;
configFilterKey: string = '';
@@ -73,6 +76,24 @@ export class CompactFilterComponent implements OnInit, OnChanges {
}
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 (changes.filterKey || changes.filterType) {
// 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);
}
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
loadAvailableKeys(): void {
if (this.apiUrl) {
@@ -278,6 +307,9 @@ export class CompactFilterComponent implements OnInit, OnChanges {
this.apiUrl = config.apiUrl;
this.connectionId = config.connectionId;
// Clear filter value when changing configuration
this.filterValue = '';
// Load available keys if API URL is provided
if (this.apiUrl) {
this.loadAvailableKeys();
@@ -304,11 +336,23 @@ export class CompactFilterComponent implements OnInit, OnChanges {
// Handle filter key change in configuration
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;
// Load available values for the selected key if it's a dropdown or multiselect
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && key) {
this.loadAvailableValues(key);
}
// 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
@@ -375,4 +419,23 @@ export class CompactFilterComponent implements OnInit, OnChanges {
// Emit the change event
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> -->
<div style="display: inline;">
<button class="btn componentbtn" (click)="toggleMenu()"><clr-icon shape="plus"></clr-icon>component</button>
<button class="btn btn-primary" (click)="openCommonFilterModal()" style="margin-left: 10px;">
<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;" *ngIf="!fromRunner">
<clr-icon shape="filter"></clr-icon> Common Filter
</button>
<div style="display: inline;">
@@ -22,7 +22,7 @@
</div>
<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;">
<li *ngFor="let widget of WidgetsMock">
@@ -42,14 +42,14 @@
<gridster [options]="options" (drop)="onDrop($event)" style="background-color: transparent;">
<gridster-item [item]="item" *ngFor="let item of dashboardArray">
<!-- <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>
</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>
</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"
(change)="toggleAddToDashboard(item)" />
</button>
@@ -57,7 +57,7 @@
<!-- <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" />
-->
<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>
</button>
@@ -72,7 +72,7 @@
</div>
<div style="text-align: center;">
<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>
</button>
</div>
@@ -335,8 +335,8 @@
</div>
<div class="clr-row" style="margin-top: 8px;">
<div class="clr-col-sm-5">
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select"
<div class="clr-col-sm-4">
<select [(ngModel)]="filter.field" (ngModelChange)="onBaseFilterFieldChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="gadgetsEditdata.commonFilterEnabled">
<option value="">Select Field</option>
<!-- Base API filters should always use columnData, not drilldownColumnData -->
@@ -345,7 +345,26 @@
</select>
</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"
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabled" />
</div>
@@ -476,8 +495,8 @@
</div>
<div class="clr-row" style="margin-top: 8px;">
<div class="clr-col-sm-5">
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select"
<div class="clr-col-sm-4">
<select [(ngModel)]="filter.field" (ngModelChange)="onDrilldownFilterFieldChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
<option value="">Select Field</option>
<option
@@ -486,7 +505,26 @@
</select>
</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"
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
</div>
@@ -629,8 +667,8 @@
</div>
<div class="clr-row" style="margin-top: 8px;">
<div class="clr-col-sm-5">
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select"
<div class="clr-col-sm-4">
<select [(ngModel)]="filter.field" (ngModelChange)="onLayerFilterFieldChange(i, j, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="layer.commonFilterEnabled">
<option value="">Select Field</option>
<option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])"
@@ -638,9 +676,28 @@
</select>
</div>
<div class="clr-col-sm-5">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
class="clr-input" placeholder="Filter Value" [disabled]="layer.commonFilterEnabled" />
<div class="clr-col-sm-3">
<select [(ngModel)]="filter.type" (ngModelChange)="onLayerFilterTypeChange(i, j, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[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 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';
// Add the CompactFilterComponent import
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) {
return !Array.isArray(arr) || arr.length === 0;
@@ -91,14 +95,14 @@ export class EditnewdashComponent implements OnInit {
name: 'Scatter Chart',
identifier: 'scatter_chart'
},
// {
// name: 'Dynamic Chart',
// identifier: 'dynamic_chart'
// },
// {
// name: 'Financial Chart',
// identifier: 'financial_chart'
// },
{
name: 'Dynamic Chart',
identifier: 'dynamic_chart'
},
{
name: 'Financial Chart',
identifier: 'financial_chart'
},
{
name: 'To Do',
identifier: 'to_do_chart'
@@ -110,6 +114,10 @@ export class EditnewdashComponent implements OnInit {
{
name: '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: "To Do Chart", componentInstance: ToDoChartComponent },
{ 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;
linesdata: any;
@@ -168,6 +177,7 @@ export class EditnewdashComponent implements OnInit {
yAxis: '',
xAxis: '',
connection: '', // Add connection field
chartType: '', // Add chartType field
// Drilldown configuration properties (base level)
drilldownEnabled: false,
drilldownApiUrl: '',
@@ -203,9 +213,15 @@ export class EditnewdashComponent implements OnInit {
private _fb: FormBuilder,
private datastoreService: DatastoreService,
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 {
// Reset the filter service when the component is initialized
this.filterService.resetFilters();
// Grid options
this.options = {
@@ -231,6 +247,13 @@ export class EditnewdashComponent implements OnInit {
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;
console.log(this.editId);
this.dashboardService.getById(this.editId).subscribe((data) => {
@@ -317,6 +340,9 @@ export class EditnewdashComponent implements OnInit {
dashboardLine: any;
dashboardName: any;
getData() {
// Reset the filter service when switching between dashboard records
this.filterService.resetFilters();
// We get the id in get current router dashboard/:id
this.route.params.subscribe(params => {
// + is used to cast string to int
@@ -435,7 +461,17 @@ export class EditnewdashComponent implements OnInit {
onDrop(ev) {
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) {
case "radar_chart":
return this.dashboardArray.push({
@@ -582,6 +618,22 @@ export class EditnewdashComponent implements OnInit {
component: GridViewComponent,
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) {
@@ -597,7 +649,14 @@ export class EditnewdashComponent implements OnInit {
}
modelid: number;
// Update the editGadget method to initialize filter properties
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.modelid = item.chartid;
console.log(this.modelid);
@@ -627,6 +686,10 @@ export class EditnewdashComponent implements OnInit {
if (item['filterOptions'] === undefined) {
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
if (item.name === 'Compact Filter') {
@@ -639,6 +702,65 @@ export class EditnewdashComponent implements OnInit {
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();
// Set default connection if none is set and we have connections
@@ -666,37 +788,6 @@ export class EditnewdashComponent implements OnInit {
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
this.drilldownColumnData = [];
@@ -705,15 +796,34 @@ export class EditnewdashComponent implements OnInit {
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;
this.getTables(datastore);
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);
}
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)) {
this.selectedyAxis = item.yAxis;
} else {
// For single yAxis values, convert to array
this.selectedyAxis = [item.yAxis];
}
console.log(this.selectedyAxis);
} else {
this.selectedyAxis = [];
}
} else {
this.selectedyAxis = [];
@@ -784,14 +894,32 @@ export class EditnewdashComponent implements OnInit {
// }
}
// Update the onSubmit method to properly save filter data
onSubmit(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 });
}
let formdata = this.entryForm.value;
let num = id;
let num = numId;
console.log(this.entryForm.value);
this.dashboardCollection.dashboard = this.dashboardCollection.dashboard.map(item => {
if (item.chartid == num) {
@@ -830,6 +958,11 @@ export class EditnewdashComponent implements OnInit {
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);
return xyz;
}
@@ -913,6 +1046,48 @@ export class EditnewdashComponent implements OnInit {
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
if (item.component && item.component.name === 'GridViewComponent') {
const gridInputs = {
@@ -978,8 +1153,8 @@ export class EditnewdashComponent implements OnInit {
drilldownXAxis: item['drilldownXAxis'],
drilldownYAxis: item['drilldownYAxis'],
drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
baseFilters: item['baseFilters'] || [], // Add base filters
drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters
baseFilters: item['baseFilters'] || [], // Add base filters with type information
drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters with type information
// Multi-layer drilldown configurations
drilldownLayers: item['drilldownLayers'] || []
};
@@ -994,18 +1169,29 @@ export class EditnewdashComponent implements OnInit {
return chartInputs;
}
// Update the applyChanges method to properly save filter data
applyChanges(id) {
console.log('Apply changes for chart ID:', id);
// Check if ID is valid
if (id === null || id === undefined) {
console.warn('Chart ID is null or undefined, using modelid instead:', this.modelid);
// 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;
}
// Update the form with selected Y-axis values if it's an array
if (!isNullArray(this.selectedyAxis)) {
console.log("get y-axis array", this.selectedyAxis);
// 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 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 });
}
@@ -1096,6 +1282,9 @@ export class EditnewdashComponent implements OnInit {
// 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
// Reset the filter service to ensure clean state
this.filterService.resetFilters();
}
goBack() {
@@ -1293,46 +1482,239 @@ export class EditnewdashComponent implements OnInit {
// 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() {
const newFilter = {
field: '',
value: ''
value: '',
type: 'text',
options: '',
availableValues: ''
};
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
removeBaseFilter(index: number) {
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
removeDrilldownFilter(index: number) {
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
removeLayerFilter(layerIndex: number, filterIndex: number) {
this.gadgetsEditdata.drilldownLayers[layerIndex].filters.splice(filterIndex, 1);

View File

@@ -1,25 +1,295 @@
<div style="display: block; height: 100%; width: 100%;">
<!-- No filter controls needed with the new simplified approach -->
<!-- Filters are now configured at the drilldown level -->
<div class="chart-container">
<!-- 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>
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
</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>
<!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
<!-- 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="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
</div>
<!-- Chart display -->
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
<div *ngIf="!noDataAvailable" class="chart-display">
<canvas baseChart
[datasets]="barChartData"
[labels]="barChartLabels"
@@ -29,4 +299,36 @@
(chartClick)="chartClicked($event)">
</canvas>
</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>

View File

@@ -1,31 +1,278 @@
// Bar Chart Component Styles
:host {
display: block;
// Chart container structure
.chart-container {
height: 100%;
width: 100%;
}
display: flex;
flex-direction: column;
.bar-chart-container {
// Filter section styling
.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;
}
}
// 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;
height: 100%;
width: 100%;
}
canvas {
display: block;
max-width: 100%;
max-height: 100%;
transition: filter 0.3s ease;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.8);
.shimmer-bar {
width: 80%;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
}
}
}
}
// Responsive design for chart container
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
// Responsive design
@media (max-width: 768px) {
.bar-chart-container {
height: 300px;
.chart-container {
.filter-controls {
flex-direction: column;
}
}
@media (max-width: 480px) {
.bar-chart-container {
height: 250px;
.filter-item {
min-width: 100%;
}
.chart-header {
.header-row {
.chart-title {
font-size: 16px;
}
}
}
.chart-content {
min-height: 250px; // Adjust for mobile
}
}
}

View File

@@ -57,21 +57,46 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
ticks: {
autoSkip: false,
maxRotation: 45,
minRotation: 45
minRotation: 45,
padding: 15,
font: {
size: 12
}
},
grid: {
display: false
}
},
y: {
beginAtZero: true
beginAtZero: true,
ticks: {
font: {
size: 12
}
}
}
},
plugins: {
legend: {
display: true,
position: 'top',
labels: {
font: {
size: 12
}
}
},
tooltip: {
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
noDataAvailable: boolean = false;
// Loading state
isLoading: boolean = false;
// Flag to prevent infinite loops
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;
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
@@ -111,6 +144,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges): void {
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
const xAxisChanged = changes.xAxis && !changes.xAxis.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 {
// Set loading state
this.isLoading = true;
// Set flag to prevent recursive calls
this.isFetchingData = true;
// If we're in drilldown mode, fetch the appropriate drilldown data
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
this.fetchDrilldownData();
// Reset flag after fetching
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
return;
}
@@ -219,8 +572,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true;
this.barChartLabels = [];
this.barChartData = [];
// Reset flag after fetching
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
return;
}
@@ -249,8 +603,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.barChartLabels = [];
this.barChartData = [];
}
// Reset flag after fetching
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
},
(error) => {
console.error('=== BAR CHART ERROR ===');
@@ -258,8 +613,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true;
this.barChartLabels = [];
this.barChartData = [];
// Reset flag after fetching
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
// Keep default data in case of error
}
);
@@ -271,8 +627,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true;
this.barChartLabels = [];
this.barChartData = [];
// Reset flag after fetching
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
}
}
@@ -434,17 +791,23 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Trigger change detection
// this.barChartData = [...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) {
// Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0;
this.barChartLabels = data.labels;
this.barChartData = data.datasets;
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 {
console.warn('Drilldown received data does not have expected structure', data);
this.noDataAvailable = true;
this.barChartLabels = [];
this.barChartData = [];
// Set loading state to false
this.isLoading = false;
}
},
(error) => {
@@ -452,12 +815,17 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true;
this.barChartLabels = [];
this.barChartData = [];
// Set loading state to false
this.isLoading = false;
// Keep current data in case of error
}
);
// Add subscription to array for cleanup
this.subscriptions.push(subscription);
// Set loading state
this.isLoading = true;
}
// Reset to original data (go back to base level)
@@ -661,6 +1029,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.originalBarChartLabels = [];
this.originalBarChartData = [];
// Clear multiselect tracking
this.openMultiselects.clear();
// Remove document click handler
this.removeDocumentClickHandler();
console.log('BarChartComponent destroyed and cleaned up');
}
}

View File

@@ -1,28 +1,310 @@
<div style="display:block">
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
<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>
<!-- 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>
</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 -->
<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
</div>
<!-- Chart display -->
<div *ngIf="!noDataAvailable">
<!-- Chart display - Always render the canvas but conditionally show/hide with CSS -->
<canvas baseChart
[datasets]="bubbleChartData"
[type]="bubbleChartType"
[options]="bubbleChartOptions"
(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>
</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">
<!-- Compact Filters -->
<div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0">
<app-compact-filter
*ngFor="let filter of baseFilters"
[filterKey]="filter.field"
(filterChange)="onFilterChange($event)">
</app-compact-filter>
<div class="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>
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator">
<span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span>
<button class="btn btn-secondary btn-sm" (click)="navigateBack()">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button class="btn btn-danger btn-sm" (click)="resetToOriginalData()">
Back to Main View
</button>
<!-- 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="chart-header">
<div class="clr-row header-row">
<div class="clr-col-6">
<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>
<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 -->
<div class="no-data-message" *ngIf="noDataAvailable">
<div class="no-data-message" *ngIf="noDataAvailable && doughnutChartLabels.length === 0">
<p>No chart data available</p>
</div>
<!-- Show chart when data is available -->
<canvas baseChart
*ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0"
[data]="doughnutChartData"
[datasets]="doughnutChartData"
[labels]="doughnutChartLabels"
[type]="doughnutChartType"
[options]="doughnutChartOptions"
@@ -42,17 +299,17 @@
</canvas>
<!-- 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>
</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">
<span class="legend-color" [style.background-color]="getLegendColor(i)"></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>

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;
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;
}
.doughnut-chart-container:hover {
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
transform: translateY(-2px);
}
// Filter section styling
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.compact-filters-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: 5px;
margin-bottom: 10px;
padding: 5px;
background: #ffffff;
border: 1px solid #e9ecef;
border-radius: 6px;
min-height: 40px;
}
gap: 15px;
}
.drilldown-indicator {
background-color: #e0e0e0;
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
margin-bottom: 15px;
border-radius: 8px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.drilldown-text {
font-weight: bold;
color: #333;
font-size: 16px;
}
.btn {
padding: 6px 12px;
border: none;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
cursor: pointer;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
transition: all 0.3s ease;
}
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.filter-input {
width: 100%;
.btn-sm {
padding: 4px 8px;
.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;
}
.btn-secondary {
background-color: #007cba;
color: white;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
&:hover {
border-color: #999;
}
}
.chart-header {
.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 {
font-size: 22px;
margin: 0;
font-size: 18px;
font-weight: 600;
color: #0a192f;
margin: 0;
text-align: center;
padding-bottom: 10px;
border-bottom: 2px solid #3498db;
}
}
}
}
.chart-wrapper {
position: relative;
// Chart wrapper and content - simplified to match shield dashboard
.chart-wrapper {
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;
}
position: relative;
.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;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
&.loading {
opacity: 0.7;
@@ -135,20 +206,15 @@
.no-data-message {
text-align: center;
padding: 30px;
padding: 20px;
color: #666;
font-size: 18px;
font-style: italic;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
.no-data-message p {
margin: 0;
canvas {
max-width: 100%;
max-height: calc(100% - 40px); // Leave space for legend
transition: filter 0.3s ease;
}
.loading-overlay {
@@ -171,69 +237,56 @@
animation: shimmer 1.5s infinite;
}
}
}
}
}
.chart-legend {
// Chart legend - simplified
.chart-legend {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
margin-top: 20px;
padding: 20px;
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);
}
margin: 20px 0;
padding: 15px;
.legend-item {
.legend-item {
display: flex;
align-items: center;
padding: 12px 20px;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 25px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
border: 1px solid #eaeaea;
cursor: pointer;
}
padding: 8px 15px;
background: #f8f9fa;
border-radius: 20px;
border: 1px solid #e9ecef;
min-width: 120px;
justify-content: space-between;
}
.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 {
width: 20px;
height: 20px;
.legend-color {
width: 15px;
height: 15px;
border-radius: 50%;
margin-right: 12px;
margin-right: 8px;
display: inline-block;
border: 2px solid white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
flex-shrink: 0;
}
.legend-label {
font-size: 16px;
font-weight: 600;
.legend-label {
font-size: 14px;
font-weight: 500;
color: #2c3e50;
margin-right: 15px;
margin-right: 10px;
white-space: nowrap;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
}
overflow: hidden;
text-overflow: ellipsis;
}
.legend-value {
font-size: 16px;
font-weight: 700;
.legend-value {
font-size: 14px;
font-weight: 600;
color: #3498db;
background: linear-gradient(135deg, #e9ecef 0%, #dde1e5 100%);
padding: 6px 12px;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
min-width: 40px;
text-align: center;
min-width: 30px;
text-align: right;
}
}
}
@keyframes shimmer {
@@ -245,46 +298,46 @@
}
}
/* Responsive design */
// Responsive design
@media (max-width: 768px) {
.doughnut-chart-container {
padding: 15px;
}
.chart-header .chart-title {
font-size: 18px;
}
.drilldown-indicator {
.chart-container {
.filter-controls {
flex-direction: column;
gap: 5px;
}
.drilldown-text {
font-size: 14px;
.filter-item {
min-width: 100%;
}
.chart-wrapper {
min-height: 200px;
.chart-header {
.header-row {
.chart-title {
font-size: 16px;
}
}
}
.chart-content {
min-height: 250px;
}
.chart-legend {
flex-direction: column;
align-items: center;
}
.legend-item {
width: 100%;
max-width: 300px;
justify-content: space-between;
}
.no-data-message {
font-size: 16px;
padding: 20px;
}
.compact-filters-container {
flex-wrap: wrap;
.chart-content {
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',
styleUrls: ['./doughnut-chart.component.scss']
})
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked {
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
@Input() xAxis: string;
@Input() yAxis: string | string[];
@Input() table: string;
@@ -36,7 +36,21 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
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 doughnutChartOptions: any = {
responsive: true,
@@ -72,6 +86,14 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
borderWidth: 2,
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
noDataAvailable: boolean = false;
// Loading state
isLoading: boolean = false;
// Flag to prevent infinite loops
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;
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
@@ -118,8 +148,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
// Validate initial data
this.validateChartData();
// Only fetch data if we have the required inputs, otherwise show default data
if (this.table && this.xAxis && this.yAxis) {
this.fetchChartData();
}
}
/**
* 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) {
// Add default data to ensure chart visibility
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
if (this.doughnutChartLabels.length !== this.doughnutChartData.length) {
const maxLength = Math.max(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[0]?.data?.length || 0);
while (this.doughnutChartLabels.length < maxLength) {
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
}
while (this.doughnutChartData.length < maxLength) {
this.doughnutChartData.push(0);
if (this.doughnutChartData[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 {
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
const xAxisChanged = changes.xAxis && !changes.xAxis.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');
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() {
@@ -198,12 +259,318 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
}
// Handle filter changes from compact filters
onFilterChange(event: { filterId: string, value: any }): void {
console.log('Compact filter changed:', event);
// The filter service will automatically trigger chart updates through the subscription
// 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
@@ -212,14 +579,18 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
}
fetchChartData(): void {
// Set loading state
this.isLoading = true;
// Set flag to prevent recursive calls
this.isFetchingData = true;
// If we're in drilldown mode, fetch the appropriate drilldown data
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
this.fetchDrilldownData();
// Reset flag after fetching
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
return;
}
@@ -289,7 +660,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
// 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}` : ''}`;
console.log('Doughnut chart data URL:', url);
console.log('Chart data URL:', url);
// 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
@@ -297,89 +668,113 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
(data: any) => {
console.log('Received doughnut chart data:', data);
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.doughnutChartLabels = [];
this.doughnutChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Reset flag after fetching
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
return;
}
// Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) {
// For doughnut charts, we need to extract the data differently
// The first dataset's data array contains the values for the doughnut chart
// Backend has already filtered the data, just display it
this.noDataAvailable = data.chartLabels.length === 0;
this.doughnutChartLabels = data.chartLabels || [];
if (data.chartData && data.chartData.length > 0) {
this.doughnutChartData = data.chartData[0].data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
} else {
this.doughnutChartData = [];
this.doughnutChartLabels = data.chartLabels;
// Handle different data structures
let chartDataValues;
if (Array.isArray(data.chartData)) {
// If chartData is already an array of values
if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
chartDataValues = data.chartData;
}
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.doughnutChartData = [...this.doughnutChartData];
// If chartData is an array with one object containing the data
else if (data.chartData.length > 0 && data.chartData[0].data) {
chartDataValues = data.chartData[0].data;
}
// Default case
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 });
} else if (data && data.labels && data.data) {
// Handle the original expected format as fallback
} else if (data && data.labels && data.datasets) {
// Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0;
this.doughnutChartLabels = data.labels || [];
this.doughnutChartData = data.data.map(value => {
// 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];
this.doughnutChartLabels = data.labels;
this.doughnutChartData = data.datasets;
console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
} else {
console.warn('Doughnut chart received data does not have expected structure', data);
// Reset to default data
console.warn('Received data does not have expected structure', data);
this.noDataAvailable = true;
this.doughnutChartLabels = [];
this.doughnutChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Keep default data instead of empty arrays
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
this.doughnutChartData = [
{
data: [30, 50, 20],
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
],
hoverBackgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
]
}
// Reset flag after fetching
];
}
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
},
(error) => {
console.error('Error fetching doughnut chart data:', error);
this.noDataAvailable = true;
this.doughnutChartLabels = [];
this.doughnutChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Reset flag after fetching
// Keep default data in case of error
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
this.doughnutChartData = [
{
data: [30, 50, 20],
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
],
hoverBackgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
]
}
];
// Reset flags after fetching
this.isFetchingData = false;
this.isLoading = false;
}
);
} else {
console.log('Missing required data for doughnut chart, showing default data:', { 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 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.doughnutChartData = [...this.doughnutChartData];
// Reset flag after fetching
console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
this.noDataAvailable = true;
this.doughnutChartLabels = [];
this.doughnutChartData = [];
// Reset flags after fetching
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
const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
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
if (data && data.chartLabels && data.chartData) {
// For doughnut charts, we need to extract the data differently
// The first dataset's data array contains the values for the doughnut chart
// Backend has already filtered the data, just display it
this.noDataAvailable = data.chartLabels.length === 0;
this.doughnutChartLabels = data.chartLabels || [];
if (data.chartData && data.chartData.length > 0) {
this.doughnutChartData = data.chartData[0].data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
} else {
this.doughnutChartData = [];
this.doughnutChartLabels = data.chartLabels;
// Handle different data structures
let chartDataValues;
if (Array.isArray(data.chartData)) {
// If chartData is already an array of values
if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
chartDataValues = data.chartData;
}
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.doughnutChartData = [...this.doughnutChartData];
// If chartData is an array with one object containing the data
else if (data.chartData.length > 0 && data.chartData[0].data) {
chartDataValues = data.chartData[0].data;
}
// Default case
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 });
} else if (data && data.labels && data.data) {
// Handle the original expected format as fallback
// Set loading state to false
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.doughnutChartLabels = data.labels || [];
this.doughnutChartData = data.data.map(value => {
// 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];
this.doughnutChartLabels = data.labels;
this.doughnutChartData = data.datasets;
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 {
console.warn('Drilldown received data does not have expected structure', data);
this.noDataAvailable = true;
this.doughnutChartLabels = [];
this.doughnutChartData = [];
// Validate and sanitize data
this.validateChartData();
// Keep current data instead of empty arrays
// Set loading state to false
this.isLoading = false;
}
},
(error) => {
console.error('Error fetching drilldown data:', error);
this.noDataAvailable = true;
this.doughnutChartLabels = [];
this.doughnutChartData = [];
// 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)
@@ -563,7 +994,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
console.log('Restored original labels');
}
if (this.originalDoughnutChartData.length > 0) {
this.doughnutChartData = [...this.originalDoughnutChartData];
this.doughnutChartData = JSON.parse(JSON.stringify(this.originalDoughnutChartData));
console.log('Restored original data');
}
@@ -605,44 +1036,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
}
}
/**
* Get color for legend item
* @param index Index of the legend item
*/
public getLegendColor(index: number): string {
// Get legend color for a specific index
getLegendColor(index: number): string {
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
public chartClicked(e: any): void {
console.log('Doughnut chart clicked:', e);
@@ -729,6 +1127,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
}
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">
<!-- 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 -->
<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>

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 { BaseChartDirective } from 'ng2-charts';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-dynamic-chart',
@@ -37,9 +39,20 @@ export class DynamicChartComponent implements OnInit, OnChanges {
@ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined;
constructor(private dashboardService: Dashboard3Service) { }
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
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
this.fetchChartData();
}
@@ -47,6 +60,12 @@ export class DynamicChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
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
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -107,6 +126,14 @@ export class DynamicChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops
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 {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@@ -139,7 +166,49 @@ export class DynamicChartComponent implements OnInit, OnChanges {
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
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;
}
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">
<!-- 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 -->
<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>

View File

@@ -1,108 +1,192 @@
.financial-chart-container {
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;
.filter-section {
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;
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 {
font-size: 20px;
margin-bottom: 15px;
}
.chart-wrapper {
min-height: 200px;
}
.no-data-message {
font-size: 16px;
padding: 20px;
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,8 @@
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';
import { AlertsService } from 'src/app/services/fnd/alerts.service';
@Component({
selector: 'app-financial-chart',
@@ -33,9 +36,21 @@ export class FinancialChartComponent implements OnInit, OnChanges {
// Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
constructor(private dashboardService: Dashboard3Service) { }
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService,
private alertService: AlertsService
) { }
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
this.fetchChartData();
}
@@ -43,6 +58,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
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
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -86,6 +109,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops
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 {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@@ -118,7 +149,49 @@ export class FinancialChartComponent implements OnInit, OnChanges {
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
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 {
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 class="dg-wrapper">
<div class="clr-row">
<div class="clr-col-8">
<h3>{{charttitle || 'Data Grid'}}</h3>
<!-- 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>
<div class="clr-row">
<!-- <div class="clr-col-8">
<h3>{{charttitle || 'Data Grid'}}</h3>
</div> -->
<!-- Add drilldown navigation controls -->
<div class="clr-col-4" *ngIf="drilldownEnabled && drilldownStack.length > 0" style="text-align: right;">
<button class="btn btn-sm btn-link" (click)="navigateBack()">

View File

@@ -1,28 +1,180 @@
// Add styles for drilldown navigation
.alert-info {
background-color: #dcedf7;
border-color: #a3d4f5;
color: #21333b;
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.alert-info .alert-icon {
color: #0072a3;
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.btn-link {
color: #0072a3;
text-decoration: none;
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.btn-link:hover {
color: #00567a;
text-decoration: underline;
.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;
}
}
.dg-wrapper {
padding: 12px;
padding: 15px;
}
.clr-row {
margin-bottom: 12px;
clr-datagrid {
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
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(
private mainservice: UsergrpmaintainceService,
private dashboardService: Dashboard3Service,
@@ -102,6 +111,12 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.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
if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
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
fetchGridData(): void {
@@ -628,6 +721,238 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
.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() {
// Unsubscribe from all subscriptions to prevent memory leaks
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.originalGridData = [];
// Clear multiselect tracking
this.openMultiselects.clear();
// Remove document click handler
this.removeDocumentClickHandler();
console.log('GridViewComponent destroyed and cleaned up');
}
}

View File

@@ -1,14 +1,283 @@
<div style="display: block">
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
<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>
<!-- 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>
</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 -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
@@ -16,12 +285,11 @@
</div>
<!-- Chart display -->
<div *ngIf="!noDataAvailable">
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
<canvas baseChart
[datasets]="lineChartData"
[labels]="lineChartLabels"
[options]="lineChartOptions"
[legend]="lineChartLegend"
[type]="lineChartType"
(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',
styleUrls: ['./line-chart.component.scss']
})
export class LineChartComponent implements OnInit, OnChanges {
export class LineChartComponent implements OnInit, OnChanges, OnDestroy {
@Input() xAxis: string;
@Input() yAxis: string | string[];
@Input() table: string;
@@ -88,6 +88,11 @@ export class LineChartComponent implements OnInit, OnChanges {
// 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
@@ -109,6 +114,12 @@ export class LineChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
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
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -140,6 +151,318 @@ export class LineChartComponent implements OnInit, OnChanges {
ngOnDestroy(): void {
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

View File

@@ -1,19 +1,287 @@
<div class="pie-chart-container">
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
</button>
<!-- 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" *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">
<!-- 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>
<p>Loading chart data...</p>
</div>
@@ -23,15 +291,15 @@
<p>No chart data available</p>
</div>
<!-- Show chart when data is available -->
<!-- Show chart when data is available or show default data -->
<canvas baseChart
*ngIf="pieChartLabels.length > 0 && pieChartData.length > 0"
[data]="pieChartData"
[datasets]="pieChartDatasets"
[labels]="pieChartLabels"
[type]="pieChartType"
[options]="pieChartOptions"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
(chartClick)="chartClicked($event)"
[style.display]="shouldShowChart() ? 'block' : 'none'">
</canvas>
</div>
<div class="chart-legend" *ngIf="showlabel && pieChartLabels && pieChartLabels.length > 0">

View File

@@ -149,10 +149,192 @@
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) {
.pie-chart-container {
padding: 15px;
height: auto;
min-height: 300px;
}
.chart-title {
@@ -179,4 +361,18 @@
font-size: 16px;
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',
styleUrls: ['./pie-chart.component.scss']
})
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
@Input() xAxis: string;
@Input() yAxis: string | 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 pieChartData: number[] = [30, 50, 20];
public pieChartDatasets: any[] = [
{
data: [30, 50, 20],
label: 'Dataset 1'
}
];
public pieChartType: string = 'pie';
public pieChartOptions: any = {
responsive: true,
@@ -96,11 +102,16 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
noDataAvailable: boolean = false;
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
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;
constructor(
private dashboardService: Dashboard3Service,
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 });
// Validate initial data
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();
}
}
ngOnChanges(changes: SimpleChanges): void {
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
const xAxisChanged = changes.xAxis && !changes.xAxis.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');
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 {
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
@@ -243,7 +588,7 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
// 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}` : ''}`;
console.log('Pie chart data URL:', url);
console.log('Chart data URL:', url);
// 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
@@ -251,12 +596,8 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
(data: any) => {
console.log('Received pie chart data:', data);
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.pieChartLabels = [];
this.pieChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Reset flag after fetching
this.isFetchingData = false;
return;
@@ -264,50 +605,57 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
// Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) {
// For pie charts, we need to extract the data differently
// The first dataset's data array contains the values for the pie chart
// Backend has already filtered the data, just display it
this.noDataAvailable = data.chartLabels.length === 0;
this.pieChartLabels = data.chartLabels || [];
if (data.chartData && data.chartData.length > 0) {
this.pieChartData = data.chartData[0].data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
this.pieChartLabels = data.chartLabels;
// Extract the actual data values from the chartData array
// chartData is an array with one object containing the data
if (data.chartData.length > 0 && data.chartData[0].data) {
this.pieChartData = data.chartData[0].data;
} else {
this.pieChartData = [];
}
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.pieChartLabels = [...this.pieChartLabels];
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 });
} else if (data && data.labels && data.data) {
// Handle the original expected format as fallback
} else if (data && data.labels && data.datasets) {
// Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0;
this.pieChartLabels = data.labels || [];
this.pieChartData = data.data.map(value => {
// 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();
this.pieChartLabels = data.labels;
this.pieChartData = data.datasets[0]?.data || [];
// Trigger change detection
this.pieChartLabels = [...this.pieChartLabels];
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 });
} else {
console.warn('Pie chart received data does not have expected structure', data);
// Reset to default data
console.warn('Received data does not have expected structure', data);
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Keep default data if no data is available
if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
this.pieChartData = [30, 50, 20];
this.pieChartDatasets = [
{
data: this.pieChartData,
label: 'Dataset 1'
}
];
}
}
// Reset flag after fetching
this.isFetchingData = false;
@@ -315,23 +663,13 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
(error) => {
console.error('Error fetching pie chart data:', error);
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Reset flag after fetching
this.isFetchingData = false;
}
);
} else {
console.log('Missing required data for pie chart, showing default data:', { 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 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];
console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
}
@@ -360,8 +698,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
} else {
console.warn('Invalid drilldown layer index:', layerIndex);
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
return;
}
}
@@ -372,8 +708,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
return;
}
@@ -470,64 +804,57 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
if (data === null) {
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
return;
}
// Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) {
// For pie charts, we need to extract the data differently
// The first dataset's data array contains the values for the pie chart
// Backend has already filtered the data, just display it
this.noDataAvailable = data.chartLabels.length === 0;
this.pieChartLabels = data.chartLabels || [];
if (data.chartData && data.chartData.length > 0) {
this.pieChartData = data.chartData[0].data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
this.pieChartLabels = data.chartLabels;
// Extract the actual data values from the chartData array
// chartData is an array with one object containing the data
if (data.chartData.length > 0 && data.chartData[0].data) {
this.pieChartData = data.chartData[0].data;
} else {
this.pieChartData = [];
}
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.pieChartLabels = [...this.pieChartLabels];
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 });
} else if (data && data.labels && data.data) {
// Handle the original expected format as fallback
} else if (data && data.labels && data.datasets) {
// Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0;
this.pieChartLabels = data.labels || [];
this.pieChartData = data.data.map(value => {
// 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();
this.pieChartLabels = data.labels;
this.pieChartData = data.datasets[0]?.data || [];
// Trigger change detection
this.pieChartLabels = [...this.pieChartLabels];
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 });
} else {
console.warn('Drilldown received data does not have expected structure', data);
this.noDataAvailable = true;
this.pieChartLabels = [];
this.pieChartData = [];
// Validate and sanitize data
this.validateChartData();
// Keep current data if no data is available
}
},
(error) => {
console.error('Error fetching drilldown data:', error);
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 {
}
}
/**
* Get color for legend item
* @param index Index of the legend item
*/
public getLegendColor(index: number): string {
// Validate chart data to ensure labels and data arrays have the same length
private validateChartData(): void {
if (this.pieChartLabels && this.pieChartData) {
// For pie charts, we need to ensure labels and data arrays have the same length
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];
}
/**
* Ensure labels and data arrays have the same length
*/
private syncLabelAndDataArrays(): void {
// Ensure we have matching arrays
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);
}
}
// Method to determine if chart should be displayed
shouldShowChart(): boolean {
// Show chart if we have data
if (this.pieChartLabels.length > 0 && this.pieChartData.length > 0) {
return true;
}
/**
* Validate and sanitize chart data
*/
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 = [];
// Show chart if we're still fetching data
if (this.isFetchingData) {
return true;
}
if (!Array.isArray(this.pieChartData)) {
this.pieChartData = [];
// Show chart if we have default data
if (this.pieChartLabels.length > 0 && this.originalPieChartLabels.length > 0) {
return true;
}
// Ensure we have some data to display
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;
return false;
}
// events
@@ -752,6 +1049,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
}
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
[datasets]="polarAreaChartData"
[labels]="polarAreaChartLabels"
[options]="polarAreaChartOptions"
[type]="polarAreaChartType"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
</canvas>
</div>
</div>

View File

@@ -1,18 +1,192 @@
// Polar Chart Component Styles
div[style*="display: block"] {
.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;
width: 100%;
height: 100%;
}
canvas {
max-width: 100%;
max-height: 100%;
.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;
}
}
// Ensure the chart container has proper sizing
:host {
display: block;
width: 100%;
height: 100%;
.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 { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-polar-chart',
@@ -33,9 +35,20 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
constructor(private dashboardService: Dashboard3Service) { }
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
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
this.fetchChartData();
}
@@ -43,6 +56,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
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
const xAxisChanged = changes.xAxis && !changes.xAxis.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'}
];
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';
// Multi-layer drilldown state tracking
@@ -85,6 +130,324 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops
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 {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@@ -117,7 +480,49 @@ export class PolarChartComponent implements OnInit, OnChanges {
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
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.');
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
// Reset flag after fetching
this.isFetchingData = false;
return;
@@ -145,32 +555,54 @@ export class PolarChartComponent implements OnInit, OnChanges {
this.noDataAvailable = data.chartLabels.length === 0;
this.polarAreaChartLabels = data.chartLabels;
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
return isNaN(Number(value)) ? 0 : Number(value);
});
} else {
this.polarAreaChartData = [];
// Assign data in the correct format (array of objects with data property)
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 });
} else if (data && data.labels && data.data) {
// Handle the original expected format as fallback
this.noDataAvailable = data.labels.length === 0;
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
return isNaN(Number(value)) ? 0 : Number(value);
});
// Trigger change detection
this.polarAreaChartData = [...this.polarAreaChartData];
// Assign data in the correct format (array of objects with data property)
this.polarAreaChartData = [
{
data: chartValues,
label: 'Dataset 1'
}
];
console.log('Updated polar chart with legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
} else {
console.warn('Polar chart received data does not have expected structure', data);
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
}
// Reset flag after fetching
this.isFetchingData = false;
@@ -179,7 +611,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.error('Error fetching polar chart data:', error);
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
// Reset flag after fetching
this.isFetchingData = false;
// 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 });
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
// Reset flag after fetching
this.isFetchingData = false;
}
@@ -219,7 +661,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.warn('Invalid drilldown layer index:', layerIndex);
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
return;
}
}
@@ -231,7 +678,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
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
const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
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.');
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
return;
}
// Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) {
// 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.polarAreaChartLabels = data.chartLabels;
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
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 {
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
}
// Trigger change detection
this.polarAreaChartData = [...this.polarAreaChartData];
@@ -325,10 +823,18 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Handle the original expected format as fallback
this.noDataAvailable = data.labels.length === 0;
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
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
this.polarAreaChartData = [...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);
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
}
},
(error) => {
console.error('Error fetching drilldown data:', error);
this.noDataAvailable = true;
this.polarAreaChartLabels = [];
this.polarAreaChartData = [];
this.polarAreaChartData = [
{
data: [],
label: 'Dataset 1'
}
];
// Keep current data in case of error
}
);
@@ -417,13 +933,13 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Get the label of the clicked element
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 (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode
this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels];
this.originalPolarAreaChartData = [...this.polarAreaChartData];
this.originalPolarAreaChartData = JSON.parse(JSON.stringify(this.polarAreaChartData));
console.log('Stored original data for drilldown');
}
@@ -491,6 +1007,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
}
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">
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
<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>
<!-- 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>
</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 -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
@@ -16,7 +285,7 @@
</div>
<!-- Chart display -->
<div *ngIf="!noDataAvailable">
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
<canvas baseChart
[datasets]="radarChartData"
[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 { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-radar-chart',
@@ -62,15 +64,40 @@ export class RadarChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops
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 {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.fetchChartData();
}
ngOnChanges(changes: SimpleChanges): void {
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
const xAxisChanged = changes.xAxis && !changes.xAxis.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 {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@@ -126,7 +463,49 @@ export class RadarChartComponent implements OnInit, OnChanges {
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
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
const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url);
@@ -321,7 +729,6 @@ export class RadarChartComponent implements OnInit, OnChanges {
this.noDataAvailable = data.chartLabels.length === 0;
this.radarChartLabels = data.chartLabels;
// 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 => ({
...dataset,
data: dataset.data ? dataset.data.map(value => {
@@ -358,6 +765,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
this.noDataAvailable = true;
this.radarChartLabels = [];
this.radarChartData = [];
// Keep current data in case of error
}
);
}
@@ -436,7 +844,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
if (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode
this.originalRadarChartLabels = [...this.radarChartLabels];
this.originalRadarChartData = [...this.radarChartData];
this.originalRadarChartData = JSON.parse(JSON.stringify(this.radarChartData));
console.log('Stored original data for drilldown');
}
@@ -504,6 +912,12 @@ export class RadarChartComponent implements OnInit, OnChanges {
}
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">
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
<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>
<!-- 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>
</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 -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
@@ -16,9 +285,10 @@
</div>
<!-- Chart display -->
<div *ngIf="!noDataAvailable">
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 70px); min-height: 300px;">
<canvas baseChart
[datasets]="scatterChartData"
[options]="scatterChartOptions"
[type]="scatterChartType"
(chartHover)="chartHovered($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 { ChartData,ChartDataset } from 'chart.js';
import { ChartData,ChartDataset,ChartOptions } from 'chart.js';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-scatter-chart',
@@ -34,9 +36,20 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
constructor(private dashboardService: Dashboard3Service) { }
constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
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
this.fetchChartData();
}
@@ -44,6 +57,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
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
const xAxisChanged = changes.xAxis && !changes.xAxis.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';
// Multi-layer drilldown state tracking
@@ -107,6 +209,417 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops
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 {
// Set flag to prevent recursive calls
this.isFetchingData = true;
@@ -139,7 +652,49 @@ export class ScatterChartComponent implements OnInit, OnChanges {
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
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}
this.noDataAvailable = data.chartLabels.length === 0;
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);
} else if (data && data.labels && data.datasets) {
// Handle the original expected format as fallback
this.noDataAvailable = data.labels.length === 0;
this.scatterChartData = data.datasets;
// Update chart options with axis titles
this.updateChartOptionsWithAxisTitles();
console.log('Updated scatter chart with legacy data format:', this.scatterChartData);
} else {
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
fetchDrilldownData(): void {
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
let drilldownFilterParams = '';
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
const filterObj = {};
this.drilldownFilters.forEach(filter => {
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value;
// 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(filterObj).length > 0) {
drilldownFilterParams = JSON.stringify(filterObj);
}
}
console.log('Drilldown filter parameters:', drilldownFilterParams);
// Use drilldown filters if available, otherwise use layer filters
const finalFilterParams = drilldownFilterParams || filterParams;
console.log('Final filter parameters:', finalFilterParams);
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
// 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}` : ''}`;
@@ -312,7 +899,7 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Fetch data from the dashboard service with 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) => {
console.log('Received drilldown data:', data);
if (data === null) {
@@ -325,7 +912,6 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) {
// 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.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
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)
resetToOriginalData(): void {
console.log('Resetting to original data');
@@ -436,16 +995,18 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Get the index of the clicked element
const clickedIndex = e.active[0].index;
// Get the label of the clicked element
// For scatter charts, we might not have labels in the same way as other charts
const clickedLabel = `Point ${clickedIndex}`;
// Get the dataset index
const datasetIndex = e.active[0].datasetIndex;
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 (this.currentDrilldownLevel === 0) {
// 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');
}
@@ -487,9 +1048,10 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Add this click to the drilldown stack
const stackEntry = {
level: nextDrilldownLevel,
datasetIndex: datasetIndex,
clickedIndex: clickedIndex,
clickedLabel: clickedLabel,
clickedValue: clickedLabel // Using label as value for now
dataPoint: dataPoint,
clickedValue: dataPoint // Using data point as value for now
};
this.drilldownStack.push(stackEntry);
@@ -513,6 +1075,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
}
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>
<th class="c-col">#</th>
<th>Item</th>
@@ -8,20 +291,17 @@
<td class="c-col">{{i + 1}}</td>
<td>{{todo}}</td>
<td style="text-align:right">
<a routerLink="." (click)="removeTodo(i)">
<a (click)="removeTodo(i)" class="remove-button">
<clr-icon shape="times"></clr-icon>
</a>
</td>
</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 { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-to-do-chart',
@@ -21,15 +24,49 @@ export class ToDoChartComponent implements OnInit, OnChanges {
@Input() datasource: string;
@Input() fieldName: string;
@Input() connection: number; // Add connection input
// Drilldown configuration inputs
@Input() drilldownEnabled: boolean = false;
@Input() drilldownApiUrl: string;
@Input() drilldownXAxis: string;
@Input() drilldownYAxis: string;
@Input() drilldownParameter: string; // Add drilldown parameter input
@Input() baseFilters: any[] = []; // Add base filters input
@Input() drilldownFilters: any[] = []; // Add drilldown filters input
// Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
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 {
// 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 {
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
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -46,26 +83,119 @@ export class ToDoChartComponent implements OnInit, OnChanges {
data: any;
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 {
// If we have the necessary data, fetch to-do data from the service
if (this.table) {
console.log('Fetching to-do data for:', { table: this.table });
if (this.table && this.xAxis) {
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
// This is a placeholder implementation - you may need to adjust based on your API
console.log('To-do chart would fetch data from table:', this.table);
// Convert baseFilters to filter parameters
let filterParams = '';
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
// For now, we'll just keep the default to-do list
// 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 {
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) {
this.todoList.push(todo);
if (todo && todo.trim() !== '') {
this.todoList.push(todo.trim());
this.todo = ''; // Clear the input field
}
}
public removeTodo(todoIx: number) {
@@ -73,4 +203,353 @@ export class ToDoChartComponent implements OnInit, OnChanges {
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

@@ -12,23 +12,23 @@ import { ModulesetupService } from 'src/app/services/builder/modulesetup.service
styleUrls: ['./dashrunnerall.component.scss']
})
export class DashrunnerallComponent implements OnInit {
addModall:boolean = false;
selected:any[] = [];
addModall: boolean = false;
selected: any[] = [];
loading = false;
data:any;
id:any;
moduleId:any;
data: any;
id: any;
moduleId: any;
modalDelete = false;
rowSelected :any= {};
rowSelected: any = {};
rows: any[];
projectname;
projectId;
error;
constructor(
private router : Router,
private route: ActivatedRoute,private dashboardService : Dashboard3Service,
private router: Router,
private route: ActivatedRoute, private dashboardService: Dashboard3Service,
// private wireframeservice : WireframeService,
private excel: ExcelService,private mainService: ModulesetupService,
private excel: ExcelService, private mainService: ModulesetupService,
private toastr: ToastrService,) { }
ngOnInit(): void {
@@ -42,41 +42,46 @@ export class DashrunnerallComponent implements OnInit {
// this.getprojectName(this.projectId);
}
getprojectName(id){
getprojectName(id) {
this.mainService.getProjectModules(id).subscribe((data) => {
console.log(data);
this.projectname=data.items[0]['projectName'];
this.projectname = data.items[0]['projectName'];
console.log(this.projectname);
});
}
getdashboard()
{
this.dashboardService.getAllDash().subscribe((data) =>{
getdashboard() {
this.dashboardService.getAllDash().subscribe((data) => {
this.data = data;
this.rows = this.data;
console.log(data);
this.error="No data Available";
this.error = "No data Available";
console.log(this.error);
});
}
openModal()
{
openModal() {
this.addModall = true;
}
gotoadd()
{
this.router.navigate(['../../dashboardbuilder'],{relativeTo:this.route});
gotoadd() {
this.router.navigate(['../../dashboardbuilder'], { relativeTo: this.route });
}
goToEdit(id:number)
{
this.router.navigate(['../dashrunner/'+id],{relativeTo:this.route});
// for runner line navigation
// goToEditData(id: number){
// 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){
this.router.navigate(['../editdata/'+id],{relativeTo:this.route});
goToEditData(id: number) {
this.router.navigate(['../editdata/' + id], { relativeTo: this.route });
}
onExport() {
@@ -84,23 +89,22 @@ export class DashrunnerallComponent implements OnInit {
moment().format('YYYYMMDD_HHmmss'))
}
gotoAction(){
this.router.navigate(["../../actions"], { relativeTo: this.route, queryParams: { m_id: this.moduleId,pname:this.projectname } });
gotoAction() {
this.router.navigate(["../../actions"], { relativeTo: this.route, queryParams: { m_id: this.moduleId, pname: this.projectname } });
}
gotoRepo(){
gotoRepo() {
this.router.navigate(["../../modulecard"], { relativeTo: this.route, queryParams: { p_id: this.projectId } });
}
onDelete(row){
onDelete(row) {
this.rowSelected = row;
console.log(this.rowSelected);
this.modalDelete = true;
}
delete(id)
{
delete(id) {
this.modalDelete = false;
console.log("in delete "+id);
this.dashboardService.deleteField(id).subscribe((data)=>{
console.log("in delete " + id);
this.dashboardService.deleteField(id).subscribe((data) => {
console.log(data);
this.ngOnInit();
});

View File

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

View File

@@ -23,6 +23,18 @@
<label for="workflow_name">{{'ACTIVE'| translate}}</label>
<input type="checkbox" formControlName="active" clrToggle value="billable" name="billable" />
</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">
<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 { ToastrService } from 'ngx-toastr';
import { ReportBuilderService } from 'src/app/services/api/report-builder.service';
import { SureconnectService } from '../../dashboardnew/sureconnect/sureconnect.service';
@Component({
selector: 'app-report-build2add',
templateUrl: './report-build2add.component.html',
@@ -11,8 +13,12 @@ import { ReportBuilderService } from 'src/app/services/api/report-builder.servic
})
export class ReportBuild2addComponent implements OnInit {
public entryForm: FormGroup;
// Add sureconnect data property
sureconnectData: any[] = [];
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 {
this.entryForm = this._fb.group({
@@ -20,9 +26,23 @@ export class ReportBuild2addComponent implements OnInit {
description:[null],
active:[null],
isSql:[false],
// Add sureConnectId field to the form
sureConnectId: [null],
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() {

View File

@@ -18,41 +18,58 @@
<clr-icon shape="plus"></clr-icon>{{'ADD' | translate}}
</button>
</div>
</div>
</div>
<clr-datagrid [clrDgLoading]="loading">
<clr-datagrid [clrDgLoading]="loading">
<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}">
{{'GO_TO' | translate}}
</ng-container></clr-dg-column>
<clr-dg-column [clrDgField]="'name'"><ng-container *clrDgHideableColumn="{hidden: false}">
{{'REPORT_NAME' | translate}}
</ng-container></clr-dg-column>
<clr-dg-column [clrDgField]="'description'"><ng-container *clrDgHideableColumn="{hidden: false}">
{{'REPORT_DESCRIPTION' | translate}}
</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}">
{{'ACTIVE' | translate}}
</ng-container></clr-dg-column>
<clr-dg-column><ng-container *clrDgHideableColumn="{hidden: false}">
<clr-icon shape="bars"></clr-icon>{{'ACTION' | translate}}
</ng-container></clr-dg-column>
<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.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>
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true" 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>
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true"
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>
</a>
<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>
<h5 style="margin-top: 0">{{'WHO_COLUMN' | translate}}</h5>
<div>{{'ACCOUNT_ID' | translate}}: <code class="clr-code">{{user.accountId}}</code></div>
@@ -87,16 +104,16 @@
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<clr-modal [(clrModalOpen)]="modaldelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
<clr-modal [(clrModalOpen)]="modaldelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
<div class="modal-body" *ngIf="rowSelected.id">
<h1 class="delete">{{'DELETE_CONFIRMATION' | translate}}</h1>
<h2 class="heading">{{rowSelected.id}}</h2>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="modaldelete = false">{{'CANCEL' | translate}}</button>
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary" >{{'DELETE' | translate}}</button>
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary">{{'DELETE' | translate}}</button>
</div>
</div>
</clr-modal>
</clr-modal>

View File

@@ -26,20 +26,35 @@
</div> -->
<div class="clr-col-md-4 clr-col-sm-12">
<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>
</button></span></div>
</div>
<div class="clr-col-md-4 clr-col-sm-12">
<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>
<!-- 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">
<label>Standard Parameters</label>
<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> -->
<clr-combobox formControlName="std_param_html" name="std_param_html" [(ngModel)]="nodeEditProperties.std_param_html" clrMulti="true"
required>
<clr-combobox formControlName="std_param_html" name="std_param_html"
[(ngModel)]="nodeEditProperties.std_param_html" clrMulti="true" required>
<ng-container *clrOptionSelected="let selected">
{{selected}}
</ng-container>
@@ -91,5 +106,4 @@
<button type="submit" form-control class="btn btn-primary" (click)="onSubmit()">UPDATE</button>
</div>
</form>
</div>
</div>

View File

@@ -3,6 +3,7 @@ import { FormBuilder, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { ReportBuilderService } from 'src/app/services/api/report-builder.service';
import { SureconnectService } from '../../dashboardnew/sureconnect/sureconnect.service';
@Component({
@@ -13,54 +14,75 @@ import { ReportBuilderService } from 'src/app/services/api/report-builder.servic
export class ReportBuild2editComponent implements OnInit {
public entryForm: FormGroup;
updated = false;
ReportData:any = {};
ReportData: any = {};
id: number;
nodeEditProperties = {
std_param_html:'',
adhoc_param_html:'',
std_param_html: '',
adhoc_param_html: '',
// column_str:'',
// conn_name:'',
date_param_req:'',
date_param_req: '',
// folderName:'',
url:'',
url: '',
// Add sureConnectId property
sureConnectId: null,
};
// Add sureconnect data property
sureconnectData: any[] = [];
};
constructor(private router: Router,
private route: ActivatedRoute,private reportBuilderService: ReportBuilderService,
private toastr: ToastrService, private _fb: FormBuilder) { }
private route: ActivatedRoute, private reportBuilderService: ReportBuilderService,
private toastr: ToastrService, private _fb: FormBuilder,
private sureconnectService: SureconnectService) { }
ngOnInit(): void {
this.id = this.route.snapshot.params["id"];
console.log("update with id = ", this.id);
this.entryForm = this._fb.group({
std_param_html : [null],
adhoc_param_html:[null],
std_param_html: [null],
adhoc_param_html: [null],
// column_str:[null],
// conn_name:[null],
date_param_req:[null],
date_param_req: [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.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;
listoddatabase(){
this.reportBuilderService.getdatabse().subscribe((data)=>{
this.databaselist=data;
listoddatabase() {
this.reportBuilderService.getdatabse().subscribe((data) => {
this.databaselist = data;
console.log(this.databaselist)
},(error) => {
}, (error) => {
console.log(error);
if(error){
if (error) {
}
});
}
builderLine;
lineId;
builderLineData:any[] = [];
builderLineData: any[] = [];
getById(id: number) {
this.reportBuilderService.getrbDetailsById(id).subscribe(
(data) => {
@@ -70,10 +92,9 @@ export class ReportBuild2editComponent implements OnInit {
this.builderLine = this.ReportData.rpt_builder2_lines;
this.lineId = this.builderLine[0].id
console.log("line data ",this.lineId, this.builderLine);
if(this.builderLine[0].model != '')
{
this.builderLineData = JSON.parse(this.builderLine[0].model) ;
console.log("line data ", this.lineId, this.builderLine);
if (this.builderLine[0].model != '') {
this.builderLineData = JSON.parse(this.builderLine[0].model);
console.log(this.builderLineData);
this.nodeEditProperties.std_param_html = this.builderLineData[0].std_param_html;
@@ -82,6 +103,11 @@ export class ReportBuild2editComponent implements OnInit {
// this.nodeEditProperties.conn_name = this.builderLineData.conn_name;
this.nodeEditProperties.date_param_req = this.builderLineData[0].date_param_req;
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) => {
@@ -92,21 +118,21 @@ export class ReportBuild2editComponent implements OnInit {
stdparams;
keysfromurl;
getkeys(){
if(this.nodeEditProperties.url !== null){
this.reportBuilderService.getcolumnDetailsByurl(this.nodeEditProperties.url).subscribe(data =>{
console.log(data);
getkeys() {
if (this.nodeEditProperties.url !== null) {
this.reportBuilderService.getcolumnDetailsByurl(this.nodeEditProperties.url).subscribe(data => {
console.log('coloum list data ', data);
this.keysfromurl = data;
this.nodeEditProperties.adhoc_param_html = this.keysfromurl;
})
}else{
} else {
this.toastr.error("URL is required");
}
}
listBuilder_Lines = {
model:{}
model: {}
}
update() {
@@ -116,6 +142,8 @@ export class ReportBuild2editComponent implements OnInit {
adhoc_param_html: this.nodeEditProperties.adhoc_param_html,
date_param_req: this.nodeEditProperties.date_param_req,
url: this.nodeEditProperties.url,
// Add sureConnectId to the data
sureConnectId: this.nodeEditProperties.sureConnectId,
};
this.builderLineData[0].std_param_html = this.nodeEditProperties.std_param_html;
@@ -124,12 +152,15 @@ export class ReportBuild2editComponent implements OnInit {
// this.builderLineData.conn_name = this.nodeEditProperties.conn_name ;
this.builderLineData[0].date_param_req = this.nodeEditProperties.date_param_req;
this.builderLineData[0].url = this.nodeEditProperties.url;
// Add sureConnectId to the data
this.builderLineData[0].sureConnectId = this.nodeEditProperties.sureConnectId;
console.log(this.builderLineData);
// this.builderLineData.splice(1);
console.log(this.builderLineData);
let tmp = JSON.stringify(this.builderLineData); //.replace(/\\/g, '')
this.listBuilder_Lines.model = tmp;
console.log(this.listBuilder_Lines);
console.log(this.listBuilder_Lines);
this.reportBuilderService.updaterbLineData(this.listBuilder_Lines, this.lineId).subscribe(
(data) => {
@@ -149,6 +180,8 @@ console.log(this.listBuilder_Lines);
onSubmit() {
this.updated = true;
// Update nodeEditProperties with form values including sureConnectId
Object.assign(this.nodeEditProperties, this.entryForm.value);
this.update();
}

View File

@@ -104,8 +104,11 @@ todayDate;
adhocList:any[];
SQLQuery;
getUrl;
// Add sureConnectId property
sureConnectId: number | null = null;
stdParamfields;
DateParam;
getById(id: number) {
this.reportBuilderService.getrbDetailsById(id).subscribe(
(data) => {
@@ -120,6 +123,8 @@ todayDate;
// this.adhocList = JSON.parse(adhocList);
this.DateParam = this.builderLineData.date_param_req;
this.getUrl = this.builderLineData.url;
// Get sureConnectId if it exists
this.sureConnectId = this.builderLineData.sureConnectId || null;
console.log(this.adhocList,this.DateParam,this.getUrl)
this.getStdParam(this.header_id);
this.featchData();
@@ -127,22 +132,58 @@ todayDate;
}
/**
* Fetch data using the URL and SureConnect connection if available
*/
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 =>{
console.log(data);
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));
this.rows = 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 = [];
});
}
dynamicHtml:any = [];
dynamicHtml: any = [];
dynamicHtmlFlag = false;
stdParmas;
stdParamFlag = false;
getStdParam(id: any){
getStdParam(id: any) {
console.log(this.builderLineData.std_param_html);
this.dynamicHtml = this.builderLineData.std_param_html;
// this.dynamicHtml = ['a.abc','b.abcde']
@@ -154,9 +195,9 @@ todayDate;
}
console.log(this.dynamicForm.value);
}
if(this.dynamicHtml == undefined || this.dynamicHtml == ''){
if (this.dynamicHtml == undefined || this.dynamicHtml == '') {
this.dynamicHtmlFlag = false;
}else{
} else {
this.dynamicHtmlFlag = true;
}
@@ -174,37 +215,37 @@ todayDate;
// }
// });
}
modo2(val){
modo2(val) {
console.log(val);
this.selectedfrom=val;
this.selectedfrom = val;
}
modo3(val){
modo3(val) {
console.log(val);
this.selectedto=val;
this.selectedto = val;
}
duplicateArray=[];
duplicateArray = [];
myDateValue: Date;
toDate:Date;
toDate: Date;
reverseAndTimeStamp(dateString) {
const reverse = new Date(dateString.split("-").reverse().join("-"));
return reverse.getTime();
}
filterDate() {
let fromdate=moment(this.myDateValue).format('DD-MM-YYYY');
console.log(fromdate)
let todate=moment(this.toDate).format('DD-MM-YYYY');
if(this.myDateValue && this.toDate){
const selectedMembers = this.array.filter(m => {
let fromdate = moment(this.myDateValue).format('DD-MM-YYYY');
console.log(fromdate)
let todate = moment(this.toDate).format('DD-MM-YYYY');
if (this.myDateValue && this.toDate) {
const selectedMembers = this.array.filter(m => {
return this.reverseAndTimeStamp(m.fromDate) >= this.reverseAndTimeStamp(fromdate) && this.reverseAndTimeStamp(m.fromDate) <= this.reverseAndTimeStamp(todate)
}
);
this.duplicateArray=selectedMembers
}else{
this.duplicateArray=this.array
}
this.duplicateArray = selectedMembers
} else {
this.duplicateArray = this.array
}
console.log(this.duplicateArray); // the result objects
this.modalselect=false;
}
this.modalselect = false;
}
dateParameter: string;
from_date: Date;
@@ -228,10 +269,10 @@ this.duplicateArray=this.array
this.to_date = new Date(this.from_date);
this.to_date.setDate(this.from_date.getDate() + 6);
console.log(this.from_date);
this.myDateValue=this.from_date;
this.myDateValue = this.from_date;
console.log(this.to_date);
console.log(this.myDateValue);
this.toDate=this.to_date;
this.toDate = this.to_date;
this.FromDatequery = this.from_date.toISOString().substring(0, 10);
this.ToDatequery = this.to_date.toISOString().substring(0, 10);
@@ -254,10 +295,10 @@ this.duplicateArray=this.array
// Calculate the date of Sunday of the previous week
this.to_date = new Date(this.from_date);
this.to_date.setDate(this.from_date.getDate() + 6);
this.myDateValue=this.from_date;
this.myDateValue = this.from_date;
console.log(this.to_date);
console.log(this.myDateValue);
this.toDate=this.to_date;
this.toDate = this.to_date;
this.FromDatequery = this.from_date.toISOString().substring(0, 10);
@@ -328,10 +369,10 @@ this.duplicateArray=this.array
// Calculate the date of the last day of the current year
this.to_date = new Date(currentDate.getFullYear(), 11, 31);
this.myDateValue=this.from_date;
this.myDateValue = this.from_date;
console.log(this.to_date);
console.log(this.myDateValue);
this.toDate=this.to_date;
this.toDate = this.to_date;
this.FromDatequery = this.from_date.toISOString().substring(0, 10);
@@ -349,48 +390,48 @@ this.duplicateArray=this.array
// Calculate the date of the last day of the previous year
this.to_date = new Date(currentDate.getFullYear() - 1, 11, 31);
this.myDateValue=this.from_date;
this.myDateValue = this.from_date;
console.log(this.to_date);
console.log(this.myDateValue);
this.toDate=this.to_date;
this.toDate = this.to_date;
this.FromDatequery = this.from_date.toISOString().substring(0, 10);
this.ToDatequery = this.to_date.toISOString().substring(0, 10);
// this.filterDate();
}
SelectdateType;
select(val:any){
select(val: any) {
console.log(val);
this.SelectdateType = val;
if(val === 'This Week'){
if (val === 'This Week') {
this.FromDatequery = null;
this.ToDatequery = null;
this.calculateThisWeek()
}else if(val === 'Last Week'){
} else if (val === 'Last Week') {
this.FromDatequery = null;
this.ToDatequery = null;
this.calculateLastWeek()
}else if(val === 'This Month'){
} else if (val === 'This Month') {
this.FromDatequery = null;
this.ToDatequery = null;
this.calculateThisMonth()
}else if(val === 'Last Month'){
} else if (val === 'Last Month') {
this.FromDatequery = null;
this.ToDatequery = null;
this.calculateLastMonth()
// }else if(val === 'To Specific FromDate To To Date'){
// this.openmodel()
}
else if(val === 'This Year'){
else if (val === 'This Year') {
this.FromDatequery = null;
this.ToDatequery = null;
this.calculateThisYear()
}else if(val === 'Last Year'){
} else if (val === 'Last Year') {
this.FromDatequery = null;
this.ToDatequery = null;
this.calculateLastYear()
}
else if(val === 'Today'){
else if (val === 'Today') {
this.FromDatequery = null;
this.ToDatequery = null;
this.myDateValue = this.todayDate;
@@ -399,7 +440,7 @@ this.duplicateArray=this.array
this.FromDatequery = this.myDateValue;
this.ToDatequery = this.toDate;
}
else if(val === '--Select Particular--'){
else if (val === '--Select Particular--') {
this.FromDatequery = null;
this.ToDatequery = null;
this.newfrom = null;
@@ -410,8 +451,8 @@ this.duplicateArray=this.array
}
openmodel(){
this.modalselect=true;
openmodel() {
this.modalselect = true;
}
onExport() {
@@ -423,15 +464,15 @@ this.duplicateArray=this.array
downloadFile(format: string) {
const date = moment().format('YYYYMMDD_HHmmss')
const reportNameWithUnderscore = this.reportName + '_' + date;
this.reportBuilderService.downloadFile(format, this.filterRows,reportNameWithUnderscore)
this.reportBuilderService.downloadFile(format, this.filterRows, reportNameWithUnderscore)
}
back(){
back() {
this.router.navigate(["../../all"], { relativeTo: this.route });
}
FormattedAdhocparameters;
adocdata;
// showPlusIconRow: number | null = 0;
onAddLines(){
FormattedAdhocparameters;
adocdata;
// showPlusIconRow: number | null = 0;
onAddLines() {
console.log(this.serverData);
const lastRow = this.serverData[this.serverData.length - 1];
if (lastRow && lastRow.fields_name !== '') {
@@ -494,8 +535,8 @@ adocdata;
}
rows:any[];
filterRows:any[];
rows: any[];
filterRows: any[];
columns: any[];
rowdata;
FromDatequery;
@@ -505,11 +546,11 @@ adocdata;
newto;
dateKey;
runtheQuery(){
console.log(this.myDateValue , this.toDate);
runtheQuery() {
console.log(this.myDateValue, this.toDate);
let query = this.SQLQuery;
// let query
if(this.dynamicForm.value){
// let query
if (this.dynamicForm.value) {
// for(let i = 0; i < this.dynamicForm.value.length; i++){
// // const query = this.SQLQuery + " AND " + this.dynamicForm.controls[i] + " = " + this.dynamicForm.value[i]
@@ -519,7 +560,7 @@ runtheQuery(){
// Iterate over the keys in dynamicForm.value
Object.keys(this.dynamicForm.value).forEach((key) => {
// Append the condition for each key to the query
if (this.dynamicForm.value[key] !== null ) {
if (this.dynamicForm.value[key] !== null) {
this.selectcolumn(this.dynamicForm.value);
// query += ` AND ${key} = '${this.dynamicForm.value[key]}'`;
}
@@ -539,27 +580,27 @@ runtheQuery(){
// query += ` AND cretaedat BETWEEN '${this.FromDatequery}' AND '${this.ToDatequery}'`;
// }else
if(this.DateParam == true){
if (this.DateParam == true) {
this.dateKey = 'createdat';
this.adhocList.forEach(key => {
if (key.includes("created_at")) {
this.dateKey ="created_at" ;
this.dateKey = "created_at";
}
});
this.adhocList.forEach(key => {
if (key.includes("createdAt")) {
this.dateKey ="createdAt" ;
this.dateKey = "createdAt";
}
});
if(this.myDateValue && this.toDate){
if(this.myDateValue){
});
if (this.myDateValue && this.toDate) {
if (this.myDateValue) {
this.newfrom = new Date(this.myDateValue);
// const year = inputDate.getFullYear();
// const month = String(inputDate.getMonth() + 1).padStart(2, "0"); // Months are zero-based, so add 1
// const day = String(inputDate.getDate()).padStart(2, "0");
// this.newfrom = `${year}-${month}-${day}`;
}
if(this.toDate){
if (this.toDate) {
this.newto = new Date(this.toDate);
// const year = inputDate.getFullYear();
// const month = String(inputDate.getMonth() + 1).padStart(2, "0"); // Months are zero-based, so add 1
@@ -569,40 +610,37 @@ runtheQuery(){
}
query += ` AND ${this.dateKey} BETWEEN '${this.newfrom}' AND '${this.newto}'`;
}
}
}
// if(this.myDateValue && this.toDate){
// query += ` AND from_date = NVL(${this.myDateValue}from_date, 'from_date') AND to_date = NVL(${this.toDate}to_date, 'to_date')`;
// }
console.log(query);
}
// if(this.FormattedAdhocparameters){
// query += this.FormattedAdhocparameters
// }
// if(this.FormattedAdhocparameters){
// query += this.FormattedAdhocparameters
// }
this.selectcolumn(this.serverData);
// query = `SELECT a.name AS name, b.dob AS dob FROM abc a, abcde b WHERE a.name = 'gaurav' AND a.abc = NVL(b.abc, 'name') AND a.abcde = NVL(b.abcde, 'test');`
// query = `SELECT a.name AS name, b.dob AS dob FROM abc a, abcde b WHERE a.name = 'gaurav' AND a.abc = NVL(b.abc, 'name') AND a.abcde = NVL(b.abcde, 'test');`
console.log(query);
this.reportBuilderService.getMasterData(query).subscribe((data) => {
// this.rows = data;
console.log(this.rows);
this.rowdata= [this.rows];
this.rowdata = [this.rows];
console.log(typeof this.rows);
if(data){
if (data) {
this.toastr.success("Run Successfully")
}
var j;
var cart = [];
for(var i = 0; i < data.length; i++)
{
for (var i = 0; i < data.length; i++) {
var columnsIn = data[i];
if(i==1)
{
for(var key in columnsIn)
{
j={prop:key , name: key};
if (i == 1) {
for (var key in columnsIn) {
j = { prop: key, name: key };
cart.push(j)
}
@@ -610,30 +648,30 @@ this.rowdata= [this.rows];
}
this.columns = cart;
});
}
getHeaders() {
let headers: string[] = [];
if(this.rows) {
});
}
getHeaders() {
let headers: string[] = [];
if (this.rows) {
this.rows.forEach((value) => {
Object.keys(value).forEach((key) => {
if(!headers.find((header) => header == key)){
if (!headers.find((header) => header == key)) {
headers.push(key)
}
})
})
}
return headers;
}
}
return headers;
}
getFilterHeaders() {
getFilterHeaders() {
let headers: string[] = [];
if(this.filterRows) {
if (this.filterRows) {
this.filterRows.forEach((value) => {
Object.keys(value).forEach((key) => {
if(!headers.find((header) => header == key)){
if (!headers.find((header) => header == key)) {
headers.push(key)
}
})
@@ -644,8 +682,8 @@ getFilterHeaders() {
selectedValues: { [key: string]: any[] } = {};
selectcolumn(data: any) {
selectedValues: { [key: string]: any[] } = {};
selectcolumn(data: any) {
if (Array.isArray(data)) {
for (const item of data) {
const columnName = item.fields_name;
@@ -695,13 +733,21 @@ selectcolumn(data: any) {
console.log(this.selectedValues);
this.filterRowsBySelectedValues();
}
}
filtered = false;
filterRowsBySelectedValues() {
filtered = false;
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
const filteredRows = [];
@@ -750,7 +796,7 @@ filterRowsBySelectedValues() {
}
}
}
if(this.FromDatequery !== null && this.ToDatequery !== null){
if (this.FromDatequery !== null && this.ToDatequery !== null) {
this.newfrom = this.FromDatequery
this.newto = this.ToDatequery
}
@@ -803,11 +849,11 @@ filterRowsBySelectedValues() {
// Set this.filtered based on allArraysEmpty and dateRangeNotSelected
this.filtered = !allArraysEmpty || !dateRangeNotSelected;
}
}
formatDate(dateObj: any): string {
formatDate(dateObj: any): string {
// Extract individual date properties
const { year, monthValue, dayOfMonth, hour, minute, second } = dateObj;
@@ -816,9 +862,9 @@ formatDate(dateObj: any): string {
// Format the date as needed (e.g., using built-in JavaScript date formatting)
return formattedDate.toLocaleString(); // Or any other desired formatting
}
}
isDate(value: any): boolean {
isDate(value: any): boolean {
return (
value instanceof Date ||
(value &&
@@ -826,6 +872,83 @@ isDate(value: any): boolean {
value.monthValue !== undefined &&
value.dayOfMonth !== undefined)
);
}
}
/**
* 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>
</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"
routerLink="/cns-portal/rerunner/all" (click)="getName()">
<div class="nav-icon-wrapper">
@@ -122,27 +130,27 @@
<a href="javascript://" clrDropdownItem (click)="switchLanguage('en')" class="modern-lang-item">
<clr-icon shape="globe" class="lang-icon"></clr-icon>
<span>English</span>
<div class="lang-flag">🇺🇸</div>
<div class="lang-flag">🇺🇸</div>
</a>
<a href="javascript://" clrDropdownItem (click)="switchLanguage('hi')" class="modern-lang-item">
<clr-icon shape="globe" class="lang-icon"></clr-icon>
<span>हिन्दी</span>
<div class="lang-flag">🇮🇳</div>
<span>हिन्दी</span>
<div class="lang-flag">🇮🇳</div>
</a>
<a href="javascript://" clrDropdownItem (click)="switchLanguage('ta')" class="modern-lang-item">
<clr-icon shape="globe" class="lang-icon"></clr-icon>
<span>தமிழ்</span>
<div class="lang-flag">🇮🇳</div>
<span>தமிழ்</span>
<div class="lang-flag">🇮🇳</div>
</a>
<a href="javascript://" clrDropdownItem (click)="switchLanguage('pa')" class="modern-lang-item">
<clr-icon shape="globe" class="lang-icon"></clr-icon>
<span>ਪੰਜਾਬੀ</span>
<div class="lang-flag">🇮🇳</div>
<span>ਪੰਜਾਬੀ</span>
<div class="lang-flag">🇮🇳</div>
</a>
<a href="javascript://" clrDropdownItem (click)="switchLanguage('ml')" class="modern-lang-item">
<clr-icon shape="globe" class="lang-icon"></clr-icon>
<span>മലയാളം</span>
<div class="lang-flag">🇮🇳</div>
<span>മലയാളം</span>
<div class="lang-flag">🇮🇳</div>
</a>
</clr-dropdown-menu>
</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 { Component, NgModule } from '@angular/core';
@@ -82,6 +105,17 @@ import { MappingruleallComponent } from './datamanagement/mappingrule/mappingrul
import { MappingruleaddComponent } from './datamanagement/mappingrule/mappingruleadd/mappingruleadd.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';
@@ -89,9 +123,9 @@ import { Data_lakeComponent } from './builder/dashboardnew/Data_lake/Data_lake.c
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 { QueryComponent } from './superadmin/query/query.component';
// import { QueryaddComponent } from './superadmin/queryadd/queryadd.component';
// import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
import { QueryComponent } from './superadmin/query/query.component';
import { QueryaddComponent } from './superadmin/queryadd/queryadd.component';
import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
@@ -132,6 +166,8 @@ const routes: Routes = [
{ path: 'editconnect/:id', component: EditsureconnectComponent },
{
path: 'reportbuild', component: ReportBuildComponent,
children: [
@@ -143,9 +179,9 @@ const routes: Routes = [
//SUPER 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/queryedit/:id', component: QueryeditComponent, 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/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,
children: [
@@ -236,8 +278,20 @@ const routes: Routes = [
],
},
{ 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
@@ -270,18 +324,186 @@ const routes: Routes = [
// 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: '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: '**', 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';
@@ -74,6 +96,9 @@ import { RadarChartComponent } from './builder/dashboardnew/gadgets/radar-chart/
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 { CompactFilterComponent } from './builder/dashboardnew/common-filter/compact-filter.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';
@@ -91,6 +116,9 @@ import { RadarRunnerComponent } from './builder/dashboardrunner/dashrunnerline/r
import { ScatterRunnerComponent } from './builder/dashboardrunner/dashrunnerline/scatter-runner/scatter-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 { 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 { 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 { QueryComponent } from './superadmin/query/query.component';
// import { QueryaddComponent } from './superadmin/queryadd/queryadd.component';
// import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
// Import Shield Dashboard Module
import { ShieldDashboardModule } from './builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.module';
@NgModule({
declarations: [
@@ -128,71 +172,58 @@ import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.c
UsermaintanceaddComponent, UsermaintanceeditComponent,
SubmenuComponent, ModulesComponent, SessionloggerComponent,
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,
// Add CompactFilterRunnerComponent to declarations
CompactFilterRunnerComponent,
ReportBuildComponent, ReportbuildeditComponent, ReportbuildqueryComponent, ReportBuild2Component, ReportBuild2editComponent,
// QueryComponent, QueryaddComponent, QueryeditComponent,
QueryComponent, QueryaddComponent, QueryeditComponent,
ExtensionComponent,
AllextensionComponent,
AddextensionComponent, EditextensionComponent, ApiregisteryComponent,
DatamanagementComponent, DatamananementworkflowComponent, BulkimportComponent, BulkimportallComponent, BulkimportaddComponent, BulkimporteditComponent, BulkimportlineComponent, BulkimporteditlineComponent, MappingruleComponent,
MappingruleallComponent, MappingruleaddComponent, MappingruleeditComponent,
ThemeCustomizationComponent,
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,
Stepper_workflowComponent,
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,
@@ -212,6 +243,9 @@ import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.c
NgChartsModule,
NgxChartsModule,
DynamicModule,
FieldTypesModule,
SharedModule,
ShieldDashboardModule,
],
providers: [
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);
}
// 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);
}
}