Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e70f85644 | ||
|
|
482805b5cf | ||
|
|
ffda17e6b1 | ||
|
|
7396843bc6 | ||
|
|
4f75ecb3e0 | ||
|
|
7c1a487114 | ||
|
|
47e9fb92e3 | ||
|
|
7f735dcada | ||
|
|
bd315f42a3 | ||
|
|
e8c1f46430 | ||
|
|
fa96ca81bd | ||
|
|
c384f44c0c | ||
|
|
50df914ca9 | ||
|
|
e0bd888c45 | ||
|
|
02b82fcaf8 | ||
|
|
557afc348f | ||
|
|
1dec787062 | ||
|
|
8853cf75cf | ||
|
|
ad57f11f8a | ||
|
|
9b775a8c63 | ||
|
|
cf4fc1be93 | ||
|
|
4c135c4901 | ||
|
|
0b738ca7ca | ||
|
|
1b17bb706d | ||
|
|
f740076d60 | ||
|
|
bedcc0822d | ||
|
|
87810acc9e | ||
|
|
96b90e5dbd | ||
|
|
ced99e0940 | ||
|
|
4f82ae8698 | ||
|
|
e6779e8f34 |
@@ -4,5 +4,6 @@ public report_name:string;
|
|||||||
public description: string;
|
public description: string;
|
||||||
public report_tags: string;
|
public report_tags: string;
|
||||||
public servicename: string;
|
public servicename: string;
|
||||||
|
// Add SureConnect reference
|
||||||
|
public sureConnectId: number | null;
|
||||||
}
|
}
|
||||||
@@ -23,8 +23,22 @@ export interface DashboardContentModel {
|
|||||||
component?: any;
|
component?: any;
|
||||||
name: string;
|
name: string;
|
||||||
type?:string;
|
type?:string;
|
||||||
// Common properties
|
// Chart properties
|
||||||
|
xAxis?: string;
|
||||||
|
yAxis?: string | string[];
|
||||||
|
chartType?: string;
|
||||||
|
charttitle?: string;
|
||||||
|
chartlegend?: boolean;
|
||||||
|
showlabel?: boolean;
|
||||||
|
chartcolor?: boolean;
|
||||||
|
slices?: boolean;
|
||||||
|
donut?: boolean;
|
||||||
|
charturl?: string;
|
||||||
|
chartparameter?: string;
|
||||||
|
datastore?: string;
|
||||||
table?: string;
|
table?: string;
|
||||||
|
datasource?: string;
|
||||||
|
fieldName?: string;
|
||||||
connection?: string;
|
connection?: string;
|
||||||
baseFilters?: any[];
|
baseFilters?: any[];
|
||||||
// Common filter properties
|
// Common filter properties
|
||||||
@@ -37,6 +51,11 @@ export interface DashboardContentModel {
|
|||||||
drilldownParameter?: string;
|
drilldownParameter?: string;
|
||||||
drilldownFilters?: any[];
|
drilldownFilters?: any[];
|
||||||
drilldownLayers?: any[];
|
drilldownLayers?: any[];
|
||||||
|
// Compact filter properties
|
||||||
|
filterKey?: string;
|
||||||
|
filterType?: string;
|
||||||
|
filterLabel?: string;
|
||||||
|
filterOptions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardModel {
|
export interface DashboardModel {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
@@ -79,7 +79,12 @@
|
|||||||
|
|
||||||
<!-- Multi-Select Filter -->
|
<!-- Multi-Select Filter -->
|
||||||
<div class="filter-control" *ngIf="filterType === 'multiselect'">
|
<div class="filter-control" *ngIf="filterType === 'multiselect'">
|
||||||
<div class="compact-multiselect-checkboxes" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;">
|
<div class="compact-multiselect-display" (click)="toggleMultiselectDropdown()" style="padding: 5px; border: 1px solid #ddd; cursor: pointer; background-color: #f8f8f8;">
|
||||||
|
<span *ngIf="filterValue && filterValue.length > 0">{{ filterValue.length }} selected</span>
|
||||||
|
<span *ngIf="!filterValue || filterValue.length === 0">{{ filterLabel || filterKey || 'Select options' }}</span>
|
||||||
|
<clr-icon shape="caret down" style="float: right; margin-top: 3px;"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="compact-multiselect-dropdown" *ngIf="showMultiselectDropdown" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; border-top: none; padding: 10px; background-color: white; position: absolute; z-index: 1000; width: calc(100% - 2px); box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||||
<div *ngFor="let option of filterOptions" class="clr-checkbox-wrapper" style="margin-bottom: 5px;">
|
<div *ngFor="let option of filterOptions" class="clr-checkbox-wrapper" style="margin-bottom: 5px;">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[id]="'multiselect-' + option"
|
[id]="'multiselect-' + option"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnDestroy } from '@angular/core';
|
||||||
import { FilterService, Filter } from './filter.service';
|
import { FilterService, Filter } from './filter.service';
|
||||||
import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
|||||||
templateUrl: './compact-filter.component.html',
|
templateUrl: './compact-filter.component.html',
|
||||||
styleUrls: ['./compact-filter.component.scss']
|
styleUrls: ['./compact-filter.component.scss']
|
||||||
})
|
})
|
||||||
export class CompactFilterComponent implements OnInit, OnChanges {
|
export class CompactFilterComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
@Input() filterKey: string = '';
|
@Input() filterKey: string = '';
|
||||||
@Input() filterType: string = 'text';
|
@Input() filterType: string = 'text';
|
||||||
@Input() filterOptions: string[] = [];
|
@Input() filterOptions: string[] = [];
|
||||||
@@ -23,6 +23,9 @@ export class CompactFilterComponent implements OnInit, OnChanges {
|
|||||||
availableKeys: string[] = [];
|
availableKeys: string[] = [];
|
||||||
availableValues: string[] = [];
|
availableValues: string[] = [];
|
||||||
|
|
||||||
|
// Multiselect dropdown state
|
||||||
|
showMultiselectDropdown: boolean = false;
|
||||||
|
|
||||||
// Configuration properties
|
// Configuration properties
|
||||||
isConfigMode: boolean = false;
|
isConfigMode: boolean = false;
|
||||||
configFilterKey: string = '';
|
configFilterKey: string = '';
|
||||||
@@ -73,6 +76,24 @@ export class CompactFilterComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
// If filterKey changes, clear the previous filter value and remove old filter from service
|
||||||
|
if (changes.filterKey) {
|
||||||
|
// Clear the previous filter value
|
||||||
|
this.filterValue = '';
|
||||||
|
|
||||||
|
// Clear filter options
|
||||||
|
this.filterOptions = [];
|
||||||
|
|
||||||
|
// Clear available values
|
||||||
|
this.availableValues = [];
|
||||||
|
|
||||||
|
// If we had a previous selected filter, clear its value in the service
|
||||||
|
if (this.selectedFilter && changes.filterKey.previousValue) {
|
||||||
|
const oldFilterId = changes.filterKey.previousValue;
|
||||||
|
this.filterService.updateFilterValue(oldFilterId, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If filterKey or filterType changes, re-register the filter
|
// If filterKey or filterType changes, re-register the filter
|
||||||
if (changes.filterKey || changes.filterType) {
|
if (changes.filterKey || changes.filterType) {
|
||||||
// Load available values for the current filter key if it's a dropdown or multiselect
|
// Load available values for the current filter key if it's a dropdown or multiselect
|
||||||
@@ -201,6 +222,14 @@ export class CompactFilterComponent implements OnInit, OnChanges {
|
|||||||
this.onFilterValueChange(dateRange);
|
this.onFilterValueChange(dateRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
// Component cleanup - remove this filter from the filter service
|
||||||
|
if (this.selectedFilter) {
|
||||||
|
// Use the proper removeFilter method which handles both filter definition and state
|
||||||
|
this.filterService.removeFilter(this.selectedFilter.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load available keys from API
|
// Load available keys from API
|
||||||
loadAvailableKeys(): void {
|
loadAvailableKeys(): void {
|
||||||
if (this.apiUrl) {
|
if (this.apiUrl) {
|
||||||
@@ -278,6 +307,9 @@ export class CompactFilterComponent implements OnInit, OnChanges {
|
|||||||
this.apiUrl = config.apiUrl;
|
this.apiUrl = config.apiUrl;
|
||||||
this.connectionId = config.connectionId;
|
this.connectionId = config.connectionId;
|
||||||
|
|
||||||
|
// Clear filter value when changing configuration
|
||||||
|
this.filterValue = '';
|
||||||
|
|
||||||
// Load available keys if API URL is provided
|
// Load available keys if API URL is provided
|
||||||
if (this.apiUrl) {
|
if (this.apiUrl) {
|
||||||
this.loadAvailableKeys();
|
this.loadAvailableKeys();
|
||||||
@@ -304,11 +336,23 @@ export class CompactFilterComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
// Handle filter key change in configuration
|
// Handle filter key change in configuration
|
||||||
onFilterKeyChange(key: string): void {
|
onFilterKeyChange(key: string): void {
|
||||||
|
// Clear the previous filter value when changing keys
|
||||||
|
this.filterValue = '';
|
||||||
|
|
||||||
|
// Clear filter options until new values are loaded
|
||||||
|
this.filterOptions = [];
|
||||||
|
|
||||||
this.configFilterKey = key;
|
this.configFilterKey = key;
|
||||||
|
|
||||||
// Load available values for the selected key if it's a dropdown or multiselect
|
// Load available values for the selected key if it's a dropdown or multiselect
|
||||||
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && key) {
|
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && key) {
|
||||||
this.loadAvailableValues(key);
|
this.loadAvailableValues(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear the filter service value for the previous filter key
|
||||||
|
if (this.selectedFilter) {
|
||||||
|
this.filterService.updateFilterValue(this.selectedFilter.id, '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle API URL change in configuration
|
// Handle API URL change in configuration
|
||||||
@@ -375,4 +419,23 @@ export class CompactFilterComponent implements OnInit, OnChanges {
|
|||||||
// Emit the change event
|
// Emit the change event
|
||||||
this.onFilterValueChange(this.filterValue);
|
this.onFilterValueChange(this.filterValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add method to toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselectDropdown(): void {
|
||||||
|
this.showMultiselectDropdown = !this.showMultiselectDropdown;
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
if (this.showMultiselectDropdown) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (!target.closest('.compact-multiselect-display') && !target.closest('.compact-multiselect-dropdown')) {
|
||||||
|
this.showMultiselectDropdown = false;
|
||||||
|
document.removeEventListener('click', handleClick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('click', handleClick);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
</ol> -->
|
</ol> -->
|
||||||
|
|
||||||
<div style="display: inline;">
|
<div style="display: inline;">
|
||||||
<button class="btn componentbtn" (click)="toggleMenu()"><clr-icon shape="plus"></clr-icon>component</button>
|
<button class="btn componentbtn" (click)="toggleMenu()" *ngIf="!fromRunner"><clr-icon shape="plus"></clr-icon>component</button>
|
||||||
<button class="btn btn-primary" (click)="openCommonFilterModal()" style="margin-left: 10px;">
|
<button class="btn btn-primary" (click)="openCommonFilterModal()" style="margin-left: 10px;" *ngIf="!fromRunner">
|
||||||
<clr-icon shape="filter"></clr-icon> Common Filter
|
<clr-icon shape="filter"></clr-icon> Common Filter
|
||||||
</button>
|
</button>
|
||||||
<div style="display: inline;">
|
<div style="display: inline;">
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
<nav class="sidenav" *ngIf="toggle" style="width: 16%;">
|
<nav class="sidenav" *ngIf="toggle && !fromRunner" style="width: 16%;">
|
||||||
<ul class="nav-list" style="list-style-type: none;">
|
<ul class="nav-list" style="list-style-type: none;">
|
||||||
<li *ngFor="let widget of WidgetsMock">
|
<li *ngFor="let widget of WidgetsMock">
|
||||||
|
|
||||||
@@ -42,14 +42,14 @@
|
|||||||
<gridster [options]="options" (drop)="onDrop($event)" style="background-color: transparent;">
|
<gridster [options]="options" (drop)="onDrop($event)" style="background-color: transparent;">
|
||||||
<gridster-item [item]="item" *ngFor="let item of dashboardArray">
|
<gridster-item [item]="item" *ngFor="let item of dashboardArray">
|
||||||
<!-- <ng-container *ngIf="addToDashboard && item.addToDashboard"> -->
|
<!-- <ng-container *ngIf="addToDashboard && item.addToDashboard"> -->
|
||||||
<button class="btn btn-icon btn-danger" style="margin-left: 10px; margin-top: 10px;" (click)="removeItem(item)">
|
<button class="btn btn-icon btn-danger" style="margin-left: 10px; margin-top: 10px;" (click)="removeItem(item)" *ngIf="!fromRunner">
|
||||||
<clr-icon shape="trash"></clr-icon>
|
<clr-icon shape="trash"></clr-icon>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-icon drag-handler" style="margin-left: 10px; margin-top: 10px;">
|
<button class="btn btn-icon drag-handler" style="margin-left: 10px; margin-top: 10px;" *ngIf="!fromRunner">
|
||||||
<clr-icon shape="drag-handle"></clr-icon>
|
<clr-icon shape="drag-handle"></clr-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-icon" style="margin-top: 10px; float: right;">
|
<button class="btn btn-icon" style="margin-top: 10px; float: right;" *ngIf="!fromRunner">
|
||||||
<input type="checkbox" clrToggle [(ngModel)]="item.addToDashboard" name="addToDashboardSwitch"
|
<input type="checkbox" clrToggle [(ngModel)]="item.addToDashboard" name="addToDashboardSwitch"
|
||||||
(change)="toggleAddToDashboard(item)" />
|
(change)="toggleAddToDashboard(item)" />
|
||||||
</button>
|
</button>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<!-- <label for="workflow_name">Add to Dasboard</label>
|
<!-- <label for="workflow_name">Add to Dasboard</label>
|
||||||
<input class="btn btn-icon" style="margin-top: 10px;float: right;" type="checkbox" clrToggle value="billable" name="billable" />
|
<input class="btn btn-icon" style="margin-top: 10px;float: right;" type="checkbox" clrToggle value="billable" name="billable" />
|
||||||
-->
|
-->
|
||||||
<button class="btn btn-icon" style="margin-top: 10px;float: right;" (click)="editGadget(item)">
|
<button class="btn btn-icon" style="margin-top: 10px;float: right;" (click)="editGadget(item)" *ngIf="!fromRunner">
|
||||||
<clr-icon shape="pencil"></clr-icon>
|
<clr-icon shape="pencil"></clr-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<button class="btn btn-outline" (click)="goBack()">Back</button>
|
<button class="btn btn-outline" (click)="goBack()">Back</button>
|
||||||
<button type="submit" class="btn btn-primary btn-adddata " (click)="UpdateLine()">
|
<button type="submit" class="btn btn-primary btn-adddata " (click)="UpdateLine()" *ngIf="!fromRunner">
|
||||||
<b>Update</b>
|
<b>Update</b>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,8 +335,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clr-row" style="margin-top: 8px;">
|
<div class="clr-row" style="margin-top: 8px;">
|
||||||
<div class="clr-col-sm-5">
|
<div class="clr-col-sm-4">
|
||||||
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select"
|
<select [(ngModel)]="filter.field" (ngModelChange)="onBaseFilterFieldChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||||
[disabled]="gadgetsEditdata.commonFilterEnabled">
|
[disabled]="gadgetsEditdata.commonFilterEnabled">
|
||||||
<option value="">Select Field</option>
|
<option value="">Select Field</option>
|
||||||
<!-- Base API filters should always use columnData, not drilldownColumnData -->
|
<!-- Base API filters should always use columnData, not drilldownColumnData -->
|
||||||
@@ -345,7 +345,26 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clr-col-sm-5">
|
<div class="clr-col-sm-3">
|
||||||
|
<select [(ngModel)]="filter.type" (ngModelChange)="onBaseFilterTypeChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||||
|
[disabled]="gadgetsEditdata.commonFilterEnabled">
|
||||||
|
<option value="text">Text</option>
|
||||||
|
<option value="dropdown">Dropdown</option>
|
||||||
|
<option value="multiselect">Multi-Select</option>
|
||||||
|
<option value="date-range">Date Range</option>
|
||||||
|
<option value="toggle">Toggle</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
|
||||||
|
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||||
|
placeholder="Option1,Option2,Option3" [disabled]="gadgetsEditdata.commonFilterEnabled" />
|
||||||
|
<div class="clr-subtext" *ngIf="filter.availableValues">
|
||||||
|
Available: {{ filter.availableValues }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
|
||||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||||
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabled" />
|
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabled" />
|
||||||
</div>
|
</div>
|
||||||
@@ -476,8 +495,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clr-row" style="margin-top: 8px;">
|
<div class="clr-row" style="margin-top: 8px;">
|
||||||
<div class="clr-col-sm-5">
|
<div class="clr-col-sm-4">
|
||||||
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select"
|
<select [(ngModel)]="filter.field" (ngModelChange)="onDrilldownFilterFieldChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||||
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
||||||
<option value="">Select Field</option>
|
<option value="">Select Field</option>
|
||||||
<option
|
<option
|
||||||
@@ -486,7 +505,26 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clr-col-sm-5">
|
<div class="clr-col-sm-3">
|
||||||
|
<select [(ngModel)]="filter.type" (ngModelChange)="onDrilldownFilterTypeChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||||
|
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
||||||
|
<option value="text">Text</option>
|
||||||
|
<option value="dropdown">Dropdown</option>
|
||||||
|
<option value="multiselect">Multi-Select</option>
|
||||||
|
<option value="date-range">Date Range</option>
|
||||||
|
<option value="toggle">Toggle</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
|
||||||
|
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||||
|
placeholder="Option1,Option2,Option3" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
|
||||||
|
<div class="clr-subtext" *ngIf="filter.availableValues">
|
||||||
|
Available: {{ filter.availableValues }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
|
||||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||||
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
|
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
|
||||||
</div>
|
</div>
|
||||||
@@ -629,8 +667,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clr-row" style="margin-top: 8px;">
|
<div class="clr-row" style="margin-top: 8px;">
|
||||||
<div class="clr-col-sm-5">
|
<div class="clr-col-sm-4">
|
||||||
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select"
|
<select [(ngModel)]="filter.field" (ngModelChange)="onLayerFilterFieldChange(i, j, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||||
[disabled]="layer.commonFilterEnabled">
|
[disabled]="layer.commonFilterEnabled">
|
||||||
<option value="">Select Field</option>
|
<option value="">Select Field</option>
|
||||||
<option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])"
|
<option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])"
|
||||||
@@ -638,9 +676,28 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clr-col-sm-5">
|
<div class="clr-col-sm-3">
|
||||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}"
|
<select [(ngModel)]="filter.type" (ngModelChange)="onLayerFilterTypeChange(i, j, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||||
class="clr-input" placeholder="Filter Value" [disabled]="layer.commonFilterEnabled" />
|
[disabled]="layer.commonFilterEnabled">
|
||||||
|
<option value="text">Text</option>
|
||||||
|
<option value="dropdown">Dropdown</option>
|
||||||
|
<option value="multiselect">Multi-Select</option>
|
||||||
|
<option value="date-range">Date Range</option>
|
||||||
|
<option value="toggle">Toggle</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
|
||||||
|
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||||
|
placeholder="Option1,Option2,Option3" [disabled]="layer.commonFilterEnabled" />
|
||||||
|
<div class="clr-subtext" *ngIf="filter.availableValues">
|
||||||
|
Available: {{ filter.availableValues }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
|
||||||
|
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||||
|
placeholder="Filter Value" [disabled]="layer.commonFilterEnabled" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clr-col-sm-2">
|
<div class="clr-col-sm-2">
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ import { SureconnectService } from '../sureconnect/sureconnect.service';
|
|||||||
import { CommonFilterComponent } from '../common-filter/common-filter.component';
|
import { CommonFilterComponent } from '../common-filter/common-filter.component';
|
||||||
// Add the CompactFilterComponent import
|
// Add the CompactFilterComponent import
|
||||||
import { CompactFilterComponent } from '../common-filter';
|
import { CompactFilterComponent } from '../common-filter';
|
||||||
|
// Add the FilterService import
|
||||||
|
import { FilterService } from '../common-filter/filter.service';
|
||||||
|
// Add the UnifiedChartComponent import
|
||||||
|
import { UnifiedChartComponent } from '../gadgets/unified-chart';
|
||||||
|
|
||||||
function isNullArray(arr) {
|
function isNullArray(arr) {
|
||||||
return !Array.isArray(arr) || arr.length === 0;
|
return !Array.isArray(arr) || arr.length === 0;
|
||||||
@@ -91,14 +95,14 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
name: 'Scatter Chart',
|
name: 'Scatter Chart',
|
||||||
identifier: 'scatter_chart'
|
identifier: 'scatter_chart'
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// name: 'Dynamic Chart',
|
name: 'Dynamic Chart',
|
||||||
// identifier: 'dynamic_chart'
|
identifier: 'dynamic_chart'
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// name: 'Financial Chart',
|
name: 'Financial Chart',
|
||||||
// identifier: 'financial_chart'
|
identifier: 'financial_chart'
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
name: 'To Do',
|
name: 'To Do',
|
||||||
identifier: 'to_do_chart'
|
identifier: 'to_do_chart'
|
||||||
@@ -110,6 +114,10 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
{
|
{
|
||||||
name: 'Compact Filter',
|
name: 'Compact Filter',
|
||||||
identifier: 'compact_filter'
|
identifier: 'compact_filter'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Unified Chart',
|
||||||
|
identifier: 'unified_chart'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -135,7 +143,8 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
{ name: "Financial Chart", componentInstance: FinancialChartComponent },
|
{ name: "Financial Chart", componentInstance: FinancialChartComponent },
|
||||||
{ name: "To Do Chart", componentInstance: ToDoChartComponent },
|
{ name: "To Do Chart", componentInstance: ToDoChartComponent },
|
||||||
{ name: "Grid View", componentInstance: GridViewComponent },
|
{ name: "Grid View", componentInstance: GridViewComponent },
|
||||||
{ name: "Compact Filter", componentInstance: CompactFilterComponent }, // Add this line
|
{ name: "Compact Filter", componentInstance: CompactFilterComponent },
|
||||||
|
{ name: "Unified Chart", componentInstance: UnifiedChartComponent },
|
||||||
];
|
];
|
||||||
model: any;
|
model: any;
|
||||||
linesdata: any;
|
linesdata: any;
|
||||||
@@ -168,6 +177,7 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
yAxis: '',
|
yAxis: '',
|
||||||
xAxis: '',
|
xAxis: '',
|
||||||
connection: '', // Add connection field
|
connection: '', // Add connection field
|
||||||
|
chartType: '', // Add chartType field
|
||||||
// Drilldown configuration properties (base level)
|
// Drilldown configuration properties (base level)
|
||||||
drilldownEnabled: false,
|
drilldownEnabled: false,
|
||||||
drilldownApiUrl: '',
|
drilldownApiUrl: '',
|
||||||
@@ -203,9 +213,15 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
private _fb: FormBuilder,
|
private _fb: FormBuilder,
|
||||||
private datastoreService: DatastoreService,
|
private datastoreService: DatastoreService,
|
||||||
private alertService: AlertsService,
|
private alertService: AlertsService,
|
||||||
private sureconnectService: SureconnectService) { } // Add SureconnectService to constructor
|
private sureconnectService: SureconnectService,
|
||||||
|
private filterService: FilterService) { } // Add SureconnectService and FilterService to constructor
|
||||||
|
|
||||||
|
// Add property to track if coming from dashboard runner
|
||||||
|
fromRunner: boolean = false;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Reset the filter service when the component is initialized
|
||||||
|
this.filterService.resetFilters();
|
||||||
|
|
||||||
// Grid options
|
// Grid options
|
||||||
this.options = {
|
this.options = {
|
||||||
@@ -231,6 +247,13 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
itemResizeCallback: this.itemResize.bind(this)
|
itemResizeCallback: this.itemResize.bind(this)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if coming from dashboard runner
|
||||||
|
this.route.queryParams.subscribe(params => {
|
||||||
|
if (params['fromRunner'] === 'true') {
|
||||||
|
this.fromRunner = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.editId = this.route.snapshot.params.id;
|
this.editId = this.route.snapshot.params.id;
|
||||||
console.log(this.editId);
|
console.log(this.editId);
|
||||||
this.dashboardService.getById(this.editId).subscribe((data) => {
|
this.dashboardService.getById(this.editId).subscribe((data) => {
|
||||||
@@ -317,6 +340,9 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
dashboardLine: any;
|
dashboardLine: any;
|
||||||
dashboardName: any;
|
dashboardName: any;
|
||||||
getData() {
|
getData() {
|
||||||
|
// Reset the filter service when switching between dashboard records
|
||||||
|
this.filterService.resetFilters();
|
||||||
|
|
||||||
// We get the id in get current router dashboard/:id
|
// We get the id in get current router dashboard/:id
|
||||||
this.route.params.subscribe(params => {
|
this.route.params.subscribe(params => {
|
||||||
// + is used to cast string to int
|
// + is used to cast string to int
|
||||||
@@ -435,7 +461,17 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
|
|
||||||
onDrop(ev) {
|
onDrop(ev) {
|
||||||
const componentType = ev.dataTransfer.getData("widgetIdentifier");
|
const componentType = ev.dataTransfer.getData("widgetIdentifier");
|
||||||
let maxChartId = this.dashboardArray?.reduce((maxId, item) => Math.max(maxId, item.chartid), 0);
|
// Safely calculate maxChartId, handling cases where chartid might be NaN or missing
|
||||||
|
let maxChartId = 0;
|
||||||
|
if (this.dashboardArray && this.dashboardArray.length > 0) {
|
||||||
|
const validChartIds = this.dashboardArray
|
||||||
|
.map(item => item.chartid)
|
||||||
|
.filter(chartid => typeof chartid === 'number' && !isNaN(chartid));
|
||||||
|
|
||||||
|
if (validChartIds.length > 0) {
|
||||||
|
maxChartId = Math.max(...validChartIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
switch (componentType) {
|
switch (componentType) {
|
||||||
case "radar_chart":
|
case "radar_chart":
|
||||||
return this.dashboardArray.push({
|
return this.dashboardArray.push({
|
||||||
@@ -582,6 +618,22 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
component: GridViewComponent,
|
component: GridViewComponent,
|
||||||
name: "Grid View"
|
name: "Grid View"
|
||||||
});
|
});
|
||||||
|
case "unified_chart":
|
||||||
|
return this.dashboardArray.push({
|
||||||
|
cols: 5,
|
||||||
|
rows: 6,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
chartid: maxChartId + 1,
|
||||||
|
component: UnifiedChartComponent,
|
||||||
|
name: "Unified Chart",
|
||||||
|
// Add default configuration for unified chart
|
||||||
|
chartType: 'bar',
|
||||||
|
xAxis: '',
|
||||||
|
yAxis: '',
|
||||||
|
table: '',
|
||||||
|
connection: undefined
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
removeItem(item) {
|
removeItem(item) {
|
||||||
@@ -597,7 +649,14 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
modelid: number;
|
modelid: number;
|
||||||
|
// Update the editGadget method to initialize filter properties
|
||||||
editGadget(item) {
|
editGadget(item) {
|
||||||
|
// If coming from dashboard runner, skip showing the config modal
|
||||||
|
if (this.fromRunner) {
|
||||||
|
console.log('Coming from dashboard runner, skipping config modal');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.modeledit = true;
|
this.modeledit = true;
|
||||||
this.modelid = item.chartid;
|
this.modelid = item.chartid;
|
||||||
console.log(this.modelid);
|
console.log(this.modelid);
|
||||||
@@ -627,6 +686,10 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
if (item['filterOptions'] === undefined) {
|
if (item['filterOptions'] === undefined) {
|
||||||
this.gadgetsEditdata['filterOptions'] = [];
|
this.gadgetsEditdata['filterOptions'] = [];
|
||||||
}
|
}
|
||||||
|
// Initialize chartType property if not present (for unified chart)
|
||||||
|
if (item['chartType'] === undefined) {
|
||||||
|
this.gadgetsEditdata['chartType'] = 'bar';
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize filterOptionsString for compact filter
|
// Initialize filterOptionsString for compact filter
|
||||||
if (item.name === 'Compact Filter') {
|
if (item.name === 'Compact Filter') {
|
||||||
@@ -639,6 +702,65 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
this.filterOptionsString = '';
|
this.filterOptionsString = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize base filters with type and options if not present
|
||||||
|
if (item['baseFilters'] === undefined) {
|
||||||
|
this.gadgetsEditdata['baseFilters'] = [];
|
||||||
|
} else {
|
||||||
|
// Ensure each base filter has type and options properties
|
||||||
|
this.gadgetsEditdata['baseFilters'] = this.gadgetsEditdata['baseFilters'].map(filter => ({
|
||||||
|
field: filter.field || '',
|
||||||
|
value: filter.value || '',
|
||||||
|
type: filter.type || 'text',
|
||||||
|
options: filter.options || '',
|
||||||
|
availableValues: filter.availableValues || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters with type and options if not present
|
||||||
|
if (item['drilldownFilters'] === undefined) {
|
||||||
|
this.gadgetsEditdata['drilldownFilters'] = [];
|
||||||
|
} else {
|
||||||
|
// Ensure each drilldown filter has type and options properties
|
||||||
|
this.gadgetsEditdata['drilldownFilters'] = this.gadgetsEditdata['drilldownFilters'].map(filter => ({
|
||||||
|
field: filter.field || '',
|
||||||
|
value: filter.value || '',
|
||||||
|
type: filter.type || 'text',
|
||||||
|
options: filter.options || '',
|
||||||
|
availableValues: filter.availableValues || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown layers with proper filter structure if not present
|
||||||
|
if (item['drilldownLayers'] === undefined) {
|
||||||
|
this.gadgetsEditdata['drilldownLayers'] = [];
|
||||||
|
} else {
|
||||||
|
// Ensure each layer has proper filter structure
|
||||||
|
this.gadgetsEditdata['drilldownLayers'] = this.gadgetsEditdata['drilldownLayers'].map(layer => {
|
||||||
|
// Initialize parameter if not present
|
||||||
|
if (layer['parameter'] === undefined) {
|
||||||
|
layer['parameter'] = '';
|
||||||
|
}
|
||||||
|
// Initialize filters if not present
|
||||||
|
if (layer['filters'] === undefined) {
|
||||||
|
layer['filters'] = [];
|
||||||
|
} else {
|
||||||
|
// Ensure each layer filter has type and options properties
|
||||||
|
layer['filters'] = layer['filters'].map(filter => ({
|
||||||
|
field: filter.field || '',
|
||||||
|
value: filter.value || '',
|
||||||
|
type: filter.type || 'text',
|
||||||
|
options: filter.options || '',
|
||||||
|
availableValues: filter.availableValues || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// Initialize common filter property for layer if not present
|
||||||
|
if (layer['commonFilterEnabled'] === undefined) {
|
||||||
|
layer['commonFilterEnabled'] = false;
|
||||||
|
}
|
||||||
|
return layer;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.getStores();
|
this.getStores();
|
||||||
|
|
||||||
// Set default connection if none is set and we have connections
|
// Set default connection if none is set and we have connections
|
||||||
@@ -666,37 +788,6 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
this.gadgetsEditdata['drilldownParameter'] = '';
|
this.gadgetsEditdata['drilldownParameter'] = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize base filters if not present
|
|
||||||
if (item['baseFilters'] === undefined) {
|
|
||||||
this.gadgetsEditdata['baseFilters'] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize drilldown filters if not present
|
|
||||||
if (item['drilldownFilters'] === undefined) {
|
|
||||||
this.gadgetsEditdata['drilldownFilters'] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize drilldown layers if not present
|
|
||||||
if (item['drilldownLayers'] === undefined) {
|
|
||||||
this.gadgetsEditdata['drilldownLayers'] = [];
|
|
||||||
} else {
|
|
||||||
// Ensure each layer has proper structure (removed parameterKey, added parameter)
|
|
||||||
this.gadgetsEditdata['drilldownLayers'].forEach((layer, index) => {
|
|
||||||
// Initialize parameter if not present
|
|
||||||
if (layer['parameter'] === undefined) {
|
|
||||||
layer['parameter'] = '';
|
|
||||||
}
|
|
||||||
// Initialize filters if not present
|
|
||||||
if (layer['filters'] === undefined) {
|
|
||||||
layer['filters'] = [];
|
|
||||||
}
|
|
||||||
// Initialize common filter property for layer if not present
|
|
||||||
if (layer['commonFilterEnabled'] === undefined) {
|
|
||||||
layer['commonFilterEnabled'] = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset drilldown column data
|
// Reset drilldown column data
|
||||||
this.drilldownColumnData = [];
|
this.drilldownColumnData = [];
|
||||||
|
|
||||||
@@ -705,15 +796,34 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
this.refreshBaseDrilldownColumns();
|
this.refreshBaseDrilldownColumns();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.datastore !== undefined || '' || null) {
|
// Check if we have either datastore or table to fetch columns
|
||||||
|
if ((item.datastore !== undefined && item.datastore !== '' && item.datastore !== null) ||
|
||||||
|
(item.table !== undefined && item.table !== '' && item.table !== null)) {
|
||||||
const datastore = item.datastore;
|
const datastore = item.datastore;
|
||||||
this.getTables(datastore);
|
|
||||||
const table = item.table;
|
const table = item.table;
|
||||||
|
|
||||||
|
// Fetch tables if datastore is available
|
||||||
|
if (datastore) {
|
||||||
|
this.getTables(datastore);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch columns if table is available
|
||||||
|
if (table) {
|
||||||
this.getColumns(datastore, table);
|
this.getColumns(datastore, table);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(item.yAxis);
|
console.log(item.yAxis);
|
||||||
|
// Set selectedyAxis regardless of whether it's an array or string
|
||||||
|
if (item.yAxis !== undefined && item.yAxis !== '' && item.yAxis !== null) {
|
||||||
if (isArray(item.yAxis)) {
|
if (isArray(item.yAxis)) {
|
||||||
this.selectedyAxis = item.yAxis;
|
this.selectedyAxis = item.yAxis;
|
||||||
|
} else {
|
||||||
|
// For single yAxis values, convert to array
|
||||||
|
this.selectedyAxis = [item.yAxis];
|
||||||
|
}
|
||||||
console.log(this.selectedyAxis);
|
console.log(this.selectedyAxis);
|
||||||
|
} else {
|
||||||
|
this.selectedyAxis = [];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.selectedyAxis = [];
|
this.selectedyAxis = [];
|
||||||
@@ -784,14 +894,32 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the onSubmit method to properly save filter data
|
||||||
onSubmit(id) {
|
onSubmit(id) {
|
||||||
console.log(id);
|
console.log(id);
|
||||||
if (!isNullArray(this.selectedyAxis)) {
|
|
||||||
console.log("get y-axis array", this.selectedyAxis);
|
// Check if ID is valid, including handling NaN
|
||||||
|
if (id === null || id === undefined || isNaN(id)) {
|
||||||
|
console.warn('Chart ID is null, undefined, or NaN, using modelid instead:', this.modelid);
|
||||||
|
id = this.modelid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have a valid numeric ID
|
||||||
|
const numId = typeof id === 'number' ? id : parseInt(id, 10);
|
||||||
|
if (isNaN(numId)) {
|
||||||
|
console.error('Unable to determine valid chart ID, aborting onSubmit');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle both array and string yAxis values
|
||||||
|
if (this.selectedyAxis !== undefined && this.selectedyAxis !== null &&
|
||||||
|
((Array.isArray(this.selectedyAxis) && this.selectedyAxis.length > 0) ||
|
||||||
|
(typeof this.selectedyAxis === 'string' && this.selectedyAxis !== ''))) {
|
||||||
|
console.log("get y-axis", this.selectedyAxis);
|
||||||
this.entryForm.patchValue({ yAxis: this.selectedyAxis });
|
this.entryForm.patchValue({ yAxis: this.selectedyAxis });
|
||||||
}
|
}
|
||||||
let formdata = this.entryForm.value;
|
let formdata = this.entryForm.value;
|
||||||
let num = id;
|
let num = numId;
|
||||||
console.log(this.entryForm.value);
|
console.log(this.entryForm.value);
|
||||||
this.dashboardCollection.dashboard = this.dashboardCollection.dashboard.map(item => {
|
this.dashboardCollection.dashboard = this.dashboardCollection.dashboard.map(item => {
|
||||||
if (item.chartid == num) {
|
if (item.chartid == num) {
|
||||||
@@ -830,6 +958,11 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
xyz.connection = this.gadgetsEditdata.connection || undefined;
|
xyz.connection = this.gadgetsEditdata.connection || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For unified chart, preserve chart configuration properties
|
||||||
|
if (item.name === 'Unified Chart') {
|
||||||
|
xyz.chartType = this.gadgetsEditdata.chartType || 'bar';
|
||||||
|
}
|
||||||
|
|
||||||
console.log(xyz);
|
console.log(xyz);
|
||||||
return xyz;
|
return xyz;
|
||||||
}
|
}
|
||||||
@@ -913,6 +1046,48 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
return commonFilterInputs;
|
return commonFilterInputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For UnifiedChartComponent, pass chart properties with chartType
|
||||||
|
if (item.name === 'Unified Chart') {
|
||||||
|
const unifiedChartInputs = {
|
||||||
|
chartType: item.chartType || 'bar',
|
||||||
|
xAxis: item.xAxis,
|
||||||
|
yAxis: item.yAxis,
|
||||||
|
table: item.table,
|
||||||
|
datastore: item.datastore,
|
||||||
|
charttitle: item.charttitle,
|
||||||
|
chartlegend: item.chartlegend,
|
||||||
|
showlabel: item.showlabel,
|
||||||
|
chartcolor: item.chartcolor,
|
||||||
|
slices: item.slices,
|
||||||
|
donut: item.donut,
|
||||||
|
charturl: item.charturl,
|
||||||
|
chartparameter: item.chartparameter,
|
||||||
|
datasource: item.datasource,
|
||||||
|
fieldName: item.name, // Using item.name as fieldName
|
||||||
|
connection: item['connection'], // Add connection field using bracket notation
|
||||||
|
// Base drilldown configuration properties
|
||||||
|
drilldownEnabled: item['drilldownEnabled'],
|
||||||
|
drilldownApiUrl: item['drilldownApiUrl'],
|
||||||
|
// Removed drilldownParameterKey since we're using URL templates
|
||||||
|
drilldownXAxis: item['drilldownXAxis'],
|
||||||
|
drilldownYAxis: item['drilldownYAxis'],
|
||||||
|
drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
|
||||||
|
baseFilters: item['baseFilters'] || [], // Add base filters
|
||||||
|
drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters
|
||||||
|
// Multi-layer drilldown configurations
|
||||||
|
drilldownLayers: item['drilldownLayers'] || []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove undefined properties to avoid passing unnecessary data
|
||||||
|
Object.keys(unifiedChartInputs).forEach(key => {
|
||||||
|
if (unifiedChartInputs[key] === undefined) {
|
||||||
|
delete unifiedChartInputs[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return unifiedChartInputs;
|
||||||
|
}
|
||||||
|
|
||||||
// For GridViewComponent, pass chart properties with drilldown support
|
// For GridViewComponent, pass chart properties with drilldown support
|
||||||
if (item.component && item.component.name === 'GridViewComponent') {
|
if (item.component && item.component.name === 'GridViewComponent') {
|
||||||
const gridInputs = {
|
const gridInputs = {
|
||||||
@@ -978,8 +1153,8 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
drilldownXAxis: item['drilldownXAxis'],
|
drilldownXAxis: item['drilldownXAxis'],
|
||||||
drilldownYAxis: item['drilldownYAxis'],
|
drilldownYAxis: item['drilldownYAxis'],
|
||||||
drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
|
drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
|
||||||
baseFilters: item['baseFilters'] || [], // Add base filters
|
baseFilters: item['baseFilters'] || [], // Add base filters with type information
|
||||||
drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters
|
drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters with type information
|
||||||
// Multi-layer drilldown configurations
|
// Multi-layer drilldown configurations
|
||||||
drilldownLayers: item['drilldownLayers'] || []
|
drilldownLayers: item['drilldownLayers'] || []
|
||||||
};
|
};
|
||||||
@@ -994,18 +1169,29 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
return chartInputs;
|
return chartInputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the applyChanges method to properly save filter data
|
||||||
applyChanges(id) {
|
applyChanges(id) {
|
||||||
console.log('Apply changes for chart ID:', id);
|
console.log('Apply changes for chart ID:', id);
|
||||||
|
|
||||||
// Check if ID is valid
|
// Check if ID is valid, including handling NaN
|
||||||
if (id === null || id === undefined) {
|
if (id === null || id === undefined || isNaN(id)) {
|
||||||
console.warn('Chart ID is null or undefined, using modelid instead:', this.modelid);
|
console.warn('Chart ID is null, undefined, or NaN, using modelid instead:', this.modelid);
|
||||||
id = this.modelid;
|
id = this.modelid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the form with selected Y-axis values if it's an array
|
// Ensure we have a valid numeric ID
|
||||||
if (!isNullArray(this.selectedyAxis)) {
|
const numId = typeof id === 'number' ? id : parseInt(id, 10);
|
||||||
console.log("get y-axis array", this.selectedyAxis);
|
if (isNaN(numId)) {
|
||||||
|
console.error('Unable to determine valid chart ID, aborting applyChanges');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the form with selected Y-axis values
|
||||||
|
// Handle both array and string yAxis values
|
||||||
|
if (this.selectedyAxis !== undefined && this.selectedyAxis !== null &&
|
||||||
|
((Array.isArray(this.selectedyAxis) && this.selectedyAxis.length > 0) ||
|
||||||
|
(typeof this.selectedyAxis === 'string' && this.selectedyAxis !== ''))) {
|
||||||
|
console.log("get y-axis", this.selectedyAxis);
|
||||||
this.entryForm.patchValue({ yAxis: this.selectedyAxis });
|
this.entryForm.patchValue({ yAxis: this.selectedyAxis });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1096,6 +1282,9 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
|
|
||||||
// Note: We don't close the modal here, allowing the user to make additional changes
|
// Note: We don't close the modal here, allowing the user to make additional changes
|
||||||
// The user can click "Save" when they're done with all changes
|
// The user can click "Save" when they're done with all changes
|
||||||
|
|
||||||
|
// Reset the filter service to ensure clean state
|
||||||
|
this.filterService.resetFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
goBack() {
|
goBack() {
|
||||||
@@ -1293,46 +1482,239 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
// We're now using removeBaseFilter and removeLayerFilter methods instead
|
// We're now using removeBaseFilter and removeLayerFilter methods instead
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add method to add a base filter
|
// Add method to handle base filter field change
|
||||||
|
onBaseFilterFieldChange(index: number, field: string) {
|
||||||
|
const filter = this.gadgetsEditdata.baseFilters[index];
|
||||||
|
if (filter) {
|
||||||
|
filter.field = field;
|
||||||
|
// If field changes, reset value and options
|
||||||
|
filter.value = '';
|
||||||
|
filter.options = '';
|
||||||
|
filter.availableValues = '';
|
||||||
|
|
||||||
|
// If we have a field and table URL, load available values
|
||||||
|
if (field && this.gadgetsEditdata.table) {
|
||||||
|
this.loadFilterValuesForField(
|
||||||
|
this.gadgetsEditdata.table,
|
||||||
|
this.gadgetsEditdata.connection,
|
||||||
|
field,
|
||||||
|
index,
|
||||||
|
'base'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to handle base filter type change
|
||||||
|
onBaseFilterTypeChange(index: number, type: string) {
|
||||||
|
const filter = this.gadgetsEditdata.baseFilters[index];
|
||||||
|
if (filter) {
|
||||||
|
filter.type = type;
|
||||||
|
// If type changes to dropdown/multiselect and we have a field, load available values
|
||||||
|
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.gadgetsEditdata.table) {
|
||||||
|
this.loadFilterValuesForField(
|
||||||
|
this.gadgetsEditdata.table,
|
||||||
|
this.gadgetsEditdata.connection,
|
||||||
|
filter.field,
|
||||||
|
index,
|
||||||
|
'base'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to handle drilldown filter field change
|
||||||
|
onDrilldownFilterFieldChange(index: number, field: string) {
|
||||||
|
const filter = this.gadgetsEditdata.drilldownFilters[index];
|
||||||
|
if (filter) {
|
||||||
|
filter.field = field;
|
||||||
|
// If field changes, reset value and options
|
||||||
|
filter.value = '';
|
||||||
|
filter.options = '';
|
||||||
|
filter.availableValues = '';
|
||||||
|
|
||||||
|
// If we have a field and drilldown API URL, load available values
|
||||||
|
if (field && this.gadgetsEditdata.drilldownApiUrl) {
|
||||||
|
this.loadFilterValuesForField(
|
||||||
|
this.gadgetsEditdata.drilldownApiUrl,
|
||||||
|
this.gadgetsEditdata.connection,
|
||||||
|
field,
|
||||||
|
index,
|
||||||
|
'drilldown'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to handle drilldown filter type change
|
||||||
|
onDrilldownFilterTypeChange(index: number, type: string) {
|
||||||
|
const filter = this.gadgetsEditdata.drilldownFilters[index];
|
||||||
|
if (filter) {
|
||||||
|
filter.type = type;
|
||||||
|
// If type changes to dropdown/multiselect and we have a field, load available values
|
||||||
|
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.gadgetsEditdata.drilldownApiUrl) {
|
||||||
|
this.loadFilterValuesForField(
|
||||||
|
this.gadgetsEditdata.drilldownApiUrl,
|
||||||
|
this.gadgetsEditdata.connection,
|
||||||
|
filter.field,
|
||||||
|
index,
|
||||||
|
'drilldown'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to handle layer filter field change
|
||||||
|
onLayerFilterFieldChange(layerIndex: number, filterIndex: number, field: string) {
|
||||||
|
const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
|
||||||
|
if (layer && layer.filters) {
|
||||||
|
const filter = layer.filters[filterIndex];
|
||||||
|
if (filter) {
|
||||||
|
filter.field = field;
|
||||||
|
// If field changes, reset value and options
|
||||||
|
filter.value = '';
|
||||||
|
filter.options = '';
|
||||||
|
filter.availableValues = '';
|
||||||
|
|
||||||
|
// If we have a field and layer API URL, load available values
|
||||||
|
if (field && layer.apiUrl) {
|
||||||
|
this.loadFilterValuesForField(
|
||||||
|
layer.apiUrl,
|
||||||
|
this.gadgetsEditdata.connection,
|
||||||
|
field,
|
||||||
|
filterIndex,
|
||||||
|
'layer',
|
||||||
|
layerIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to handle layer filter type change
|
||||||
|
onLayerFilterTypeChange(layerIndex: number, filterIndex: number, type: string) {
|
||||||
|
const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
|
||||||
|
if (layer && layer.filters) {
|
||||||
|
const filter = layer.filters[filterIndex];
|
||||||
|
if (filter) {
|
||||||
|
filter.type = type;
|
||||||
|
// If type changes to dropdown/multiselect and we have a field, load available values
|
||||||
|
if ((type === 'dropdown' || type === 'multiselect') && filter.field && layer.apiUrl) {
|
||||||
|
this.loadFilterValuesForField(
|
||||||
|
layer.apiUrl,
|
||||||
|
this.gadgetsEditdata.connection,
|
||||||
|
filter.field,
|
||||||
|
filterIndex,
|
||||||
|
'layer',
|
||||||
|
layerIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to load filter values for a specific field
|
||||||
|
loadFilterValuesForField(
|
||||||
|
apiUrl: string,
|
||||||
|
connectionId: string | undefined,
|
||||||
|
field: string,
|
||||||
|
filterIndex: number,
|
||||||
|
filterType: 'base' | 'drilldown' | 'layer',
|
||||||
|
layerIndex?: number
|
||||||
|
) {
|
||||||
|
if (apiUrl && field) {
|
||||||
|
const connectionIdNum = connectionId ? parseInt(connectionId, 10) : undefined;
|
||||||
|
this.alertService.getValuesFromUrl(apiUrl, connectionIdNum, field).subscribe(
|
||||||
|
(values: string[]) => {
|
||||||
|
// Update the filter with available values
|
||||||
|
if (filterType === 'base') {
|
||||||
|
const filter = this.gadgetsEditdata.baseFilters[filterIndex];
|
||||||
|
if (filter) {
|
||||||
|
filter.availableValues = values.join(', ');
|
||||||
|
// For dropdown/multiselect types, also update the options
|
||||||
|
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
|
||||||
|
filter.options = filter.availableValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (filterType === 'drilldown') {
|
||||||
|
const filter = this.gadgetsEditdata.drilldownFilters[filterIndex];
|
||||||
|
if (filter) {
|
||||||
|
filter.availableValues = values.join(', ');
|
||||||
|
// For dropdown/multiselect types, also update the options
|
||||||
|
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
|
||||||
|
filter.options = filter.availableValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (filterType === 'layer' && layerIndex !== undefined) {
|
||||||
|
const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
|
||||||
|
if (layer && layer.filters) {
|
||||||
|
const filter = layer.filters[filterIndex];
|
||||||
|
if (filter) {
|
||||||
|
filter.availableValues = values.join(', ');
|
||||||
|
// For dropdown/multiselect types, also update the options
|
||||||
|
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
|
||||||
|
filter.options = filter.availableValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error loading available values for field:', field, error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to add a base filter with default properties
|
||||||
addBaseFilter() {
|
addBaseFilter() {
|
||||||
const newFilter = {
|
const newFilter = {
|
||||||
field: '',
|
field: '',
|
||||||
value: ''
|
value: '',
|
||||||
|
type: 'text',
|
||||||
|
options: '',
|
||||||
|
availableValues: ''
|
||||||
};
|
};
|
||||||
this.gadgetsEditdata.baseFilters.push(newFilter);
|
this.gadgetsEditdata.baseFilters.push(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add method to add a drilldown filter with default properties
|
||||||
|
addDrilldownFilter() {
|
||||||
|
const newFilter = {
|
||||||
|
field: '',
|
||||||
|
value: '',
|
||||||
|
type: 'text',
|
||||||
|
options: '',
|
||||||
|
availableValues: ''
|
||||||
|
};
|
||||||
|
this.gadgetsEditdata.drilldownFilters.push(newFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to add a layer filter with default properties
|
||||||
|
addLayerFilter(layerIndex: number) {
|
||||||
|
const newFilter = {
|
||||||
|
field: '',
|
||||||
|
value: '',
|
||||||
|
type: 'text',
|
||||||
|
options: '',
|
||||||
|
availableValues: ''
|
||||||
|
};
|
||||||
|
if (!this.gadgetsEditdata.drilldownLayers[layerIndex].filters) {
|
||||||
|
this.gadgetsEditdata.drilldownLayers[layerIndex].filters = [];
|
||||||
|
}
|
||||||
|
this.gadgetsEditdata.drilldownLayers[layerIndex].filters.push(newFilter);
|
||||||
|
}
|
||||||
|
|
||||||
// Add method to remove a base filter
|
// Add method to remove a base filter
|
||||||
removeBaseFilter(index: number) {
|
removeBaseFilter(index: number) {
|
||||||
this.gadgetsEditdata.baseFilters.splice(index, 1);
|
this.gadgetsEditdata.baseFilters.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add method to add a drilldown filter
|
|
||||||
addDrilldownFilter() {
|
|
||||||
const newFilter = {
|
|
||||||
field: '',
|
|
||||||
value: ''
|
|
||||||
};
|
|
||||||
this.gadgetsEditdata.drilldownFilters.push(newFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add method to remove a drilldown filter
|
// Add method to remove a drilldown filter
|
||||||
removeDrilldownFilter(index: number) {
|
removeDrilldownFilter(index: number) {
|
||||||
this.gadgetsEditdata.drilldownFilters.splice(index, 1);
|
this.gadgetsEditdata.drilldownFilters.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add method to add a layer filter
|
|
||||||
addLayerFilter(layerIndex: number) {
|
|
||||||
const newFilter = {
|
|
||||||
field: '',
|
|
||||||
value: ''
|
|
||||||
};
|
|
||||||
if (!this.gadgetsEditdata.drilldownLayers[layerIndex].filters) {
|
|
||||||
this.gadgetsEditdata.drilldownLayers[layerIndex].filters = [];
|
|
||||||
}
|
|
||||||
this.gadgetsEditdata.drilldownLayers[layerIndex].filters.push(newFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add method to remove a layer filter
|
// Add method to remove a layer filter
|
||||||
removeLayerFilter(layerIndex: number, filterIndex: number) {
|
removeLayerFilter(layerIndex: number, filterIndex: number) {
|
||||||
this.gadgetsEditdata.drilldownLayers[layerIndex].filters.splice(filterIndex, 1);
|
this.gadgetsEditdata.drilldownLayers[layerIndex].filters.splice(filterIndex, 1);
|
||||||
|
|||||||
@@ -1,25 +1,295 @@
|
|||||||
<div style="display: block; height: 100%; width: 100%;">
|
<div class="chart-container">
|
||||||
<!-- No filter controls needed with the new simplified approach -->
|
<!-- Filter Controls Section -->
|
||||||
<!-- Filters are now configured at the drilldown level -->
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
|
<!-- Base Filters -->
|
||||||
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
|
<h4>Base Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Text Filter -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<input type="text"
|
||||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
[(ngModel)]="filter.value"
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
</button>
|
[placeholder]="filter.field"
|
||||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
class="clr-input filter-text-input">
|
||||||
Back to Main View
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No data message -->
|
<!-- Dropdown Filter -->
|
||||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="chart-header">
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Bar Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<div class="chart-content" [class.loading]="isLoading">
|
||||||
|
|
||||||
|
<div *ngIf="noDataAvailable" class="no-data-message">
|
||||||
No data available
|
No data available
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chart display -->
|
|
||||||
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
<div *ngIf="!noDataAvailable" class="chart-display">
|
||||||
<canvas baseChart
|
<canvas baseChart
|
||||||
[datasets]="barChartData"
|
[datasets]="barChartData"
|
||||||
[labels]="barChartLabels"
|
[labels]="barChartLabels"
|
||||||
@@ -29,4 +299,36 @@
|
|||||||
(chartClick)="chartClicked($event)">
|
(chartClick)="chartClicked($event)">
|
||||||
</canvas>
|
</canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="loading-overlay" *ngIf="isLoading">
|
||||||
|
<div class="shimmer-bar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- sheield dashboard -->
|
||||||
|
<!--
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-header">
|
||||||
|
<h3>Deal Stage Wise Progress</h3>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<div class="chart-content" [class.loading]="isLoading">
|
||||||
|
<canvas
|
||||||
|
baseChart
|
||||||
|
[data]="barChartData"
|
||||||
|
[options]="barChartOptions"
|
||||||
|
[type]="barChartType"
|
||||||
|
(chartClick)="chartClicked($event)"
|
||||||
|
(chartHover)="chartHovered($event)">
|
||||||
|
</canvas>
|
||||||
|
<div class="loading-overlay" *ngIf="isLoading">
|
||||||
|
<div class="shimmer-bar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1,31 +1,278 @@
|
|||||||
// Bar Chart Component Styles
|
// Chart container structure
|
||||||
:host {
|
.chart-container {
|
||||||
display: block;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
// Filter section styling
|
||||||
|
.filter-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar-chart-container {
|
.filter-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
min-width: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.filter-text-input,
|
||||||
|
.filter-select,
|
||||||
|
.filter-date {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.multiselect-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-value {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart header styling
|
||||||
|
.chart-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart wrapper and content
|
||||||
|
.chart-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.chart-content {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 300px; // Ensure minimum height for chart
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
|
||||||
|
.chart-display {
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-display {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
canvas {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
transition: filter 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Responsive design for chart container
|
.loading-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
|
||||||
|
.shimmer-bar {
|
||||||
|
width: 80%;
|
||||||
|
height: 20px;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.bar-chart-container {
|
.chart-container {
|
||||||
height: 300px;
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
.header-row {
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
.chart-content {
|
||||||
.bar-chart-container {
|
min-height: 250px; // Adjust for mobile
|
||||||
height: 250px;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,21 +57,46 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
ticks: {
|
ticks: {
|
||||||
autoSkip: false,
|
autoSkip: false,
|
||||||
maxRotation: 45,
|
maxRotation: 45,
|
||||||
minRotation: 45
|
minRotation: 45,
|
||||||
|
padding: 15,
|
||||||
|
font: {
|
||||||
|
size: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: true
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
font: {
|
||||||
|
size: 12
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: true,
|
display: true,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
|
labels: {
|
||||||
|
font: {
|
||||||
|
size: 12
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: true
|
enabled: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
bottom: 60,
|
||||||
|
left: 15,
|
||||||
|
right: 15,
|
||||||
|
top: 15
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,12 +109,20 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
// No data state
|
// No data state
|
||||||
noDataAvailable: boolean = false;
|
noDataAvailable: boolean = false;
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
isLoading: boolean = false;
|
||||||
|
|
||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
// Subscriptions to unsubscribe on destroy
|
// Subscriptions to unsubscribe on destroy
|
||||||
private subscriptions: Subscription[] = [];
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private dashboardService: Dashboard3Service,
|
private dashboardService: Dashboard3Service,
|
||||||
private filterService: FilterService
|
private filterService: FilterService
|
||||||
@@ -111,6 +144,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('BarChartComponent input changes:', changes);
|
console.log('BarChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -141,15 +180,329 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
fetchChartData(): void {
|
fetchChartData(): void {
|
||||||
|
// Set loading state
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
this.isFetchingData = true;
|
this.isFetchingData = true;
|
||||||
|
|
||||||
// If we're in drilldown mode, fetch the appropriate drilldown data
|
// If we're in drilldown mode, fetch the appropriate drilldown data
|
||||||
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
|
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
|
||||||
this.fetchDrilldownData();
|
this.fetchDrilldownData();
|
||||||
// Reset flag after fetching
|
// Reset flags after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
|
this.isLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,8 +572,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.barChartLabels = [];
|
this.barChartLabels = [];
|
||||||
this.barChartData = [];
|
this.barChartData = [];
|
||||||
// Reset flag after fetching
|
// Reset flags after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
|
this.isLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,8 +603,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.barChartLabels = [];
|
this.barChartLabels = [];
|
||||||
this.barChartData = [];
|
this.barChartData = [];
|
||||||
}
|
}
|
||||||
// Reset flag after fetching
|
// Reset flags after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
|
this.isLoading = false;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('=== BAR CHART ERROR ===');
|
console.error('=== BAR CHART ERROR ===');
|
||||||
@@ -258,8 +613,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.barChartLabels = [];
|
this.barChartLabels = [];
|
||||||
this.barChartData = [];
|
this.barChartData = [];
|
||||||
// Reset flag after fetching
|
// Reset flags after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
|
this.isLoading = false;
|
||||||
// Keep default data in case of error
|
// Keep default data in case of error
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -271,8 +627,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.barChartLabels = [];
|
this.barChartLabels = [];
|
||||||
this.barChartData = [];
|
this.barChartData = [];
|
||||||
// Reset flag after fetching
|
// Reset flags after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,17 +791,23 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
// Trigger change detection
|
// Trigger change detection
|
||||||
// this.barChartData = [...this.barChartData];
|
// this.barChartData = [...this.barChartData];
|
||||||
console.log('Updated bar chart with drilldown data:', { labels: this.barChartLabels, data: this.barChartData });
|
console.log('Updated bar chart with drilldown data:', { labels: this.barChartLabels, data: this.barChartData });
|
||||||
|
// Set loading state to false
|
||||||
|
this.isLoading = false;
|
||||||
} else if (data && data.labels && data.datasets) {
|
} else if (data && data.labels && data.datasets) {
|
||||||
// Backend has already filtered the data, just display it
|
// Backend has already filtered the data, just display it
|
||||||
this.noDataAvailable = data.labels.length === 0;
|
this.noDataAvailable = data.labels.length === 0;
|
||||||
this.barChartLabels = data.labels;
|
this.barChartLabels = data.labels;
|
||||||
this.barChartData = data.datasets;
|
this.barChartData = data.datasets;
|
||||||
console.log('Updated bar chart with drilldown legacy data format:', { labels: this.barChartLabels, data: this.barChartData });
|
console.log('Updated bar chart with drilldown legacy data format:', { labels: this.barChartLabels, data: this.barChartData });
|
||||||
|
// Set loading state to false
|
||||||
|
this.isLoading = false;
|
||||||
} else {
|
} else {
|
||||||
console.warn('Drilldown received data does not have expected structure', data);
|
console.warn('Drilldown received data does not have expected structure', data);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.barChartLabels = [];
|
this.barChartLabels = [];
|
||||||
this.barChartData = [];
|
this.barChartData = [];
|
||||||
|
// Set loading state to false
|
||||||
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -452,12 +815,17 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.barChartLabels = [];
|
this.barChartLabels = [];
|
||||||
this.barChartData = [];
|
this.barChartData = [];
|
||||||
|
// Set loading state to false
|
||||||
|
this.isLoading = false;
|
||||||
// Keep current data in case of error
|
// Keep current data in case of error
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add subscription to array for cleanup
|
// Add subscription to array for cleanup
|
||||||
this.subscriptions.push(subscription);
|
this.subscriptions.push(subscription);
|
||||||
|
|
||||||
|
// Set loading state
|
||||||
|
this.isLoading = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset to original data (go back to base level)
|
// Reset to original data (go back to base level)
|
||||||
@@ -661,6 +1029,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.originalBarChartLabels = [];
|
this.originalBarChartLabels = [];
|
||||||
this.originalBarChartData = [];
|
this.originalBarChartData = [];
|
||||||
|
|
||||||
|
// Clear multiselect tracking
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
|
||||||
console.log('BarChartComponent destroyed and cleaned up');
|
console.log('BarChartComponent destroyed and cleaned up');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,310 @@
|
|||||||
<div style="display:block">
|
<div style="display:block; height: 100%; width: 100%;">
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Filter Controls Section -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<!-- Base Filters -->
|
||||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
<h4>Base Filters</h4>
|
||||||
</button>
|
<div class="filter-controls">
|
||||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
Back to Main View
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Bubble Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart container -->
|
||||||
|
<div style="position: relative; height: calc(100% - 80px); width: 100%; padding: 0 10px 30px 10px;">
|
||||||
|
<!-- Loading indicator -->
|
||||||
|
<div *ngIf="!dataLoaded" style="text-align: center; padding: 20px; color: #666; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; width: 100%;">
|
||||||
|
Loading data...
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- No data message -->
|
<!-- No data message -->
|
||||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
<div *ngIf="dataLoaded && (noDataAvailable || !isChartDataValid())" style="text-align: center; padding: 20px; color: #666; font-style: italic; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; width: 100%;">
|
||||||
No data available
|
No data available
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chart display -->
|
<!-- Chart display - Always render the canvas but conditionally show/hide with CSS -->
|
||||||
<div *ngIf="!noDataAvailable">
|
|
||||||
<canvas baseChart
|
<canvas baseChart
|
||||||
[datasets]="bubbleChartData"
|
[datasets]="bubbleChartData"
|
||||||
[type]="bubbleChartType"
|
[type]="bubbleChartType"
|
||||||
[options]="bubbleChartOptions"
|
[options]="bubbleChartOptions"
|
||||||
(chartHover)="chartHovered($event)"
|
(chartHover)="chartHovered($event)"
|
||||||
(chartClick)="chartClicked($event)">
|
(chartClick)="chartClicked($event)"
|
||||||
|
[style.visibility]="dataLoaded && !noDataAvailable && isChartDataValid() ? 'visible' : 'hidden'"
|
||||||
|
[style.position]="'absolute'"
|
||||||
|
[style.top]="'0'"
|
||||||
|
[style.left]="'0'"
|
||||||
|
[style.height]="'100%'"
|
||||||
|
[style.width]="'100%'"
|
||||||
|
[style.padding]="'0 10px 20px 10px'">
|
||||||
</canvas>
|
</canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,296 @@
|
|||||||
<div class="doughnut-chart-container">
|
<div class="chart-container">
|
||||||
<!-- Compact Filters -->
|
<!-- Filter Controls Section -->
|
||||||
<div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0">
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<app-compact-filter
|
<!-- Base Filters -->
|
||||||
*ngFor="let filter of baseFilters"
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
[filterKey]="filter.field"
|
<h4>Base Filters</h4>
|
||||||
(filterChange)="onFilterChange($event)">
|
<div class="filter-controls">
|
||||||
</app-compact-filter>
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Dropdown Filter -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator">
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
<span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<select [(ngModel)]="filter.value"
|
||||||
<button class="btn btn-secondary btn-sm" (click)="navigateBack()">
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
class="clr-select filter-select">
|
||||||
</button>
|
<option value="">Select {{ filter.field }}</option>
|
||||||
<button class="btn btn-danger btn-sm" (click)="resetToOriginalData()">
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
Back to Main View
|
</select>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
<div class="chart-header">
|
<div class="chart-header">
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
<div class="chart-content" [class.loading]="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
|
<div class="chart-content" [class.loading]="isLoading">
|
||||||
<!-- Show no data message -->
|
<!-- Show no data message -->
|
||||||
<div class="no-data-message" *ngIf="noDataAvailable">
|
<div class="no-data-message" *ngIf="noDataAvailable && doughnutChartLabels.length === 0">
|
||||||
<p>No chart data available</p>
|
<p>No chart data available</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Show chart when data is available -->
|
<!-- Show chart when data is available -->
|
||||||
<canvas baseChart
|
<canvas baseChart
|
||||||
*ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0"
|
[datasets]="doughnutChartData"
|
||||||
[data]="doughnutChartData"
|
|
||||||
[labels]="doughnutChartLabels"
|
[labels]="doughnutChartLabels"
|
||||||
[type]="doughnutChartType"
|
[type]="doughnutChartType"
|
||||||
[options]="doughnutChartOptions"
|
[options]="doughnutChartOptions"
|
||||||
@@ -42,17 +299,17 @@
|
|||||||
</canvas>
|
</canvas>
|
||||||
|
|
||||||
<!-- Loading overlay -->
|
<!-- Loading overlay -->
|
||||||
<div class="loading-overlay" *ngIf="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
|
<div class="loading-overlay" *ngIf="isLoading">
|
||||||
<div class="shimmer-donut"></div>
|
<div class="shimmer-donut"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chart-legend" *ngIf="!noDataAvailable && showlabel && doughnutChartLabels && doughnutChartLabels.length > 0">
|
<div class="chart-legend" *ngIf="showlabel && doughnutChartLabels && doughnutChartLabels.length > 0">
|
||||||
<div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index">
|
<div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index">
|
||||||
<span class="legend-color" [style.background-color]="getLegendColor(i)"></span>
|
<span class="legend-color" [style.background-color]="getLegendColor(i)"></span>
|
||||||
<span class="legend-label">{{ label }}</span>
|
<span class="legend-label">{{ label }}</span>
|
||||||
<span class="legend-value">{{ doughnutChartData && doughnutChartData[i] !== undefined ? doughnutChartData[i] : 0 }}</span>
|
<span class="legend-value">{{ doughnutChartData && doughnutChartData[0] && doughnutChartData[0].data && doughnutChartData[0].data[i] !== undefined ? doughnutChartData[0].data[i] : 0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,129 +1,200 @@
|
|||||||
.doughnut-chart-container {
|
// Chart container structure - simplified to match shield dashboard
|
||||||
|
.chart-container {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 400px; // Ensure minimum height
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 400px;
|
|
||||||
min-height: 400px;
|
// Filter section styling
|
||||||
padding: 20px;
|
.filter-section {
|
||||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
margin-bottom: 20px;
|
||||||
border-radius: 12px;
|
padding: 15px;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
border: 1px solid #ddd;
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
border-radius: 4px;
|
||||||
transition: all 0.3s ease;
|
background-color: #f9f9f9;
|
||||||
border: 1px solid #eaeaea;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.doughnut-chart-container:hover {
|
.filter-group {
|
||||||
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
|
margin-bottom: 15px;
|
||||||
transform: translateY(-2px);
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-filters-container {
|
.filter-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 5px;
|
gap: 15px;
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 5px;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 6px;
|
|
||||||
min-height: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.drilldown-indicator {
|
.filter-item {
|
||||||
background-color: #e0e0e0;
|
flex: 1 1 300px;
|
||||||
|
min-width: 250px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin-bottom: 15px;
|
background: white;
|
||||||
border-radius: 8px;
|
border: 1px solid #e0e0e0;
|
||||||
text-align: center;
|
border-radius: 4px;
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.drilldown-text {
|
.filter-label {
|
||||||
font-weight: bold;
|
font-weight: 500;
|
||||||
color: #333;
|
margin-bottom: 8px;
|
||||||
font-size: 16px;
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.filter-text-input,
|
||||||
|
.filter-select,
|
||||||
|
.filter-date {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.multiselect-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-value {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 6px 12px;
|
font-size: 13px;
|
||||||
border: none;
|
}
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background-color: #007cba;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background-color: #dc3545;
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chart header styling
|
||||||
.chart-header {
|
.chart-header {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
.chart-title {
|
.chart-title {
|
||||||
font-size: 22px;
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #0a192f;
|
color: #0a192f;
|
||||||
margin: 0;
|
}
|
||||||
text-align: center;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 2px solid #3498db;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chart wrapper and content - simplified to match shield dashboard
|
||||||
.chart-wrapper {
|
.chart-wrapper {
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 250px;
|
position: relative;
|
||||||
margin: 15px 0;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-wrapper canvas {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-wrapper canvas:hover {
|
|
||||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
|
|
||||||
transform: scale(1.02);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-content {
|
.chart-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
min-height: 300px;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
&.loading {
|
&.loading {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
@@ -135,20 +206,15 @@
|
|||||||
|
|
||||||
.no-data-message {
|
.no-data-message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 30px;
|
padding: 20px;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 18px;
|
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-data-message p {
|
canvas {
|
||||||
margin: 0;
|
max-width: 100%;
|
||||||
|
max-height: calc(100% - 40px); // Leave space for legend
|
||||||
|
transition: filter 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-overlay {
|
.loading-overlay {
|
||||||
@@ -172,68 +238,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart legend - simplified
|
||||||
.chart-legend {
|
.chart-legend {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
margin-top: 20px;
|
margin: 20px 0;
|
||||||
padding: 20px;
|
padding: 15px;
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-item {
|
.legend-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 20px;
|
padding: 8px 15px;
|
||||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
background: #f8f9fa;
|
||||||
border-radius: 25px;
|
border-radius: 20px;
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
border: 1px solid #e9ecef;
|
||||||
transition: all 0.3s ease;
|
min-width: 120px;
|
||||||
border: 1px solid #eaeaea;
|
justify-content: space-between;
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-item:hover {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
|
||||||
border-color: #3498db;
|
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-color {
|
.legend-color {
|
||||||
width: 20px;
|
width: 15px;
|
||||||
height: 20px;
|
height: 15px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-right: 12px;
|
margin-right: 8px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: 2px solid white;
|
flex-shrink: 0;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-label {
|
.legend-label {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
margin-right: 15px;
|
margin-right: 10px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-value {
|
.legend-value {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: #3498db;
|
color: #3498db;
|
||||||
background: linear-gradient(135deg, #e9ecef 0%, #dde1e5 100%);
|
min-width: 30px;
|
||||||
padding: 6px 12px;
|
text-align: right;
|
||||||
border-radius: 12px;
|
}
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
}
|
||||||
min-width: 40px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
@@ -245,46 +298,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive design */
|
// Responsive design
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.doughnut-chart-container {
|
.chart-container {
|
||||||
padding: 15px;
|
.filter-controls {
|
||||||
}
|
|
||||||
|
|
||||||
.chart-header .chart-title {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drilldown-indicator {
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.drilldown-text {
|
.filter-item {
|
||||||
font-size: 14px;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper {
|
.chart-header {
|
||||||
min-height: 200px;
|
.header-row {
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-content {
|
||||||
|
min-height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-legend {
|
.chart-legend {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
|
||||||
|
|
||||||
.legend-item {
|
.legend-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-data-message {
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-filters-container {
|
.chart-content {
|
||||||
flex-wrap: wrap;
|
min-height: 250px;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
max-height: calc(100% - 60px); // More space for legend on mobile
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
|
|||||||
templateUrl: './doughnut-chart.component.html',
|
templateUrl: './doughnut-chart.component.html',
|
||||||
styleUrls: ['./doughnut-chart.component.scss']
|
styleUrls: ['./doughnut-chart.component.scss']
|
||||||
})
|
})
|
||||||
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
|
||||||
@Input() xAxis: string;
|
@Input() xAxis: string;
|
||||||
@Input() yAxis: string | string[];
|
@Input() yAxis: string | string[];
|
||||||
@Input() table: string;
|
@Input() table: string;
|
||||||
@@ -36,7 +36,21 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||||
|
|
||||||
public doughnutChartLabels: string[] = ["Category A", "Category B", "Category C"];
|
public doughnutChartLabels: string[] = ["Category A", "Category B", "Category C"];
|
||||||
public doughnutChartData: number[] = [30, 50, 20];
|
public doughnutChartData: any[] = [
|
||||||
|
{
|
||||||
|
data: [30, 50, 20],
|
||||||
|
backgroundColor: [
|
||||||
|
'#FF6384',
|
||||||
|
'#36A2EB',
|
||||||
|
'#FFCE56'
|
||||||
|
],
|
||||||
|
hoverBackgroundColor: [
|
||||||
|
'#FF6384',
|
||||||
|
'#36A2EB',
|
||||||
|
'#FFCE56'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
public doughnutChartType: string = "doughnut";
|
public doughnutChartType: string = "doughnut";
|
||||||
public doughnutChartOptions: any = {
|
public doughnutChartOptions: any = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
@@ -72,6 +86,14 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#fff'
|
borderColor: '#fff'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
top: 20,
|
||||||
|
bottom: 20,
|
||||||
|
left: 20,
|
||||||
|
right: 20
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,12 +118,20 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
// No data state
|
// No data state
|
||||||
noDataAvailable: boolean = false;
|
noDataAvailable: boolean = false;
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
isLoading: boolean = false;
|
||||||
|
|
||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
// Subscriptions to unsubscribe on destroy
|
// Subscriptions to unsubscribe on destroy
|
||||||
private subscriptions: Subscription[] = [];
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private dashboardService: Dashboard3Service,
|
private dashboardService: Dashboard3Service,
|
||||||
private filterService: FilterService
|
private filterService: FilterService
|
||||||
@@ -118,8 +148,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
|
|
||||||
// Validate initial data
|
// Validate initial data
|
||||||
this.validateChartData();
|
this.validateChartData();
|
||||||
|
// Only fetch data if we have the required inputs, otherwise show default data
|
||||||
|
if (this.table && this.xAxis && this.yAxis) {
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate and sanitize chart data
|
* Validate and sanitize chart data
|
||||||
@@ -138,17 +171,33 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
|
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
|
||||||
// Add default data to ensure chart visibility
|
// Add default data to ensure chart visibility
|
||||||
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
|
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
|
||||||
this.doughnutChartData = [30, 50, 20];
|
this.doughnutChartData = [
|
||||||
|
{
|
||||||
|
data: [30, 50, 20],
|
||||||
|
backgroundColor: [
|
||||||
|
'#FF6384',
|
||||||
|
'#36A2EB',
|
||||||
|
'#FFCE56'
|
||||||
|
],
|
||||||
|
hoverBackgroundColor: [
|
||||||
|
'#FF6384',
|
||||||
|
'#36A2EB',
|
||||||
|
'#FFCE56'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have matching arrays
|
// Ensure we have matching arrays
|
||||||
if (this.doughnutChartLabels.length !== this.doughnutChartData.length) {
|
if (this.doughnutChartLabels.length !== (this.doughnutChartData[0]?.data?.length || 0)) {
|
||||||
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length);
|
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData[0]?.data?.length || 0);
|
||||||
while (this.doughnutChartLabels.length < maxLength) {
|
while (this.doughnutChartLabels.length < maxLength) {
|
||||||
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
|
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
|
||||||
}
|
}
|
||||||
while (this.doughnutChartData.length < maxLength) {
|
if (this.doughnutChartData[0]) {
|
||||||
this.doughnutChartData.push(0);
|
while (this.doughnutChartData[0].data.length < maxLength) {
|
||||||
|
this.doughnutChartData[0].data.push(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,6 +213,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('DoughnutChartComponent input changes:', changes);
|
console.log('DoughnutChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -185,6 +240,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
console.log('Chart configuration changed, fetching new data');
|
console.log('Chart configuration changed, fetching new data');
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have the required inputs and haven't fetched data yet, fetch it
|
||||||
|
if ((xAxisChanged || yAxisChanged || tableChanged) && this.table && this.xAxis && this.yAxis && !this.isFetchingData) {
|
||||||
|
console.log('Required inputs available, fetching data');
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewChecked() {
|
ngAfterViewChecked() {
|
||||||
@@ -198,12 +259,318 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
// Clean up document click handler
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle filter changes from compact filters
|
// Initialize filter values with proper default values based on type
|
||||||
onFilterChange(event: { filterId: string, value: any }): void {
|
private initializeFilterValues(): void {
|
||||||
console.log('Compact filter changed:', event);
|
console.log('Initializing filter values');
|
||||||
// The filter service will automatically trigger chart updates through the subscription
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public method to refresh data when filters change
|
// Public method to refresh data when filters change
|
||||||
@@ -212,14 +579,18 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchChartData(): void {
|
fetchChartData(): void {
|
||||||
|
// Set loading state
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
this.isFetchingData = true;
|
this.isFetchingData = true;
|
||||||
|
|
||||||
// If we're in drilldown mode, fetch the appropriate drilldown data
|
// If we're in drilldown mode, fetch the appropriate drilldown data
|
||||||
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
|
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
|
||||||
this.fetchDrilldownData();
|
this.fetchDrilldownData();
|
||||||
// Reset flag after fetching
|
// Reset flags after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
|
this.isLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +660,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
console.log('Doughnut chart data URL:', url);
|
console.log('Chart data URL:', url);
|
||||||
|
|
||||||
// Fetch data from the dashboard service with parameter field and value
|
// Fetch data from the dashboard service with parameter field and value
|
||||||
// For base level, we pass empty parameter and value, but now also pass filters
|
// For base level, we pass empty parameter and value, but now also pass filters
|
||||||
@@ -297,89 +668,113 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
(data: any) => {
|
(data: any) => {
|
||||||
console.log('Received doughnut chart data:', data);
|
console.log('Received doughnut chart data:', data);
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
console.warn('Doughnut chart API returned null data. Check if the API endpoint is working correctly.');
|
console.warn('API returned null data. Check if the API endpoint is working correctly.');
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.doughnutChartLabels = [];
|
this.doughnutChartLabels = [];
|
||||||
this.doughnutChartData = [];
|
this.doughnutChartData = [];
|
||||||
// Validate and sanitize data to show default data
|
// Reset flags after fetching
|
||||||
this.validateChartData();
|
|
||||||
// Reset flag after fetching
|
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
|
this.isLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the actual data structure returned by the API
|
// Handle the actual data structure returned by the API
|
||||||
if (data && data.chartLabels && data.chartData) {
|
if (data && data.chartLabels && data.chartData) {
|
||||||
// For doughnut charts, we need to extract the data differently
|
// Backend has already filtered the data, just display it
|
||||||
// The first dataset's data array contains the values for the doughnut chart
|
|
||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.doughnutChartLabels = data.chartLabels || [];
|
this.doughnutChartLabels = data.chartLabels;
|
||||||
if (data.chartData && data.chartData.length > 0) {
|
|
||||||
this.doughnutChartData = data.chartData[0].data.map(value => {
|
// Handle different data structures
|
||||||
// Convert to number if it's not already
|
let chartDataValues;
|
||||||
const numValue = Number(value);
|
if (Array.isArray(data.chartData)) {
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
// If chartData is already an array of values
|
||||||
});
|
if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
|
||||||
} else {
|
chartDataValues = data.chartData;
|
||||||
this.doughnutChartData = [];
|
|
||||||
}
|
}
|
||||||
// Ensure labels and data arrays have the same length
|
// If chartData is an array with one object containing the data
|
||||||
this.syncLabelAndDataArrays();
|
else if (data.chartData.length > 0 && data.chartData[0].data) {
|
||||||
// Validate and sanitize data
|
chartDataValues = data.chartData[0].data;
|
||||||
this.validateChartData();
|
}
|
||||||
// Trigger change detection
|
// Default case
|
||||||
this.doughnutChartData = [...this.doughnutChartData];
|
else {
|
||||||
|
chartDataValues = data.chartData;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chartDataValues = [data.chartData];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.doughnutChartData = [
|
||||||
|
{
|
||||||
|
data: chartDataValues,
|
||||||
|
backgroundColor: this.chartColors.slice(0, chartDataValues.length),
|
||||||
|
hoverBackgroundColor: this.chartColors.slice(0, chartDataValues.length)
|
||||||
|
}
|
||||||
|
];
|
||||||
console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||||
} else if (data && data.labels && data.data) {
|
} else if (data && data.labels && data.datasets) {
|
||||||
// Handle the original expected format as fallback
|
// Backend has already filtered the data, just display it
|
||||||
this.noDataAvailable = data.labels.length === 0;
|
this.noDataAvailable = data.labels.length === 0;
|
||||||
this.doughnutChartLabels = data.labels || [];
|
this.doughnutChartLabels = data.labels;
|
||||||
this.doughnutChartData = data.data.map(value => {
|
this.doughnutChartData = data.datasets;
|
||||||
// Convert to number if it's not already
|
|
||||||
const numValue = Number(value);
|
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
|
||||||
});
|
|
||||||
// Ensure labels and data arrays have the same length
|
|
||||||
this.syncLabelAndDataArrays();
|
|
||||||
// Validate and sanitize data
|
|
||||||
this.validateChartData();
|
|
||||||
// Trigger change detection
|
|
||||||
this.doughnutChartData = [...this.doughnutChartData];
|
|
||||||
console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||||
} else {
|
} else {
|
||||||
console.warn('Doughnut chart received data does not have expected structure', data);
|
console.warn('Received data does not have expected structure', data);
|
||||||
// Reset to default data
|
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.doughnutChartLabels = [];
|
// Keep default data instead of empty arrays
|
||||||
this.doughnutChartData = [];
|
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
|
||||||
// Validate and sanitize data to show default data
|
this.doughnutChartData = [
|
||||||
this.validateChartData();
|
{
|
||||||
|
data: [30, 50, 20],
|
||||||
|
backgroundColor: [
|
||||||
|
'#FF6384',
|
||||||
|
'#36A2EB',
|
||||||
|
'#FFCE56'
|
||||||
|
],
|
||||||
|
hoverBackgroundColor: [
|
||||||
|
'#FF6384',
|
||||||
|
'#36A2EB',
|
||||||
|
'#FFCE56'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
// Reset flag after fetching
|
];
|
||||||
|
}
|
||||||
|
// Reset flags after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
|
this.isLoading = false;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('Error fetching doughnut chart data:', error);
|
console.error('Error fetching doughnut chart data:', error);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.doughnutChartLabels = [];
|
// Keep default data in case of error
|
||||||
this.doughnutChartData = [];
|
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
|
||||||
// Validate and sanitize data to show default data
|
this.doughnutChartData = [
|
||||||
this.validateChartData();
|
{
|
||||||
// Reset flag after fetching
|
data: [30, 50, 20],
|
||||||
|
backgroundColor: [
|
||||||
|
'#FF6384',
|
||||||
|
'#36A2EB',
|
||||||
|
'#FFCE56'
|
||||||
|
],
|
||||||
|
hoverBackgroundColor: [
|
||||||
|
'#FF6384',
|
||||||
|
'#36A2EB',
|
||||||
|
'#FFCE56'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// Reset flags after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log('Missing required data for doughnut chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||||
// Don't set noDataAvailable to true when there's no required data
|
this.noDataAvailable = true;
|
||||||
// This allows static data to be displayed
|
this.doughnutChartLabels = [];
|
||||||
this.noDataAvailable = false;
|
this.doughnutChartData = [];
|
||||||
// Validate the chart data to ensure we have some data to display
|
// Reset flags after fetching
|
||||||
this.validateChartData();
|
|
||||||
// Force a redraw to ensure the chart displays
|
|
||||||
this.doughnutChartData = [...this.doughnutChartData];
|
|
||||||
// Reset flag after fetching
|
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,6 +870,35 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add common filters to drilldown filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with drilldown filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add drilldown filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const drilldownFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters
|
||||||
|
Object.keys(commonFilters).forEach(key => {
|
||||||
|
const value = commonFilters[key];
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
mergedFilterObj[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
console.log('Drilldown data URL:', url);
|
console.log('Drilldown data URL:', url);
|
||||||
@@ -494,59 +918,66 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
|
|
||||||
// Handle the actual data structure returned by the API
|
// Handle the actual data structure returned by the API
|
||||||
if (data && data.chartLabels && data.chartData) {
|
if (data && data.chartLabels && data.chartData) {
|
||||||
// For doughnut charts, we need to extract the data differently
|
// Backend has already filtered the data, just display it
|
||||||
// The first dataset's data array contains the values for the doughnut chart
|
|
||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.doughnutChartLabels = data.chartLabels || [];
|
this.doughnutChartLabels = data.chartLabels;
|
||||||
if (data.chartData && data.chartData.length > 0) {
|
|
||||||
this.doughnutChartData = data.chartData[0].data.map(value => {
|
// Handle different data structures
|
||||||
// Convert to number if it's not already
|
let chartDataValues;
|
||||||
const numValue = Number(value);
|
if (Array.isArray(data.chartData)) {
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
// If chartData is already an array of values
|
||||||
});
|
if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
|
||||||
} else {
|
chartDataValues = data.chartData;
|
||||||
this.doughnutChartData = [];
|
|
||||||
}
|
}
|
||||||
// Ensure labels and data arrays have the same length
|
// If chartData is an array with one object containing the data
|
||||||
this.syncLabelAndDataArrays();
|
else if (data.chartData.length > 0 && data.chartData[0].data) {
|
||||||
// Validate and sanitize data
|
chartDataValues = data.chartData[0].data;
|
||||||
this.validateChartData();
|
}
|
||||||
// Trigger change detection
|
// Default case
|
||||||
this.doughnutChartData = [...this.doughnutChartData];
|
else {
|
||||||
|
chartDataValues = data.chartData;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chartDataValues = [data.chartData];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.doughnutChartData = [
|
||||||
|
{
|
||||||
|
data: chartDataValues,
|
||||||
|
backgroundColor: this.chartColors.slice(0, chartDataValues.length),
|
||||||
|
hoverBackgroundColor: this.chartColors.slice(0, chartDataValues.length)
|
||||||
|
}
|
||||||
|
];
|
||||||
console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||||
} else if (data && data.labels && data.data) {
|
// Set loading state to false
|
||||||
// Handle the original expected format as fallback
|
this.isLoading = false;
|
||||||
|
} else if (data && data.labels && data.datasets) {
|
||||||
|
// Backend has already filtered the data, just display it
|
||||||
this.noDataAvailable = data.labels.length === 0;
|
this.noDataAvailable = data.labels.length === 0;
|
||||||
this.doughnutChartLabels = data.labels || [];
|
this.doughnutChartLabels = data.labels;
|
||||||
this.doughnutChartData = data.data.map(value => {
|
this.doughnutChartData = data.datasets;
|
||||||
// Convert to number if it's not already
|
|
||||||
const numValue = Number(value);
|
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
|
||||||
});
|
|
||||||
// Ensure labels and data arrays have the same length
|
|
||||||
this.syncLabelAndDataArrays();
|
|
||||||
// Validate and sanitize data
|
|
||||||
this.validateChartData();
|
|
||||||
// Trigger change detection
|
|
||||||
this.doughnutChartData = [...this.doughnutChartData];
|
|
||||||
console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||||
|
// Set loading state to false
|
||||||
|
this.isLoading = false;
|
||||||
} else {
|
} else {
|
||||||
console.warn('Drilldown received data does not have expected structure', data);
|
console.warn('Drilldown received data does not have expected structure', data);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.doughnutChartLabels = [];
|
// Keep current data instead of empty arrays
|
||||||
this.doughnutChartData = [];
|
// Set loading state to false
|
||||||
// Validate and sanitize data
|
this.isLoading = false;
|
||||||
this.validateChartData();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('Error fetching drilldown data:', error);
|
console.error('Error fetching drilldown data:', error);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.doughnutChartLabels = [];
|
|
||||||
this.doughnutChartData = [];
|
|
||||||
// Keep current data in case of error
|
// Keep current data in case of error
|
||||||
|
// Set loading state to false
|
||||||
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Set loading state
|
||||||
|
this.isLoading = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset to original data (go back to base level)
|
// Reset to original data (go back to base level)
|
||||||
@@ -563,7 +994,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
console.log('Restored original labels');
|
console.log('Restored original labels');
|
||||||
}
|
}
|
||||||
if (this.originalDoughnutChartData.length > 0) {
|
if (this.originalDoughnutChartData.length > 0) {
|
||||||
this.doughnutChartData = [...this.originalDoughnutChartData];
|
this.doughnutChartData = JSON.parse(JSON.stringify(this.originalDoughnutChartData));
|
||||||
console.log('Restored original data');
|
console.log('Restored original data');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,44 +1036,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Get legend color for a specific index
|
||||||
* Get color for legend item
|
getLegendColor(index: number): string {
|
||||||
* @param index Index of the legend item
|
|
||||||
*/
|
|
||||||
public getLegendColor(index: number): string {
|
|
||||||
return this.chartColors[index % this.chartColors.length];
|
return this.chartColors[index % this.chartColors.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure labels and data arrays have the same length
|
|
||||||
*/
|
|
||||||
private syncLabelAndDataArrays(): void {
|
|
||||||
// Handle empty arrays
|
|
||||||
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length);
|
|
||||||
|
|
||||||
// Pad the shorter array with default values
|
|
||||||
while (this.doughnutChartLabels.length < maxLength) {
|
|
||||||
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (this.doughnutChartData.length < maxLength) {
|
|
||||||
this.doughnutChartData.push(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate the longer array if needed
|
|
||||||
if (this.doughnutChartLabels.length > maxLength) {
|
|
||||||
this.doughnutChartLabels = this.doughnutChartLabels.slice(0, maxLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.doughnutChartData.length > maxLength) {
|
|
||||||
this.doughnutChartData = this.doughnutChartData.slice(0, maxLength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// events
|
// events
|
||||||
public chartClicked(e: any): void {
|
public chartClicked(e: any): void {
|
||||||
console.log('Doughnut chart clicked:', e);
|
console.log('Doughnut chart clicked:', e);
|
||||||
@@ -729,6 +1127,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
}
|
}
|
||||||
|
|
||||||
public chartHovered(e: any): void {
|
public chartHovered(e: any): void {
|
||||||
console.log(e);
|
console.log('Doughnut chart hovered:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,285 @@
|
|||||||
<div class="dynamic-chart-container">
|
<div class="dynamic-chart-container">
|
||||||
|
<!-- Filter Controls Section -->
|
||||||
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
|
<!-- Base Filters -->
|
||||||
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
|
<h4>Base Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Dynamic Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Existing content -->
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Drilldown mode indicator -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import { Component, OnInit, ViewChild, Input, OnChanges, SimpleChanges } from '@
|
|||||||
import { ChartConfiguration, ChartData, ChartDataset } from 'chart.js';
|
import { ChartConfiguration, ChartData, ChartDataset } from 'chart.js';
|
||||||
import { BaseChartDirective } from 'ng2-charts';
|
import { BaseChartDirective } from 'ng2-charts';
|
||||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dynamic-chart',
|
selector: 'app-dynamic-chart',
|
||||||
@@ -37,9 +39,20 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
@ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined;
|
@ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined;
|
||||||
|
|
||||||
constructor(private dashboardService: Dashboard3Service) { }
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchChartData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize with default data
|
// Initialize with default data
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
@@ -47,6 +60,12 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
|||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('DynamicChartComponent input changes:', changes);
|
console.log('DynamicChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -107,6 +126,14 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
|||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
|
// Subscriptions to unsubscribe on destroy
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
fetchChartData(): void {
|
fetchChartData(): void {
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
this.isFetchingData = true;
|
this.isFetchingData = true;
|
||||||
@@ -139,7 +166,49 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
|||||||
filterParams = JSON.stringify(filterObj);
|
filterParams = JSON.stringify(filterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Base filter parameters:', filterParams);
|
|
||||||
|
// Add common filters to filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/dynamic?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/dynamic?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
@@ -504,4 +573,322 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
this.dynamicChartData = _dynamicChartData;
|
this.dynamicChartData = _dynamicChartData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||||
|
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
|
||||||
|
// Remove document click handler if it exists
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,285 @@
|
|||||||
<div class="financial-chart-container">
|
<div class="financial-chart-container">
|
||||||
|
<!-- Filter Controls Section -->
|
||||||
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
|
<!-- Base Filters -->
|
||||||
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
|
<h4>Base Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Financial Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Existing content -->
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Drilldown mode indicator -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||||
|
|||||||
@@ -1,108 +1,192 @@
|
|||||||
.financial-chart-container {
|
.filter-section {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 400px;
|
|
||||||
min-height: 400px;
|
|
||||||
padding: 20px;
|
|
||||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
border: 1px solid #eaeaea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.financial-chart-container:hover {
|
|
||||||
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-title {
|
|
||||||
font-size: 26px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #2c3e50;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
text-align: center;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
border-bottom: 2px solid #3498db;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-wrapper {
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 250px;
|
|
||||||
margin: 15px 0;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-wrapper canvas {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-wrapper canvas:hover {
|
|
||||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
|
|
||||||
transform: scale(1.02);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-indicator, .no-data-message {
|
|
||||||
text-align: center;
|
|
||||||
padding: 30px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 18px;
|
|
||||||
font-style: italic;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-indicator p, .no-data-message p {
|
|
||||||
margin: 10px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
border: 4px solid #f3f3f3;
|
|
||||||
border-top: 4px solid #3498db;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.financial-chart-container {
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
min-width: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.filter-text-input,
|
||||||
|
.filter-select,
|
||||||
|
.filter-date {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.multiselect-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-value {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New header row styling
|
||||||
|
.header-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
.chart-title {
|
.chart-title {
|
||||||
font-size: 20px;
|
margin: 0;
|
||||||
margin-bottom: 15px;
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper {
|
// Responsive design
|
||||||
min-height: 200px;
|
@media (max-width: 768px) {
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-data-message {
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
.chart-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 20px;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-financial-chart',
|
selector: 'app-financial-chart',
|
||||||
@@ -33,9 +36,21 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
|||||||
// Multi-layer drilldown configuration inputs
|
// Multi-layer drilldown configuration inputs
|
||||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||||
|
|
||||||
constructor(private dashboardService: Dashboard3Service) { }
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService,
|
||||||
|
private alertService: AlertsService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchChartData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize with default data
|
// Initialize with default data
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
@@ -43,6 +58,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
|||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('FinancialChartComponent input changes:', changes);
|
console.log('FinancialChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
// Load filter options for dropdown/multiselect filters
|
||||||
|
this.loadFilterOptions();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -86,6 +109,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
|||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
|
// Subscriptions to unsubscribe on destroy
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
fetchChartData(): void {
|
fetchChartData(): void {
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
this.isFetchingData = true;
|
this.isFetchingData = true;
|
||||||
@@ -118,7 +149,49 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
|||||||
filterParams = JSON.stringify(filterObj);
|
filterParams = JSON.stringify(filterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Base filter parameters:', filterParams);
|
|
||||||
|
// Add common filters to filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/financial?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/financial?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
@@ -496,4 +569,520 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
|||||||
public chartHovered(e: any): void {
|
public chartHovered(e: any): void {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
// Ensure filter has required properties
|
||||||
|
if (!filter.type) filter.type = 'text';
|
||||||
|
if (!filter.options) filter.options = '';
|
||||||
|
if (!filter.availableValues) filter.availableValues = '';
|
||||||
|
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
// Ensure filter has required properties
|
||||||
|
if (!filter.type) filter.type = 'text';
|
||||||
|
if (!filter.options) filter.options = '';
|
||||||
|
if (!filter.availableValues) filter.availableValues = '';
|
||||||
|
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
// Ensure filter has required properties
|
||||||
|
if (!filter.type) filter.type = 'text';
|
||||||
|
if (!filter.options) filter.options = '';
|
||||||
|
if (!filter.availableValues) filter.availableValues = '';
|
||||||
|
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||||
|
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
|
||||||
|
// Remove document click handler if it exists
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load filter options for dropdown and multiselect filters
|
||||||
|
private loadFilterOptions(): void {
|
||||||
|
console.log('Loading filter options');
|
||||||
|
|
||||||
|
// Load options for base filters
|
||||||
|
if (this.baseFilters && this.table) {
|
||||||
|
this.baseFilters.forEach((filter, index) => {
|
||||||
|
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
|
||||||
|
this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load options for drilldown filters
|
||||||
|
if (this.drilldownFilters && this.drilldownApiUrl) {
|
||||||
|
this.drilldownFilters.forEach((filter, index) => {
|
||||||
|
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
|
||||||
|
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load options for layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach((layer, layerIndex) => {
|
||||||
|
if (layer.filters && layer.apiUrl) {
|
||||||
|
layer.filters.forEach((filter, filterIndex) => {
|
||||||
|
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
|
||||||
|
this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load filter values for a specific field
|
||||||
|
private loadFilterValuesForField(
|
||||||
|
apiUrl: string,
|
||||||
|
connectionId: number | undefined,
|
||||||
|
field: string,
|
||||||
|
filterIndex: number,
|
||||||
|
filterType: 'base' | 'drilldown' | 'layer',
|
||||||
|
layerIndex?: number
|
||||||
|
): void {
|
||||||
|
if (apiUrl && field) {
|
||||||
|
this.alertService.getValuesFromUrl(apiUrl, connectionId, field).subscribe(
|
||||||
|
(values: string[]) => {
|
||||||
|
console.log(`Loaded filter values for ${filterType} filter ${field}:`, values);
|
||||||
|
|
||||||
|
// Update the filter with available values
|
||||||
|
if (filterType === 'base') {
|
||||||
|
const filter = this.baseFilters[filterIndex];
|
||||||
|
if (filter) {
|
||||||
|
filter.availableValues = values.join(', ');
|
||||||
|
// For dropdown/multiselect types, also update the options
|
||||||
|
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
|
||||||
|
filter.options = filter.availableValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (filterType === 'drilldown') {
|
||||||
|
const filter = this.drilldownFilters[filterIndex];
|
||||||
|
if (filter) {
|
||||||
|
filter.availableValues = values.join(', ');
|
||||||
|
// For dropdown/multiselect types, also update the options
|
||||||
|
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
|
||||||
|
filter.options = filter.availableValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (filterType === 'layer' && layerIndex !== undefined) {
|
||||||
|
const layer = this.drilldownLayers[layerIndex];
|
||||||
|
if (layer && layer.filters) {
|
||||||
|
const filter = layer.filters[filterIndex];
|
||||||
|
if (filter) {
|
||||||
|
filter.availableValues = values.join(', ');
|
||||||
|
// For dropdown/multiselect types, also update the options
|
||||||
|
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
|
||||||
|
filter.options = filter.availableValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error loading available values for field:', field, error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter field change
|
||||||
|
onBaseFilterFieldChange(index: number, field: string): void {
|
||||||
|
const filter = this.baseFilters[index];
|
||||||
|
if (filter) {
|
||||||
|
filter.field = field;
|
||||||
|
// If field changes, reset value and options
|
||||||
|
filter.value = '';
|
||||||
|
filter.options = '';
|
||||||
|
filter.availableValues = '';
|
||||||
|
|
||||||
|
// If we have a field and table URL, load available values
|
||||||
|
if (field && this.table && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
|
||||||
|
this.loadFilterValuesForField(this.table, this.connection, field, index, 'base');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter type change
|
||||||
|
onBaseFilterTypeChange(index: number, type: string): void {
|
||||||
|
const filter = this.baseFilters[index];
|
||||||
|
if (filter) {
|
||||||
|
filter.type = type;
|
||||||
|
// If type changes to dropdown/multiselect and we have a field, load available values
|
||||||
|
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.table) {
|
||||||
|
this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter field change
|
||||||
|
onDrilldownFilterFieldChange(index: number, field: string): void {
|
||||||
|
const filter = this.drilldownFilters[index];
|
||||||
|
if (filter) {
|
||||||
|
filter.field = field;
|
||||||
|
// If field changes, reset value and options
|
||||||
|
filter.value = '';
|
||||||
|
filter.options = '';
|
||||||
|
filter.availableValues = '';
|
||||||
|
|
||||||
|
// If we have a field and drilldown API URL, load available values
|
||||||
|
if (field && this.drilldownApiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
|
||||||
|
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, field, index, 'drilldown');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter type change
|
||||||
|
onDrilldownFilterTypeChange(index: number, type: string): void {
|
||||||
|
const filter = this.drilldownFilters[index];
|
||||||
|
if (filter) {
|
||||||
|
filter.type = type;
|
||||||
|
// If type changes to dropdown/multiselect and we have a field, load available values
|
||||||
|
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.drilldownApiUrl) {
|
||||||
|
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter field change
|
||||||
|
onLayerFilterFieldChange(layerIndex: number, filterIndex: number, field: string): void {
|
||||||
|
const layer = this.drilldownLayers[layerIndex];
|
||||||
|
if (layer && layer.filters) {
|
||||||
|
const filter = layer.filters[filterIndex];
|
||||||
|
if (filter) {
|
||||||
|
filter.field = field;
|
||||||
|
// If field changes, reset value and options
|
||||||
|
filter.value = '';
|
||||||
|
filter.options = '';
|
||||||
|
filter.availableValues = '';
|
||||||
|
|
||||||
|
// If we have a field and layer API URL, load available values
|
||||||
|
if (field && layer.apiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
|
||||||
|
this.loadFilterValuesForField(layer.apiUrl, this.connection, field, filterIndex, 'layer', layerIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter type change
|
||||||
|
onLayerFilterTypeChange(layerIndex: number, filterIndex: number, type: string): void {
|
||||||
|
const layer = this.drilldownLayers[layerIndex];
|
||||||
|
if (layer && layer.filters) {
|
||||||
|
const filter = layer.filters[filterIndex];
|
||||||
|
if (filter) {
|
||||||
|
filter.type = type;
|
||||||
|
// If type changes to dropdown/multiselect and we have a field, load available values
|
||||||
|
if ((type === 'dropdown' || type === 'multiselect') && filter.field && layer.apiUrl) {
|
||||||
|
this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,254 @@
|
|||||||
<div style="display: block;">
|
<div style="display: block;">
|
||||||
<div class="dg-wrapper">
|
<div class="dg-wrapper">
|
||||||
<div class="clr-row">
|
<!-- Filter Controls Section -->
|
||||||
<div class="clr-col-8">
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<h3>{{charttitle || 'Data Grid'}}</h3>
|
<!-- Base Filters -->
|
||||||
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
|
<h4>Base Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clr-row">
|
||||||
|
<!-- <div class="clr-col-8">
|
||||||
|
<h3>{{charttitle || 'Data Grid'}}</h3>
|
||||||
|
</div> -->
|
||||||
<!-- Add drilldown navigation controls -->
|
<!-- Add drilldown navigation controls -->
|
||||||
<div class="clr-col-4" *ngIf="drilldownEnabled && drilldownStack.length > 0" style="text-align: right;">
|
<div class="clr-col-4" *ngIf="drilldownEnabled && drilldownStack.length > 0" style="text-align: right;">
|
||||||
<button class="btn btn-sm btn-link" (click)="navigateBack()">
|
<button class="btn btn-sm btn-link" (click)="navigateBack()">
|
||||||
|
|||||||
@@ -1,28 +1,180 @@
|
|||||||
// Add styles for drilldown navigation
|
.filter-section {
|
||||||
.alert-info {
|
margin-bottom: 20px;
|
||||||
background-color: #dcedf7;
|
padding: 15px;
|
||||||
border-color: #a3d4f5;
|
border: 1px solid #ddd;
|
||||||
color: #21333b;
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-info .alert-icon {
|
.filter-group {
|
||||||
color: #0072a3;
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-link {
|
.filter-controls {
|
||||||
color: #0072a3;
|
display: flex;
|
||||||
text-decoration: none;
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-link:hover {
|
.filter-item {
|
||||||
color: #00567a;
|
flex: 1 1 300px;
|
||||||
text-decoration: underline;
|
min-width: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.filter-text-input,
|
||||||
|
.filter-select,
|
||||||
|
.filter-date {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.multiselect-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-value {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dg-wrapper {
|
.dg-wrapper {
|
||||||
padding: 12px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clr-row {
|
clr-datagrid {
|
||||||
margin-bottom: 12px;
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +66,15 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
// Add subscriptions to unsubscribe on destroy
|
// Add subscriptions to unsubscribe on destroy
|
||||||
private subscriptions: Subscription[] = [];
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
// Add a flag to track if filters have been initialized
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
|
// Add properties to track open multiselect dropdowns
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
|
||||||
|
// Add property to track document click handler
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private mainservice: UsergrpmaintainceService,
|
private mainservice: UsergrpmaintainceService,
|
||||||
private dashboardService: Dashboard3Service,
|
private dashboardService: Dashboard3Service,
|
||||||
@@ -102,6 +111,12 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange;
|
const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange;
|
||||||
const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.firstChange;
|
const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.firstChange;
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Respond to input changes
|
// Respond to input changes
|
||||||
if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
|
if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
|
||||||
drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
|
drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
|
||||||
@@ -112,6 +127,84 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Dynamic headers for the grid
|
// Dynamic headers for the grid
|
||||||
|
|
||||||
fetchGridData(): void {
|
fetchGridData(): void {
|
||||||
@@ -628,6 +721,238 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
.replace(/^./, str => str.toUpperCase());
|
.replace(/^./, str => str.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchGridData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchGridData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchGridData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchGridData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchGridData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchGridData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchGridData();
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||||
console.log('GridViewComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
console.log('GridViewComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||||
@@ -644,6 +969,12 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.drilldownStack = [];
|
this.drilldownStack = [];
|
||||||
this.originalGridData = [];
|
this.originalGridData = [];
|
||||||
|
|
||||||
|
// Clear multiselect tracking
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
|
||||||
console.log('GridViewComponent destroyed and cleaned up');
|
console.log('GridViewComponent destroyed and cleaned up');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,283 @@
|
|||||||
<div style="display: block">
|
<div style="display: block; height: 100%; width: 100%;">
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Filter Controls Section -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<!-- Base Filters -->
|
||||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
<h4>Base Filters</h4>
|
||||||
</button>
|
<div class="filter-controls">
|
||||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
Back to Main View
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Line Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- No data message -->
|
<!-- No data message -->
|
||||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
||||||
@@ -16,12 +285,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chart display -->
|
<!-- Chart display -->
|
||||||
<div *ngIf="!noDataAvailable">
|
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
||||||
<canvas baseChart
|
<canvas baseChart
|
||||||
[datasets]="lineChartData"
|
[datasets]="lineChartData"
|
||||||
[labels]="lineChartLabels"
|
[labels]="lineChartLabels"
|
||||||
[options]="lineChartOptions"
|
[options]="lineChartOptions"
|
||||||
|
|
||||||
[legend]="lineChartLegend"
|
[legend]="lineChartLegend"
|
||||||
[type]="lineChartType"
|
[type]="lineChartType"
|
||||||
(chartHover)="chartHovered($event)"
|
(chartHover)="chartHovered($event)"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
|
|||||||
templateUrl: './line-chart.component.html',
|
templateUrl: './line-chart.component.html',
|
||||||
styleUrls: ['./line-chart.component.scss']
|
styleUrls: ['./line-chart.component.scss']
|
||||||
})
|
})
|
||||||
export class LineChartComponent implements OnInit, OnChanges {
|
export class LineChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
@Input() xAxis: string;
|
@Input() xAxis: string;
|
||||||
@Input() yAxis: string | string[];
|
@Input() yAxis: string | string[];
|
||||||
@Input() table: string;
|
@Input() table: string;
|
||||||
@@ -88,6 +88,11 @@ export class LineChartComponent implements OnInit, OnChanges {
|
|||||||
// Subscriptions to unsubscribe on destroy
|
// Subscriptions to unsubscribe on destroy
|
||||||
private subscriptions: Subscription[] = [];
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private dashboardService: Dashboard3Service,
|
private dashboardService: Dashboard3Service,
|
||||||
private filterService: FilterService
|
private filterService: FilterService
|
||||||
@@ -109,6 +114,12 @@ export class LineChartComponent implements OnInit, OnChanges {
|
|||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('LineChartComponent input changes:', changes);
|
console.log('LineChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -140,6 +151,318 @@ export class LineChartComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
// Clean up document click handler
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public method to refresh data when filters change
|
// Public method to refresh data when filters change
|
||||||
|
|||||||
@@ -1,19 +1,287 @@
|
|||||||
<div class="pie-chart-container">
|
<div class="pie-chart-container">
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Filter Controls Section -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<!-- Base Filters -->
|
||||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
<h4>Base Filters</h4>
|
||||||
</button>
|
<div class="filter-controls">
|
||||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
Back to Main View
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
</button>
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
<!-- Show loading indicator -->
|
<!-- Show loading indicator -->
|
||||||
<div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable">
|
<div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable && !isFetchingData">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
<p>Loading chart data...</p>
|
<p>Loading chart data...</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,15 +291,15 @@
|
|||||||
<p>No chart data available</p>
|
<p>No chart data available</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Show chart when data is available -->
|
<!-- Show chart when data is available or show default data -->
|
||||||
<canvas baseChart
|
<canvas baseChart
|
||||||
*ngIf="pieChartLabels.length > 0 && pieChartData.length > 0"
|
[datasets]="pieChartDatasets"
|
||||||
[data]="pieChartData"
|
|
||||||
[labels]="pieChartLabels"
|
[labels]="pieChartLabels"
|
||||||
[type]="pieChartType"
|
[type]="pieChartType"
|
||||||
[options]="pieChartOptions"
|
[options]="pieChartOptions"
|
||||||
(chartHover)="chartHovered($event)"
|
(chartHover)="chartHovered($event)"
|
||||||
(chartClick)="chartClicked($event)">
|
(chartClick)="chartClicked($event)"
|
||||||
|
[style.display]="shouldShowChart() ? 'block' : 'none'">
|
||||||
</canvas>
|
</canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-legend" *ngIf="showlabel && pieChartLabels && pieChartLabels.length > 0">
|
<div class="chart-legend" *ngIf="showlabel && pieChartLabels && pieChartLabels.length > 0">
|
||||||
|
|||||||
@@ -149,10 +149,192 @@
|
|||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive design */
|
// Filter section styles
|
||||||
|
.filter-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
min-width: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.filter-text-input,
|
||||||
|
.filter-select,
|
||||||
|
.filter-date {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.multiselect-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-value {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New header row styling
|
||||||
|
.header-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
text-align: left;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.pie-chart-container {
|
.pie-chart-container {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
height: auto;
|
||||||
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-title {
|
.chart-title {
|
||||||
@@ -179,4 +361,18 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
|
|||||||
templateUrl: './pie-chart.component.html',
|
templateUrl: './pie-chart.component.html',
|
||||||
styleUrls: ['./pie-chart.component.scss']
|
styleUrls: ['./pie-chart.component.scss']
|
||||||
})
|
})
|
||||||
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
|
||||||
@Input() xAxis: string;
|
@Input() xAxis: string;
|
||||||
@Input() yAxis: string | string[];
|
@Input() yAxis: string | string[];
|
||||||
@Input() table: string;
|
@Input() table: string;
|
||||||
@@ -37,6 +37,12 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
|
|
||||||
public pieChartLabels: string[] = ['Category A', 'Category B', 'Category C'];
|
public pieChartLabels: string[] = ['Category A', 'Category B', 'Category C'];
|
||||||
public pieChartData: number[] = [30, 50, 20];
|
public pieChartData: number[] = [30, 50, 20];
|
||||||
|
public pieChartDatasets: any[] = [
|
||||||
|
{
|
||||||
|
data: [30, 50, 20],
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
public pieChartType: string = 'pie';
|
public pieChartType: string = 'pie';
|
||||||
public pieChartOptions: any = {
|
public pieChartOptions: any = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
@@ -96,11 +102,16 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
noDataAvailable: boolean = false;
|
noDataAvailable: boolean = false;
|
||||||
|
|
||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
isFetchingData: boolean = false;
|
||||||
|
|
||||||
// Subscriptions to unsubscribe on destroy
|
// Subscriptions to unsubscribe on destroy
|
||||||
private subscriptions: Subscription[] = [];
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private dashboardService: Dashboard3Service,
|
private dashboardService: Dashboard3Service,
|
||||||
private filterService: FilterService
|
private filterService: FilterService
|
||||||
@@ -127,12 +138,28 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||||
// Validate initial data
|
// Validate initial data
|
||||||
this.validateChartData();
|
this.validateChartData();
|
||||||
|
// Initialize datasets with default data
|
||||||
|
this.pieChartDatasets = [
|
||||||
|
{
|
||||||
|
data: this.pieChartData,
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// Only fetch data if we have the required inputs, otherwise show default data
|
||||||
|
if (this.table && this.xAxis && this.yAxis) {
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('PieChartComponent input changes:', changes);
|
console.log('PieChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -154,10 +181,328 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
console.log('Chart configuration changed, fetching new data');
|
console.log('Chart configuration changed, fetching new data');
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have the required inputs and haven't fetched data yet, fetch it
|
||||||
|
if ((xAxisChanged || yAxisChanged || tableChanged) && this.table && this.xAxis && this.yAxis && !this.isFetchingData) {
|
||||||
|
console.log('Required inputs available, fetching data');
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
// Clean up document click handler
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public method to refresh data when filters change
|
// Public method to refresh data when filters change
|
||||||
@@ -243,7 +588,7 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
console.log('Pie chart data URL:', url);
|
console.log('Chart data URL:', url);
|
||||||
|
|
||||||
// Fetch data from the dashboard service with parameter field and value
|
// Fetch data from the dashboard service with parameter field and value
|
||||||
// For base level, we pass empty parameter and value, but now also pass filters
|
// For base level, we pass empty parameter and value, but now also pass filters
|
||||||
@@ -251,12 +596,8 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
(data: any) => {
|
(data: any) => {
|
||||||
console.log('Received pie chart data:', data);
|
console.log('Received pie chart data:', data);
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
console.warn('Pie chart API returned null data. Check if the API endpoint is working correctly.');
|
console.warn('API returned null data. Check if the API endpoint is working correctly.');
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.pieChartLabels = [];
|
|
||||||
this.pieChartData = [];
|
|
||||||
// Validate and sanitize data to show default data
|
|
||||||
this.validateChartData();
|
|
||||||
// Reset flag after fetching
|
// Reset flag after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
return;
|
return;
|
||||||
@@ -264,50 +605,57 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
|
|
||||||
// Handle the actual data structure returned by the API
|
// Handle the actual data structure returned by the API
|
||||||
if (data && data.chartLabels && data.chartData) {
|
if (data && data.chartLabels && data.chartData) {
|
||||||
// For pie charts, we need to extract the data differently
|
// Backend has already filtered the data, just display it
|
||||||
// The first dataset's data array contains the values for the pie chart
|
|
||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.pieChartLabels = data.chartLabels || [];
|
this.pieChartLabels = data.chartLabels;
|
||||||
if (data.chartData && data.chartData.length > 0) {
|
|
||||||
this.pieChartData = data.chartData[0].data.map(value => {
|
// Extract the actual data values from the chartData array
|
||||||
// Convert to number if it's not already
|
// chartData is an array with one object containing the data
|
||||||
const numValue = Number(value);
|
if (data.chartData.length > 0 && data.chartData[0].data) {
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
this.pieChartData = data.chartData[0].data;
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.pieChartData = [];
|
this.pieChartData = [];
|
||||||
}
|
}
|
||||||
// Ensure labels and data arrays have the same length
|
|
||||||
this.syncLabelAndDataArrays();
|
|
||||||
// Validate and sanitize data
|
|
||||||
this.validateChartData();
|
|
||||||
// Trigger change detection
|
// Trigger change detection
|
||||||
|
this.pieChartLabels = [...this.pieChartLabels];
|
||||||
this.pieChartData = [...this.pieChartData];
|
this.pieChartData = [...this.pieChartData];
|
||||||
|
this.pieChartDatasets = [
|
||||||
|
{
|
||||||
|
data: this.pieChartData,
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||||
} else if (data && data.labels && data.data) {
|
} else if (data && data.labels && data.datasets) {
|
||||||
// Handle the original expected format as fallback
|
// Backend has already filtered the data, just display it
|
||||||
this.noDataAvailable = data.labels.length === 0;
|
this.noDataAvailable = data.labels.length === 0;
|
||||||
this.pieChartLabels = data.labels || [];
|
this.pieChartLabels = data.labels;
|
||||||
this.pieChartData = data.data.map(value => {
|
this.pieChartData = data.datasets[0]?.data || [];
|
||||||
// Convert to number if it's not already
|
|
||||||
const numValue = Number(value);
|
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
|
||||||
});
|
|
||||||
// Ensure labels and data arrays have the same length
|
|
||||||
this.syncLabelAndDataArrays();
|
|
||||||
// Validate and sanitize data
|
|
||||||
this.validateChartData();
|
|
||||||
// Trigger change detection
|
// Trigger change detection
|
||||||
|
this.pieChartLabels = [...this.pieChartLabels];
|
||||||
this.pieChartData = [...this.pieChartData];
|
this.pieChartData = [...this.pieChartData];
|
||||||
|
this.pieChartDatasets = [
|
||||||
|
{
|
||||||
|
data: this.pieChartData,
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||||
} else {
|
} else {
|
||||||
console.warn('Pie chart received data does not have expected structure', data);
|
console.warn('Received data does not have expected structure', data);
|
||||||
// Reset to default data
|
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.pieChartLabels = [];
|
// Keep default data if no data is available
|
||||||
this.pieChartData = [];
|
if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
|
||||||
// Validate and sanitize data to show default data
|
this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
|
||||||
this.validateChartData();
|
this.pieChartData = [30, 50, 20];
|
||||||
|
this.pieChartDatasets = [
|
||||||
|
{
|
||||||
|
data: this.pieChartData,
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Reset flag after fetching
|
// Reset flag after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
@@ -315,23 +663,13 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
(error) => {
|
(error) => {
|
||||||
console.error('Error fetching pie chart data:', error);
|
console.error('Error fetching pie chart data:', error);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.pieChartLabels = [];
|
|
||||||
this.pieChartData = [];
|
|
||||||
// Validate and sanitize data to show default data
|
|
||||||
this.validateChartData();
|
|
||||||
// Reset flag after fetching
|
// Reset flag after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log('Missing required data for pie chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||||
// Don't set noDataAvailable to true when there's no required data
|
this.noDataAvailable = true;
|
||||||
// This allows static data to be displayed
|
|
||||||
this.noDataAvailable = false;
|
|
||||||
// Validate the chart data to ensure we have some data to display
|
|
||||||
this.validateChartData();
|
|
||||||
// Force a redraw to ensure the chart displays
|
|
||||||
this.pieChartData = [...this.pieChartData];
|
|
||||||
// Reset flag after fetching
|
// Reset flag after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
}
|
}
|
||||||
@@ -360,8 +698,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
} else {
|
} else {
|
||||||
console.warn('Invalid drilldown layer index:', layerIndex);
|
console.warn('Invalid drilldown layer index:', layerIndex);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.pieChartLabels = [];
|
|
||||||
this.pieChartData = [];
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,8 +708,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
|
if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
|
||||||
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
|
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.pieChartLabels = [];
|
|
||||||
this.pieChartData = [];
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,64 +804,57 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
if (data === null) {
|
if (data === null) {
|
||||||
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
|
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.pieChartLabels = [];
|
|
||||||
this.pieChartData = [];
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the actual data structure returned by the API
|
// Handle the actual data structure returned by the API
|
||||||
if (data && data.chartLabels && data.chartData) {
|
if (data && data.chartLabels && data.chartData) {
|
||||||
// For pie charts, we need to extract the data differently
|
// Backend has already filtered the data, just display it
|
||||||
// The first dataset's data array contains the values for the pie chart
|
|
||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.pieChartLabels = data.chartLabels || [];
|
this.pieChartLabels = data.chartLabels;
|
||||||
if (data.chartData && data.chartData.length > 0) {
|
|
||||||
this.pieChartData = data.chartData[0].data.map(value => {
|
// Extract the actual data values from the chartData array
|
||||||
// Convert to number if it's not already
|
// chartData is an array with one object containing the data
|
||||||
const numValue = Number(value);
|
if (data.chartData.length > 0 && data.chartData[0].data) {
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
this.pieChartData = data.chartData[0].data;
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.pieChartData = [];
|
this.pieChartData = [];
|
||||||
}
|
}
|
||||||
// Ensure labels and data arrays have the same length
|
|
||||||
this.syncLabelAndDataArrays();
|
|
||||||
// Validate and sanitize data
|
|
||||||
this.validateChartData();
|
|
||||||
// Trigger change detection
|
// Trigger change detection
|
||||||
|
this.pieChartLabels = [...this.pieChartLabels];
|
||||||
this.pieChartData = [...this.pieChartData];
|
this.pieChartData = [...this.pieChartData];
|
||||||
|
this.pieChartDatasets = [
|
||||||
|
{
|
||||||
|
data: this.pieChartData,
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||||
} else if (data && data.labels && data.data) {
|
} else if (data && data.labels && data.datasets) {
|
||||||
// Handle the original expected format as fallback
|
// Backend has already filtered the data, just display it
|
||||||
this.noDataAvailable = data.labels.length === 0;
|
this.noDataAvailable = data.labels.length === 0;
|
||||||
this.pieChartLabels = data.labels || [];
|
this.pieChartLabels = data.labels;
|
||||||
this.pieChartData = data.data.map(value => {
|
this.pieChartData = data.datasets[0]?.data || [];
|
||||||
// Convert to number if it's not already
|
|
||||||
const numValue = Number(value);
|
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
|
||||||
});
|
|
||||||
// Ensure labels and data arrays have the same length
|
|
||||||
this.syncLabelAndDataArrays();
|
|
||||||
// Validate and sanitize data
|
|
||||||
this.validateChartData();
|
|
||||||
// Trigger change detection
|
// Trigger change detection
|
||||||
|
this.pieChartLabels = [...this.pieChartLabels];
|
||||||
this.pieChartData = [...this.pieChartData];
|
this.pieChartData = [...this.pieChartData];
|
||||||
|
this.pieChartDatasets = [
|
||||||
|
{
|
||||||
|
data: this.pieChartData,
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||||
} else {
|
} else {
|
||||||
console.warn('Drilldown received data does not have expected structure', data);
|
console.warn('Drilldown received data does not have expected structure', data);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.pieChartLabels = [];
|
// Keep current data if no data is available
|
||||||
this.pieChartData = [];
|
|
||||||
// Validate and sanitize data
|
|
||||||
this.validateChartData();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('Error fetching drilldown data:', error);
|
console.error('Error fetching drilldown data:', error);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.pieChartLabels = [];
|
|
||||||
this.pieChartData = [];
|
|
||||||
// Keep current data in case of error
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -588,82 +915,52 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Validate chart data to ensure labels and data arrays have the same length
|
||||||
* Get color for legend item
|
private validateChartData(): void {
|
||||||
* @param index Index of the legend item
|
if (this.pieChartLabels && this.pieChartData) {
|
||||||
*/
|
// For pie charts, we need to ensure labels and data arrays have the same length
|
||||||
public getLegendColor(index: number): string {
|
const labelCount = this.pieChartLabels.length;
|
||||||
|
const dataCount = this.pieChartData.length;
|
||||||
|
|
||||||
|
if (labelCount !== dataCount) {
|
||||||
|
console.warn('Pie chart labels and data arrays have different lengths:', { labels: labelCount, data: dataCount });
|
||||||
|
// Pad or truncate data array to match label count
|
||||||
|
if (dataCount < labelCount) {
|
||||||
|
// Pad with zeros
|
||||||
|
while (this.pieChartData.length < labelCount) {
|
||||||
|
this.pieChartData.push(0);
|
||||||
|
}
|
||||||
|
} else if (dataCount > labelCount) {
|
||||||
|
// Truncate data array
|
||||||
|
this.pieChartData = this.pieChartData.slice(0, labelCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get legend color for a specific index
|
||||||
|
getLegendColor(index: number): string {
|
||||||
return this.chartColors[index % this.chartColors.length];
|
return this.chartColors[index % this.chartColors.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Method to determine if chart should be displayed
|
||||||
* Ensure labels and data arrays have the same length
|
shouldShowChart(): boolean {
|
||||||
*/
|
// Show chart if we have data
|
||||||
private syncLabelAndDataArrays(): void {
|
if (this.pieChartLabels.length > 0 && this.pieChartData.length > 0) {
|
||||||
// Ensure we have matching arrays
|
return true;
|
||||||
if (this.pieChartLabels.length !== this.pieChartData.length) {
|
|
||||||
const maxLength = Math.max(this.pieChartLabels.length, this.pieChartData.length);
|
|
||||||
while (this.pieChartLabels.length < maxLength) {
|
|
||||||
this.pieChartLabels.push(`Label ${this.pieChartLabels.length + 1}`);
|
|
||||||
}
|
|
||||||
while (this.pieChartData.length < maxLength) {
|
|
||||||
this.pieChartData.push(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Show chart if we're still fetching data
|
||||||
* Validate and sanitize chart data
|
if (this.isFetchingData) {
|
||||||
*/
|
return true;
|
||||||
private validateChartData(): void {
|
|
||||||
console.log('Validating chart data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
|
||||||
|
|
||||||
// Ensure we have valid arrays
|
|
||||||
if (!Array.isArray(this.pieChartLabels)) {
|
|
||||||
this.pieChartLabels = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(this.pieChartData)) {
|
// Show chart if we have default data
|
||||||
this.pieChartData = [];
|
if (this.pieChartLabels.length > 0 && this.originalPieChartLabels.length > 0) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have some data to display
|
return false;
|
||||||
if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
|
|
||||||
// Add default data to ensure chart visibility
|
|
||||||
this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
|
|
||||||
this.pieChartData = [30, 50, 20];
|
|
||||||
console.log('Added default data for chart display');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure labels and data arrays have the same length
|
|
||||||
this.syncLabelAndDataArrays();
|
|
||||||
|
|
||||||
// Ensure all data values are numbers
|
|
||||||
this.pieChartData = this.pieChartData.map(value => {
|
|
||||||
const numValue = Number(value);
|
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('After validation:', { labels: this.pieChartLabels, data: this.pieChartData });
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewChecked() {
|
|
||||||
// Debugging: Log component state after view checks
|
|
||||||
console.log('PieChartComponent state:', {
|
|
||||||
labels: this.pieChartLabels,
|
|
||||||
data: this.pieChartData,
|
|
||||||
hasData: this.pieChartLabels.length > 0 && this.pieChartData.length > 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if chart data is valid and ready to display
|
|
||||||
*/
|
|
||||||
public isChartDataValid(): boolean {
|
|
||||||
return this.pieChartLabels && this.pieChartData &&
|
|
||||||
Array.isArray(this.pieChartLabels) && Array.isArray(this.pieChartData) &&
|
|
||||||
this.pieChartLabels.length > 0 && this.pieChartData.length > 0 &&
|
|
||||||
this.pieChartLabels.length === this.pieChartData.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// events
|
// events
|
||||||
@@ -752,6 +1049,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public chartHovered(e: any): void {
|
public chartHovered(e: any): void {
|
||||||
console.log(e);
|
console.log('Pie chart hovered:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewChecked(): void {
|
||||||
|
// This lifecycle hook can be used if needed for post-render operations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,292 @@
|
|||||||
|
<div style="display: block; height: 100%; width: 100%;">
|
||||||
|
<!-- Filter Controls Section -->
|
||||||
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
|
<!-- Base Filters -->
|
||||||
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
|
<h4>Base Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
<div style="display: block">
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Polar Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="position: relative; height: calc(100% - 80px); padding: 0 10px 30px 10px;">
|
||||||
<canvas baseChart
|
<canvas baseChart
|
||||||
[datasets]="polarAreaChartData"
|
[datasets]="polarAreaChartData"
|
||||||
[labels]="polarAreaChartLabels"
|
[labels]="polarAreaChartLabels"
|
||||||
|
[options]="polarAreaChartOptions"
|
||||||
[type]="polarAreaChartType"
|
[type]="polarAreaChartType"
|
||||||
(chartHover)="chartHovered($event)"
|
(chartHover)="chartHovered($event)"
|
||||||
(chartClick)="chartClicked($event)">
|
(chartClick)="chartClicked($event)">
|
||||||
</canvas>
|
</canvas>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,18 +1,192 @@
|
|||||||
// Polar Chart Component Styles
|
.filter-section {
|
||||||
div[style*="display: block"] {
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
min-width: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.filter-text-input,
|
||||||
|
.filter-select,
|
||||||
|
.filter-date {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
.multiselect-display {
|
||||||
max-width: 100%;
|
display: flex;
|
||||||
max-height: 100%;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.multiselect-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the chart container has proper sizing
|
.multiselect-value {
|
||||||
:host {
|
color: #666;
|
||||||
display: block;
|
font-size: 12px;
|
||||||
width: 100%;
|
margin-right: 8px;
|
||||||
height: 100%;
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New header row styling
|
||||||
|
.header-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-polar-chart',
|
selector: 'app-polar-chart',
|
||||||
@@ -33,9 +35,20 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
// Multi-layer drilldown configuration inputs
|
// Multi-layer drilldown configuration inputs
|
||||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||||
|
|
||||||
constructor(private dashboardService: Dashboard3Service) { }
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchChartData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize with default data
|
// Initialize with default data
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
@@ -43,6 +56,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('PolarChartComponent input changes:', changes);
|
console.log('PolarChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -71,6 +90,32 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
{ data: [ 300, 500, 100, 40, 120 ], label: 'Series 1'}
|
{ data: [ 300, 500, 100, 40, 120 ], label: 'Series 1'}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public polarAreaChartOptions: any = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 10,
|
||||||
|
right: 10,
|
||||||
|
top: 10,
|
||||||
|
bottom: 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'top'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
r: {
|
||||||
|
ticks: {
|
||||||
|
backdropColor: 'rgba(0, 0, 0, 0)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public polarAreaChartType: string = 'polarArea';
|
public polarAreaChartType: string = 'polarArea';
|
||||||
|
|
||||||
// Multi-layer drilldown state tracking
|
// Multi-layer drilldown state tracking
|
||||||
@@ -85,6 +130,324 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
|
// Subscriptions to unsubscribe on destroy
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
fetchChartData(): void {
|
fetchChartData(): void {
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
this.isFetchingData = true;
|
this.isFetchingData = true;
|
||||||
@@ -117,7 +480,49 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
filterParams = JSON.stringify(filterObj);
|
filterParams = JSON.stringify(filterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Base filter parameters:', filterParams);
|
|
||||||
|
// Add common filters to filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
@@ -132,7 +537,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
console.warn('Polar chart API returned null data. Check if the API endpoint is working correctly.');
|
console.warn('Polar chart API returned null data. Check if the API endpoint is working correctly.');
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.polarAreaChartLabels = [];
|
this.polarAreaChartLabels = [];
|
||||||
this.polarAreaChartData = [];
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
// Reset flag after fetching
|
// Reset flag after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
return;
|
return;
|
||||||
@@ -145,32 +555,54 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.polarAreaChartLabels = data.chartLabels;
|
this.polarAreaChartLabels = data.chartLabels;
|
||||||
if (data.chartData && data.chartData.length > 0) {
|
if (data.chartData && data.chartData.length > 0) {
|
||||||
this.polarAreaChartData = data.chartData[0].data.map(value => {
|
// Convert the data to the expected format for polar area charts
|
||||||
|
const chartValues = data.chartData[0].data.map(value => {
|
||||||
// Convert to number if it's not already
|
// Convert to number if it's not already
|
||||||
return isNaN(Number(value)) ? 0 : Number(value);
|
return isNaN(Number(value)) ? 0 : Number(value);
|
||||||
});
|
});
|
||||||
} else {
|
// Assign data in the correct format (array of objects with data property)
|
||||||
this.polarAreaChartData = [];
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: chartValues,
|
||||||
|
label: data.chartData[0].label || 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
// Trigger change detection
|
|
||||||
this.polarAreaChartData = [...this.polarAreaChartData];
|
|
||||||
console.log('Updated polar chart with data:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
|
console.log('Updated polar chart with data:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
|
||||||
} else if (data && data.labels && data.data) {
|
} else if (data && data.labels && data.data) {
|
||||||
// Handle the original expected format as fallback
|
// Handle the original expected format as fallback
|
||||||
this.noDataAvailable = data.labels.length === 0;
|
this.noDataAvailable = data.labels.length === 0;
|
||||||
this.polarAreaChartLabels = data.labels;
|
this.polarAreaChartLabels = data.labels;
|
||||||
this.polarAreaChartData = data.data.map(value => {
|
// Convert the data to the expected format for polar area charts
|
||||||
|
const chartValues = data.data.map(value => {
|
||||||
// Convert to number if it's not already
|
// Convert to number if it's not already
|
||||||
return isNaN(Number(value)) ? 0 : Number(value);
|
return isNaN(Number(value)) ? 0 : Number(value);
|
||||||
});
|
});
|
||||||
// Trigger change detection
|
// Assign data in the correct format (array of objects with data property)
|
||||||
this.polarAreaChartData = [...this.polarAreaChartData];
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: chartValues,
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
console.log('Updated polar chart with legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
|
console.log('Updated polar chart with legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
|
||||||
} else {
|
} else {
|
||||||
console.warn('Polar chart received data does not have expected structure', data);
|
console.warn('Polar chart received data does not have expected structure', data);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.polarAreaChartLabels = [];
|
this.polarAreaChartLabels = [];
|
||||||
this.polarAreaChartData = [];
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
// Reset flag after fetching
|
// Reset flag after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
@@ -179,7 +611,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
console.error('Error fetching polar chart data:', error);
|
console.error('Error fetching polar chart data:', error);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.polarAreaChartLabels = [];
|
this.polarAreaChartLabels = [];
|
||||||
this.polarAreaChartData = [];
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
// Reset flag after fetching
|
// Reset flag after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
// Keep default data in case of error
|
// Keep default data in case of error
|
||||||
@@ -189,7 +626,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
console.log('Missing required data for polar chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
console.log('Missing required data for polar chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.polarAreaChartLabels = [];
|
this.polarAreaChartLabels = [];
|
||||||
this.polarAreaChartData = [];
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
// Reset flag after fetching
|
// Reset flag after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
}
|
}
|
||||||
@@ -219,7 +661,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
console.warn('Invalid drilldown layer index:', layerIndex);
|
console.warn('Invalid drilldown layer index:', layerIndex);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.polarAreaChartLabels = [];
|
this.polarAreaChartLabels = [];
|
||||||
this.polarAreaChartData = [];
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,7 +678,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
|
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.polarAreaChartLabels = [];
|
this.polarAreaChartLabels = [];
|
||||||
this.polarAreaChartData = [];
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +739,35 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add common filters to drilldown filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with drilldown filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add drilldown filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const drilldownFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters
|
||||||
|
Object.keys(commonFilters).forEach(key => {
|
||||||
|
const value = commonFilters[key];
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
mergedFilterObj[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
console.log('Drilldown data URL:', url);
|
console.log('Drilldown data URL:', url);
|
||||||
@@ -300,23 +781,40 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
|
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.polarAreaChartLabels = [];
|
this.polarAreaChartLabels = [];
|
||||||
this.polarAreaChartData = [];
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the actual data structure returned by the API
|
// Handle the actual data structure returned by the API
|
||||||
if (data && data.chartLabels && data.chartData) {
|
if (data && data.chartLabels && data.chartData) {
|
||||||
// For polar charts, we need to extract the data differently
|
// For polar charts, we need to extract the data differently
|
||||||
// The first dataset's data array contains the values for the polar chart
|
|
||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.polarAreaChartLabels = data.chartLabels;
|
this.polarAreaChartLabels = data.chartLabels;
|
||||||
if (data.chartData && data.chartData.length > 0) {
|
if (data.chartData && data.chartData.length > 0) {
|
||||||
this.polarAreaChartData = data.chartData[0].data.map(value => {
|
// Convert the data to the expected format for polar area charts
|
||||||
|
const chartValues = data.chartData[0].data.map(value => {
|
||||||
// Convert to number if it's not already
|
// Convert to number if it's not already
|
||||||
return isNaN(Number(value)) ? 0 : Number(value);
|
return isNaN(Number(value)) ? 0 : Number(value);
|
||||||
});
|
});
|
||||||
|
// Assign data in the correct format (array of objects with data property)
|
||||||
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: chartValues,
|
||||||
|
label: data.chartData[0].label || 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
this.polarAreaChartData = [];
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
// Trigger change detection
|
// Trigger change detection
|
||||||
this.polarAreaChartData = [...this.polarAreaChartData];
|
this.polarAreaChartData = [...this.polarAreaChartData];
|
||||||
@@ -325,10 +823,18 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
// Handle the original expected format as fallback
|
// Handle the original expected format as fallback
|
||||||
this.noDataAvailable = data.labels.length === 0;
|
this.noDataAvailable = data.labels.length === 0;
|
||||||
this.polarAreaChartLabels = data.labels;
|
this.polarAreaChartLabels = data.labels;
|
||||||
this.polarAreaChartData = data.data.map(value => {
|
// Convert the data to the expected format for polar area charts
|
||||||
|
const chartValues = data.data.map(value => {
|
||||||
// Convert to number if it's not already
|
// Convert to number if it's not already
|
||||||
return isNaN(Number(value)) ? 0 : Number(value);
|
return isNaN(Number(value)) ? 0 : Number(value);
|
||||||
});
|
});
|
||||||
|
// Assign data in the correct format (array of objects with data property)
|
||||||
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: chartValues,
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
// Trigger change detection
|
// Trigger change detection
|
||||||
this.polarAreaChartData = [...this.polarAreaChartData];
|
this.polarAreaChartData = [...this.polarAreaChartData];
|
||||||
console.log('Updated polar chart with drilldown legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
|
console.log('Updated polar chart with drilldown legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
|
||||||
@@ -336,14 +842,24 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
console.warn('Drilldown received data does not have expected structure', data);
|
console.warn('Drilldown received data does not have expected structure', data);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.polarAreaChartLabels = [];
|
this.polarAreaChartLabels = [];
|
||||||
this.polarAreaChartData = [];
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('Error fetching drilldown data:', error);
|
console.error('Error fetching drilldown data:', error);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.polarAreaChartLabels = [];
|
this.polarAreaChartLabels = [];
|
||||||
this.polarAreaChartData = [];
|
this.polarAreaChartData = [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
label: 'Dataset 1'
|
||||||
|
}
|
||||||
|
];
|
||||||
// Keep current data in case of error
|
// Keep current data in case of error
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -417,13 +933,13 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
// Get the label of the clicked element
|
// Get the label of the clicked element
|
||||||
const clickedLabel = this.polarAreaChartLabels[clickedIndex];
|
const clickedLabel = this.polarAreaChartLabels[clickedIndex];
|
||||||
|
|
||||||
console.log('Clicked on polar area:', { index: clickedIndex, label: clickedLabel });
|
console.log('Clicked on polar point:', { index: clickedIndex, label: clickedLabel });
|
||||||
|
|
||||||
// If we're not at the base level, store original data
|
// If we're not at the base level, store original data
|
||||||
if (this.currentDrilldownLevel === 0) {
|
if (this.currentDrilldownLevel === 0) {
|
||||||
// Store original data before entering drilldown mode
|
// Store original data before entering drilldown mode
|
||||||
this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels];
|
this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels];
|
||||||
this.originalPolarAreaChartData = [...this.polarAreaChartData];
|
this.originalPolarAreaChartData = JSON.parse(JSON.stringify(this.polarAreaChartData));
|
||||||
console.log('Stored original data for drilldown');
|
console.log('Stored original data for drilldown');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,6 +1007,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public chartHovered(e: any): void {
|
public chartHovered(e: any): void {
|
||||||
console.log(e);
|
console.log('Polar chart hovered:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
// Clean up document click handler
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,283 @@
|
|||||||
<div style="display: block">
|
<div style="display: block; height: 100%; width: 100%;">
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Filter Controls Section -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<!-- Base Filters -->
|
||||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
<h4>Base Filters</h4>
|
||||||
</button>
|
<div class="filter-controls">
|
||||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
Back to Main View
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Radar Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- No data message -->
|
<!-- No data message -->
|
||||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
||||||
@@ -16,7 +285,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chart display -->
|
<!-- Chart display -->
|
||||||
<div *ngIf="!noDataAvailable">
|
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
||||||
<canvas baseChart
|
<canvas baseChart
|
||||||
[datasets]="radarChartData"
|
[datasets]="radarChartData"
|
||||||
[labels]="radarChartLabels"
|
[labels]="radarChartLabels"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-radar-chart',
|
selector: 'app-radar-chart',
|
||||||
@@ -62,15 +64,40 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
constructor(private dashboardService: Dashboard3Service) { }
|
// Subscriptions to unsubscribe on destroy
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchChartData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('RadarChartComponent input changes:', changes);
|
console.log('RadarChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -94,6 +121,316 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
fetchChartData(): void {
|
fetchChartData(): void {
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
this.isFetchingData = true;
|
this.isFetchingData = true;
|
||||||
@@ -126,7 +463,49 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
filterParams = JSON.stringify(filterObj);
|
filterParams = JSON.stringify(filterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Base filter parameters:', filterParams);
|
|
||||||
|
// Add common filters to filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/radar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/radar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
@@ -298,6 +677,35 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add common filters to drilldown filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with drilldown filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add drilldown filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const drilldownFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters
|
||||||
|
Object.keys(commonFilters).forEach(key => {
|
||||||
|
const value = commonFilters[key];
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
mergedFilterObj[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
console.log('Drilldown data URL:', url);
|
console.log('Drilldown data URL:', url);
|
||||||
@@ -321,7 +729,6 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.radarChartLabels = data.chartLabels;
|
this.radarChartLabels = data.chartLabels;
|
||||||
// For radar charts, we need to ensure the data is properly formatted
|
// For radar charts, we need to ensure the data is properly formatted
|
||||||
// Each dataset should have a data array with numeric values
|
|
||||||
this.radarChartData = data.chartData.map(dataset => ({
|
this.radarChartData = data.chartData.map(dataset => ({
|
||||||
...dataset,
|
...dataset,
|
||||||
data: dataset.data ? dataset.data.map(value => {
|
data: dataset.data ? dataset.data.map(value => {
|
||||||
@@ -358,6 +765,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.radarChartLabels = [];
|
this.radarChartLabels = [];
|
||||||
this.radarChartData = [];
|
this.radarChartData = [];
|
||||||
|
// Keep current data in case of error
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -436,7 +844,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
if (this.currentDrilldownLevel === 0) {
|
if (this.currentDrilldownLevel === 0) {
|
||||||
// Store original data before entering drilldown mode
|
// Store original data before entering drilldown mode
|
||||||
this.originalRadarChartLabels = [...this.radarChartLabels];
|
this.originalRadarChartLabels = [...this.radarChartLabels];
|
||||||
this.originalRadarChartData = [...this.radarChartData];
|
this.originalRadarChartData = JSON.parse(JSON.stringify(this.radarChartData));
|
||||||
console.log('Stored original data for drilldown');
|
console.log('Stored original data for drilldown');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,6 +912,12 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public chartHovered(e: any): void {
|
public chartHovered(e: any): void {
|
||||||
console.log(e);
|
console.log('Radar chart hovered:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
// Clean up document click handler
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,283 @@
|
|||||||
<div style="display: block">
|
<div style="display: block; height: 100%; width: 100%;">
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Filter Controls Section -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<!-- Base Filters -->
|
||||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
<h4>Base Filters</h4>
|
||||||
</button>
|
<div class="filter-controls">
|
||||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
Back to Main View
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Scatter Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- No data message -->
|
<!-- No data message -->
|
||||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
||||||
@@ -16,9 +285,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chart display -->
|
<!-- Chart display -->
|
||||||
<div *ngIf="!noDataAvailable">
|
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 70px); min-height: 300px;">
|
||||||
<canvas baseChart
|
<canvas baseChart
|
||||||
[datasets]="scatterChartData"
|
[datasets]="scatterChartData"
|
||||||
|
[options]="scatterChartOptions"
|
||||||
[type]="scatterChartType"
|
[type]="scatterChartType"
|
||||||
(chartHover)="chartHovered($event)"
|
(chartHover)="chartHovered($event)"
|
||||||
(chartClick)="chartClicked($event)">
|
(chartClick)="chartClicked($event)">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
import { ChartData,ChartDataset } from 'chart.js';
|
import { ChartData,ChartDataset,ChartOptions } from 'chart.js';
|
||||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-scatter-chart',
|
selector: 'app-scatter-chart',
|
||||||
@@ -34,9 +36,20 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
// Multi-layer drilldown configuration inputs
|
// Multi-layer drilldown configuration inputs
|
||||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||||
|
|
||||||
constructor(private dashboardService: Dashboard3Service) { }
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchChartData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize with default data
|
// Initialize with default data
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
@@ -44,6 +57,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('ScatterChartComponent input changes:', changes);
|
console.log('ScatterChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -94,6 +113,89 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public scatterChartOptions: any = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'linear',
|
||||||
|
position: 'bottom',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'X Axis'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: 10,
|
||||||
|
callback: function(value: any) {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
// Format large numbers for better readability
|
||||||
|
if (Math.abs(value) >= 1000000) {
|
||||||
|
return (value / 1000000).toFixed(1) + 'M';
|
||||||
|
} else if (Math.abs(value) >= 1000) {
|
||||||
|
return (value / 1000).toFixed(1) + 'K';
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
color: 'rgba(0, 0, 0, 0.1)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Y Axis'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: 10,
|
||||||
|
callback: function(value: any) {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
// Format large numbers for better readability
|
||||||
|
if (Math.abs(value) >= 1000000) {
|
||||||
|
return (value / 1000000).toFixed(1) + 'M';
|
||||||
|
} else if (Math.abs(value) >= 1000) {
|
||||||
|
return (value / 1000).toFixed(1) + 'K';
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
color: 'rgba(0, 0, 0, 0.1)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'top',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context: any) {
|
||||||
|
return `(${context.parsed.x}, ${context.parsed.y})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 15,
|
||||||
|
right: 15,
|
||||||
|
top: 15,
|
||||||
|
bottom: 60 // Add padding at the bottom to ensure X-axis visibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public scatterChartType: string = 'scatter';
|
public scatterChartType: string = 'scatter';
|
||||||
|
|
||||||
// Multi-layer drilldown state tracking
|
// Multi-layer drilldown state tracking
|
||||||
@@ -107,6 +209,417 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
|
// Subscriptions to unsubscribe on destroy
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform data to scatter chart format
|
||||||
|
private transformToScatterData(labels: any[], data: any[]): ChartDataset[] {
|
||||||
|
// For scatter charts, we need to transform the data into scatter format
|
||||||
|
// Scatter charts expect data in the format: {x: number, y: number}
|
||||||
|
console.log('Transforming data to scatter format:', { labels, data });
|
||||||
|
|
||||||
|
// If we have the expected scatter data format, return it as is
|
||||||
|
if (data && data.length > 0 && data[0].data && data[0].data.length > 0 &&
|
||||||
|
typeof data[0].data[0] === 'object' && data[0].data[0].hasOwnProperty('x') &&
|
||||||
|
data[0].data[0].hasOwnProperty('y')) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform the data properly for scatter chart
|
||||||
|
// Assuming labels are x-values and data[0].data are y-values
|
||||||
|
if (labels && data && data.length > 0 && data[0].data) {
|
||||||
|
const yValues = data[0].data;
|
||||||
|
const label = data[0].label || 'Dataset 1';
|
||||||
|
|
||||||
|
// Create scatter points from labels (x) and data (y)
|
||||||
|
const scatterPoints = [];
|
||||||
|
const minLength = Math.min(labels.length, yValues.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < minLength; i++) {
|
||||||
|
// Convert to numbers if they're strings
|
||||||
|
const x = typeof labels[i] === 'string' ? parseFloat(labels[i]) : labels[i];
|
||||||
|
const y = typeof yValues[i] === 'string' ? parseFloat(yValues[i]) : yValues[i];
|
||||||
|
|
||||||
|
// Only add valid points
|
||||||
|
if (!isNaN(x) && !isNaN(y)) {
|
||||||
|
scatterPoints.push({ x, y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate different colors for each point to avoid all points showing the same color
|
||||||
|
const backgroundColors = [];
|
||||||
|
const borderColors = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < scatterPoints.length; i++) {
|
||||||
|
// Generate a color based on the point index for variety
|
||||||
|
const hue = (i * 137.508) % 360; // Use golden angle to spread colors
|
||||||
|
backgroundColors.push(`hsla(${hue}, 70%, 50%, 0.6)`);
|
||||||
|
borderColors.push(`hsla(${hue}, 70%, 40%, 1)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a single dataset with all scatter points
|
||||||
|
const scatterDatasets: ChartDataset[] = [
|
||||||
|
{
|
||||||
|
data: scatterPoints,
|
||||||
|
label: label,
|
||||||
|
pointRadius: 8,
|
||||||
|
pointHoverRadius: 10,
|
||||||
|
backgroundColor: backgroundColors,
|
||||||
|
borderColor: borderColors,
|
||||||
|
borderWidth: 1,
|
||||||
|
pointHoverBackgroundColor: 'rgba(255, 99, 132, 1)',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('Transformed scatter data:', scatterDatasets);
|
||||||
|
return scatterDatasets;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, create a default scatter dataset
|
||||||
|
const scatterDatasets: ChartDataset[] = [
|
||||||
|
{
|
||||||
|
data: [
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
{ x: 2, y: 3 },
|
||||||
|
{ x: 3, y: -2 },
|
||||||
|
{ x: 4, y: 4 },
|
||||||
|
{ x: 5, y: -3 },
|
||||||
|
],
|
||||||
|
label: 'Dataset 1',
|
||||||
|
pointRadius: 10,
|
||||||
|
backgroundColor: [
|
||||||
|
'red',
|
||||||
|
'green',
|
||||||
|
'blue',
|
||||||
|
'purple',
|
||||||
|
'yellow',
|
||||||
|
'brown',
|
||||||
|
'magenta',
|
||||||
|
'cyan',
|
||||||
|
'orange',
|
||||||
|
'pink'
|
||||||
|
],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return scatterDatasets;
|
||||||
|
}
|
||||||
|
|
||||||
fetchChartData(): void {
|
fetchChartData(): void {
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
this.isFetchingData = true;
|
this.isFetchingData = true;
|
||||||
@@ -139,7 +652,49 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
filterParams = JSON.stringify(filterObj);
|
filterParams = JSON.stringify(filterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Base filter parameters:', filterParams);
|
|
||||||
|
// Add common filters to filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/scatter?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/scatter?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
@@ -165,11 +720,19 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
// Scatter charts expect data in the format: {x: number, y: number}
|
// Scatter charts expect data in the format: {x: number, y: number}
|
||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
|
this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
|
||||||
|
|
||||||
|
// Update chart options with axis titles
|
||||||
|
this.updateChartOptionsWithAxisTitles();
|
||||||
|
|
||||||
console.log('Updated scatter chart with data:', this.scatterChartData);
|
console.log('Updated scatter chart with data:', this.scatterChartData);
|
||||||
} else if (data && data.labels && data.datasets) {
|
} else if (data && data.labels && data.datasets) {
|
||||||
// Handle the original expected format as fallback
|
// Handle the original expected format as fallback
|
||||||
this.noDataAvailable = data.labels.length === 0;
|
this.noDataAvailable = data.labels.length === 0;
|
||||||
this.scatterChartData = data.datasets;
|
this.scatterChartData = data.datasets;
|
||||||
|
|
||||||
|
// Update chart options with axis titles
|
||||||
|
this.updateChartOptionsWithAxisTitles();
|
||||||
|
|
||||||
console.log('Updated scatter chart with legacy data format:', this.scatterChartData);
|
console.log('Updated scatter chart with legacy data format:', this.scatterChartData);
|
||||||
} else {
|
} else {
|
||||||
console.warn('Scatter chart received data does not have expected structure', data);
|
console.warn('Scatter chart received data does not have expected structure', data);
|
||||||
@@ -197,6 +760,20 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update chart options with axis titles
|
||||||
|
private updateChartOptionsWithAxisTitles(): void {
|
||||||
|
// Update X axis title
|
||||||
|
if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.x) {
|
||||||
|
this.scatterChartOptions.scales.x.title.text = this.xAxis || 'X Axis';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Y axis title
|
||||||
|
if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.y) {
|
||||||
|
const yAxisLabel = Array.isArray(this.yAxis) ? this.yAxis[0] : this.yAxis;
|
||||||
|
this.scatterChartOptions.scales.y.title.text = yAxisLabel || 'Y Axis';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch drilldown data based on current drilldown level
|
// Fetch drilldown data based on current drilldown level
|
||||||
fetchDrilldownData(): void {
|
fetchDrilldownData(): void {
|
||||||
console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
|
console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
|
||||||
@@ -287,24 +864,34 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert drilldownFilters to filter parameters for drilldown level
|
// Add common filters to drilldown filter parameters
|
||||||
let drilldownFilterParams = '';
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
const filterObj = {};
|
// Merge common filters with drilldown filters
|
||||||
this.drilldownFilters.forEach(filter => {
|
const mergedFilterObj = {};
|
||||||
if (filter.field && filter.value) {
|
|
||||||
filterObj[filter.field] = filter.value;
|
// Add drilldown filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const drilldownFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters
|
||||||
|
Object.keys(commonFilters).forEach(key => {
|
||||||
|
const value = commonFilters[key];
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
mergedFilterObj[key] = value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (Object.keys(filterObj).length > 0) {
|
|
||||||
drilldownFilterParams = JSON.stringify(filterObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('Drilldown filter parameters:', drilldownFilterParams);
|
|
||||||
|
|
||||||
// Use drilldown filters if available, otherwise use layer filters
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
const finalFilterParams = drilldownFilterParams || filterParams;
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
console.log('Final filter parameters:', finalFilterParams);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
@@ -312,7 +899,7 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
// Fetch data from the dashboard service with parameter field and value
|
// Fetch data from the dashboard service with parameter field and value
|
||||||
// Backend handles filtering, we just pass the parameter field and value
|
// Backend handles filtering, we just pass the parameter field and value
|
||||||
this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, finalFilterParams).subscribe(
|
this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
console.log('Received drilldown data:', data);
|
console.log('Received drilldown data:', data);
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
@@ -325,7 +912,6 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
// Handle the actual data structure returned by the API
|
// Handle the actual data structure returned by the API
|
||||||
if (data && data.chartLabels && data.chartData) {
|
if (data && data.chartLabels && data.chartData) {
|
||||||
// For scatter charts, we need to transform the data into scatter format
|
// For scatter charts, we need to transform the data into scatter format
|
||||||
// Scatter charts expect data in the format: {x: number, y: number}
|
|
||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
|
this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
|
||||||
console.log('Updated scatter chart with drilldown data:', this.scatterChartData);
|
console.log('Updated scatter chart with drilldown data:', this.scatterChartData);
|
||||||
@@ -349,33 +935,6 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform chart data to scatter chart format
|
|
||||||
private transformToScatterData(labels: string[], datasets: any[]): ChartDataset[] {
|
|
||||||
// For scatter charts, we need to transform the data into scatter format
|
|
||||||
// Scatter charts expect data in the format: {x: number, y: number}
|
|
||||||
|
|
||||||
// This is a simple transformation - in a real implementation, you might want to
|
|
||||||
// create a more sophisticated mapping based on your data structure
|
|
||||||
return datasets.map((dataset, index) => {
|
|
||||||
// Create scatter data points
|
|
||||||
const scatterData = labels.map((label, i) => {
|
|
||||||
// Use x-axis data as x coordinate, y-axis data as y coordinate
|
|
||||||
const xValue = dataset.data[i] || 0;
|
|
||||||
const yValue = i < datasets.length ? (datasets[i].data[index] || 0) : 0;
|
|
||||||
|
|
||||||
return { x: xValue, y: yValue };
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: scatterData,
|
|
||||||
label: dataset.label || `Dataset ${index + 1}`,
|
|
||||||
backgroundColor: dataset.backgroundColor || `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.6)`,
|
|
||||||
borderColor: dataset.borderColor || 'rgba(0, 0, 0, 1)',
|
|
||||||
pointRadius: dataset.pointRadius || 5
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset to original data (go back to base level)
|
// Reset to original data (go back to base level)
|
||||||
resetToOriginalData(): void {
|
resetToOriginalData(): void {
|
||||||
console.log('Resetting to original data');
|
console.log('Resetting to original data');
|
||||||
@@ -436,16 +995,18 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
// Get the index of the clicked element
|
// Get the index of the clicked element
|
||||||
const clickedIndex = e.active[0].index;
|
const clickedIndex = e.active[0].index;
|
||||||
|
|
||||||
// Get the label of the clicked element
|
// Get the dataset index
|
||||||
// For scatter charts, we might not have labels in the same way as other charts
|
const datasetIndex = e.active[0].datasetIndex;
|
||||||
const clickedLabel = `Point ${clickedIndex}`;
|
|
||||||
|
|
||||||
console.log('Clicked on scatter point:', { index: clickedIndex, label: clickedLabel });
|
// Get the data point
|
||||||
|
const dataPoint = this.scatterChartData[datasetIndex].data[clickedIndex];
|
||||||
|
|
||||||
|
console.log('Clicked on scatter point:', { datasetIndex, clickedIndex, dataPoint });
|
||||||
|
|
||||||
// If we're not at the base level, store original data
|
// If we're not at the base level, store original data
|
||||||
if (this.currentDrilldownLevel === 0) {
|
if (this.currentDrilldownLevel === 0) {
|
||||||
// Store original data before entering drilldown mode
|
// Store original data before entering drilldown mode
|
||||||
this.originalScatterChartData = [...this.scatterChartData];
|
this.originalScatterChartData = JSON.parse(JSON.stringify(this.scatterChartData));
|
||||||
console.log('Stored original data for drilldown');
|
console.log('Stored original data for drilldown');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,9 +1048,10 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
// Add this click to the drilldown stack
|
// Add this click to the drilldown stack
|
||||||
const stackEntry = {
|
const stackEntry = {
|
||||||
level: nextDrilldownLevel,
|
level: nextDrilldownLevel,
|
||||||
|
datasetIndex: datasetIndex,
|
||||||
clickedIndex: clickedIndex,
|
clickedIndex: clickedIndex,
|
||||||
clickedLabel: clickedLabel,
|
dataPoint: dataPoint,
|
||||||
clickedValue: clickedLabel // Using label as value for now
|
clickedValue: dataPoint // Using data point as value for now
|
||||||
};
|
};
|
||||||
|
|
||||||
this.drilldownStack.push(stackEntry);
|
this.drilldownStack.push(stackEntry);
|
||||||
@@ -513,6 +1075,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public chartHovered(e: any): void {
|
public chartHovered(e: any): void {
|
||||||
console.log(e);
|
console.log('Scatter chart hovered:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
// Clean up document click handler
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,287 @@
|
|||||||
<table class="table">
|
<div class="to-do-chart-container">
|
||||||
|
<!-- Filter Controls Section -->
|
||||||
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
|
<!-- Base Filters -->
|
||||||
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
|
<h4>Base Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'To Do Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Existing content -->
|
||||||
|
<div class="todo-table-container">
|
||||||
|
<table class="table todo-table">
|
||||||
<thead>
|
<thead>
|
||||||
<th class="c-col">#</th>
|
<th class="c-col">#</th>
|
||||||
<th>Item</th>
|
<th>Item</th>
|
||||||
@@ -8,20 +291,17 @@
|
|||||||
<td class="c-col">{{i + 1}}</td>
|
<td class="c-col">{{i + 1}}</td>
|
||||||
<td>{{todo}}</td>
|
<td>{{todo}}</td>
|
||||||
<td style="text-align:right">
|
<td style="text-align:right">
|
||||||
<a routerLink="." (click)="removeTodo(i)">
|
<a (click)="removeTodo(i)" class="remove-button">
|
||||||
<clr-icon shape="times"></clr-icon>
|
<clr-icon shape="times"></clr-icon>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<input [(ngModel)]="todo" placeholder="Add Todo" class="clr-input">
|
|
||||||
</td>
|
|
||||||
<td style="text-align:right">
|
|
||||||
<a routerLink="." color='primary' (click)="addTodo(todo)">
|
|
||||||
<clr-icon shape="plus"></clr-icon>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="add-todo-section">
|
||||||
|
<input [(ngModel)]="todo" (keyup.enter)="addTodo(todo)" placeholder="Add Todo" class="clr-input todo-input">
|
||||||
|
<button (click)="addTodo(todo)" class="btn btn-primary add-button">
|
||||||
|
<clr-icon shape="plus"></clr-icon> Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
|
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-to-do-chart',
|
selector: 'app-to-do-chart',
|
||||||
@@ -21,15 +24,49 @@ export class ToDoChartComponent implements OnInit, OnChanges {
|
|||||||
@Input() datasource: string;
|
@Input() datasource: string;
|
||||||
@Input() fieldName: string;
|
@Input() fieldName: string;
|
||||||
@Input() connection: number; // Add connection input
|
@Input() connection: number; // Add connection input
|
||||||
|
// Drilldown configuration inputs
|
||||||
|
@Input() drilldownEnabled: boolean = false;
|
||||||
|
@Input() drilldownApiUrl: string;
|
||||||
|
@Input() drilldownXAxis: string;
|
||||||
|
@Input() drilldownYAxis: string;
|
||||||
|
@Input() drilldownParameter: string; // Add drilldown parameter input
|
||||||
|
@Input() baseFilters: any[] = []; // Add base filters input
|
||||||
|
@Input() drilldownFilters: any[] = []; // Add drilldown filters input
|
||||||
|
// Multi-layer drilldown configuration inputs
|
||||||
|
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||||
|
|
||||||
constructor() { }
|
// Multi-layer drilldown state tracking
|
||||||
|
drilldownStack: any[] = []; // Stack to track drilldown navigation history
|
||||||
|
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
|
||||||
|
originalTodoList: string[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchToDoData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize with default data
|
||||||
|
this.fetchToDoData();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('ToDoChartComponent input changes:', changes);
|
console.log('ToDoChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -46,26 +83,119 @@ export class ToDoChartComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
data: any;
|
data: any;
|
||||||
todo: string;
|
todo: string;
|
||||||
todoList = ['todo 1'];
|
todoList: string[] = [];
|
||||||
|
|
||||||
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
|
// Subscriptions to unsubscribe on destroy
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
fetchToDoData(): void {
|
fetchToDoData(): void {
|
||||||
// If we have the necessary data, fetch to-do data from the service
|
// If we have the necessary data, fetch to-do data from the service
|
||||||
if (this.table) {
|
if (this.table && this.xAxis) {
|
||||||
console.log('Fetching to-do data for:', { table: this.table });
|
console.log('Fetching to-do data for:', { table: this.table, xAxis: this.xAxis, connection: this.connection });
|
||||||
|
|
||||||
// For to-do chart, we might want to fetch data differently
|
// Convert baseFilters to filter parameters
|
||||||
// This is a placeholder implementation - you may need to adjust based on your API
|
let filterParams = '';
|
||||||
console.log('To-do chart would fetch data from table:', this.table);
|
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||||
|
const filterObj = {};
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.field && filter.value) {
|
||||||
|
filterObj[filter.field] = filter.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (Object.keys(filterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(filterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// In a real implementation, you would connect to your service here
|
// Add common filters to filter parameters
|
||||||
// For now, we'll just keep the default to-do list
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Missing required data for to-do chart:', { table: this.table });
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
|
|
||||||
|
// Fetch data from the dashboard service
|
||||||
|
this.dashboardService.getChartData(this.table, 'todo', this.xAxis, '', this.connection, '', '', filterParams).subscribe(
|
||||||
|
(data: any) => {
|
||||||
|
console.log('Received to-do chart data:', data);
|
||||||
|
if (data === null) {
|
||||||
|
console.warn('To-do chart API returned null data. Check if the API endpoint is working correctly.');
|
||||||
|
this.todoList = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the actual data structure returned by the API
|
||||||
|
if (data && data.chartLabels) {
|
||||||
|
// Use chartLabels as the todo items
|
||||||
|
this.todoList = data.chartLabels;
|
||||||
|
} else if (data && data.labels) {
|
||||||
|
// Fallback to labels if chartLabels is not available
|
||||||
|
this.todoList = data.labels;
|
||||||
|
} else {
|
||||||
|
console.warn('To-do chart received data does not have expected structure', data);
|
||||||
|
this.todoList = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error fetching to-do chart data:', error);
|
||||||
|
this.todoList = [];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('Missing required data for to-do chart:', { table: this.table, xAxis: this.xAxis });
|
||||||
|
// Initialize with default data if no table is specified
|
||||||
|
if (this.todoList.length === 0) {
|
||||||
|
this.todoList = ['Sample Task 1', 'Sample Task 2', 'Sample Task 3'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addTodo(todo: string) {
|
public addTodo(todo: string) {
|
||||||
this.todoList.push(todo);
|
if (todo && todo.trim() !== '') {
|
||||||
|
this.todoList.push(todo.trim());
|
||||||
|
this.todo = ''; // Clear the input field
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeTodo(todoIx: number) {
|
public removeTodo(todoIx: number) {
|
||||||
@@ -73,4 +203,353 @@ export class ToDoChartComponent implements OnInit, OnChanges {
|
|||||||
this.todoList.splice(todoIx, 1);
|
this.todoList.splice(todoIx, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigate back to previous drilldown level
|
||||||
|
navigateBack(): void {
|
||||||
|
console.log('Navigating back, current stack:', this.drilldownStack);
|
||||||
|
console.log('Current level:', this.currentDrilldownLevel);
|
||||||
|
|
||||||
|
if (this.drilldownStack.length > 0) {
|
||||||
|
// Remove the last entry from the stack
|
||||||
|
const removedEntry = this.drilldownStack.pop();
|
||||||
|
console.log('Removed entry from stack:', removedEntry);
|
||||||
|
|
||||||
|
// Update the current drilldown level
|
||||||
|
this.currentDrilldownLevel = this.drilldownStack.length;
|
||||||
|
console.log('New level after pop:', this.currentDrilldownLevel);
|
||||||
|
console.log('Stack after pop:', this.drilldownStack);
|
||||||
|
|
||||||
|
if (this.drilldownStack.length > 0) {
|
||||||
|
// Fetch data for the previous level
|
||||||
|
console.log('Fetching data for previous level');
|
||||||
|
this.fetchToDoData();
|
||||||
|
} else {
|
||||||
|
// Back to base level
|
||||||
|
console.log('Back to base level, resetting to original data');
|
||||||
|
this.todoList = [...this.originalTodoList];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Already at base level, reset to original data
|
||||||
|
console.log('Already at base level, resetting to original data');
|
||||||
|
this.todoList = [...this.originalTodoList];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchToDoData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchToDoData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchToDoData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchToDoData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchToDoData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchToDoData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchToDoData();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||||
|
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
|
||||||
|
// Remove document click handler if it exists
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './unified-chart.component';
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -51,8 +51,7 @@ export class DashrunnerallComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getdashboard()
|
getdashboard() {
|
||||||
{
|
|
||||||
this.dashboardService.getAllDash().subscribe((data) => {
|
this.dashboardService.getAllDash().subscribe((data) => {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.rows = this.data;
|
this.rows = this.data;
|
||||||
@@ -62,17 +61,23 @@ export class DashrunnerallComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
openModal()
|
openModal() {
|
||||||
{
|
|
||||||
this.addModall = true;
|
this.addModall = true;
|
||||||
}
|
}
|
||||||
gotoadd()
|
gotoadd() {
|
||||||
{
|
|
||||||
this.router.navigate(['../../dashboardbuilder'], { relativeTo: this.route });
|
this.router.navigate(['../../dashboardbuilder'], { relativeTo: this.route });
|
||||||
}
|
}
|
||||||
goToEdit(id:number)
|
// for runner line navigation
|
||||||
{
|
// goToEditData(id: number){
|
||||||
this.router.navigate(['../dashrunner/'+id],{relativeTo:this.route});
|
// this.router.navigate(['../editdata/'+id],{relativeTo:this.route});
|
||||||
|
// }
|
||||||
|
goToEdit(id: number) {
|
||||||
|
// Navigate to editnewdash component instead of dashrunnerline
|
||||||
|
// Pass a query parameter to indicate this is from dashboard runner
|
||||||
|
this.router.navigate(['../../dashboardbuilder/editdashn/' + id], {
|
||||||
|
relativeTo: this.route,
|
||||||
|
queryParams: { fromRunner: true }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
goToEditData(id: number) {
|
goToEditData(id: number) {
|
||||||
@@ -96,8 +101,7 @@ export class DashrunnerallComponent implements OnInit {
|
|||||||
console.log(this.rowSelected);
|
console.log(this.rowSelected);
|
||||||
this.modalDelete = true;
|
this.modalDelete = true;
|
||||||
}
|
}
|
||||||
delete(id)
|
delete(id) {
|
||||||
{
|
|
||||||
this.modalDelete = false;
|
this.modalDelete = false;
|
||||||
console.log("in delete " + id);
|
console.log("in delete " + id);
|
||||||
this.dashboardService.deleteField(id).subscribe((data) => {
|
this.dashboardService.deleteField(id).subscribe((data) => {
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export class ReportbuildqueryComponent implements OnInit {
|
|||||||
name;
|
name;
|
||||||
databaseName;
|
databaseName;
|
||||||
databasename(val) {
|
databasename(val) {
|
||||||
console.log(val);
|
console.log('connection ', val);
|
||||||
this.databaseName = val.name;
|
this.databaseName = val.name;
|
||||||
this.selecteddatabase = val.conn_string;
|
this.selecteddatabase = val.conn_string;
|
||||||
console.log(this.selecteddatabase);
|
console.log(this.selecteddatabase);
|
||||||
|
|||||||
@@ -23,6 +23,18 @@
|
|||||||
<label for="workflow_name">{{'ACTIVE'| translate}}</label>
|
<label for="workflow_name">{{'ACTIVE'| translate}}</label>
|
||||||
<input type="checkbox" formControlName="active" clrToggle value="billable" name="billable" />
|
<input type="checkbox" formControlName="active" clrToggle value="billable" name="billable" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SureConnect Dropdown -->
|
||||||
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
|
<label for="sureConnectId">SureConnect Connection</label>
|
||||||
|
<select formControlName="sureConnectId" class="clr-select">
|
||||||
|
<option value="">-- Select SureConnect --</option>
|
||||||
|
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||||
|
{{conn.connection_name || conn.id}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<div class="clr-col-md-4 clr-col-sm-12">
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
<label for="url">Get URL</label>
|
<label for="url">Get URL</label>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { FormBuilder, FormGroup } from '@angular/forms';
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { ReportBuilderService } from 'src/app/services/api/report-builder.service';
|
import { ReportBuilderService } from 'src/app/services/api/report-builder.service';
|
||||||
|
import { SureconnectService } from '../../dashboardnew/sureconnect/sureconnect.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-report-build2add',
|
selector: 'app-report-build2add',
|
||||||
templateUrl: './report-build2add.component.html',
|
templateUrl: './report-build2add.component.html',
|
||||||
@@ -11,8 +13,12 @@ import { ReportBuilderService } from 'src/app/services/api/report-builder.servic
|
|||||||
})
|
})
|
||||||
export class ReportBuild2addComponent implements OnInit {
|
export class ReportBuild2addComponent implements OnInit {
|
||||||
public entryForm: FormGroup;
|
public entryForm: FormGroup;
|
||||||
|
// Add sureconnect data property
|
||||||
|
sureconnectData: any[] = [];
|
||||||
|
|
||||||
constructor(private _fb: FormBuilder, private router: Router,private toastr: ToastrService,
|
constructor(private _fb: FormBuilder, private router: Router,private toastr: ToastrService,
|
||||||
private route: ActivatedRoute,private reportBuilderService: ReportBuilderService) { }
|
private route: ActivatedRoute,private reportBuilderService: ReportBuilderService,
|
||||||
|
private sureconnectService: SureconnectService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.entryForm = this._fb.group({
|
this.entryForm = this._fb.group({
|
||||||
@@ -20,9 +26,23 @@ export class ReportBuild2addComponent implements OnInit {
|
|||||||
description:[null],
|
description:[null],
|
||||||
active:[null],
|
active:[null],
|
||||||
isSql:[false],
|
isSql:[false],
|
||||||
|
// Add sureConnectId field to the form
|
||||||
|
sureConnectId: [null],
|
||||||
Rpt_builder2_lines: this._fb.array([this.initLinesFormReport()]),
|
Rpt_builder2_lines: this._fb.array([this.initLinesFormReport()]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load sureconnect data
|
||||||
|
this.loadSureconnectData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to load sureconnect data
|
||||||
|
loadSureconnectData() {
|
||||||
|
this.sureconnectService.getAll().subscribe((data: any[]) => {
|
||||||
|
this.sureconnectData = data;
|
||||||
|
console.log('Sureconnect data loaded:', this.sureconnectData);
|
||||||
|
}, (error) => {
|
||||||
|
console.log('Error loading sureconnect data:', error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initLinesFormReport() {
|
initLinesFormReport() {
|
||||||
|
|||||||
@@ -22,37 +22,54 @@
|
|||||||
|
|
||||||
<clr-datagrid [clrDgLoading]="loading">
|
<clr-datagrid [clrDgLoading]="loading">
|
||||||
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{'LOADING' | translate}}</clr-spinner></ng-template>
|
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{'LOADING' | translate}}</clr-spinner></ng-template>
|
||||||
<div *ngIf="error;else loadingSpinner">{{error}}</div></clr-dg-placeholder>
|
<div *ngIf="error;else loadingSpinner">{{error}}</div>
|
||||||
|
</clr-dg-placeholder>
|
||||||
|
|
||||||
|
|
||||||
<clr-dg-column [clrDgField]="''"><ng-container *clrDgHideableColumn="{hidden: false}">
|
<clr-dg-column [clrDgField]="''"><ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
{{'GO_TO' | translate}}
|
{{'GO_TO' | translate}}
|
||||||
</ng-container></clr-dg-column>
|
</ng-container></clr-dg-column>
|
||||||
|
|
||||||
<clr-dg-column [clrDgField]="'name'"><ng-container *clrDgHideableColumn="{hidden: false}">
|
<clr-dg-column [clrDgField]="'name'"><ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
{{'REPORT_NAME' | translate}}
|
{{'REPORT_NAME' | translate}}
|
||||||
</ng-container></clr-dg-column>
|
</ng-container></clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'description'"><ng-container *clrDgHideableColumn="{hidden: false}">
|
<clr-dg-column [clrDgField]="'description'"><ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
{{'REPORT_DESCRIPTION' | translate}}
|
{{'REPORT_DESCRIPTION' | translate}}
|
||||||
</ng-container></clr-dg-column>
|
</ng-container></clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-column [clrDgField]="''"><ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
|
Sureconnect
|
||||||
|
</ng-container></clr-dg-column>
|
||||||
|
|
||||||
<clr-dg-column [clrDgField]="'active'"><ng-container *clrDgHideableColumn="{hidden: false}">
|
<clr-dg-column [clrDgField]="'active'"><ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
{{'ACTIVE' | translate}}
|
{{'ACTIVE' | translate}}
|
||||||
</ng-container></clr-dg-column>
|
</ng-container></clr-dg-column>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<clr-dg-column><ng-container *clrDgHideableColumn="{hidden: false}">
|
<clr-dg-column><ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
<clr-icon shape="bars"></clr-icon>{{'ACTION' | translate}}
|
<clr-icon shape="bars"></clr-icon>{{'ACTION' | translate}}
|
||||||
</ng-container></clr-dg-column>
|
</ng-container></clr-dg-column>
|
||||||
|
|
||||||
<clr-dg-row *clrDgItems="let user of gridData?.slice()?.reverse();" [clrDgItem]="user">
|
<clr-dg-row *clrDgItems="let user of gridData?.slice()?.reverse();" [clrDgItem]="user">
|
||||||
<clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;" (click)="goToLines(user)"> {{'SET_UP' | translate}}</span></clr-dg-cell>
|
<clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;"
|
||||||
|
(click)="goToLines(user)"> {{'SET_UP' | translate}}</span></clr-dg-cell>
|
||||||
<clr-dg-cell id="word">{{user.reportName}}</clr-dg-cell>
|
<clr-dg-cell id="word">{{user.reportName}}</clr-dg-cell>
|
||||||
<clr-dg-cell id="word">{{user.description}}</clr-dg-cell>
|
<clr-dg-cell id="word">{{user.description}}</clr-dg-cell>
|
||||||
|
|
||||||
|
<clr-dg-cell id="word">{{user.sureconnect_name}}</clr-dg-cell>
|
||||||
<clr-dg-cell id="word">{{user.active}}</clr-dg-cell>
|
<clr-dg-cell id="word">{{user.active}}</clr-dg-cell>
|
||||||
<clr-dg-cell>
|
<clr-dg-cell>
|
||||||
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true" class="tooltip tooltip-sm tooltip-top-left">
|
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true"
|
||||||
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error" style="color: red;"></clr-icon></span>
|
class="tooltip tooltip-sm tooltip-top-left">
|
||||||
|
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error"
|
||||||
|
style="color: red;"></clr-icon></span>
|
||||||
<span class="tooltip-content"> {{'DELETE' | translate}}</span>
|
<span class="tooltip-content"> {{'DELETE' | translate}}</span>
|
||||||
</a>
|
</a>
|
||||||
<clr-signpost>
|
<clr-signpost>
|
||||||
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span>
|
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success"
|
||||||
|
style="color: rgb(0, 130, 236);"></clr-icon></span>
|
||||||
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
||||||
<h5 style="margin-top: 0">{{'WHO_COLUMN' | translate}}</h5>
|
<h5 style="margin-top: 0">{{'WHO_COLUMN' | translate}}</h5>
|
||||||
<div>{{'ACCOUNT_ID' | translate}}: <code class="clr-code">{{user.accountId}}</code></div>
|
<div>{{'ACCOUNT_ID' | translate}}: <code class="clr-code">{{user.accountId}}</code></div>
|
||||||
|
|||||||
@@ -26,20 +26,35 @@
|
|||||||
</div> -->
|
</div> -->
|
||||||
<div class="clr-col-md-4 clr-col-sm-12">
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
<label for="url">Get URL</label>
|
<label for="url">Get URL</label>
|
||||||
<div> <input type="text" class="clr-input" formControlName="url" name="url" [(ngModel)]="nodeEditProperties.url" placeholder="Enter Url" style="width: 76%"> <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%"> <span><button
|
||||||
|
class="btn btn-icon btn-primary" (click)="getkeys()">
|
||||||
<clr-icon shape="view-list"></clr-icon>
|
<clr-icon shape="view-list"></clr-icon>
|
||||||
</button></span></div>
|
</button></span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="clr-col-md-4 clr-col-sm-12">
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
<label for="workflow_name">Include Date filter</label>
|
<label for="workflow_name">Include Date filter</label>
|
||||||
<input type="checkbox" formControlName="date_param_req" name="date_param_req" [(ngModel)]="nodeEditProperties.date_param_req" clrToggle />
|
<input type="checkbox" formControlName="date_param_req" name="date_param_req"
|
||||||
|
[(ngModel)]="nodeEditProperties.date_param_req" clrToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SureConnect Dropdown -->
|
||||||
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
|
<label for="sureConnectId">SureConnect Connection</label>
|
||||||
|
<select formControlName="sureConnectId" class="clr-select">
|
||||||
|
<option value="">-- Select SureConnect --</option>
|
||||||
|
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||||
|
{{conn.name || conn.id}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="clr-col-md-4 clr-col-sm-12">
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
<label>Standard Parameters</label>
|
<label>Standard Parameters</label>
|
||||||
<clr-combobox-container style="margin-top: 0; padding-top: 0;">
|
<clr-combobox-container style="margin-top: 0; padding-top: 0;">
|
||||||
<!-- <label style="padding-bottom: 5px; padding-top:0px; font-weight: lighter;" class="p1">Select Left Side Filter</label> -->
|
<!-- <label style="padding-bottom: 5px; padding-top:0px; font-weight: lighter;" class="p1">Select Left Side Filter</label> -->
|
||||||
<clr-combobox formControlName="std_param_html" name="std_param_html" [(ngModel)]="nodeEditProperties.std_param_html" clrMulti="true"
|
<clr-combobox formControlName="std_param_html" name="std_param_html"
|
||||||
required>
|
[(ngModel)]="nodeEditProperties.std_param_html" clrMulti="true" required>
|
||||||
<ng-container *clrOptionSelected="let selected">
|
<ng-container *clrOptionSelected="let selected">
|
||||||
{{selected}}
|
{{selected}}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -92,4 +107,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@ import { FormBuilder, FormGroup } from '@angular/forms';
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { ReportBuilderService } from 'src/app/services/api/report-builder.service';
|
import { ReportBuilderService } from 'src/app/services/api/report-builder.service';
|
||||||
|
import { SureconnectService } from '../../dashboardnew/sureconnect/sureconnect.service';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -23,11 +24,17 @@ export class ReportBuild2editComponent implements OnInit {
|
|||||||
date_param_req: '',
|
date_param_req: '',
|
||||||
// folderName:'',
|
// folderName:'',
|
||||||
url: '',
|
url: '',
|
||||||
|
// Add sureConnectId property
|
||||||
|
sureConnectId: null,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
// Add sureconnect data property
|
||||||
|
sureconnectData: any[] = [];
|
||||||
|
|
||||||
constructor(private router: Router,
|
constructor(private router: Router,
|
||||||
private route: ActivatedRoute, private reportBuilderService: ReportBuilderService,
|
private route: ActivatedRoute, private reportBuilderService: ReportBuilderService,
|
||||||
private toastr: ToastrService, private _fb: FormBuilder) { }
|
private toastr: ToastrService, private _fb: FormBuilder,
|
||||||
|
private sureconnectService: SureconnectService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.id = this.route.snapshot.params["id"];
|
this.id = this.route.snapshot.params["id"];
|
||||||
@@ -41,11 +48,26 @@ export class ReportBuild2editComponent implements OnInit {
|
|||||||
date_param_req: [null],
|
date_param_req: [null],
|
||||||
// folderName:[null],
|
// folderName:[null],
|
||||||
url: [null],
|
url: [null],
|
||||||
|
// Add sureConnectId to form
|
||||||
|
sureConnectId: [null],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load sureconnect data first, then load report data
|
||||||
|
this.loadSureconnectData();
|
||||||
this.getById(this.id);
|
this.getById(this.id);
|
||||||
this.listoddatabase();
|
this.listoddatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add method to load sureconnect data
|
||||||
|
loadSureconnectData() {
|
||||||
|
this.sureconnectService.getAll().subscribe((data: any[]) => {
|
||||||
|
this.sureconnectData = data;
|
||||||
|
console.log('Sureconnect data loaded:', this.sureconnectData);
|
||||||
|
}, (error) => {
|
||||||
|
console.log('Error loading sureconnect data:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
databaselist;
|
databaselist;
|
||||||
listoddatabase() {
|
listoddatabase() {
|
||||||
this.reportBuilderService.getdatabse().subscribe((data) => {
|
this.reportBuilderService.getdatabse().subscribe((data) => {
|
||||||
@@ -71,8 +93,7 @@ export class ReportBuild2editComponent implements OnInit {
|
|||||||
this.builderLine = this.ReportData.rpt_builder2_lines;
|
this.builderLine = this.ReportData.rpt_builder2_lines;
|
||||||
this.lineId = this.builderLine[0].id
|
this.lineId = this.builderLine[0].id
|
||||||
console.log("line data ", this.lineId, this.builderLine);
|
console.log("line data ", this.lineId, this.builderLine);
|
||||||
if(this.builderLine[0].model != '')
|
if (this.builderLine[0].model != '') {
|
||||||
{
|
|
||||||
this.builderLineData = JSON.parse(this.builderLine[0].model);
|
this.builderLineData = JSON.parse(this.builderLine[0].model);
|
||||||
console.log(this.builderLineData);
|
console.log(this.builderLineData);
|
||||||
|
|
||||||
@@ -82,6 +103,11 @@ export class ReportBuild2editComponent implements OnInit {
|
|||||||
// this.nodeEditProperties.conn_name = this.builderLineData.conn_name;
|
// this.nodeEditProperties.conn_name = this.builderLineData.conn_name;
|
||||||
this.nodeEditProperties.date_param_req = this.builderLineData[0].date_param_req;
|
this.nodeEditProperties.date_param_req = this.builderLineData[0].date_param_req;
|
||||||
this.nodeEditProperties.url = this.builderLineData[0].url;
|
this.nodeEditProperties.url = this.builderLineData[0].url;
|
||||||
|
// Set sureConnectId if it exists in the data
|
||||||
|
this.nodeEditProperties.sureConnectId = this.builderLineData[0].sureConnectId || null;
|
||||||
|
|
||||||
|
// Update form with loaded data
|
||||||
|
this.entryForm.patchValue(this.nodeEditProperties);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
@@ -95,7 +121,7 @@ export class ReportBuild2editComponent implements OnInit {
|
|||||||
getkeys() {
|
getkeys() {
|
||||||
if (this.nodeEditProperties.url !== null) {
|
if (this.nodeEditProperties.url !== null) {
|
||||||
this.reportBuilderService.getcolumnDetailsByurl(this.nodeEditProperties.url).subscribe(data => {
|
this.reportBuilderService.getcolumnDetailsByurl(this.nodeEditProperties.url).subscribe(data => {
|
||||||
console.log(data);
|
console.log('coloum list data ', data);
|
||||||
this.keysfromurl = data;
|
this.keysfromurl = data;
|
||||||
this.nodeEditProperties.adhoc_param_html = this.keysfromurl;
|
this.nodeEditProperties.adhoc_param_html = this.keysfromurl;
|
||||||
})
|
})
|
||||||
@@ -116,6 +142,8 @@ export class ReportBuild2editComponent implements OnInit {
|
|||||||
adhoc_param_html: this.nodeEditProperties.adhoc_param_html,
|
adhoc_param_html: this.nodeEditProperties.adhoc_param_html,
|
||||||
date_param_req: this.nodeEditProperties.date_param_req,
|
date_param_req: this.nodeEditProperties.date_param_req,
|
||||||
url: this.nodeEditProperties.url,
|
url: this.nodeEditProperties.url,
|
||||||
|
// Add sureConnectId to the data
|
||||||
|
sureConnectId: this.nodeEditProperties.sureConnectId,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.builderLineData[0].std_param_html = this.nodeEditProperties.std_param_html;
|
this.builderLineData[0].std_param_html = this.nodeEditProperties.std_param_html;
|
||||||
@@ -124,6 +152,9 @@ export class ReportBuild2editComponent implements OnInit {
|
|||||||
// this.builderLineData.conn_name = this.nodeEditProperties.conn_name ;
|
// this.builderLineData.conn_name = this.nodeEditProperties.conn_name ;
|
||||||
this.builderLineData[0].date_param_req = this.nodeEditProperties.date_param_req;
|
this.builderLineData[0].date_param_req = this.nodeEditProperties.date_param_req;
|
||||||
this.builderLineData[0].url = this.nodeEditProperties.url;
|
this.builderLineData[0].url = this.nodeEditProperties.url;
|
||||||
|
// Add sureConnectId to the data
|
||||||
|
this.builderLineData[0].sureConnectId = this.nodeEditProperties.sureConnectId;
|
||||||
|
|
||||||
console.log(this.builderLineData);
|
console.log(this.builderLineData);
|
||||||
// this.builderLineData.splice(1);
|
// this.builderLineData.splice(1);
|
||||||
console.log(this.builderLineData);
|
console.log(this.builderLineData);
|
||||||
@@ -149,6 +180,8 @@ console.log(this.listBuilder_Lines);
|
|||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.updated = true;
|
this.updated = true;
|
||||||
|
// Update nodeEditProperties with form values including sureConnectId
|
||||||
|
Object.assign(this.nodeEditProperties, this.entryForm.value);
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,8 +104,11 @@ todayDate;
|
|||||||
adhocList:any[];
|
adhocList:any[];
|
||||||
SQLQuery;
|
SQLQuery;
|
||||||
getUrl;
|
getUrl;
|
||||||
|
// Add sureConnectId property
|
||||||
|
sureConnectId: number | null = null;
|
||||||
stdParamfields;
|
stdParamfields;
|
||||||
DateParam;
|
DateParam;
|
||||||
|
|
||||||
getById(id: number) {
|
getById(id: number) {
|
||||||
this.reportBuilderService.getrbDetailsById(id).subscribe(
|
this.reportBuilderService.getrbDetailsById(id).subscribe(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -120,6 +123,8 @@ todayDate;
|
|||||||
// this.adhocList = JSON.parse(adhocList);
|
// this.adhocList = JSON.parse(adhocList);
|
||||||
this.DateParam = this.builderLineData.date_param_req;
|
this.DateParam = this.builderLineData.date_param_req;
|
||||||
this.getUrl = this.builderLineData.url;
|
this.getUrl = this.builderLineData.url;
|
||||||
|
// Get sureConnectId if it exists
|
||||||
|
this.sureConnectId = this.builderLineData.sureConnectId || null;
|
||||||
console.log(this.adhocList,this.DateParam,this.getUrl)
|
console.log(this.adhocList,this.DateParam,this.getUrl)
|
||||||
this.getStdParam(this.header_id);
|
this.getStdParam(this.header_id);
|
||||||
this.featchData();
|
this.featchData();
|
||||||
@@ -127,14 +132,50 @@ todayDate;
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data using the URL and SureConnect connection if available
|
||||||
|
*/
|
||||||
featchData(){
|
featchData(){
|
||||||
|
// If sureConnectId is available, we might want to modify the request
|
||||||
|
// For now, we'll use the existing implementation but with better error handling
|
||||||
this.reportBuilderService.getAllDetailsByurl(this.getUrl).subscribe(data =>{
|
this.reportBuilderService.getAllDetailsByurl(this.getUrl).subscribe(data =>{
|
||||||
console.log(data);
|
console.log(data);
|
||||||
if(data.body){
|
if(data.body){
|
||||||
|
// Check if the response is XML (starts with <) or JSON
|
||||||
|
if (typeof data.body === 'string' && data.body.trim().startsWith('<')) {
|
||||||
|
// Handle XML response
|
||||||
|
console.log('Received XML response, parsing to JSON');
|
||||||
|
try {
|
||||||
|
this.rows = this.parseXMLToJSON(data.body);
|
||||||
|
this.filterRows = [...this.rows]; // Create a copy
|
||||||
|
console.log('Parsed XML data:', this.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing XML:', error);
|
||||||
|
this.rows = [];
|
||||||
|
this.filterRows = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle JSON response
|
||||||
|
try {
|
||||||
console.log(JSON.parse(data.body));
|
console.log(JSON.parse(data.body));
|
||||||
this.rows = JSON.parse(data.body);
|
this.rows = JSON.parse(data.body);
|
||||||
this.filterRows = JSON.parse(data.body);
|
this.filterRows = JSON.parse(data.body);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing JSON:', error);
|
||||||
|
this.rows = [];
|
||||||
|
this.filterRows = [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no body, initialize with empty arrays
|
||||||
|
this.rows = [];
|
||||||
|
this.filterRows = [];
|
||||||
|
}
|
||||||
|
}, error => {
|
||||||
|
// Handle HTTP errors
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
|
this.rows = [];
|
||||||
|
this.filterRows = [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,13 +636,10 @@ this.rowdata= [this.rows];
|
|||||||
var j;
|
var j;
|
||||||
var cart = [];
|
var cart = [];
|
||||||
|
|
||||||
for(var i = 0; i < data.length; i++)
|
for (var i = 0; i < data.length; i++) {
|
||||||
{
|
|
||||||
var columnsIn = data[i];
|
var columnsIn = data[i];
|
||||||
if(i==1)
|
if (i == 1) {
|
||||||
{
|
for (var key in columnsIn) {
|
||||||
for(var key in columnsIn)
|
|
||||||
{
|
|
||||||
j = { prop: key, name: key };
|
j = { prop: key, name: key };
|
||||||
cart.push(j)
|
cart.push(j)
|
||||||
|
|
||||||
@@ -702,6 +740,14 @@ selectcolumn(data: any) {
|
|||||||
|
|
||||||
filtered = false;
|
filtered = false;
|
||||||
filterRowsBySelectedValues() {
|
filterRowsBySelectedValues() {
|
||||||
|
// Check if rows is defined and iterable
|
||||||
|
if (!this.rows || !Array.isArray(this.rows)) {
|
||||||
|
console.warn('Rows is not defined or not an array');
|
||||||
|
this.filterRows = [];
|
||||||
|
this.filtered = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create a filteredRows array to store the filtered data
|
// Create a filteredRows array to store the filtered data
|
||||||
const filteredRows = [];
|
const filteredRows = [];
|
||||||
|
|
||||||
@@ -828,4 +874,81 @@ isDate(value: any): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple XML parser to convert XML to JSON array
|
||||||
|
* Handles the specific format: <User><id>101</id><name>John Doe</name><email>john.doe@example.com</email></User>
|
||||||
|
* @param xmlString The XML string to parse
|
||||||
|
* @returns Array of objects representing the XML data
|
||||||
|
*/
|
||||||
|
private parseXMLToJSON(xmlString: string): any[] {
|
||||||
|
// Remove any XML declaration
|
||||||
|
xmlString = xmlString.replace(/<\?xml[^>]*\?>/g, '');
|
||||||
|
|
||||||
|
// Extract the root element name (e.g., "User" from <User>...</User>)
|
||||||
|
const rootMatch = xmlString.match(/<(\w+)(?:\s[^>]*)?>/);
|
||||||
|
if (!rootMatch) {
|
||||||
|
console.warn('Could not identify root element in XML');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootElement = rootMatch[1];
|
||||||
|
const results: any[] = [];
|
||||||
|
|
||||||
|
// Create a regex to match all instances of the root element
|
||||||
|
const regex = new RegExp(`<${rootElement}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${rootElement}>`, 'g');
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(xmlString)) !== null) {
|
||||||
|
const elementContent = match[1];
|
||||||
|
const item: any = {};
|
||||||
|
|
||||||
|
// Extract all tags and their values
|
||||||
|
const tagRegex = /<(\w+)>(.*?)<\/\1>/g;
|
||||||
|
let tagMatch;
|
||||||
|
|
||||||
|
while ((tagMatch = tagRegex.exec(elementContent)) !== null) {
|
||||||
|
const tagName = tagMatch[1];
|
||||||
|
const tagValue = tagMatch[2];
|
||||||
|
// Try to convert to appropriate type
|
||||||
|
item[tagName] = this.convertValueType(tagValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert string values to appropriate types (number, boolean, etc.)
|
||||||
|
* @param value The string value to convert
|
||||||
|
* @returns The value converted to the appropriate type
|
||||||
|
*/
|
||||||
|
private convertValueType(value: string): any {
|
||||||
|
// Check for empty value
|
||||||
|
if (value === '') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for boolean values
|
||||||
|
if (value.toLowerCase() === 'true') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (value.toLowerCase() === 'false') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for numeric values
|
||||||
|
if (!isNaN(Number(value)) && !isNaN(parseFloat(value))) {
|
||||||
|
// Check if it's an integer or float
|
||||||
|
if (Number.isInteger(parseFloat(value))) {
|
||||||
|
return parseInt(value, 10);
|
||||||
|
} else {
|
||||||
|
return parseFloat(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as string if no other type matches
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -39,6 +39,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a href="javascript://" class="nav-link nav-icon modern-nav-icon" routerLinkActive="active"
|
||||||
|
routerLink="/cns-portal/shield-dashboard">
|
||||||
|
<div class="nav-icon-wrapper">
|
||||||
|
<clr-icon shape="shield" solid></clr-icon>
|
||||||
|
<span class="nav-tooltip">Shield Dashboard</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
<a href="javascript://" class="nav-link nav-icon modern-nav-icon" routerLinkActive="active"
|
<a href="javascript://" class="nav-link nav-icon modern-nav-icon" routerLinkActive="active"
|
||||||
routerLink="/cns-portal/rerunner/all" (click)="getName()">
|
routerLink="/cns-portal/rerunner/all" (click)="getName()">
|
||||||
<div class="nav-icon-wrapper">
|
<div class="nav-icon-wrapper">
|
||||||
@@ -122,27 +130,27 @@
|
|||||||
<a href="javascript://" clrDropdownItem (click)="switchLanguage('en')" class="modern-lang-item">
|
<a href="javascript://" clrDropdownItem (click)="switchLanguage('en')" class="modern-lang-item">
|
||||||
<clr-icon shape="globe" class="lang-icon"></clr-icon>
|
<clr-icon shape="globe" class="lang-icon"></clr-icon>
|
||||||
<span>English</span>
|
<span>English</span>
|
||||||
<div class="lang-flag">ðºð¸</div>
|
<div class="lang-flag">🇺🇸</div>
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript://" clrDropdownItem (click)="switchLanguage('hi')" class="modern-lang-item">
|
<a href="javascript://" clrDropdownItem (click)="switchLanguage('hi')" class="modern-lang-item">
|
||||||
<clr-icon shape="globe" class="lang-icon"></clr-icon>
|
<clr-icon shape="globe" class="lang-icon"></clr-icon>
|
||||||
<span>हिनà¥à¤¦à¥</span>
|
<span>हिन्दी</span>
|
||||||
<div class="lang-flag">ð®ð³</div>
|
<div class="lang-flag">🇮🇳</div>
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript://" clrDropdownItem (click)="switchLanguage('ta')" class="modern-lang-item">
|
<a href="javascript://" clrDropdownItem (click)="switchLanguage('ta')" class="modern-lang-item">
|
||||||
<clr-icon shape="globe" class="lang-icon"></clr-icon>
|
<clr-icon shape="globe" class="lang-icon"></clr-icon>
|
||||||
<span>தமிழà¯</span>
|
<span>தமிழ்</span>
|
||||||
<div class="lang-flag">ð®ð³</div>
|
<div class="lang-flag">🇮🇳</div>
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript://" clrDropdownItem (click)="switchLanguage('pa')" class="modern-lang-item">
|
<a href="javascript://" clrDropdownItem (click)="switchLanguage('pa')" class="modern-lang-item">
|
||||||
<clr-icon shape="globe" class="lang-icon"></clr-icon>
|
<clr-icon shape="globe" class="lang-icon"></clr-icon>
|
||||||
<span>ਪੰà¨à¨¾à¨¬à©</span>
|
<span>ਪੰਜਾਬੀ</span>
|
||||||
<div class="lang-flag">ð®ð³</div>
|
<div class="lang-flag">🇮🇳</div>
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript://" clrDropdownItem (click)="switchLanguage('ml')" class="modern-lang-item">
|
<a href="javascript://" clrDropdownItem (click)="switchLanguage('ml')" class="modern-lang-item">
|
||||||
<clr-icon shape="globe" class="lang-icon"></clr-icon>
|
<clr-icon shape="globe" class="lang-icon"></clr-icon>
|
||||||
<span>മലയാളà´</span>
|
<span>മലയാളം</span>
|
||||||
<div class="lang-flag">ð®ð³</div>
|
<div class="lang-flag">🇮🇳</div>
|
||||||
</a>
|
</a>
|
||||||
</clr-dropdown-menu>
|
</clr-dropdown-menu>
|
||||||
</clr-dropdown>
|
</clr-dropdown>
|
||||||
|
|||||||
@@ -1,3 +1,26 @@
|
|||||||
|
import { Ad10Component } from './BuilderComponents/angulardatatype/Ad10/Ad10.component';
|
||||||
|
|
||||||
|
import { DefatestComponent } from './BuilderComponents/defu/Defatest/Defatest.component';
|
||||||
|
|
||||||
|
|
||||||
|
import { ChildformComponent } from './BuilderComponents/stpkg/Childform/Childform.component';
|
||||||
|
import { DistrictComponent } from './BuilderComponents/testdata/District/District.component';
|
||||||
|
import { StateComponent } from './BuilderComponents/testdata/State/State.component';
|
||||||
|
import { CountryComponent } from './BuilderComponents/testdata/Country/Country.component';
|
||||||
|
import { Ad9Component } from './BuilderComponents/angulardatatype/Ad9/Ad9.component';
|
||||||
|
import { Ad8Component } from './BuilderComponents/angulardatatype/Ad8/Ad8.component';
|
||||||
|
import { Ad7Component } from './BuilderComponents/angulardatatype/Ad7/Ad7.component';
|
||||||
|
import { Ad6Component } from './BuilderComponents/angulardatatype/Ad6/Ad6.component';
|
||||||
|
import { Adv5Component } from './BuilderComponents/angulardatatype/Adv5/Adv5.component';
|
||||||
|
import { Adv4Component } from './BuilderComponents/angulardatatype/Adv4/Adv4.component';
|
||||||
|
import { SupportComponent } from './BuilderComponents/angulardatatype/Support/Support.component';
|
||||||
|
import { Adv3Component } from './BuilderComponents/angulardatatype/Adv3/Adv3.component';
|
||||||
|
import { Dv2Component } from './BuilderComponents/angulardatatype/Dv2/Dv2.component';
|
||||||
|
import { Adv1Component } from './BuilderComponents/angulardatatype/Adv1/Adv1.component';
|
||||||
|
import { Basicp3Component } from './BuilderComponents/angulardatatype/Basicp3/Basicp3.component';
|
||||||
|
import { Basicp2Component } from './BuilderComponents/angulardatatype/Basicp2/Basicp2.component';
|
||||||
|
import { Basicp1Component } from './BuilderComponents/angulardatatype/Basicp1/Basicp1.component';
|
||||||
|
|
||||||
|
|
||||||
import { SequencegenaratorComponent } from './fnd/sequencegenarator/sequencegenarator.component';
|
import { SequencegenaratorComponent } from './fnd/sequencegenarator/sequencegenarator.component';
|
||||||
import { Component, NgModule } from '@angular/core';
|
import { Component, NgModule } from '@angular/core';
|
||||||
@@ -82,6 +105,17 @@ import { MappingruleallComponent } from './datamanagement/mappingrule/mappingrul
|
|||||||
import { MappingruleaddComponent } from './datamanagement/mappingrule/mappingruleadd/mappingruleadd.component';
|
import { MappingruleaddComponent } from './datamanagement/mappingrule/mappingruleadd/mappingruleadd.component';
|
||||||
import { MappingruleeditComponent } from './datamanagement/mappingrule/mappingruleedit/mappingruleedit.component';
|
import { MappingruleeditComponent } from './datamanagement/mappingrule/mappingruleedit/mappingruleedit.component';
|
||||||
import { Stepper_workflowComponent } from './BuilderComponents/stepperworkflow/Stepper_workflow/Stepper_workflow.component';
|
import { Stepper_workflowComponent } from './BuilderComponents/stepperworkflow/Stepper_workflow/Stepper_workflow.component';
|
||||||
|
import { AllapiregisteryComponent } from './fnd/apiregistery/allapiregistery/allapiregistery.component';
|
||||||
|
import { AddapiregisteryComponent } from './fnd/apiregistery/addapiregistery/addapiregistery.component';
|
||||||
|
import { EditapiregisteryComponent } from './fnd/apiregistery/editapiregistery/editapiregistery.component';
|
||||||
|
import { ApiregisterylineComponent } from './fnd/apiregistery/Apiregisteryline/Apiregisteryline.component';
|
||||||
|
import { Customer_informationComponent } from './BuilderComponents/angulardatatype/Customer_information/Customer_information.component';
|
||||||
|
import { Deployment_typeComponent } from './BuilderComponents/angulardatatype/Deployment_type/Deployment_type.component';
|
||||||
|
import { ManufacturerComponent } from './BuilderComponents/angulardatatype/Manufacturer/Manufacturer.component';
|
||||||
|
import { Order_summaryComponent } from './BuilderComponents/angulardatatype/Order_summary/Order_summary.component';
|
||||||
|
import { ProductComponent } from './BuilderComponents/angulardatatype/Product/Product.component';
|
||||||
|
import { TypesComponent } from './BuilderComponents/angulardatatype/Types/Types.component';
|
||||||
|
import { Test2Component } from './BuilderComponents/testdata/Test2/Test2.component';
|
||||||
import { Token_registeryComponent } from './fnd/Token_registery/Token_registery.component';
|
import { Token_registeryComponent } from './fnd/Token_registery/Token_registery.component';
|
||||||
import { MyworkspaceComponent } from './admin/myworkspace/myworkspace.component';
|
import { MyworkspaceComponent } from './admin/myworkspace/myworkspace.component';
|
||||||
import { ThemeCustomizationComponent } from './theme-customization/theme-customization.component';
|
import { ThemeCustomizationComponent } from './theme-customization/theme-customization.component';
|
||||||
@@ -89,9 +123,9 @@ import { Data_lakeComponent } from './builder/dashboardnew/Data_lake/Data_lake.c
|
|||||||
import { SureconnectComponent } from './builder/dashboardnew/sureconnect/sureconnect.component';
|
import { SureconnectComponent } from './builder/dashboardnew/sureconnect/sureconnect.component';
|
||||||
import { EditsureconnectComponent } from './builder/dashboardnew/sureconnect/editsureconnect/editsureconnect.component';
|
import { EditsureconnectComponent } from './builder/dashboardnew/sureconnect/editsureconnect/editsureconnect.component';
|
||||||
import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.component';
|
import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.component';
|
||||||
// import { QueryComponent } from './superadmin/query/query.component';
|
import { QueryComponent } from './superadmin/query/query.component';
|
||||||
// import { QueryaddComponent } from './superadmin/queryadd/queryadd.component';
|
import { QueryaddComponent } from './superadmin/queryadd/queryadd.component';
|
||||||
// import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
|
import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -132,6 +166,8 @@ const routes: Routes = [
|
|||||||
{ path: 'editconnect/:id', component: EditsureconnectComponent },
|
{ path: 'editconnect/:id', component: EditsureconnectComponent },
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'reportbuild', component: ReportBuildComponent,
|
path: 'reportbuild', component: ReportBuildComponent,
|
||||||
children: [
|
children: [
|
||||||
@@ -143,9 +179,9 @@ const routes: Routes = [
|
|||||||
|
|
||||||
|
|
||||||
//SUPER ADMIN
|
//SUPER ADMIN
|
||||||
// { path: 'query', component: QueryComponent, canActivate: [AuthGuard], data: { roles: [Role.Admin] } },
|
{ path: 'query', component: QueryComponent, canActivate: [AuthGuard], data: { roles: [Role.Admin] } },
|
||||||
// { path: 'reportQuery/:id/queryadd', component: QueryaddComponent, canActivate: [AuthGuard], data: { roles: [Role.Admin] } },
|
{ path: 'reportQuery/:id/queryadd', component: QueryaddComponent, canActivate: [AuthGuard], data: { roles: [Role.Admin] } },
|
||||||
// { path: 'reportQuery/queryedit/:id', component: QueryeditComponent, canActivate: [AuthGuard], data: { roles: [Role.Admin] } },
|
{ path: 'reportQuery/queryedit/:id', component: QueryeditComponent, canActivate: [AuthGuard], data: { roles: [Role.Admin] } },
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -189,6 +225,12 @@ const routes: Routes = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Shield Dashboard
|
||||||
|
{
|
||||||
|
path: 'shield-dashboard',
|
||||||
|
loadChildren: () => import('./builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard-routing.module').then(m => m.ShieldDashboardRoutingModule)
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'dashboardrunner', component: DashboardrunnerComponent,
|
path: 'dashboardrunner', component: DashboardrunnerComponent,
|
||||||
children: [
|
children: [
|
||||||
@@ -236,8 +278,20 @@ const routes: Routes = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: 'SequenceGenerator', component: SequencegenaratorComponent },
|
{ path: 'SequenceGenerator', component: SequencegenaratorComponent },
|
||||||
{ path: 'apiregistery', component: ApiregisteryComponent },
|
|
||||||
|
|
||||||
|
// Api registery
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'apiregistery', component: ApiregisteryComponent,
|
||||||
|
children: [
|
||||||
|
{ path: '', redirectTo: 'all', pathMatch: 'full' },
|
||||||
|
{ path: 'all', component: AllapiregisteryComponent },
|
||||||
|
{ path: 'add', component: AddapiregisteryComponent },
|
||||||
|
{ path: 'edit/:id', component: EditapiregisteryComponent },
|
||||||
|
{ path: 'line/:id', component: ApiregisterylineComponent },
|
||||||
|
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
// DATA MANAGEMENT
|
// DATA MANAGEMENT
|
||||||
|
|
||||||
@@ -270,18 +324,186 @@ const routes: Routes = [
|
|||||||
|
|
||||||
// buildercomponents
|
// buildercomponents
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Country', component: CountryComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Adv3', component: Adv3Component },
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Ad10', component: Ad10Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Childform', component: ChildformComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'District', component: DistrictComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'State', component: StateComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Country', component: CountryComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Ad9', component: Ad9Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Ad8', component: Ad8Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Ad7', component: Ad7Component },
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Ad6', component: Ad6Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Adv5', component: Adv5Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Support', component: SupportComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Adv3', component: Adv3Component },
|
||||||
|
|
||||||
|
|
||||||
{ path: 'tokenregistery', component: Token_registeryComponent },
|
{ path: 'tokenregistery', component: Token_registeryComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Defatest', component: DefatestComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Country', component: CountryComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Defatest', component: DefatestComponent },
|
||||||
|
|
||||||
|
{ path: 'Test2', component: Test2Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Country', component: CountryComponent },
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Test2', component: Test2Component },
|
||||||
|
|
||||||
|
{ path: 'Childform', component: ChildformComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'District', component: DistrictComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'State', component: StateComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Country', component: CountryComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Ad9', component: Ad9Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Ad8', component: Ad8Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Ad7', component: Ad7Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Ad6', component: Ad6Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Adv5', component: Adv5Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Adv4', component: Adv4Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Support', component: SupportComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Adv3', component: Adv3Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Dv2', component: Dv2Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Adv1', component: Adv1Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Basicp3', component: Basicp3Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Basicp2', component: Basicp2Component },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Basicp1', component: Basicp1Component },
|
||||||
|
{ path: 'cust', component: Customer_informationComponent },
|
||||||
|
|
||||||
|
{ path: 'Order_summary', component: Order_summaryComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Types', component: TypesComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Product', component: ProductComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Manufacturer', component: ManufacturerComponent },
|
||||||
|
|
||||||
|
|
||||||
|
{ path: 'Deployment_type', component: Deployment_typeComponent },
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{ path: 'Stepper_workflow', component: Stepper_workflowComponent },
|
{ path: 'Stepper_workflow', component: Stepper_workflowComponent },
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{ path: '**', component: PageNotFoundComponent },
|
{ path: '**', component: PageNotFoundComponent },
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
|
import { Ad10Component } from './BuilderComponents/angulardatatype/Ad10/Ad10.component';
|
||||||
|
|
||||||
|
import { DefatestComponent } from './BuilderComponents/defu/Defatest/Defatest.component';
|
||||||
|
|
||||||
|
import { ChildformComponent } from './BuilderComponents/stpkg/Childform/Childform.component';
|
||||||
|
import { DistrictComponent } from './BuilderComponents/testdata/District/District.component';
|
||||||
|
import { StateComponent } from './BuilderComponents/testdata/State/State.component';
|
||||||
|
import { CountryComponent } from './BuilderComponents/testdata/Country/Country.component';
|
||||||
|
import { Ad9Component } from './BuilderComponents/angulardatatype/Ad9/Ad9.component';
|
||||||
|
import { Ad8Component } from './BuilderComponents/angulardatatype/Ad8/Ad8.component';
|
||||||
|
import { Ad7Component } from './BuilderComponents/angulardatatype/Ad7/Ad7.component';
|
||||||
|
import { Ad6Component } from './BuilderComponents/angulardatatype/Ad6/Ad6.component';
|
||||||
|
import { Adv5Component } from './BuilderComponents/angulardatatype/Adv5/Adv5.component';
|
||||||
|
import { Adv4Component } from './BuilderComponents/angulardatatype/Adv4/Adv4.component';
|
||||||
|
import { SupportComponent } from './BuilderComponents/angulardatatype/Support/Support.component';
|
||||||
|
import { Adv3Component } from './BuilderComponents/angulardatatype/Adv3/Adv3.component';
|
||||||
|
import { Dv2Component } from './BuilderComponents/angulardatatype/Dv2/Dv2.component';
|
||||||
|
import { Adv1Component } from './BuilderComponents/angulardatatype/Adv1/Adv1.component';
|
||||||
|
import { Basicp3Component } from './BuilderComponents/angulardatatype/Basicp3/Basicp3.component';
|
||||||
|
import { Basicp2Component } from './BuilderComponents/angulardatatype/Basicp2/Basicp2.component';
|
||||||
|
import { Basicp1Component } from './BuilderComponents/angulardatatype/Basicp1/Basicp1.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
@@ -74,6 +96,9 @@ import { RadarChartComponent } from './builder/dashboardnew/gadgets/radar-chart/
|
|||||||
import { ScatterChartComponent } from './builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component';
|
import { ScatterChartComponent } from './builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component';
|
||||||
import { ToDoChartComponent } from './builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component';
|
import { ToDoChartComponent } from './builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component';
|
||||||
import { ScheduleComponent } from './builder/dashboardnew/schedule/schedule.component';
|
import { ScheduleComponent } from './builder/dashboardnew/schedule/schedule.component';
|
||||||
|
import { CommonFilterComponent } from './builder/dashboardnew/common-filter/common-filter.component';
|
||||||
|
import { ChartWrapperComponent } from './builder/dashboardnew/common-filter/chart-wrapper.component';
|
||||||
|
import { CompactFilterComponent } from './builder/dashboardnew/common-filter/compact-filter.component';
|
||||||
import { AddextensionComponent } from './fnd/extension/addextension/addextension.component';
|
import { AddextensionComponent } from './fnd/extension/addextension/addextension.component';
|
||||||
import { AllextensionComponent } from './fnd/extension/allextension/allextension.component';
|
import { AllextensionComponent } from './fnd/extension/allextension/allextension.component';
|
||||||
import { EditextensionComponent } from './fnd/extension/editextension/editextension.component';
|
import { EditextensionComponent } from './fnd/extension/editextension/editextension.component';
|
||||||
@@ -91,6 +116,9 @@ import { RadarRunnerComponent } from './builder/dashboardrunner/dashrunnerline/r
|
|||||||
import { ScatterRunnerComponent } from './builder/dashboardrunner/dashrunnerline/scatter-runner/scatter-runner.component';
|
import { ScatterRunnerComponent } from './builder/dashboardrunner/dashrunnerline/scatter-runner/scatter-runner.component';
|
||||||
import { TodoRunnerComponent } from './builder/dashboardrunner/dashrunnerline/todo-runner/todo-runner.component';
|
import { TodoRunnerComponent } from './builder/dashboardrunner/dashrunnerline/todo-runner/todo-runner.component';
|
||||||
|
|
||||||
|
// Import CompactFilterRunnerComponent
|
||||||
|
import { CompactFilterRunnerComponent } from './builder/dashboardrunner/dashrunnerline/compact-filter-runner/compact-filter-runner.component';
|
||||||
|
|
||||||
import { ApiregisteryComponent } from './fnd/apiregistery/apiregistery.component';
|
import { ApiregisteryComponent } from './fnd/apiregistery/apiregistery.component';
|
||||||
|
|
||||||
import { BulkimportComponent } from './datamanagement/bulkimport/bulkimport.component';
|
import { BulkimportComponent } from './datamanagement/bulkimport/bulkimport.component';
|
||||||
@@ -106,18 +134,34 @@ import { MappingruleaddComponent } from './datamanagement/mappingrule/mappingrul
|
|||||||
import { MappingruleallComponent } from './datamanagement/mappingrule/mappingruleall/mappingruleall.component';
|
import { MappingruleallComponent } from './datamanagement/mappingrule/mappingruleall/mappingruleall.component';
|
||||||
import { MappingruleeditComponent } from './datamanagement/mappingrule/mappingruleedit/mappingruleedit.component';
|
import { MappingruleeditComponent } from './datamanagement/mappingrule/mappingruleedit/mappingruleedit.component';
|
||||||
import { Stepper_workflowComponent } from './BuilderComponents/stepperworkflow/Stepper_workflow/Stepper_workflow.component';
|
import { Stepper_workflowComponent } from './BuilderComponents/stepperworkflow/Stepper_workflow/Stepper_workflow.component';
|
||||||
|
import { AllapiregisteryComponent } from './fnd/apiregistery/allapiregistery/allapiregistery.component';
|
||||||
|
import { AddapiregisteryComponent } from './fnd/apiregistery/addapiregistery/addapiregistery.component';
|
||||||
|
import { EditapiregisteryComponent } from './fnd/apiregistery/editapiregistery/editapiregistery.component';
|
||||||
|
import { ApiregisterylineComponent } from './fnd/apiregistery/Apiregisteryline/Apiregisteryline.component';
|
||||||
|
import { Customer_informationComponent } from './BuilderComponents/angulardatatype/Customer_information/Customer_information.component';
|
||||||
|
import { Deployment_typeComponent } from './BuilderComponents/angulardatatype/Deployment_type/Deployment_type.component';
|
||||||
|
import { ManufacturerComponent } from './BuilderComponents/angulardatatype/Manufacturer/Manufacturer.component';
|
||||||
|
import { Order_summaryComponent } from './BuilderComponents/angulardatatype/Order_summary/Order_summary.component';
|
||||||
|
import { ProductComponent } from './BuilderComponents/angulardatatype/Product/Product.component';
|
||||||
|
import { TypesComponent } from './BuilderComponents/angulardatatype/Types/Types.component';
|
||||||
|
import { Test2Component } from './BuilderComponents/testdata/Test2/Test2.component';
|
||||||
import { Token_registeryComponent } from './fnd/Token_registery/Token_registery.component';
|
import { Token_registeryComponent } from './fnd/Token_registery/Token_registery.component';
|
||||||
import { MyworkspaceComponent } from './admin/myworkspace/myworkspace.component';
|
import { MyworkspaceComponent } from './admin/myworkspace/myworkspace.component';
|
||||||
import { ThemeCustomizationComponent } from './theme-customization/theme-customization.component';
|
import { ThemeCustomizationComponent } from './theme-customization/theme-customization.component';
|
||||||
|
import { QueryComponent } from './superadmin/query/query.component';
|
||||||
|
import { QueryaddComponent } from './superadmin/queryadd/queryadd.component';
|
||||||
|
import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
|
||||||
|
|
||||||
|
import { FieldTypesModule } from '../../shared/components/field-types/field-types.module';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { Data_lakeComponent } from './builder/dashboardnew/Data_lake/Data_lake.component';
|
import { Data_lakeComponent } from './builder/dashboardnew/Data_lake/Data_lake.component';
|
||||||
import { CronJobBuilderComponent } from './builder/dashboardnew/Data_lake/cron-job-builder/cron-job-builder.component';
|
import { CronJobBuilderComponent } from './builder/dashboardnew/Data_lake/cron-job-builder/cron-job-builder.component';
|
||||||
import { SureconnectComponent } from './builder/dashboardnew/sureconnect/sureconnect.component';
|
import { SureconnectComponent } from './builder/dashboardnew/sureconnect/sureconnect.component';
|
||||||
import { EditsureconnectComponent } from './builder/dashboardnew/sureconnect/editsureconnect/editsureconnect.component';
|
import { EditsureconnectComponent } from './builder/dashboardnew/sureconnect/editsureconnect/editsureconnect.component';
|
||||||
import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.component';
|
import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.component';
|
||||||
|
|
||||||
// import { QueryComponent } from './superadmin/query/query.component';
|
// Import Shield Dashboard Module
|
||||||
// import { QueryaddComponent } from './superadmin/queryadd/queryadd.component';
|
import { ShieldDashboardModule } from './builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.module';
|
||||||
// import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -128,71 +172,58 @@ import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.c
|
|||||||
UsermaintanceaddComponent, UsermaintanceeditComponent,
|
UsermaintanceaddComponent, UsermaintanceeditComponent,
|
||||||
SubmenuComponent, ModulesComponent, SessionloggerComponent,
|
SubmenuComponent, ModulesComponent, SessionloggerComponent,
|
||||||
DashboardnewComponent, EditformnewdashComponent, EditnewdashComponent, ScheduleComponent,
|
DashboardnewComponent, EditformnewdashComponent, EditnewdashComponent, ScheduleComponent,
|
||||||
DoughnutChartComponent, LineChartComponent, RadarChartComponent, BarChartComponent, BubbleChartComponent, DynamicChartComponent, ScatterChartComponent, PolarChartComponent, PieChartComponent, FinancialChartComponent, ToDoChartComponent, GridViewComponent,
|
CommonFilterComponent, ChartWrapperComponent, CompactFilterComponent, DoughnutChartComponent, LineChartComponent, RadarChartComponent, BarChartComponent, BubbleChartComponent, DynamicChartComponent, ScatterChartComponent, PolarChartComponent, PieChartComponent, FinancialChartComponent, ToDoChartComponent, GridViewComponent,
|
||||||
DashrunnerlineComponent, BarRunnerComponent, LineRunnerComponent, DoughnutRunnerComponent, GridRunnerComponent, PieRunnerComponent, PolarRunnerComponent, RadarRunnerComponent, ScatterRunnerComponent, TodoRunnerComponent, BubbleRunnerComponent,
|
DashrunnerlineComponent, BarRunnerComponent, LineRunnerComponent, DoughnutRunnerComponent, GridRunnerComponent, PieRunnerComponent, PolarRunnerComponent, RadarRunnerComponent, ScatterRunnerComponent, TodoRunnerComponent, BubbleRunnerComponent,
|
||||||
|
// Add CompactFilterRunnerComponent to declarations
|
||||||
|
CompactFilterRunnerComponent,
|
||||||
ReportBuildComponent, ReportbuildeditComponent, ReportbuildqueryComponent, ReportBuild2Component, ReportBuild2editComponent,
|
ReportBuildComponent, ReportbuildeditComponent, ReportbuildqueryComponent, ReportBuild2Component, ReportBuild2editComponent,
|
||||||
// QueryComponent, QueryaddComponent, QueryeditComponent,
|
QueryComponent, QueryaddComponent, QueryeditComponent,
|
||||||
ExtensionComponent,
|
ExtensionComponent,
|
||||||
AllextensionComponent,
|
AllextensionComponent,
|
||||||
AddextensionComponent, EditextensionComponent, ApiregisteryComponent,
|
AddextensionComponent, EditextensionComponent, ApiregisteryComponent, AllapiregisteryComponent, AddapiregisteryComponent, EditapiregisteryComponent,
|
||||||
DatamanagementComponent, DatamananementworkflowComponent, BulkimportComponent, BulkimportallComponent, BulkimportaddComponent, BulkimporteditComponent, BulkimportlineComponent, BulkimporteditlineComponent, MappingruleComponent,
|
|
||||||
MappingruleallComponent, MappingruleaddComponent, MappingruleeditComponent,
|
ApiregisterylineComponent,
|
||||||
ThemeCustomizationComponent,
|
DatamanagementComponent, DatamananementworkflowComponent, BulkimportComponent, BulkimportallComponent, BulkimportaddComponent, BulkimporteditComponent, BulkimportlineComponent, BulkimporteditlineComponent, MappingruleComponent, MappingruleallComponent,
|
||||||
|
MappingruleaddComponent,
|
||||||
|
MappingruleeditComponent, Stepper_workflowComponent, Customer_informationComponent,
|
||||||
Data_lakeComponent,
|
Data_lakeComponent,
|
||||||
SureconnectComponent,
|
SureconnectComponent,
|
||||||
EditsureconnectComponent,
|
EditsureconnectComponent,
|
||||||
OauthComponent,
|
OauthComponent,
|
||||||
CronJobBuilderComponent,
|
CronJobBuilderComponent,
|
||||||
|
// FileUploadListComponent,
|
||||||
|
|
||||||
|
|
||||||
// buildercomponents
|
// buildercomponents
|
||||||
|
|
||||||
|
|
||||||
|
ThemeCustomizationComponent,
|
||||||
|
Ad10Component,
|
||||||
Token_registeryComponent,
|
Token_registeryComponent,
|
||||||
|
DefatestComponent,
|
||||||
|
Test2Component,
|
||||||
|
Order_summaryComponent,
|
||||||
|
TypesComponent,
|
||||||
|
ProductComponent,
|
||||||
|
ManufacturerComponent,
|
||||||
|
Deployment_typeComponent,
|
||||||
|
ChildformComponent,
|
||||||
|
DistrictComponent,
|
||||||
|
StateComponent,
|
||||||
|
CountryComponent,
|
||||||
|
Ad9Component,
|
||||||
|
Ad8Component,
|
||||||
|
Ad7Component,
|
||||||
|
Ad6Component,
|
||||||
|
Adv5Component,
|
||||||
|
Adv4Component,
|
||||||
|
SupportComponent,
|
||||||
|
Adv3Component,
|
||||||
|
Dv2Component,
|
||||||
|
Adv1Component,
|
||||||
|
Basicp3Component,
|
||||||
|
Basicp2Component,
|
||||||
|
Basicp1Component,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Stepper_workflowComponent,
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
QRCodeModule,
|
QRCodeModule,
|
||||||
@@ -212,6 +243,9 @@ import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.c
|
|||||||
NgChartsModule,
|
NgChartsModule,
|
||||||
NgxChartsModule,
|
NgxChartsModule,
|
||||||
DynamicModule,
|
DynamicModule,
|
||||||
|
FieldTypesModule,
|
||||||
|
SharedModule,
|
||||||
|
ShieldDashboardModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CookieService,
|
CookieService,
|
||||||
|
|||||||
@@ -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 { }
|
||||||
@@ -69,4 +69,13 @@ export class AlertsService {
|
|||||||
}
|
}
|
||||||
return this.apiRequest.get(apiUrl);
|
return this.apiRequest.get(apiUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get values for a specific key from API
|
||||||
|
public getValuesFromUrl(url: string, sureId: number | undefined, key: string): Observable<any> {
|
||||||
|
let apiUrl = `chart/getValue?apiUrl=${url}&key=${key}`;
|
||||||
|
if (sureId) {
|
||||||
|
apiUrl += `&sureId=${sureId}`;
|
||||||
|
}
|
||||||
|
return this.apiRequest.get(apiUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user