56 Commits

Author SHA1 Message Date
Gaurav Kumar
c5d730ae22 Update editnewdash.component.ts 2025-11-05 10:49:53 +05:30
Gaurav Kumar
40438fcc1b unified 2025-11-04 19:11:00 +05:30
Gaurav Kumar
71be18b21f unified 2025-11-04 18:47:53 +05:30
Gaurav Kumar
1263805d61 chart 2025-11-04 18:37:45 +05:30
Gaurav Kumar
0e6e4899e5 unifield 2025-11-04 17:41:18 +05:30
Gaurav Kumar
29e50253af edit new dash 2025-11-04 13:02:23 +05:30
Gaurav Kumar
68b793ea60 property 2025-11-04 11:55:49 +05:30
Gaurav Kumar
5f0594c93c dynamic field 2025-11-03 23:37:57 +05:30
Gaurav Kumar
e7ee226b66 chart type 2025-11-03 23:29:13 +05:30
Gaurav Kumar
ff2f5a1c34 hartc 2025-11-03 19:25:02 +05:30
Gaurav Kumar
fc36794b22 chart type 2025-11-03 18:19:58 +05:30
Gaurav Kumar
75ccca8caf config 2025-11-03 16:50:53 +05:30
Gaurav Kumar
c4dfb0283c new 2025-11-03 15:01:46 +05:30
Gaurav Kumar
57acdb0300 chart config 2025-11-03 12:52:53 +05:30
Gaurav Kumar
aee28f604f Update chart-type.service.ts 2025-11-03 10:48:55 +05:30
Gaurav Kumar
8375e53fdf service 2025-11-03 09:58:44 +05:30
Gaurav Kumar
db7a9c727d Update chart-type-manager.component.ts 2025-11-03 09:54:49 +05:30
Gaurav Kumar
6e91af551e routing 2025-11-03 09:06:52 +05:30
Gaurav Kumar
de5479cc07 chart 2025-11-03 09:06:04 +05:30
Gaurav Kumar
807058e40d chart 2025-11-01 16:20:59 +05:30
Gaurav Kumar
c6022b0e22 bar chart 2025-11-01 12:34:16 +05:30
Gaurav Kumar
9aed6e0d43 Update unified-chart.component.html 2025-11-01 12:18:01 +05:30
Gaurav Kumar
64b664e625 Update editnewdash.component.ts 2025-11-01 11:00:10 +05:30
Gaurav Kumar
49f1a2fbf2 Update unified-chart.component.ts 2025-11-01 10:54:41 +05:30
Gaurav Kumar
6bfe890cd9 Update editnewdash.component.ts 2025-11-01 10:03:00 +05:30
Gaurav Kumar
3e70f85644 dashboard 2025-10-31 17:19:18 +05:30
Gaurav Kumar
482805b5cf Update editnewdash.component.ts 2025-10-31 17:17:01 +05:30
Gaurav Kumar
ffda17e6b1 unified 2025-10-31 16:35:15 +05:30
Gaurav Kumar
7396843bc6 Update report-build2all.component.html 2025-10-31 16:03:37 +05:30
Gaurav Kumar
4f75ecb3e0 builder 2025-10-31 15:51:28 +05:30
Gaurav Kumar
7c1a487114 dashboard 2025-10-31 10:47:43 +05:30
Gaurav Kumar
47e9fb92e3 Update compact-filter.component.ts 2025-10-30 12:46:39 +05:30
Gaurav Kumar
7f735dcada Update editnewdash.component.ts 2025-10-30 12:29:22 +05:30
Gaurav Kumar
bd315f42a3 compact 2025-10-29 17:55:05 +05:30
Gaurav Kumar
e8c1f46430 Update editnewdash.component.html 2025-10-29 17:30:13 +05:30
Gaurav Kumar
fa96ca81bd runner 2025-10-29 17:18:26 +05:30
Gaurav Kumar
c384f44c0c doughnut 2025-10-29 17:01:50 +05:30
Gaurav Kumar
50df914ca9 pie 2025-10-29 16:52:45 +05:30
Gaurav Kumar
e0bd888c45 polr 2025-10-29 16:14:45 +05:30
Gaurav Kumar
02b82fcaf8 Update polar-chart.component.ts 2025-10-29 16:07:59 +05:30
Gaurav Kumar
557afc348f bubble 2025-10-29 15:55:01 +05:30
Gaurav Kumar
1dec787062 Update bubble-chart.component.ts 2025-10-29 15:50:08 +05:30
Gaurav Kumar
8853cf75cf Update bubble-chart.component.ts 2025-10-29 15:42:21 +05:30
Gaurav Kumar
ad57f11f8a bubble 2025-10-29 15:34:10 +05:30
Gaurav Kumar
9b775a8c63 fin 2025-10-29 12:59:20 +05:30
Gaurav Kumar
cf4fc1be93 todo 2025-10-29 12:21:24 +05:30
Gaurav Kumar
4c135c4901 to do 2025-10-29 12:16:54 +05:30
Gaurav Kumar
0b738ca7ca scatter 2025-10-29 11:35:09 +05:30
Gaurav Kumar
1b17bb706d scatter 2025-10-29 11:11:36 +05:30
Gaurav Kumar
f740076d60 bar chart 2025-10-28 17:04:02 +05:30
Gaurav Kumar
bedcc0822d chart 2025-10-28 16:31:45 +05:30
Gaurav Kumar
87810acc9e line 2025-10-28 15:41:36 +05:30
Gaurav Kumar
96b90e5dbd barchart 2025-10-28 15:32:37 +05:30
Gaurav Kumar
ced99e0940 grid view 2025-10-28 15:11:40 +05:30
Gaurav Kumar
4f82ae8698 grid view 2025-10-28 12:40:01 +05:30
Gaurav Kumar
e6779e8f34 filter 2025-10-28 12:29:10 +05:30
112 changed files with 23700 additions and 2108 deletions

View File

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

View File

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

View File

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

View File

@@ -1,61 +1,67 @@
<ol class="breadcrumb breadcrumb-arrow font-trirong">
<li><a href="javascript://" [routerLink]="['/cns-portal/dashboard/order']"><clr-icon shape="home"></clr-icon></a></li>
<li><a href="javascript://"><clr-icon shape="dashboard"></clr-icon> {{ 'Dashboard_builder' | translate }}</a></li>
</ol>
<br>
<div class="dg-wrapper">
<div class="clr-row">
<div class="clr-col-8">
<h3>{{ 'Dashboard_builder' | translate }}</h3>
</div>
<div class="clr-col-4" style="text-align: right;">
<button class="btn btn-success" [routerLink]="['/cns-portal/shield-dashboard']">
<clr-icon shape="shield"></clr-icon>Shield Dashboard
</button>
<button id="add" class="btn btn-primary" (click)="gotorunner()">
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
</button>
<button class="btn btn-outline" (click)="onExport()">
<clr-icon shape="export"></clr-icon> {{ 'EXPORT_XLSX' | translate }}
</button>
<button id="add" class="btn btn-primary" (click)="gotoadd()">
<clr-icon shape="plus"></clr-icon> {{ 'ADD' | translate }}
</button>
</div>
</div>
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{ 'Loading' | translate }} ...... </clr-spinner></ng-template>
<div *ngIf="error;else loadingSpinner">{{error}}</div></clr-dg-placeholder>
<clr-dg-column [clrDgField]="''">
<ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Go_to' | translate }}
</ng-container>
</clr-dg-column>
<clr-dg-column [clrDgField]="'dashboard_name'">
<ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Dashboard_Name' | translate }}
</ng-container>
</clr-dg-column>
<clr-dg-column [clrDgField]="'description'">
<ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Description' | translate }}
</ng-container>
</clr-dg-column >
<clr-dg-column [clrDgField]="'secuirity_profile'">
<ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Security_Profile' | translate }}
</ng-container>
</clr-dg-column >
<clr-dg-column [clrDgField]="'add_to_home'">
<ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Add_to_home' | translate }}
</ng-container>
</clr-dg-column >
<!-- <clr-dg-column [clrDgField]="'formType'">
<li><a href="javascript://" [routerLink]="['/cns-portal/dashboard/order']"><clr-icon shape="home"></clr-icon></a></li>
<li><a href="javascript://"><clr-icon shape="dashboard"></clr-icon> {{ 'Dashboard_builder' | translate }}</a></li>
</ol>
<br>
<div class="dg-wrapper">
<div class="clr-row">
<div class="clr-col-8">x
<h3>{{ 'Dashboard_builder' | translate }}</h3>
</div>
<div class="clr-col-4" style="text-align: right;">
<button class="btn btn-success" [routerLink]="['/cns-portal/shield-dashboard']">
<clr-icon shape="shield"></clr-icon>Shield Dashboard
</button>
<button id="add" class="btn btn-primary" (click)="gotorunner()">
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
</button>
<!-- Add Chart Config button -->
<button class="btn btn-primary" (click)="openChartConfig()">
<clr-icon shape="cog"></clr-icon> Chart Config
</button>
<button class="btn btn-outline" (click)="onExport()">
<clr-icon shape="export"></clr-icon> {{ 'EXPORT_XLSX' | translate }}
</button>
<button id="add" class="btn btn-primary" (click)="gotoadd()">
<clr-icon shape="plus"></clr-icon> {{ 'ADD' | translate }}
</button>
</div>
</div>
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{ 'Loading' | translate }} ......
</clr-spinner></ng-template>
<div *ngIf="error;else loadingSpinner">{{error}}</div>
</clr-dg-placeholder>
<clr-dg-column [clrDgField]="''">
<ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Go_to' | translate }}
</ng-container>
</clr-dg-column>
<clr-dg-column [clrDgField]="'dashboard_name'">
<ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Dashboard_Name' | translate }}
</ng-container>
</clr-dg-column>
<clr-dg-column [clrDgField]="'description'">
<ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Description' | translate }}
</ng-container>
</clr-dg-column>
<clr-dg-column [clrDgField]="'secuirity_profile'">
<ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Security_Profile' | translate }}
</ng-container>
</clr-dg-column>
<clr-dg-column [clrDgField]="'add_to_home'">
<ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Add_to_home' | translate }}
</ng-container>
</clr-dg-column>
<!-- <clr-dg-column [clrDgField]="'formType'">
<ng-container *clrDgHideableColumn="{hidden: false}">
Build Status
</ng-container>
@@ -65,81 +71,86 @@
Testing
</ng-container>
</clr-dg-column > -->
<clr-dg-column [clrDgField]="'action'"> <ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Action' | translate }}
</ng-container></clr-dg-column>
<clr-dg-row *clrDgItems="let user of data?.slice()?.reverse();" [clrDgItem]="user">
<clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;" (click)="goToEdit(user.id)"> {{ 'SET_UP' | translate }}</span></clr-dg-cell>
<clr-dg-cell>{{user.dashboard_name}}</clr-dg-cell>
<clr-dg-cell>{{user.description}}</clr-dg-cell>
<clr-dg-cell>{{user.secuirity_profile}}</clr-dg-cell>
<clr-dg-cell>{{user.add_to_home}}</clr-dg-cell>
<clr-dg-column [clrDgField]="'action'"> <ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Action' | translate }}
</ng-container></clr-dg-column>
<!-- <clr-dg-cell><input type="radio" id="cb1" class="dots" [ngStyle]="{'background-color': user.build == true ? 'green' : 'red'}"></clr-dg-cell>
<clr-dg-row *clrDgItems="let user of data?.slice()?.reverse();" [clrDgItem]="user">
<clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;"
(click)="goToEdit(user.id)"> {{ 'SET_UP' | translate }}</span></clr-dg-cell>
<clr-dg-cell>{{user.dashboard_name}}</clr-dg-cell>
<clr-dg-cell>{{user.description}}</clr-dg-cell>
<clr-dg-cell>{{user.secuirity_profile}}</clr-dg-cell>
<clr-dg-cell>{{user.add_to_home}}</clr-dg-cell>
<!-- <clr-dg-cell><input type="radio" id="cb1" class="dots" [ngStyle]="{'background-color': user.build == true ? 'green' : 'red'}"></clr-dg-cell>
<clr-dg-cell>{{user.testing}}</clr-dg-cell> -->
<clr-dg-cell>
<!-- <span style="cursor: pointer; padding: 10px; "><clr-icon shape="form" (click)="goToEdit(user.id)" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span> -->
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true" class="tooltip tooltip-sm tooltip-top-left">
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error" style="color: red;"></clr-icon></span>
<span class="tooltip-content">{{ 'Delete' | translate }}</span>
</a>
<clr-signpost>
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span>
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
<h5 style="margin-top: 0">{{ 'Who_Column' | translate }}</h5>
<div>{{ 'Account_ID' | translate }}: <code class="clr-code">{{ user.accountId }}</code></div>
<div>{{ 'Created_At' | translate }}: <code class="clr-code">{{ user.createdAt | date }}</code></div>
<div>{{ 'Created_By' | translate }}: <code class="clr-code">{{ user.createdBy }}</code></div>
<div>{{ 'Updated_At' | translate }}: <code class="clr-code">{{ user.updatedAt | date }}</code></div>
<div>{{ 'Updated_By' | translate }}: <code class="clr-code">{{ user.updatedBy }}</code></div>
</clr-signpost-content>
</clr-signpost>
</clr-dg-cell>
<clr-dg-action-overflow>
<button class="action-item" (click)="goToEditData(user.id)">{{ 'Edit' | translate }} <clr-icon shape="edit" class="is-error"></clr-icon></button>
<!-- <button class="action-item" (click)="onDelete(user)">Delete<clr-icon shape="trash" class="is-error"></clr-icon></button> -->
</clr-dg-action-overflow>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="10">
<clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">{{ 'Users_per_page' | translate }}</clr-dg-page-size>
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
{{ 'of' | translate }} {{pagination.totalItems}} {{ 'users' | translate }}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
<clr-dg-cell>
<!-- <span style="cursor: pointer; padding: 10px; "><clr-icon shape="form" (click)="goToEdit(user.id)" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span> -->
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-sm tooltip-top-left">
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error"
style="color: red;"></clr-icon></span>
<span class="tooltip-content">{{ 'Delete' | translate }}</span>
</a>
<clr-signpost>
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success"
style="color: rgb(0, 130, 236);"></clr-icon></span>
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
<h5 style="margin-top: 0">{{ 'Who_Column' | translate }}</h5>
<div>{{ 'Account_ID' | translate }}: <code class="clr-code">{{ user.accountId }}</code></div>
<div>{{ 'Created_At' | translate }}: <code class="clr-code">{{ user.createdAt | date }}</code></div>
<div>{{ 'Created_By' | translate }}: <code class="clr-code">{{ user.createdBy }}</code></div>
<div>{{ 'Updated_At' | translate }}: <code class="clr-code">{{ user.updatedAt | date }}</code></div>
<div>{{ 'Updated_By' | translate }}: <code class="clr-code">{{ user.updatedBy }}</code></div>
</clr-signpost-content>
</clr-signpost>
</clr-dg-cell>
<clr-dg-action-overflow>
<button class="action-item" (click)="goToEditData(user.id)">{{ 'Edit' | translate }} <clr-icon shape="edit"
class="is-error"></clr-icon></button>
<!-- <button class="action-item" (click)="onDelete(user)">Delete<clr-icon shape="trash" class="is-error"></clr-icon></button> -->
</clr-dg-action-overflow>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="10">
<clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">{{ 'Users_per_page' | translate }}</clr-dg-page-size>
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
{{ 'of' | translate }} {{pagination.totalItems}} {{ 'users' | translate }}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
<clr-modal [(clrModalOpen)]="addModall" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
<div class="modal-body">
<div class="s-order-dash-pg">
<div class="chart-box" id="word1" (click)="gotoadd()"><br>
<img style="margin: auto; display: block;" src="/assets/images/fromscratch.png" height="90" width="90">
<h5 class="center"> <b>{{ 'Start_from_scratch' | translate }}</b> </h5>
</div>
<div class="chart-box" id="word1"><br>
<img style="margin: auto; display: block;" src="/assets/images/copytemplate.png" height="90" width="90">
<h5 class="center"> <b>{{ 'Import_from_template' | translate }}</b> </h5>
</div>
<div class="chart-box" id="word1"><br>
<img style="margin: auto; display: block;" src="/assets/images/database.png" height="90" width="90">
<h5 class="center"> <b>{{ 'Import_from_public_project' | translate }}</b> </h5>
</div>
</div>
<clr-modal [(clrModalOpen)]="addModall" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
<div class="modal-body">
<div class="s-order-dash-pg">
<div class="chart-box" id="word1" (click)="gotoadd()"><br>
<img style="margin: auto; display: block;" src="/assets/images/fromscratch.png" height="90" width="90">
<h5 class="center"> <b>{{ 'Start_from_scratch' | translate }}</b> </h5>
</div>
<div class="chart-box" id="word1" ><br>
<img style="margin: auto; display: block;" src="/assets/images/copytemplate.png" height="90" width="90">
<h5 class="center"> <b>{{ 'Import_from_template' | translate }}</b> </h5>
</div>
<div class="chart-box" id="word1"><br>
<img style="margin: auto; display: block;" src="/assets/images/database.png" height="90" width="90">
<h5 class="center"> <b>{{ 'Import_from_public_project' | translate }}</b> </h5>
</div>
</div>
</div>
</clr-modal>
<clr-modal [(clrModalOpen)]="modalDelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
<div class="modal-body" *ngIf="rowSelected.id">
<h1 class="delete">{{ 'Are_you_sure_want_to_delete' | translate }}</h1>
<h2 class="heading">{{rowSelected.id}}</h2>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="modalDelete = false">{{ 'Cancel' | translate }}</button>
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary" >{{ 'Delete' | translate }}</button>
</div>
</div>
</clr-modal>
</div>
</clr-modal>
<clr-modal [(clrModalOpen)]="modalDelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
<div class="modal-body" *ngIf="rowSelected.id">
<h1 class="delete">{{ 'Are_you_sure_want_to_delete' | translate }}</h1>
<h2 class="heading">{{rowSelected.id}}</h2>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="modalDelete = false">{{ 'Cancel' | translate }}</button>
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary">{{ 'Delete' | translate }}</button>
</div>
</div>
</clr-modal>

View File

@@ -24,6 +24,7 @@ export class AllnewdashComponent implements OnInit {
projectname;
projectId;
error;
chartConfigManagerOpen = false;
constructor(
private router : Router,
private route: ActivatedRoute,private dashboardService : DashboardService,
@@ -121,4 +122,9 @@ export class AllnewdashComponent implements OnInit {
// this.router.navigate(['../editdashn'],{relativeTo:this.route});
// }
// Add method to open chart configuration manager
openChartConfig(): void {
this.router.navigate(['../chart-types'],{relativeTo:this.route});
}
}

View File

@@ -0,0 +1,630 @@
<div class="chart-config-manager">
<h2>Chart Configuration Manager</h2>
<!-- Chart Types Section -->
<clr-tabs>
<clr-tab>
<button clrTabLink>Chart Types</button>
<clr-tab-content>
<div class="card">
<div class="card-header">
<h3>Chart Types</h3>
<button class="btn btn-sm btn-primary" (click)="showAddChartTypeForm = true">
Add Chart Type
</button>
</div>
<!-- Add Chart Type Form -->
<div class="card-block" *ngIf="showAddChartTypeForm">
<form clrForm>
<clr-input-container>
<label>Name</label>
<input clrInput type="text" [(ngModel)]="newChartType.name" name="chartTypeName" required />
</clr-input-container>
<clr-input-container>
<label>Display Name</label>
<input clrInput type="text" [(ngModel)]="newChartType.displayName" name="chartTypeDisplayName" />
</clr-input-container>
<clr-textarea-container>
<label>Description</label>
<textarea clrTextarea [(ngModel)]="newChartType.description" name="chartTypeDescription"></textarea>
</clr-textarea-container>
<clr-checkbox-container>
<label>Is Active</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="newChartType.isActive" name="chartTypeIsActive" />
<label>Active</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" (click)="createChartType()" [disabled]="!newChartType.name">Save</button>
<button class="btn" (click)="showAddChartTypeForm = false">Cancel</button>
</div>
</form>
</div>
<!-- Chart Types Table -->
<div class="card-block">
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Display Name</clr-dg-column>
<clr-dg-column>Description</clr-dg-column>
<clr-dg-column>Status</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let chartType of chartTypes" [clrDgItem]="chartType">
<clr-dg-cell>{{chartType.name}}</clr-dg-cell>
<clr-dg-cell>{{chartType.displayName}}</clr-dg-cell>
<clr-dg-cell>{{chartType.description}}</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="chartType.isActive" [class.label-danger]="!chartType.isActive">
{{chartType.isActive ? 'Active' : 'Inactive'}}
</span>
</clr-dg-cell>
<clr-dg-cell>
<button class="btn btn-sm btn-icon" (click)="selectChartTypeForEdit(chartType)">
<cds-icon shape="pencil"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteChartType(chartType.id)">
<cds-icon shape="trash"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="onChartTypeSelect(chartType)">
<cds-icon shape="eye"></cds-icon>
</button>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{chartTypes.length}} chart types
</clr-dg-footer>
</clr-datagrid>
</div>
<!-- Edit Chart Type Form -->
<div class="card-block" *ngIf="selectedChartType">
<h4>Edit Chart Type</h4>
<form clrForm>
<clr-input-container>
<label>Name</label>
<input clrInput type="text" [(ngModel)]="selectedChartType.name" name="editChartTypeName" required />
</clr-input-container>
<clr-input-container>
<label>Display Name</label>
<input clrInput type="text" [(ngModel)]="selectedChartType.displayName" name="editChartTypeDisplayName" />
</clr-input-container>
<clr-textarea-container>
<label>Description</label>
<textarea clrTextarea [(ngModel)]="selectedChartType.description" name="editChartTypeDescription"></textarea>
</clr-textarea-container>
<clr-checkbox-container>
<label>Is Active</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="selectedChartType.isActive" name="editChartTypeIsActive" />
<label>Active</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" (click)="updateChartType()" [disabled]="!selectedChartType.name">Update</button>
<button class="btn" (click)="selectedChartType = null">Cancel</button>
</div>
</form>
</div>
</div>
</clr-tab-content>
</clr-tab>
<!-- UI Components Section -->
<clr-tab *ngIf="selectedChartType">
<button clrTabLink>UI Components</button>
<clr-tab-content>
<div class="card">
<div class="card-header">
<h3>UI Components for {{selectedChartType?.name}}</h3>
<button class="btn btn-sm btn-primary" (click)="showAddUiComponentForm = true">
Add UI Component
</button>
</div>
<!-- Add UI Component Form -->
<div class="card-block" *ngIf="showAddUiComponentForm">
<form clrForm>
<clr-input-container>
<label>Component Name</label>
<input clrInput type="text" [(ngModel)]="newUiComponent.componentName" name="componentName" required />
</clr-input-container>
<clr-input-container>
<label>Component Type</label>
<input clrInput type="text" [(ngModel)]="newUiComponent.componentType" name="componentType" />
</clr-input-container>
<clr-input-container>
<label>Display Label</label>
<input clrInput type="text" [(ngModel)]="newUiComponent.displayLabel" name="displayLabel" />
</clr-input-container>
<clr-input-container>
<label>Placeholder</label>
<input clrInput type="text" [(ngModel)]="newUiComponent.placeholder" name="placeholder" />
</clr-input-container>
<clr-input-container>
<label>Sort Order</label>
<input clrInput type="number" [(ngModel)]="newUiComponent.sortOrder" name="sortOrder" />
</clr-input-container>
<clr-checkbox-container>
<label>Is Required</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="newUiComponent.isRequired" name="isRequired" />
<label>Required</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" (click)="createUiComponent()" [disabled]="!newUiComponent.componentName">Save</button>
<button class="btn" (click)="showAddUiComponentForm = false">Cancel</button>
</div>
</form>
</div>
<!-- UI Components Table -->
<div class="card-block">
<clr-datagrid>
<clr-dg-column>Component Name</clr-dg-column>
<clr-dg-column>Component Type</clr-dg-column>
<clr-dg-column>Display Label</clr-dg-column>
<clr-dg-column>Placeholder</clr-dg-column>
<clr-dg-column>Required</clr-dg-column>
<clr-dg-column>Sort Order</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let uiComponent of uiComponents" [clrDgItem]="uiComponent">
<clr-dg-cell>{{uiComponent.componentName}}</clr-dg-cell>
<clr-dg-cell>{{uiComponent.componentType}}</clr-dg-cell>
<clr-dg-cell>{{uiComponent.displayLabel}}</clr-dg-cell>
<clr-dg-cell>{{uiComponent.placeholder}}</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="uiComponent.isRequired" [class.label-danger]="!uiComponent.isRequired">
{{uiComponent.isRequired ? 'Yes' : 'No'}}
</span>
</clr-dg-cell>
<clr-dg-cell>{{uiComponent.sortOrder}}</clr-dg-cell>
<clr-dg-cell>
<button class="btn btn-sm btn-icon" (click)="selectUiComponentForEdit(uiComponent)">
<cds-icon shape="pencil"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteUiComponent(uiComponent.id)">
<cds-icon shape="trash"></cds-icon>
</button>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{uiComponents.length}} UI components
</clr-dg-footer>
</clr-datagrid>
</div>
<!-- Edit UI Component Form -->
<div class="card-block" *ngIf="selectedUiComponent">
<h4>Edit UI Component</h4>
<form clrForm>
<clr-input-container>
<label>Component Name</label>
<input clrInput type="text" [(ngModel)]="selectedUiComponent.componentName" name="editComponentName" required />
</clr-input-container>
<clr-input-container>
<label>Component Type</label>
<input clrInput type="text" [(ngModel)]="selectedUiComponent.componentType" name="editComponentType" />
</clr-input-container>
<clr-input-container>
<label>Display Label</label>
<input clrInput type="text" [(ngModel)]="selectedUiComponent.displayLabel" name="editDisplayLabel" />
</clr-input-container>
<clr-input-container>
<label>Placeholder</label>
<input clrInput type="text" [(ngModel)]="selectedUiComponent.placeholder" name="editPlaceholder" />
</clr-input-container>
<clr-input-container>
<label>Sort Order</label>
<input clrInput type="number" [(ngModel)]="selectedUiComponent.sortOrder" name="editSortOrder" />
</clr-input-container>
<clr-checkbox-container>
<label>Is Required</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="selectedUiComponent.isRequired" name="editIsRequired" />
<label>Required</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" (click)="updateUiComponent()" [disabled]="!selectedUiComponent.componentName">Update</button>
<button class="btn" (click)="selectedUiComponent = null">Cancel</button>
</div>
</form>
</div>
</div>
</clr-tab-content>
</clr-tab>
<!-- Component Properties Section -->
<clr-tab *ngIf="selectedUiComponent">
<button clrTabLink>Component Properties</button>
<clr-tab-content>
<div class="card">
<div class="card-header">
<h3>Properties for {{selectedUiComponent?.componentName}}</h3>
<button class="btn btn-sm btn-primary" (click)="showAddComponentPropertyForm = true">
Add Property
</button>
</div>
<!-- Add Component Property Form -->
<div class="card-block" *ngIf="showAddComponentPropertyForm">
<form clrForm>
<clr-input-container>
<label>Property Name</label>
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyName" name="propertyName" required />
</clr-input-container>
<clr-input-container>
<label>Property Value</label>
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyValue" name="propertyValue" />
</clr-input-container>
<clr-input-container>
<label>Property Type</label>
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyType" name="propertyType" />
</clr-input-container>
<div class="form-actions">
<button class="btn btn-primary" (click)="createComponentProperty()" [disabled]="!newComponentProperty.propertyName">Save</button>
<button class="btn" (click)="showAddComponentPropertyForm = false">Cancel</button>
</div>
</form>
</div>
<!-- Component Properties Table -->
<div class="card-block">
<clr-datagrid>
<clr-dg-column>Property Name</clr-dg-column>
<clr-dg-column>Property Value</clr-dg-column>
<clr-dg-column>Property Type</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let property of componentProperties" [clrDgItem]="property">
<clr-dg-cell>{{property.propertyName}}</clr-dg-cell>
<clr-dg-cell>{{property.propertyValue}}</clr-dg-cell>
<clr-dg-cell>{{property.propertyType}}</clr-dg-cell>
<clr-dg-cell>
<button class="btn btn-sm btn-icon" (click)="selectComponentPropertyForEdit(property)">
<cds-icon shape="pencil"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteComponentProperty(property.id)">
<cds-icon shape="trash"></cds-icon>
</button>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{componentProperties.length}} properties
</clr-dg-footer>
</clr-datagrid>
</div>
<!-- Edit Component Property Form -->
<div class="card-block" *ngIf="selectedComponentProperty">
<h4>Edit Component Property</h4>
<form clrForm>
<clr-input-container>
<label>Property Name</label>
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyName" name="editPropertyName" required />
</clr-input-container>
<clr-input-container>
<label>Property Value</label>
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyValue" name="editPropertyValue" />
</clr-input-container>
<clr-input-container>
<label>Property Type</label>
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyType" name="editPropertyType" />
</clr-input-container>
<div class="form-actions">
<button class="btn btn-primary" (click)="updateComponentProperty()" [disabled]="!selectedComponentProperty.propertyName">Update</button>
<button class="btn" (click)="selectedComponentProperty = null">Cancel</button>
</div>
</form>
</div>
</div>
</clr-tab-content>
</clr-tab>
<!-- Chart Templates Section -->
<clr-tab *ngIf="selectedChartType">
<button clrTabLink>Chart Templates</button>
<clr-tab-content>
<div class="card">
<div class="card-header">
<h3>Templates for {{selectedChartType?.name}}</h3>
<button class="btn btn-sm btn-primary" (click)="showAddChartTemplateForm = true">
Add Template
</button>
</div>
<!-- Add Chart Template Form -->
<div class="card-block" *ngIf="showAddChartTemplateForm">
<form clrForm>
<clr-input-container>
<label>Template Name</label>
<input clrInput type="text" [(ngModel)]="newChartTemplate.templateName" name="templateName" required />
</clr-input-container>
<clr-textarea-container>
<label>HTML Template</label>
<textarea clrTextarea [(ngModel)]="newChartTemplate.templateHtml" name="templateHtml" rows="5"></textarea>
</clr-textarea-container>
<clr-textarea-container>
<label>CSS Template</label>
<textarea clrTextarea [(ngModel)]="newChartTemplate.templateCss" name="templateCss" rows="5"></textarea>
</clr-textarea-container>
<clr-checkbox-container>
<label>Is Default</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="newChartTemplate.isDefault" name="isDefault" />
<label>Default Template</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" (click)="createChartTemplate()" [disabled]="!newChartTemplate.templateName">Save</button>
<button class="btn" (click)="showAddChartTemplateForm = false">Cancel</button>
</div>
</form>
</div>
<!-- Chart Templates Table -->
<div class="card-block">
<clr-datagrid>
<clr-dg-column>Template Name</clr-dg-column>
<clr-dg-column>Is Default</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let template of chartTemplates" [clrDgItem]="template">
<clr-dg-cell>{{template.templateName}}</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="template.isDefault" [class.label-danger]="!template.isDefault">
{{template.isDefault ? 'Yes' : 'No'}}
</span>
</clr-dg-cell>
<clr-dg-cell>
<button class="btn btn-sm btn-icon" (click)="selectChartTemplateForEdit(template)">
<cds-icon shape="pencil"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteChartTemplate(template.id)">
<cds-icon shape="trash"></cds-icon>
</button>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{chartTemplates.length}} templates
</clr-dg-footer>
</clr-datagrid>
</div>
<!-- Edit Chart Template Form -->
<div class="card-block" *ngIf="selectedChartTemplate">
<h4>Edit Chart Template</h4>
<form clrForm>
<clr-input-container>
<label>Template Name</label>
<input clrInput type="text" [(ngModel)]="selectedChartTemplate.templateName" name="editTemplateName" required />
</clr-input-container>
<clr-textarea-container>
<label>HTML Template</label>
<textarea clrTextarea [(ngModel)]="selectedChartTemplate.templateHtml" name="editTemplateHtml" rows="5"></textarea>
</clr-textarea-container>
<clr-textarea-container>
<label>CSS Template</label>
<textarea clrTextarea [(ngModel)]="selectedChartTemplate.templateCss" name="editTemplateCss" rows="5"></textarea>
</clr-textarea-container>
<clr-checkbox-container>
<label>Is Default</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="selectedChartTemplate.isDefault" name="editIsDefault" />
<label>Default Template</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" (click)="updateChartTemplate()" [disabled]="!selectedChartTemplate.templateName">Update</button>
<button class="btn" (click)="selectedChartTemplate = null">Cancel</button>
</div>
</form>
</div>
</div>
</clr-tab-content>
</clr-tab>
<!-- Dynamic Fields Section -->
<clr-tab *ngIf="selectedChartType">
<button clrTabLink>Dynamic Fields</button>
<clr-tab-content>
<div class="card">
<div class="card-header">
<h3>Dynamic Fields for {{selectedChartType?.name}}</h3>
<button class="btn btn-sm btn-primary" (click)="showAddDynamicFieldForm = true">
Add Field
</button>
</div>
<!-- Add Dynamic Field Form -->
<div class="card-block" *ngIf="showAddDynamicFieldForm">
<form clrForm>
<clr-input-container>
<label>Field Name</label>
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldName" name="fieldName" required />
</clr-input-container>
<clr-input-container>
<label>Field Label</label>
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldLabel" name="fieldLabel" />
</clr-input-container>
<clr-input-container>
<label>Field Type</label>
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldType" name="fieldType" />
</clr-input-container>
<clr-textarea-container>
<label>Field Options</label>
<textarea clrTextarea [(ngModel)]="newDynamicField.fieldOptions" name="fieldOptions"></textarea>
</clr-textarea-container>
<clr-input-container>
<label>Sort Order</label>
<input clrInput type="number" [(ngModel)]="newDynamicField.sortOrder" name="fieldSortOrder" />
</clr-input-container>
<clr-checkbox-container>
<label>Is Required</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.isRequired" name="fieldIsRequired" />
<label>Required</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<clr-checkbox-container>
<label>Show in UI</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.showInUi" name="fieldShowInUi" />
<label>Show in UI</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" (click)="createDynamicField()" [disabled]="!newDynamicField.fieldName">Save</button>
<button class="btn" (click)="showAddDynamicFieldForm = false">Cancel</button>
</div>
</form>
</div>
<!-- Dynamic Fields Table -->
<div class="card-block">
<clr-datagrid>
<clr-dg-column>Field Name</clr-dg-column>
<clr-dg-column>Field Label</clr-dg-column>
<clr-dg-column>Field Type</clr-dg-column>
<clr-dg-column>Required</clr-dg-column>
<clr-dg-column>Show in UI</clr-dg-column>
<clr-dg-column>Sort Order</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let field of dynamicFields" [clrDgItem]="field">
<clr-dg-cell>{{field.fieldName}}</clr-dg-cell>
<clr-dg-cell>{{field.fieldLabel}}</clr-dg-cell>
<clr-dg-cell>{{field.fieldType}}</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="field.isRequired" [class.label-danger]="!field.isRequired">
{{field.isRequired ? 'Yes' : 'No'}}
</span>
</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="field.showInUi" [class.label-danger]="!field.showInUi">
{{field.showInUi ? 'Yes' : 'No'}}
</span>
</clr-dg-cell>
<clr-dg-cell>{{field.sortOrder}}</clr-dg-cell>
<clr-dg-cell>
<button class="btn btn-sm btn-icon" (click)="selectDynamicFieldForEdit(field)">
<cds-icon shape="pencil"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteDynamicField(field.id)">
<cds-icon shape="trash"></cds-icon>
</button>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{dynamicFields.length}} fields
</clr-dg-footer>
</clr-datagrid>
</div>
<!-- Edit Dynamic Field Form -->
<div class="card-block" *ngIf="selectedDynamicField">
<h4>Edit Dynamic Field</h4>
<form clrForm>
<clr-input-container>
<label>Field Name</label>
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldName" name="editFieldName" required />
</clr-input-container>
<clr-input-container>
<label>Field Label</label>
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldLabel" name="editFieldLabel" />
</clr-input-container>
<clr-input-container>
<label>Field Type</label>
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldType" name="editFieldType" />
</clr-input-container>
<clr-textarea-container>
<label>Field Options</label>
<textarea clrTextarea [(ngModel)]="selectedDynamicField.fieldOptions" name="editFieldOptions"></textarea>
</clr-textarea-container>
<clr-input-container>
<label>Sort Order</label>
<input clrInput type="number" [(ngModel)]="selectedDynamicField.sortOrder" name="editFieldSortOrder" />
</clr-input-container>
<clr-checkbox-container>
<label>Is Required</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.isRequired" name="editFieldIsRequired" />
<label>Required</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<clr-checkbox-container>
<label>Show in UI</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.showInUi" name="editFieldShowInUi" />
<label>Show in UI</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" (click)="updateDynamicField()" [disabled]="!selectedDynamicField.fieldName">Update</button>
<button class="btn" (click)="selectedDynamicField = null">Cancel</button>
</div>
</form>
</div>
</div>
</clr-tab-content>
</clr-tab>
</clr-tabs>
</div>

View File

@@ -0,0 +1,60 @@
.chart-config-manager {
padding: 20px;
.card {
margin-bottom: 20px;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin: 0;
}
}
.card-block {
padding: 15px;
form {
margin-bottom: 20px;
}
}
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
clr-datagrid {
min-height: 200px;
}
.label {
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
}
.label-success {
background-color: #318700;
color: white;
}
.label-danger {
background-color: #e62200;
color: white;
}
.required {
color: red;
}
.alert {
margin-bottom: 20px;
}
}

View File

@@ -0,0 +1,658 @@
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';
import { ClrLoadingState } from '@clr/angular';
interface ChartType {
id: number;
name: string;
displayName: string;
description: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
interface UiComponent {
id: number;
chartType: ChartType;
componentName: string;
componentType: string;
displayLabel: string;
placeholder: string;
isRequired: boolean;
sortOrder: number;
createdAt: string;
updatedAt: string;
}
interface ComponentProperty {
id: number;
component: UiComponent;
propertyName: string;
propertyValue: string;
propertyType: string;
createdAt: string;
updatedAt: string;
}
interface ChartTemplate {
id: number;
chartType: ChartType;
templateName: string;
templateHtml: string;
templateCss: string;
isDefault: boolean;
createdAt: string;
updatedAt: string;
}
interface DynamicField {
id: number;
chartType: ChartType;
fieldName: string;
fieldLabel: string;
fieldType: string;
fieldOptions: string;
isRequired: boolean;
showInUi: boolean;
sortOrder: number;
createdAt: string;
updatedAt: string;
}
@Component({
selector: 'app-chart-config-manager',
templateUrl: './chart-config-manager.component.html',
styleUrls: ['./chart-config-manager.component.scss']
})
export class ChartConfigManagerComponent implements OnInit {
// Chart Types
chartTypes: ChartType[] = [];
selectedChartType: ChartType | null = null;
newChartType: Partial<ChartType> = {};
showAddChartTypeForm = false;
chartTypeLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
// UI Components
uiComponents: UiComponent[] = [];
selectedUiComponent: UiComponent | null = null;
newUiComponent: Partial<UiComponent> = {};
showAddUiComponentForm = false;
uiComponentLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
// Component Properties
componentProperties: ComponentProperty[] = [];
selectedComponentProperty: ComponentProperty | null = null;
newComponentProperty: Partial<ComponentProperty> = {};
showAddComponentPropertyForm = false;
componentPropertyLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
// Chart Templates
chartTemplates: ChartTemplate[] = [];
selectedChartTemplate: ChartTemplate | null = null;
newChartTemplate: Partial<ChartTemplate> = {};
showAddChartTemplateForm = false;
chartTemplateLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
// Dynamic Fields
dynamicFields: DynamicField[] = [];
selectedDynamicField: DynamicField | null = null;
newDynamicField: Partial<DynamicField> = {};
showAddDynamicFieldForm = false;
dynamicFieldLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
// Error handling
errorMessage: string | null = null;
successMessage: string | null = null;
// API base URL
private apiUrl = environment.apiUrl || 'http://localhost:8080/api';
constructor(private http: HttpClient) { }
ngOnInit(): void {
this.loadChartTypes();
}
// Show error message
private showError(message: string): void {
this.errorMessage = message;
setTimeout(() => {
this.errorMessage = null;
}, 5000);
}
// Show success message
private showSuccess(message: string): void {
this.successMessage = message;
setTimeout(() => {
this.successMessage = null;
}, 3000);
}
// Chart Type Methods
loadChartTypes(): void {
this.chartTypeLoadingState = ClrLoadingState.LOADING;
this.http.get<ChartType[]>(`${this.apiUrl}/chart-types`).subscribe({
next: (data) => {
this.chartTypes = data;
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading chart types:', error);
this.showError('Error loading chart types: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTypeLoadingState = ClrLoadingState.ERROR;
}
});
}
createChartType(): void {
if (!this.newChartType.name) {
this.showError('Chart type name is required');
return;
}
this.chartTypeLoadingState = ClrLoadingState.LOADING;
this.http.post<ChartType>(`${this.apiUrl}/chart-types`, this.newChartType).subscribe({
next: (data) => {
this.chartTypes.push(data);
this.newChartType = {};
this.showAddChartTypeForm = false;
this.showSuccess('Chart type created successfully');
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating chart type:', error);
this.showError('Error creating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTypeLoadingState = ClrLoadingState.ERROR;
}
});
}
updateChartType(): void {
if (!this.selectedChartType || !this.selectedChartType.name) {
this.showError('Chart type name is required');
return;
}
this.chartTypeLoadingState = ClrLoadingState.LOADING;
this.http.put<ChartType>(`${this.apiUrl}/chart-types/${this.selectedChartType.id}`, this.selectedChartType).subscribe({
next: (data) => {
const index = this.chartTypes.findIndex(ct => ct.id === data.id);
if (index !== -1) {
this.chartTypes[index] = data;
}
this.selectedChartType = null;
this.showSuccess('Chart type updated successfully');
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating chart type:', error);
this.showError('Error updating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTypeLoadingState = ClrLoadingState.ERROR;
}
});
}
deleteChartType(id: number): void {
if (!confirm('Are you sure you want to delete this chart type?')) {
return;
}
this.chartTypeLoadingState = ClrLoadingState.LOADING;
this.http.delete(`${this.apiUrl}/chart-types/${id}`).subscribe({
next: () => {
this.chartTypes = this.chartTypes.filter(ct => ct.id !== id);
// Also clear related data if the deleted chart type was selected
if (this.selectedChartType && this.selectedChartType.id === id) {
this.selectedChartType = null;
this.uiComponents = [];
this.chartTemplates = [];
this.dynamicFields = [];
}
this.showSuccess('Chart type deleted successfully');
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting chart type:', error);
this.showError('Error deleting chart type: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTypeLoadingState = ClrLoadingState.ERROR;
}
});
}
selectChartTypeForEdit(chartType: ChartType): void {
this.selectedChartType = { ...chartType };
}
// UI Component Methods
loadUiComponents(chartTypeId: number): void {
this.uiComponentLoadingState = ClrLoadingState.LOADING;
this.http.get<UiComponent[]>(`${this.apiUrl}/ui-components/chart-type/${chartTypeId}`).subscribe({
next: (data) => {
this.uiComponents = data;
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading UI components:', error);
this.showError('Error loading UI components: ' + (error.error?.message || error.message || 'Unknown error'));
this.uiComponentLoadingState = ClrLoadingState.ERROR;
}
});
}
createUiComponent(): void {
if (!this.selectedChartType) {
this.showError('Please select a chart type first');
return;
}
if (!this.newUiComponent.componentName) {
this.showError('Component name is required');
return;
}
this.uiComponentLoadingState = ClrLoadingState.LOADING;
const uiComponentData = {
...this.newUiComponent,
chartType: { id: this.selectedChartType.id }
};
this.http.post<UiComponent>(`${this.apiUrl}/ui-components`, uiComponentData).subscribe({
next: (data) => {
this.uiComponents.push(data);
this.newUiComponent = {};
this.showAddUiComponentForm = false;
this.showSuccess('UI component created successfully');
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating UI component:', error);
this.showError('Error creating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
this.uiComponentLoadingState = ClrLoadingState.ERROR;
}
});
}
updateUiComponent(): void {
if (!this.selectedUiComponent || !this.selectedUiComponent.componentName) {
this.showError('Component name is required');
return;
}
this.uiComponentLoadingState = ClrLoadingState.LOADING;
this.http.put<UiComponent>(`${this.apiUrl}/ui-components/${this.selectedUiComponent.id}`, this.selectedUiComponent).subscribe({
next: (data) => {
const index = this.uiComponents.findIndex(uc => uc.id === data.id);
if (index !== -1) {
this.uiComponents[index] = data;
}
this.selectedUiComponent = null;
this.showSuccess('UI component updated successfully');
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating UI component:', error);
this.showError('Error updating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
this.uiComponentLoadingState = ClrLoadingState.ERROR;
}
});
}
deleteUiComponent(id: number): void {
if (!confirm('Are you sure you want to delete this UI component?')) {
return;
}
this.uiComponentLoadingState = ClrLoadingState.LOADING;
this.http.delete(`${this.apiUrl}/ui-components/${id}`).subscribe({
next: () => {
this.uiComponents = this.uiComponents.filter(uc => uc.id !== id);
if (this.selectedUiComponent && this.selectedUiComponent.id === id) {
this.selectedUiComponent = null;
}
this.showSuccess('UI component deleted successfully');
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting UI component:', error);
this.showError('Error deleting UI component: ' + (error.error?.message || error.message || 'Unknown error'));
this.uiComponentLoadingState = ClrLoadingState.ERROR;
}
});
}
selectUiComponentForEdit(uiComponent: UiComponent): void {
this.selectedUiComponent = { ...uiComponent };
}
// Component Property Methods
loadComponentProperties(componentId: number): void {
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
this.http.get<ComponentProperty[]>(`${this.apiUrl}/component-properties/component/${componentId}`).subscribe({
next: (data) => {
this.componentProperties = data;
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading component properties:', error);
this.showError('Error loading component properties: ' + (error.error?.message || error.message || 'Unknown error'));
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
}
});
}
createComponentProperty(): void {
if (!this.selectedUiComponent) {
this.showError('Please select a UI component first');
return;
}
if (!this.newComponentProperty.propertyName) {
this.showError('Property name is required');
return;
}
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
const componentPropertyData = {
...this.newComponentProperty,
component: { id: this.selectedUiComponent.id }
};
this.http.post<ComponentProperty>(`${this.apiUrl}/component-properties`, componentPropertyData).subscribe({
next: (data) => {
this.componentProperties.push(data);
this.newComponentProperty = {};
this.showAddComponentPropertyForm = false;
this.showSuccess('Component property created successfully');
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating component property:', error);
this.showError('Error creating component property: ' + (error.error?.message || error.message || 'Unknown error'));
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
}
});
}
updateComponentProperty(): void {
if (!this.selectedComponentProperty || !this.selectedComponentProperty.propertyName) {
this.showError('Property name is required');
return;
}
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
this.http.put<ComponentProperty>(`${this.apiUrl}/component-properties/${this.selectedComponentProperty.id}`, this.selectedComponentProperty).subscribe({
next: (data) => {
const index = this.componentProperties.findIndex(cp => cp.id === data.id);
if (index !== -1) {
this.componentProperties[index] = data;
}
this.selectedComponentProperty = null;
this.showSuccess('Component property updated successfully');
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating component property:', error);
this.showError('Error updating component property: ' + (error.error?.message || error.message || 'Unknown error'));
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
}
});
}
deleteComponentProperty(id: number): void {
if (!confirm('Are you sure you want to delete this component property?')) {
return;
}
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
this.http.delete(`${this.apiUrl}/component-properties/${id}`).subscribe({
next: () => {
this.componentProperties = this.componentProperties.filter(cp => cp.id !== id);
if (this.selectedComponentProperty && this.selectedComponentProperty.id === id) {
this.selectedComponentProperty = null;
}
this.showSuccess('Component property deleted successfully');
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting component property:', error);
this.showError('Error deleting component property: ' + (error.error?.message || error.message || 'Unknown error'));
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
}
});
}
selectComponentPropertyForEdit(componentProperty: ComponentProperty): void {
this.selectedComponentProperty = { ...componentProperty };
}
// Chart Template Methods
loadChartTemplates(chartTypeId: number): void {
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
this.http.get<ChartTemplate[]>(`${this.apiUrl}/chart-templates/chart-type/${chartTypeId}`).subscribe({
next: (data) => {
this.chartTemplates = data;
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading chart templates:', error);
this.showError('Error loading chart templates: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
}
});
}
createChartTemplate(): void {
if (!this.selectedChartType) {
this.showError('Please select a chart type first');
return;
}
if (!this.newChartTemplate.templateName) {
this.showError('Template name is required');
return;
}
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
const chartTemplateData = {
...this.newChartTemplate,
chartType: { id: this.selectedChartType.id }
};
this.http.post<ChartTemplate>(`${this.apiUrl}/chart-templates`, chartTemplateData).subscribe({
next: (data) => {
this.chartTemplates.push(data);
this.newChartTemplate = {};
this.showAddChartTemplateForm = false;
this.showSuccess('Chart template created successfully');
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating chart template:', error);
this.showError('Error creating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
}
});
}
updateChartTemplate(): void {
if (!this.selectedChartTemplate || !this.selectedChartTemplate.templateName) {
this.showError('Template name is required');
return;
}
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
this.http.put<ChartTemplate>(`${this.apiUrl}/chart-templates/${this.selectedChartTemplate.id}`, this.selectedChartTemplate).subscribe({
next: (data) => {
const index = this.chartTemplates.findIndex(ct => ct.id === data.id);
if (index !== -1) {
this.chartTemplates[index] = data;
}
this.selectedChartTemplate = null;
this.showSuccess('Chart template updated successfully');
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating chart template:', error);
this.showError('Error updating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
}
});
}
deleteChartTemplate(id: number): void {
if (!confirm('Are you sure you want to delete this chart template?')) {
return;
}
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
this.http.delete(`${this.apiUrl}/chart-templates/${id}`).subscribe({
next: () => {
this.chartTemplates = this.chartTemplates.filter(ct => ct.id !== id);
if (this.selectedChartTemplate && this.selectedChartTemplate.id === id) {
this.selectedChartTemplate = null;
}
this.showSuccess('Chart template deleted successfully');
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting chart template:', error);
this.showError('Error deleting chart template: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
}
});
}
selectChartTemplateForEdit(chartTemplate: ChartTemplate): void {
this.selectedChartTemplate = { ...chartTemplate };
}
// Dynamic Field Methods
loadDynamicFields(chartTypeId: number): void {
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
this.http.get<DynamicField[]>(`${this.apiUrl}/dynamic-fields/chart-type/${chartTypeId}`).subscribe({
next: (data) => {
this.dynamicFields = data;
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading dynamic fields:', error);
this.showError('Error loading dynamic fields: ' + (error.error?.message || error.message || 'Unknown error'));
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
}
});
}
createDynamicField(): void {
if (!this.selectedChartType) {
this.showError('Please select a chart type first');
return;
}
if (!this.newDynamicField.fieldName) {
this.showError('Field name is required');
return;
}
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
const dynamicFieldData = {
...this.newDynamicField,
chartType: { id: this.selectedChartType.id }
};
this.http.post<DynamicField>(`${this.apiUrl}/dynamic-fields`, dynamicFieldData).subscribe({
next: (data) => {
this.dynamicFields.push(data);
this.newDynamicField = {};
this.showAddDynamicFieldForm = false;
this.showSuccess('Dynamic field created successfully');
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating dynamic field:', error);
this.showError('Error creating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
}
});
}
updateDynamicField(): void {
if (!this.selectedDynamicField || !this.selectedDynamicField.fieldName) {
this.showError('Field name is required');
return;
}
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
this.http.put<DynamicField>(`${this.apiUrl}/dynamic-fields/${this.selectedDynamicField.id}`, this.selectedDynamicField).subscribe({
next: (data) => {
const index = this.dynamicFields.findIndex(df => df.id === data.id);
if (index !== -1) {
this.dynamicFields[index] = data;
}
this.selectedDynamicField = null;
this.showSuccess('Dynamic field updated successfully');
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating dynamic field:', error);
this.showError('Error updating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
}
});
}
deleteDynamicField(id: number): void {
if (!confirm('Are you sure you want to delete this dynamic field?')) {
return;
}
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
this.http.delete(`${this.apiUrl}/dynamic-fields/${id}`).subscribe({
next: () => {
this.dynamicFields = this.dynamicFields.filter(df => df.id !== id);
if (this.selectedDynamicField && this.selectedDynamicField.id === id) {
this.selectedDynamicField = null;
}
this.showSuccess('Dynamic field deleted successfully');
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting dynamic field:', error);
this.showError('Error deleting dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
}
});
}
selectDynamicFieldForEdit(dynamicField: DynamicField): void {
this.selectedDynamicField = { ...dynamicField };
}
// Helper methods
onChartTypeSelect(chartType: ChartType): void {
this.selectedChartType = chartType;
this.loadUiComponents(chartType.id);
this.loadChartTemplates(chartType.id);
this.loadDynamicFields(chartType.id);
}
resetForms(): void {
this.newChartType = {};
this.newUiComponent = {};
this.newComponentProperty = {};
this.newChartTemplate = {};
this.newDynamicField = {};
this.showAddChartTypeForm = false;
this.showAddUiComponentForm = false;
this.showAddComponentPropertyForm = false;
this.showAddChartTemplateForm = false;
this.showAddDynamicFieldForm = false;
this.selectedChartType = null;
this.selectedUiComponent = null;
this.selectedComponentProperty = null;
this.selectedChartTemplate = null;
this.selectedDynamicField = null;
}
}

View File

@@ -0,0 +1,495 @@
<div class="chart-config-manager">
<h2>Chart Configuration Manager</h2>
<!-- Test message to verify component is loading -->
<div class="alert alert-info">
<div class="alert-item">
<span class="alert-text">Chart Config Manager Component Loaded Successfully</span>
</div>
</div>
<!-- Error and Success Messages -->
<div class="alert alert-danger" *ngIf="errorMessage">
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ errorMessage }}</span>
</div>
</div>
<div class="alert alert-success" *ngIf="successMessage">
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ successMessage }}</span>
</div>
</div>
<!-- Chart Types Section -->
<clr-tab>
<button clrTabLink>Chart Types</button>
<clr-tab-content>
<div class="card">
<div class="card-header">
<h3>Chart Types</h3>
<button class="btn btn-sm btn-primary" (click)="showAddChartTypeForm = true">
<cds-icon shape="plus"></cds-icon> Add Chart Type
</button>
</div>
<!-- Add Chart Type Form -->
<div class="card-block" *ngIf="showAddChartTypeForm">
<app-chart-type-form
[chartType]="newChartType"
(save)="createChartType()"
(cancel)="showAddChartTypeForm = false">
</app-chart-type-form>
</div>
<!-- Chart Types Table -->
<div class="card-block">
<clr-datagrid [clrDgLoading]="chartTypeLoadingState === ClrLoadingState.LOADING">
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Display Name</clr-dg-column>
<clr-dg-column>Description</clr-dg-column>
<clr-dg-column>Status</clr-dg-column>
<clr-dg-column>Created At</clr-dg-column>
<clr-dg-column>Updated At</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let chartType of chartTypes" [clrDgItem]="chartType">
<clr-dg-cell>{{chartType.name}}</clr-dg-cell>
<clr-dg-cell>{{chartType.displayName}}</clr-dg-cell>
<clr-dg-cell>{{chartType.description}}</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="chartType.isActive" [class.label-danger]="!chartType.isActive">
{{chartType.isActive ? 'Active' : 'Inactive'}}
</span>
</clr-dg-cell>
<clr-dg-cell>{{chartType.createdAt | date:'short'}}</clr-dg-cell>
<clr-dg-cell>{{chartType.updatedAt | date:'short'}}</clr-dg-cell>
<clr-dg-cell>
<button class="btn btn-sm btn-icon" (click)="selectChartTypeForEdit(chartType)" title="Edit" [disabled]="chartTypeLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="chartTypeLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="chartTypeLoadingState !== ClrLoadingState.LOADING" shape="pencil"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteChartType(chartType.id)" title="Delete" [disabled]="chartTypeLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="chartTypeLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="chartTypeLoadingState !== ClrLoadingState.LOADING" shape="trash"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="onChartTypeSelect(chartType)" title="View Details" [disabled]="chartTypeLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="chartTypeLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="chartTypeLoadingState !== ClrLoadingState.LOADING" shape="eye"></cds-icon>
</button>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{chartTypes.length}} chart type(s)
</clr-dg-footer>
</clr-datagrid>
</div>
<!-- Edit Chart Type Form -->
<div class="card-block" *ngIf="selectedChartType">
<h4>Edit Chart Type</h4>
<app-chart-type-form
[chartType]="selectedChartType"
[isEdit]="true"
(save)="updateChartType()"
(cancel)="selectedChartType = null">
</app-chart-type-form>
</div>
</div>
</clr-tab-content>
</clr-tab>
<!-- UI Components Section -->
<clr-tab *ngIf="selectedChartType">
<button clrTabLink>UI Components</button>
<clr-tab-content>
<div class="card">
<div class="card-header">
<h3>UI Components for {{selectedChartType?.name}}</h3>
<button class="btn btn-sm btn-primary" (click)="showAddUiComponentForm = true">
<cds-icon shape="plus"></cds-icon> Add UI Component
</button>
</div>
<!-- Add UI Component Form -->
<div class="card-block" *ngIf="showAddUiComponentForm">
<app-ui-component-form
[uiComponent]="newUiComponent"
(save)="createUiComponent()"
(cancel)="showAddUiComponentForm = false">
</app-ui-component-form>
</div>
<!-- UI Components Table -->
<div class="card-block">
<clr-datagrid [clrDgLoading]="uiComponentLoadingState === ClrLoadingState.LOADING">
<clr-dg-column>Component Name</clr-dg-column>
<clr-dg-column>Component Type</clr-dg-column>
<clr-dg-column>Display Label</clr-dg-column>
<clr-dg-column>Required</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let uiComponent of uiComponents" [clrDgItem]="uiComponent">
<clr-dg-cell>{{uiComponent.componentName}}</clr-dg-cell>
<clr-dg-cell>{{uiComponent.componentType}}</clr-dg-cell>
<clr-dg-cell>{{uiComponent.displayLabel}}</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="uiComponent.isRequired" [class.label-danger]="!uiComponent.isRequired">
{{uiComponent.isRequired ? 'Yes' : 'No'}}
</span>
</clr-dg-cell>
<clr-dg-cell class="action-cell">
<div class="action-buttons">
<button class="btn btn-sm btn-icon" (click)="selectUiComponentForEdit(uiComponent)" title="Edit" [disabled]="uiComponentLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="uiComponentLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="uiComponentLoadingState !== ClrLoadingState.LOADING" shape="pencil" aria-label="Edit"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteUiComponent(uiComponent.id)" title="Delete" [disabled]="uiComponentLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="uiComponentLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="uiComponentLoadingState !== ClrLoadingState.LOADING" shape="trash" aria-label="Delete"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="onUiComponentSelect(uiComponent)" title="View Properties" [disabled]="uiComponentLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="uiComponentLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="uiComponentLoadingState !== ClrLoadingState.LOADING" shape="eye" aria-label="View Properties"></cds-icon>
</button>
</div>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{uiComponents.length}} UI component(s)
</clr-dg-footer>
</clr-datagrid>
</div>
<!-- Edit UI Component Form -->
<div class="card-block" *ngIf="selectedUiComponent">
<h4>Edit UI Component</h4>
<app-ui-component-form
[uiComponent]="selectedUiComponent"
[isEdit]="true"
(save)="updateUiComponent()"
(cancel)="selectedUiComponent = null">
</app-ui-component-form>
</div>
</div>
</clr-tab-content>
</clr-tab>
<!-- Component Properties Section -->
<clr-tab *ngIf="selectedUiComponent">
<button clrTabLink>Component Properties</button>
<clr-tab-content>
<div class="card">
<div class="card-header">
<h3>Properties for {{selectedUiComponent?.componentName}}</h3>
<button class="btn btn-sm btn-primary" (click)="showAddComponentPropertyForm = true">
<cds-icon shape="plus"></cds-icon> Add Property
</button>
</div>
<!-- Add Component Property Form -->
<div class="card-block" *ngIf="showAddComponentPropertyForm">
<app-component-property-form
[componentProperty]="newComponentProperty"
(save)="createComponentProperty()"
(cancel)="showAddComponentPropertyForm = false">
</app-component-property-form>
</div>
<!-- Component Properties Table -->
<div class="card-block">
<clr-datagrid [clrDgLoading]="componentPropertyLoadingState === ClrLoadingState.LOADING">
<clr-dg-column>Property Name</clr-dg-column>
<clr-dg-column>Property Value</clr-dg-column>
<clr-dg-column>Property Type</clr-dg-column>
<clr-dg-column>Created At</clr-dg-column>
<clr-dg-column>Updated At</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let property of componentProperties" [clrDgItem]="property">
<clr-dg-cell>{{property.propertyName}}</clr-dg-cell>
<clr-dg-cell>{{property.propertyValue}}</clr-dg-cell>
<clr-dg-cell>{{property.propertyType}}</clr-dg-cell>
<clr-dg-cell>{{property.createdAt | date:'short'}}</clr-dg-cell>
<clr-dg-cell>{{property.updatedAt | date:'short'}}</clr-dg-cell>
<clr-dg-cell>
<button class="btn btn-sm btn-icon" (click)="selectComponentPropertyForEdit(property)" title="Edit" [disabled]="componentPropertyLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="componentPropertyLoadingState !== ClrLoadingState.LOADING" shape="pencil"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteComponentProperty(property.id)" title="Delete" [disabled]="componentPropertyLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="componentPropertyLoadingState !== ClrLoadingState.LOADING" shape="trash"></cds-icon>
</button>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{componentProperties.length}} propertie(s)
</clr-dg-footer>
</clr-datagrid>
</div>
<!-- Edit Component Property Form -->
<div class="card-block" *ngIf="selectedComponentProperty">
<h4>Edit Component Property</h4>
<app-component-property-form
[componentProperty]="selectedComponentProperty"
[isEdit]="true"
(save)="updateComponentProperty()"
(cancel)="selectedComponentProperty = null">
</app-component-property-form>
</div>
</div>
</clr-tab-content>
</clr-tab>
<!-- Chart Templates Section -->
<clr-tab *ngIf="selectedChartType">
<button clrTabLink>Chart Templates</button>
<clr-tab-content>
<div class="card">
<div class="card-header">
<h3>Templates for {{selectedChartType?.name}}</h3>
<button class="btn btn-sm btn-primary" (click)="showAddChartTemplateForm = true">
<cds-icon shape="plus"></cds-icon> Add Template
</button>
</div>
<!-- Add Chart Template Form -->
<div class="card-block" *ngIf="showAddChartTemplateForm">
<app-chart-template-form
[chartTemplate]="newChartTemplate"
(save)="createChartTemplate()"
(cancel)="showAddChartTemplateForm = false">
</app-chart-template-form>
</div>
<!-- Chart Templates Table -->
<div class="card-block">
<clr-datagrid [clrDgLoading]="chartTemplateLoadingState === ClrLoadingState.LOADING">
<clr-dg-column>Template Name</clr-dg-column>
<clr-dg-column>Is Default</clr-dg-column>
<clr-dg-column>Created At</clr-dg-column>
<clr-dg-column>Updated At</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let template of chartTemplates" [clrDgItem]="template">
<clr-dg-cell>{{template.templateName}}</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="template.isDefault" [class.label-danger]="!template.isDefault">
{{template.isDefault ? 'Yes' : 'No'}}
</span>
</clr-dg-cell>
<clr-dg-cell>{{template.createdAt | date:'short'}}</clr-dg-cell>
<clr-dg-cell>{{template.updatedAt | date:'short'}}</clr-dg-cell>
<clr-dg-cell>
<button class="btn btn-sm btn-icon" (click)="selectChartTemplateForEdit(template)" title="Edit" [disabled]="chartTemplateLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="chartTemplateLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="chartTemplateLoadingState !== ClrLoadingState.LOADING" shape="pencil"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteChartTemplate(template.id)" title="Delete" [disabled]="chartTemplateLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="chartTemplateLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="chartTemplateLoadingState !== ClrLoadingState.LOADING" shape="trash"></cds-icon>
</button>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{chartTemplates.length}} template(s)
</clr-dg-footer>
</clr-datagrid>
</div>
<!-- Edit Chart Template Form -->
<div class="card-block" *ngIf="selectedChartTemplate">
<h4>Edit Chart Template</h4>
<app-chart-template-form
[chartTemplate]="selectedChartTemplate"
[isEdit]="true"
(save)="updateChartTemplate()"
(cancel)="selectedChartTemplate = null">
</app-chart-template-form>
</div>
</div>
</clr-tab-content>
</clr-tab>
<!-- Dynamic Fields Section -->
<clr-tab *ngIf="selectedChartType">
<button clrTabLink>Dynamic Fields</button>
<clr-tab-content>
<div class="card">
<div class="card-header">
<h3>Dynamic Fields for {{selectedChartType?.name}}</h3>
<button class="btn btn-sm btn-primary" (click)="showAddDynamicFieldForm = true">
<cds-icon shape="plus"></cds-icon> Add Field
</button>
</div>
<!-- Add Dynamic Field Form -->
<div class="card-block" *ngIf="showAddDynamicFieldForm">
<form clrForm>
<clr-input-container>
<label>Field Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldName" name="fieldName" required />
</clr-input-container>
<clr-input-container>
<label>Field Label</label>
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldLabel" name="fieldLabel" />
</clr-input-container>
<clr-input-container>
<label>Field Type</label>
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldType" name="fieldType" />
</clr-input-container>
<clr-textarea-container>
<label>Field Options</label>
<textarea clrTextarea [(ngModel)]="newDynamicField.fieldOptions" name="fieldOptions"></textarea>
</clr-textarea-container>
<clr-input-container>
<label>Sort Order</label>
<input clrInput type="number" [(ngModel)]="newDynamicField.sortOrder" name="fieldSortOrder" />
</clr-input-container>
<clr-checkbox-container>
<label>Is Required</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.isRequired" name="fieldIsRequired" />
<label>Required</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<clr-checkbox-container>
<label>Show in UI</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.showInUi" name="fieldShowInUi" />
<label>Show in UI</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" (click)="createDynamicField()" [disabled]="!newDynamicField.fieldName || dynamicFieldLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="dynamicFieldLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
Save
</button>
<button class="btn" (click)="showAddDynamicFieldForm = false">Cancel</button>
</div>
</form>
</div>
<!-- Dynamic Fields Table -->
<div class="card-block">
<clr-datagrid [clrDgLoading]="dynamicFieldLoadingState === ClrLoadingState.LOADING">
<clr-dg-column>Field Name</clr-dg-column>
<clr-dg-column>Field Label</clr-dg-column>
<clr-dg-column>Field Type</clr-dg-column>
<clr-dg-column>Required</clr-dg-column>
<clr-dg-column>Show in UI</clr-dg-column>
<clr-dg-column>Sort Order</clr-dg-column>
<clr-dg-column>Created At</clr-dg-column>
<clr-dg-column>Updated At</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let field of dynamicFields" [clrDgItem]="field">
<clr-dg-cell>{{field.fieldName}}</clr-dg-cell>
<clr-dg-cell>{{field.fieldLabel}}</clr-dg-cell>
<clr-dg-cell>{{field.fieldType}}</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="field.isRequired" [class.label-danger]="!field.isRequired">
{{field.isRequired ? 'Yes' : 'No'}}
</span>
</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="field.showInUi" [class.label-danger]="!field.showInUi">
{{field.showInUi ? 'Yes' : 'No'}}
</span>
</clr-dg-cell>
<clr-dg-cell>{{field.sortOrder}}</clr-dg-cell>
<clr-dg-cell>{{field.createdAt | date:'short'}}</clr-dg-cell>
<clr-dg-cell>{{field.updatedAt | date:'short'}}</clr-dg-cell>
<clr-dg-cell>
<button class="btn btn-sm btn-icon" (click)="selectDynamicFieldForEdit(field)" title="Edit">
<cds-icon shape="pencil"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteDynamicField(field.id)" title="Delete">
<cds-icon shape="trash"></cds-icon>
</button>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{dynamicFields.length}} field(s)
</clr-dg-footer>
</clr-datagrid>
</div>
<!-- Edit Dynamic Field Form -->
<div class="card-block" *ngIf="selectedDynamicField">
<h4>Edit Dynamic Field</h4>
<form clrForm>
<clr-input-container>
<label>Field Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldName" name="editFieldName" required />
</clr-input-container>
<clr-input-container>
<label>Field Label</label>
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldLabel" name="editFieldLabel" />
</clr-input-container>
<clr-input-container>
<label>Field Type</label>
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldType" name="editFieldType" />
</clr-input-container>
<clr-textarea-container>
<label>Field Options</label>
<textarea clrTextarea [(ngModel)]="selectedDynamicField.fieldOptions" name="editFieldOptions"></textarea>
</clr-textarea-container>
<clr-input-container>
<label>Sort Order</label>
<input clrInput type="number" [(ngModel)]="selectedDynamicField.sortOrder" name="editFieldSortOrder" />
</clr-input-container>
<clr-checkbox-container>
<label>Is Required</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.isRequired" name="editFieldIsRequired" />
<label>Required</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<clr-checkbox-container>
<label>Show in UI</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.showInUi" name="editFieldShowInUi" />
<label>Show in UI</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" (click)="updateDynamicField()" [disabled]="!selectedDynamicField.fieldName || dynamicFieldLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="dynamicFieldLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
Update
</button>
<button class="btn" (click)="selectedDynamicField = null">Cancel</button>
</div>
</form>
</div>
</div>
</clr-tab-content>
</clr-tab>
</div>

View File

@@ -0,0 +1,97 @@
.chart-config-manager {
padding: 20px;
.card {
margin-bottom: 20px;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin: 0;
}
}
.card-block {
padding: 15px;
form {
margin-bottom: 20px;
}
}
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
clr-datagrid {
min-height: 200px;
}
.label {
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
}
.label-success {
background-color: #318700;
color: white;
}
.label-danger {
background-color: #e62200;
color: white;
}
.required {
color: red;
}
.alert {
margin-bottom: 20px;
}
clr-tab-content {
padding: 15px 0;
}
}
.action-cell {
min-width: 120px;
}
.action-buttons {
display: flex;
gap: 5px;
flex-wrap: nowrap;
align-items: center;
}
.action-buttons .btn-icon {
min-width: 36px;
display: flex;
align-items: center;
justify-content: center;
}
// Ensure icons are visible
cds-icon {
display: inline-block !important;
}
clr-spinner {
margin: 0;
}cds-icon {
display: inline-block !important;
}
clr-spinner {
margin: 0;
}

View File

@@ -0,0 +1,710 @@
import { Component, OnInit } from '@angular/core';
import { ClrLoadingState } from '@clr/angular';
import { ChartTypeService } from '../chart-type-manager/chart-type.service';
import { UiComponentService } from './ui-component.service';
import { ComponentPropertyService } from './component-property.service';
import { ChartTemplateService } from './chart-template.service';
import { DynamicFieldService } from './dynamic-field.service';
export interface ChartType {
id: number;
name: string;
displayName: string;
description: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
export interface UiComponent {
id: number;
chartType: ChartType;
componentName: string;
componentType: string;
displayLabel: string;
placeholder: string;
isRequired: boolean;
sortOrder: number;
createdAt: string;
updatedAt: string;
}
export interface ComponentProperty {
id: number;
component: UiComponent;
propertyName: string;
propertyValue: string;
propertyType: string;
createdAt: string;
updatedAt: string;
}
export interface ChartTemplate {
id: number;
chartType: ChartType;
templateName: string;
templateHtml: string;
templateCss: string;
isDefault: boolean;
createdAt: string;
updatedAt: string;
}
export interface DynamicField {
id: number;
chartType: ChartType;
fieldName: string;
fieldLabel: string;
fieldType: string;
fieldOptions: string;
isRequired: boolean;
showInUi: boolean;
sortOrder: number;
createdAt: string;
updatedAt: string;
}
@Component({
selector: 'app-chart-config-manager',
templateUrl: './chart-config-manager.component.html',
styleUrls: ['./chart-config-manager.component.scss']
})
export class ChartConfigManagerComponent implements OnInit {
// Chart Types
chartTypes: ChartType[] = [];
selectedChartType: ChartType | null = null;
newChartType: Partial<ChartType> = {};
showAddChartTypeForm = false;
chartTypeLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
// UI Components
uiComponents: UiComponent[] = [];
selectedUiComponent: UiComponent | null = null;
newUiComponent: Partial<UiComponent> = {};
showAddUiComponentForm = false;
uiComponentLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
// Component Properties
componentProperties: ComponentProperty[] = [];
selectedComponentProperty: ComponentProperty | null = null;
newComponentProperty: Partial<ComponentProperty> = {};
showAddComponentPropertyForm = false;
componentPropertyLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
// Chart Templates
chartTemplates: ChartTemplate[] = [];
selectedChartTemplate: ChartTemplate | null = null;
newChartTemplate: Partial<ChartTemplate> = {};
showAddChartTemplateForm = false;
chartTemplateLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
// Dynamic Fields
dynamicFields: DynamicField[] = [];
selectedDynamicField: DynamicField | null = null;
newDynamicField: Partial<DynamicField> = {};
showAddDynamicFieldForm = false;
dynamicFieldLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
// Make ClrLoadingState available to template
readonly ClrLoadingState = ClrLoadingState;
// Error handling
errorMessage: string | null = null;
successMessage: string | null = null;
constructor(
private chartTypeService: ChartTypeService,
private uiComponentService: UiComponentService,
private componentPropertyService: ComponentPropertyService,
private chartTemplateService: ChartTemplateService,
private dynamicFieldService: DynamicFieldService
) { }
ngOnInit(): void {
console.log('ChartConfigManagerComponent initialized');
this.loadChartTypes();
}
// Show error message
private showError(message: string): void {
this.errorMessage = message;
setTimeout(() => {
this.errorMessage = null;
}, 5000);
}
// Show success message
private showSuccess(message: string): void {
this.successMessage = message;
setTimeout(() => {
this.successMessage = null;
}, 3000);
}
// Chart Type Methods
loadChartTypes(): void {
this.chartTypeLoadingState = ClrLoadingState.LOADING;
console.log('Loading chart types...');
this.chartTypeService.getAllChartTypes().subscribe({
next: (data) => {
console.log('Chart types loaded:', data);
this.chartTypes = data;
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading chart types:', error);
this.showError('Error loading chart types: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTypeLoadingState = ClrLoadingState.ERROR;
}
});
}
createChartType(): void {
if (!this.newChartType.name) {
this.showError('Chart type name is required');
return;
}
this.chartTypeLoadingState = ClrLoadingState.LOADING;
this.chartTypeService.createChartType(this.newChartType).subscribe({
next: (data) => {
this.chartTypes.push(data);
this.newChartType = {};
this.showAddChartTypeForm = false;
this.showSuccess('Chart type created successfully');
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating chart type:', error);
this.showError('Error creating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTypeLoadingState = ClrLoadingState.ERROR;
}
});
}
updateChartType(): void {
if (!this.selectedChartType || !this.selectedChartType.name) {
this.showError('Chart type name is required');
return;
}
this.chartTypeLoadingState = ClrLoadingState.LOADING;
this.chartTypeService.updateChartType(this.selectedChartType.id, this.selectedChartType).subscribe({
next: (data) => {
const index = this.chartTypes.findIndex(ct => ct.id === data.id);
if (index !== -1) {
this.chartTypes[index] = data;
}
this.selectedChartType = null;
this.showSuccess('Chart type updated successfully');
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating chart type:', error);
this.showError('Error updating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTypeLoadingState = ClrLoadingState.ERROR;
}
});
}
deleteChartType(id: number): void {
if (!confirm('Are you sure you want to delete this chart type? This will also delete all related UI components, templates, and fields.')) {
return;
}
this.chartTypeLoadingState = ClrLoadingState.LOADING;
this.chartTypeService.deleteChartType(id).subscribe({
next: () => {
this.chartTypes = this.chartTypes.filter(ct => ct.id !== id);
// Clear related data if the deleted chart type was selected
if (this.selectedChartType && this.selectedChartType.id === id) {
this.selectedChartType = null;
this.uiComponents = [];
this.chartTemplates = [];
this.dynamicFields = [];
}
this.showSuccess('Chart type deleted successfully');
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting chart type:', error);
this.showError('Error deleting chart type: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTypeLoadingState = ClrLoadingState.ERROR;
}
});
}
selectChartTypeForEdit(chartType: ChartType): void {
this.selectedChartType = { ...chartType };
}
// UI Component Methods
loadUiComponents(chartTypeId: number): void {
if (!chartTypeId) {
this.uiComponents = [];
return;
}
this.uiComponentLoadingState = ClrLoadingState.LOADING;
this.uiComponentService.getUiComponentsByChartType(chartTypeId).subscribe({
next: (data) => {
this.uiComponents = data;
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading UI components:', error);
this.showError('Error loading UI components: ' + (error.error?.message || error.message || 'Unknown error'));
this.uiComponentLoadingState = ClrLoadingState.ERROR;
}
});
}
createUiComponent(): void {
if (!this.selectedChartType) {
this.showError('Please select a chart type first');
return;
}
if (!this.newUiComponent.componentName) {
this.showError('Component name is required');
return;
}
this.uiComponentLoadingState = ClrLoadingState.LOADING;
// Create a complete chartType object with only the ID
const chartTypeWithId = {
id: this.selectedChartType.id,
name: '',
displayName: '',
description: '',
isActive: true,
createdAt: '',
updatedAt: ''
};
const uiComponentData = {
...this.newUiComponent,
chartType: chartTypeWithId
};
this.uiComponentService.createUiComponent(uiComponentData).subscribe({
next: (data) => {
this.uiComponents.push(data);
this.newUiComponent = {};
this.showAddUiComponentForm = false;
this.showSuccess('UI component created successfully');
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating UI component:', error);
this.showError('Error creating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
this.uiComponentLoadingState = ClrLoadingState.ERROR;
}
});
}
updateUiComponent(): void {
if (!this.selectedUiComponent || !this.selectedUiComponent.componentName) {
this.showError('Component name is required');
return;
}
this.uiComponentLoadingState = ClrLoadingState.LOADING;
this.uiComponentService.updateUiComponent(this.selectedUiComponent.id, this.selectedUiComponent).subscribe({
next: (data) => {
const index = this.uiComponents.findIndex(uc => uc.id === data.id);
if (index !== -1) {
this.uiComponents[index] = data;
}
this.selectedUiComponent = null;
this.showSuccess('UI component updated successfully');
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating UI component:', error);
this.showError('Error updating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
this.uiComponentLoadingState = ClrLoadingState.ERROR;
}
});
}
deleteUiComponent(id: number): void {
if (!confirm('Are you sure you want to delete this UI component?')) {
return;
}
this.uiComponentLoadingState = ClrLoadingState.LOADING;
this.uiComponentService.deleteUiComponent(id).subscribe({
next: () => {
this.uiComponents = this.uiComponents.filter(uc => uc.id !== id);
this.showSuccess('UI component deleted successfully');
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting UI component:', error);
this.showError('Error deleting UI component: ' + (error.error?.message || error.message || 'Unknown error'));
this.uiComponentLoadingState = ClrLoadingState.ERROR;
}
});
}
selectUiComponentForEdit(uiComponent: UiComponent): void {
this.selectedUiComponent = { ...uiComponent };
}
// Component Property Methods
loadComponentProperties(componentId: number): void {
if (!componentId) {
this.componentProperties = [];
return;
}
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
this.componentPropertyService.getComponentPropertiesByComponent(componentId).subscribe({
next: (data) => {
this.componentProperties = data;
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading component properties:', error);
this.showError('Error loading component properties: ' + (error.error?.message || error.message || 'Unknown error'));
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
}
});
}
createComponentProperty(): void {
if (!this.selectedUiComponent) {
this.showError('Please select a UI component first');
return;
}
if (!this.newComponentProperty.propertyName) {
this.showError('Property name is required');
return;
}
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
// Create a complete component object with only the ID
const componentWithId = {
id: this.selectedUiComponent.id
} as UiComponent;
const componentPropertyData = {
...this.newComponentProperty,
component: componentWithId
};
this.componentPropertyService.createComponentProperty(componentPropertyData).subscribe({
next: (data) => {
this.componentProperties.push(data);
this.newComponentProperty = {};
this.showAddComponentPropertyForm = false;
this.showSuccess('Component property created successfully');
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating component property:', error);
this.showError('Error creating component property: ' + (error.error?.message || error.message || 'Unknown error'));
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
}
});
}
updateComponentProperty(): void {
if (!this.selectedComponentProperty || !this.selectedComponentProperty.propertyName) {
this.showError('Property name is required');
return;
}
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
this.componentPropertyService.updateComponentProperty(this.selectedComponentProperty.id, this.selectedComponentProperty).subscribe({
next: (data) => {
const index = this.componentProperties.findIndex(cp => cp.id === data.id);
if (index !== -1) {
this.componentProperties[index] = data;
}
this.selectedComponentProperty = null;
this.showSuccess('Component property updated successfully');
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating component property:', error);
this.showError('Error updating component property: ' + (error.error?.message || error.message || 'Unknown error'));
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
}
});
}
deleteComponentProperty(id: number): void {
if (!confirm('Are you sure you want to delete this component property?')) {
return;
}
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
this.componentPropertyService.deleteComponentProperty(id).subscribe({
next: () => {
this.componentProperties = this.componentProperties.filter(cp => cp.id !== id);
this.showSuccess('Component property deleted successfully');
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting component property:', error);
this.showError('Error deleting component property: ' + (error.error?.message || error.message || 'Unknown error'));
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
}
});
}
selectComponentPropertyForEdit(componentProperty: ComponentProperty): void {
this.selectedComponentProperty = { ...componentProperty };
}
// Chart Template Methods
loadChartTemplates(chartTypeId: number): void {
if (!chartTypeId) {
this.chartTemplates = [];
return;
}
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
this.chartTemplateService.getChartTemplatesByChartType(chartTypeId).subscribe({
next: (data) => {
this.chartTemplates = data;
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading chart templates:', error);
this.showError('Error loading chart templates: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
}
});
}
createChartTemplate(): void {
if (!this.selectedChartType) {
this.showError('Please select a chart type first');
return;
}
if (!this.newChartTemplate.templateName) {
this.showError('Template name is required');
return;
}
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
// Remove the chartType from the template data since we're passing it as a parameter
const chartTemplateData = { ...this.newChartTemplate };
// Remove chartType property if it exists
delete (chartTemplateData as any).chartType;
this.chartTemplateService.createChartTemplate(chartTemplateData, this.selectedChartType.id).subscribe({
next: (data) => {
this.chartTemplates.push(data);
this.newChartTemplate = {};
this.showAddChartTemplateForm = false;
this.showSuccess('Chart template created successfully');
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating chart template:', error);
this.showError('Error creating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
}
});
}
updateChartTemplate(): void {
if (!this.selectedChartTemplate || !this.selectedChartTemplate.templateName) {
this.showError('Template name is required');
return;
}
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
this.chartTemplateService.updateChartTemplate(this.selectedChartTemplate.id, this.selectedChartTemplate).subscribe({
next: (data) => {
const index = this.chartTemplates.findIndex(ct => ct.id === data.id);
if (index !== -1) {
this.chartTemplates[index] = data;
}
this.selectedChartTemplate = null;
this.showSuccess('Chart template updated successfully');
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating chart template:', error);
this.showError('Error updating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
}
});
}
deleteChartTemplate(id: number): void {
if (!confirm('Are you sure you want to delete this chart template?')) {
return;
}
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
this.chartTemplateService.deleteChartTemplate(id).subscribe({
next: () => {
this.chartTemplates = this.chartTemplates.filter(ct => ct.id !== id);
this.showSuccess('Chart template deleted successfully');
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting chart template:', error);
this.showError('Error deleting chart template: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
}
});
}
selectChartTemplateForEdit(chartTemplate: ChartTemplate): void {
this.selectedChartTemplate = { ...chartTemplate };
}
// Dynamic Field Methods
loadDynamicFields(chartTypeId: number): void {
if (!chartTypeId) {
this.dynamicFields = [];
return;
}
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
this.dynamicFieldService.getDynamicFieldsByChartType(chartTypeId).subscribe({
next: (data) => {
this.dynamicFields = data;
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading dynamic fields:', error);
this.showError('Error loading dynamic fields: ' + (error.error?.message || error.message || 'Unknown error'));
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
}
});
}
createDynamicField(): void {
if (!this.selectedChartType) {
this.showError('Please select a chart type first');
return;
}
if (!this.newDynamicField.fieldName) {
this.showError('Field name is required');
return;
}
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
// Remove the chartType from the dynamic field data since we're passing it as a parameter
const dynamicFieldData = { ...this.newDynamicField };
// Remove chartType property if it exists
delete (dynamicFieldData as any).chartType;
this.dynamicFieldService.createDynamicField(dynamicFieldData, this.selectedChartType.id).subscribe({
next: (data) => {
this.dynamicFields.push(data);
this.newDynamicField = {};
this.showAddDynamicFieldForm = false;
this.showSuccess('Dynamic field created successfully');
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating dynamic field:', error);
this.showError('Error creating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
}
});
}
updateDynamicField(): void {
if (!this.selectedDynamicField || !this.selectedDynamicField.fieldName) {
this.showError('Field name is required');
return;
}
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
this.dynamicFieldService.updateDynamicField(this.selectedDynamicField.id, this.selectedDynamicField).subscribe({
next: (data) => {
const index = this.dynamicFields.findIndex(df => df.id === data.id);
if (index !== -1) {
this.dynamicFields[index] = data;
}
this.selectedDynamicField = null;
this.showSuccess('Dynamic field updated successfully');
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating dynamic field:', error);
this.showError('Error updating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
}
});
}
deleteDynamicField(id: number): void {
if (!confirm('Are you sure you want to delete this dynamic field?')) {
return;
}
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
this.dynamicFieldService.deleteDynamicField(id).subscribe({
next: () => {
this.dynamicFields = this.dynamicFields.filter(df => df.id !== id);
this.showSuccess('Dynamic field deleted successfully');
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting dynamic field:', error);
this.showError('Error deleting dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
}
});
}
selectDynamicFieldForEdit(dynamicField: DynamicField): void {
this.selectedDynamicField = { ...dynamicField };
}
// Helper methods
onChartTypeSelect(chartType: ChartType): void {
this.selectedChartType = chartType;
this.loadUiComponents(chartType.id);
this.loadChartTemplates(chartType.id);
this.loadDynamicFields(chartType.id);
}
onUiComponentSelect(uiComponent: UiComponent): void {
this.selectedUiComponent = uiComponent;
this.loadComponentProperties(uiComponent.id);
// Scroll to the Component Properties tab
setTimeout(() => {
const element = document.querySelector('clr-tab[ng-reflect-ng-if="true"] button[clr-tab-link]:nth-child(3)');
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, 100);
}
resetForms(): void {
this.newChartType = {};
this.newUiComponent = {};
this.newComponentProperty = {};
this.newChartTemplate = {};
this.newDynamicField = {};
this.showAddChartTypeForm = false;
this.showAddUiComponentForm = false;
this.showAddComponentPropertyForm = false;
this.showAddChartTemplateForm = false;
this.showAddDynamicFieldForm = false;
this.selectedChartType = null;
this.selectedUiComponent = null;
this.selectedComponentProperty = null;
this.selectedChartTemplate = null;
this.selectedDynamicField = null;
}
}

View File

@@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiRequestService } from 'src/app/services/api/api-request.service';
import { ChartTemplate } from './chart-config-manager.component';
@Injectable({
providedIn: 'root'
})
export class ChartTemplateService {
private chartTemplatesUrl = 'api/chart-templates';
constructor(private apiRequest: ApiRequestService) { }
// Get all chart templates for a chart type
getChartTemplatesByChartType(chartTypeId: number): Observable<ChartTemplate[]> {
const url = `${this.chartTemplatesUrl}/chart-type/${chartTypeId}`;
return this.apiRequest.get(url);
}
// Get chart template by ID
getChartTemplateById(id: number): Observable<ChartTemplate> {
const url = `${this.chartTemplatesUrl}/${id}`;
return this.apiRequest.get(url);
}
// Create new chart template with optional chart type ID as parameter
createChartTemplate(chartTemplate: Partial<ChartTemplate>, chartTypeId?: number): Observable<ChartTemplate> {
let url = this.chartTemplatesUrl;
if (chartTypeId) {
url = `${this.chartTemplatesUrl}?chartTypeId=${chartTypeId}`;
}
return this.apiRequest.post(url, chartTemplate);
}
// Update chart template
updateChartTemplate(id: number, chartTemplate: ChartTemplate): Observable<ChartTemplate> {
const url = `${this.chartTemplatesUrl}/${id}`;
return this.apiRequest.put(url, chartTemplate);
}
// Delete chart template
deleteChartTemplate(id: number): Observable<void> {
const url = `${this.chartTemplatesUrl}/${id}`;
return this.apiRequest.delete(url);
}
}

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiRequestService } from 'src/app/services/api/api-request.service';
import { ComponentProperty } from './chart-config-manager.component';
@Injectable({
providedIn: 'root'
})
export class ComponentPropertyService {
private componentPropertiesUrl = 'api/component-properties';
constructor(private apiRequest: ApiRequestService) { }
// Get all component properties for a component
getComponentPropertiesByComponent(componentId: number): Observable<ComponentProperty[]> {
const url = `${this.componentPropertiesUrl}/component/${componentId}`;
return this.apiRequest.get(url);
}
// Get component property by ID
getComponentPropertyById(id: number): Observable<ComponentProperty> {
const url = `${this.componentPropertiesUrl}/${id}`;
return this.apiRequest.get(url);
}
// Create new component property
createComponentProperty(componentProperty: Partial<ComponentProperty>): Observable<ComponentProperty> {
return this.apiRequest.post(this.componentPropertiesUrl, componentProperty);
}
// Update component property
updateComponentProperty(id: number, componentProperty: ComponentProperty): Observable<ComponentProperty> {
const url = `${this.componentPropertiesUrl}/${id}`;
return this.apiRequest.put(url, componentProperty);
}
// Delete component property
deleteComponentProperty(id: number): Observable<void> {
const url = `${this.componentPropertiesUrl}/${id}`;
return this.apiRequest.delete(url);
}
}

View File

@@ -0,0 +1,125 @@
import { Injectable } from '@angular/core';
import { Observable, forkJoin } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiRequestService } from 'src/app/services/api/api-request.service';
import { ChartType, UiComponent, ComponentProperty, ChartTemplate, DynamicField } from './chart-config-manager.component';
@Injectable({
providedIn: 'root'
})
export class DynamicChartLoaderService {
private chartTypesUrl = 'api/chart-types';
private uiComponentsUrl = 'api/ui-components';
private componentPropertiesUrl = 'api/component-properties';
private chartTemplatesUrl = 'api/chart-templates';
private dynamicFieldsUrl = 'api/dynamic-fields';
constructor(private apiRequest: ApiRequestService) { }
/**
* Load all chart configurations dynamically
* This method fetches all chart types and their associated components, templates, and fields
*/
loadAllChartConfigurations(): Observable<any> {
console.log('Loading all chart configurations dynamically');
// Load all chart types first
return this.apiRequest.get(this.chartTypesUrl).pipe(
map(chartTypes => {
console.log('Loaded chart types:', chartTypes);
return chartTypes;
})
);
}
/**
* Load complete configuration for a specific chart type
* This includes UI components, templates, and dynamic fields
*/
loadChartConfiguration(chartTypeId: number): Observable<{
chartType: ChartType,
uiComponents: UiComponent[],
templates: ChartTemplate[],
dynamicFields: DynamicField[]
}> {
console.log(`Loading complete configuration for chart type ${chartTypeId}`);
// Load all related data in parallel
return forkJoin({
chartType: this.apiRequest.get(`${this.chartTypesUrl}/${chartTypeId}`),
uiComponents: this.apiRequest.get(`${this.uiComponentsUrl}/chart-type/${chartTypeId}`),
templates: this.apiRequest.get(`${this.chartTemplatesUrl}/chart-type/${chartTypeId}`),
dynamicFields: this.apiRequest.get(`${this.dynamicFieldsUrl}/chart-type/${chartTypeId}`)
}).pipe(
map(result => {
console.log(`Loaded complete configuration for chart type ${chartTypeId}:`, result);
return result;
})
);
}
/**
* Load chart template for a specific chart type
* This is used to render the chart UI dynamically
*/
loadChartTemplate(chartTypeId: number): Observable<ChartTemplate[]> {
console.log(`Loading chart templates for chart type ${chartTypeId}`);
return this.apiRequest.get(`${this.chartTemplatesUrl}/chart-type/${chartTypeId}`);
}
/**
* Load UI components for a specific chart type
* These define what configuration fields are needed for the chart
*/
loadUiComponents(chartTypeId: number): Observable<UiComponent[]> {
console.log(`Loading UI components for chart type ${chartTypeId}`);
return this.apiRequest.get(`${this.uiComponentsUrl}/chart-type/${chartTypeId}`);
}
/**
* Load dynamic fields for a specific chart type
* These define additional dynamic fields that can be used in the chart
*/
loadDynamicFields(chartTypeId: number): Observable<DynamicField[]> {
console.log(`Loading dynamic fields for chart type ${chartTypeId}`);
return this.apiRequest.get(`${this.dynamicFieldsUrl}/chart-type/${chartTypeId}`);
}
/**
* Get chart type by name
* This is useful for finding a chart type by its name rather than ID
*/
// getChartTypeByName(name: string): Observable<ChartType | null> {
// console.log(`Finding chart type by name: ${name}`);
// return this.apiRequest.get(`${this.chartTypesUrl}/byname?chartName=${name}`).pipe(
// map((chartTypes: ChartType[]) => {
// console.log('Available chart types:', chartTypes);
// const chartType = chartTypes.find(ct => ct.name === name);
// console.log(`Found chart type for name ${name}:`, chartType);
// return chartType || null;
// })
// );
// }
getChartTypeByName(name: string): Observable<any> {
console.log(`Finding chart type by name: ${name}`);
return this.apiRequest.get(`${this.chartTypesUrl}/byname?chartName=${name}`);
}
/**
* Load all active chart types
* This is used to populate the chart selection in the dashboard editor
*/
loadActiveChartTypes(): Observable<ChartType[]> {
console.log('Loading active chart types');
return this.apiRequest.get(`${this.chartTypesUrl}`).pipe(
map((chartTypes: ChartType[]) => {
const activeChartTypes = chartTypes.filter(ct => ct.isActive);
console.log('Loaded active chart types:', activeChartTypes);
return activeChartTypes;
})
);
}
}

View File

@@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiRequestService } from 'src/app/services/api/api-request.service';
import { DynamicField } from './chart-config-manager.component';
@Injectable({
providedIn: 'root'
})
export class DynamicFieldService {
private dynamicFieldsUrl = 'api/dynamic-fields';
constructor(private apiRequest: ApiRequestService) { }
// Get all dynamic fields for a chart type
getDynamicFieldsByChartType(chartTypeId: number): Observable<DynamicField[]> {
const url = `${this.dynamicFieldsUrl}/chart-type/${chartTypeId}`;
return this.apiRequest.get(url);
}
// Get dynamic field by ID
getDynamicFieldById(id: number): Observable<DynamicField> {
const url = `${this.dynamicFieldsUrl}/${id}`;
return this.apiRequest.get(url);
}
// Create new dynamic field with optional chart type ID as parameter
createDynamicField(dynamicField: Partial<DynamicField>, chartTypeId?: number): Observable<DynamicField> {
let url = this.dynamicFieldsUrl;
if (chartTypeId) {
url = `${this.dynamicFieldsUrl}?chartTypeId=${chartTypeId}`;
}
return this.apiRequest.post(url, dynamicField);
}
// Update dynamic field
updateDynamicField(id: number, dynamicField: DynamicField): Observable<DynamicField> {
const url = `${this.dynamicFieldsUrl}/${id}`;
return this.apiRequest.put(url, dynamicField);
}
// Delete dynamic field
deleteDynamicField(id: number): Observable<void> {
const url = `${this.dynamicFieldsUrl}/${id}`;
return this.apiRequest.delete(url);
}
}

View File

@@ -0,0 +1,33 @@
<div class="chart-template-form">
<form clrForm (ngSubmit)="onSubmit()" #chartTemplateForm="ngForm">
<clr-input-container>
<label>Template Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="chartTemplate.templateName" name="templateName" required />
</clr-input-container>
<clr-textarea-container>
<label>HTML Template</label>
<textarea clrTextarea [(ngModel)]="chartTemplate.templateHtml" name="templateHtml" rows="5" placeholder="<div class='chart-container'>...</div>"></textarea>
</clr-textarea-container>
<clr-textarea-container>
<label>CSS Template</label>
<textarea clrTextarea [(ngModel)]="chartTemplate.templateCss" name="templateCss" rows="5" placeholder=".chart-container { ... }"></textarea>
</clr-textarea-container>
<clr-checkbox-container>
<label>Is Default</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="chartTemplate.isDefault" name="isDefault" />
<label>Default Template</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!chartTemplate.templateName">
{{ isEdit ? 'Update' : 'Save' }}
</button>
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,17 @@
.chart-template-form {
padding: 20px;
background-color: #f9f9f9;
border-radius: 4px;
margin-bottom: 20px;
}
.form-actions {
margin-top: 20px;
display: flex;
gap: 10px;
}
textarea {
font-family: 'Courier New', monospace;
font-size: 12px;
}

View File

@@ -0,0 +1,22 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { ChartTemplate } from '../chart-config-manager.component';
@Component({
selector: 'app-chart-template-form',
templateUrl: './chart-template-form.component.html',
styleUrls: ['./chart-template-form.component.scss']
})
export class ChartTemplateFormComponent {
@Input() chartTemplate: Partial<ChartTemplate> = {};
@Input() isEdit = false;
@Output() save = new EventEmitter<Partial<ChartTemplate>>();
@Output() cancel = new EventEmitter<void>();
onSubmit(): void {
this.save.emit(this.chartTemplate);
}
onCancel(): void {
this.cancel.emit();
}
}

View File

@@ -0,0 +1,33 @@
<div class="chart-type-form">
<form clrForm (ngSubmit)="onSubmit()" #chartTypeForm="ngForm">
<clr-input-container>
<label>Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="chartType.name" name="chartTypeName" required />
</clr-input-container>
<clr-input-container>
<label>Display Name</label>
<input clrInput type="text" [(ngModel)]="chartType.displayName" name="chartTypeDisplayName" />
</clr-input-container>
<clr-textarea-container>
<label>Description</label>
<textarea clrTextarea [(ngModel)]="chartType.description" name="chartTypeDescription" rows="3"></textarea>
</clr-textarea-container>
<clr-checkbox-container>
<label>Is Active</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="chartType.isActive" name="chartTypeIsActive" />
<label>Active</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!chartType.name">
{{ isEdit ? 'Update' : 'Save' }}
</button>
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,12 @@
.chart-type-form {
padding: 20px;
background-color: #f9f9f9;
border-radius: 4px;
margin-bottom: 20px;
}
.form-actions {
margin-top: 20px;
display: flex;
gap: 10px;
}

View File

@@ -0,0 +1,22 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { ChartType } from '../chart-config-manager.component';
@Component({
selector: 'app-chart-type-form',
templateUrl: './chart-type-form.component.html',
styleUrls: ['./chart-type-form.component.scss']
})
export class ChartTypeFormComponent {
@Input() chartType: Partial<ChartType> = {};
@Input() isEdit = false;
@Output() save = new EventEmitter<Partial<ChartType>>();
@Output() cancel = new EventEmitter<void>();
onSubmit(): void {
this.save.emit(this.chartType);
}
onCancel(): void {
this.cancel.emit();
}
}

View File

@@ -0,0 +1,28 @@
<div class="component-property-form">
<form clrForm (ngSubmit)="onSubmit()" #componentPropertyForm="ngForm">
<clr-input-container>
<label>Property Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="componentProperty.propertyName" name="propertyName" required />
</clr-input-container>
<clr-input-container>
<label>Property Value</label>
<input clrInput type="text" [(ngModel)]="componentProperty.propertyValue" name="propertyValue" />
</clr-input-container>
<clr-select-container>
<label>Property Type</label>
<select clrSelect [(ngModel)]="componentProperty.propertyType" name="propertyType">
<option value="">Select a type</option>
<option *ngFor="let type of propertyTypes" [value]="type">{{ type | titlecase }}</option>
</select>
</clr-select-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!componentProperty.propertyName">
{{ isEdit ? 'Update' : 'Save' }}
</button>
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,12 @@
.component-property-form {
padding: 20px;
background-color: #f9f9f9;
border-radius: 4px;
margin-bottom: 20px;
}
.form-actions {
margin-top: 20px;
display: flex;
gap: 10px;
}

View File

@@ -0,0 +1,31 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { ComponentProperty } from '../chart-config-manager.component';
@Component({
selector: 'app-component-property-form',
templateUrl: './component-property-form.component.html',
styleUrls: ['./component-property-form.component.scss']
})
export class ComponentPropertyFormComponent {
@Input() componentProperty: Partial<ComponentProperty> = {};
@Input() isEdit = false;
@Output() save = new EventEmitter<Partial<ComponentProperty>>();
@Output() cancel = new EventEmitter<void>();
propertyTypes = [
'string',
'number',
'boolean',
'array',
'object',
'function'
];
onSubmit(): void {
this.save.emit(this.componentProperty);
}
onCancel(): void {
this.cancel.emit();
}
}

View File

@@ -0,0 +1,54 @@
<div class="dynamic-field-form">
<form clrForm (ngSubmit)="onSubmit()" #dynamicFieldForm="ngForm">
<clr-input-container>
<label>Field Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="dynamicField.fieldName" name="fieldName" required />
</clr-input-container>
<clr-input-container>
<label>Field Label</label>
<input clrInput type="text" [(ngModel)]="dynamicField.fieldLabel" name="fieldLabel" />
</clr-input-container>
<clr-select-container>
<label>Field Type</label>
<select clrSelect [(ngModel)]="dynamicField.fieldType" name="fieldType">
<option value="">Select a type</option>
<option *ngFor="let type of fieldTypes" [value]="type">{{ type | titlecase }}</option>
</select>
</clr-select-container>
<clr-textarea-container>
<label>Field Options (JSON format)</label>
<textarea clrTextarea [(ngModel)]="dynamicField.fieldOptions" name="fieldOptions" rows="3" placeholder='{"option1": "Option 1", "option2": "Option 2"}'></textarea>
</clr-textarea-container>
<clr-input-container>
<label>Sort Order</label>
<input clrInput type="number" [(ngModel)]="dynamicField.sortOrder" name="fieldSortOrder" />
</clr-input-container>
<clr-checkbox-container>
<label>Is Required</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="dynamicField.isRequired" name="fieldIsRequired" />
<label>Required</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<clr-checkbox-container>
<label>Show in UI</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="dynamicField.showInUi" name="fieldShowInUi" />
<label>Show in UI</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!dynamicField.fieldName">
{{ isEdit ? 'Update' : 'Save' }}
</button>
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,17 @@
.dynamic-field-form {
padding: 20px;
background-color: #f9f9f9;
border-radius: 4px;
margin-bottom: 20px;
}
.form-actions {
margin-top: 20px;
display: flex;
gap: 10px;
}
textarea {
font-family: 'Courier New', monospace;
font-size: 12px;
}

View File

@@ -0,0 +1,34 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { DynamicField } from '../chart-config-manager.component';
@Component({
selector: 'app-dynamic-field-form',
templateUrl: './dynamic-field-form.component.html',
styleUrls: ['./dynamic-field-form.component.scss']
})
export class DynamicFieldFormComponent {
@Input() dynamicField: Partial<DynamicField> = {};
@Input() isEdit = false;
@Output() save = new EventEmitter<Partial<DynamicField>>();
@Output() cancel = new EventEmitter<void>();
fieldTypes = [
'text',
'number',
'email',
'password',
'date',
'select',
'checkbox',
'radio',
'textarea'
];
onSubmit(): void {
this.save.emit(this.dynamicField);
}
onCancel(): void {
this.cancel.emit();
}
}

View File

@@ -0,0 +1,46 @@
<div class="ui-component-form">
<form clrForm (ngSubmit)="onSubmit()" #uiComponentForm="ngForm">
<clr-input-container>
<label>Component Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="uiComponent.componentName" name="componentName" required />
</clr-input-container>
<clr-select-container>
<label>Component Type</label>
<select clrSelect [(ngModel)]="uiComponent.componentType" name="componentType">
<option value="">Select a type</option>
<option *ngFor="let type of componentTypes" [value]="type">{{ type | titlecase }}</option>
</select>
</clr-select-container>
<clr-input-container>
<label>Display Label</label>
<input clrInput type="text" [(ngModel)]="uiComponent.displayLabel" name="displayLabel" />
</clr-input-container>
<clr-input-container>
<label>Placeholder</label>
<input clrInput type="text" [(ngModel)]="uiComponent.placeholder" name="placeholder" />
</clr-input-container>
<clr-input-container>
<label>Sort Order</label>
<input clrInput type="number" [(ngModel)]="uiComponent.sortOrder" name="sortOrder" />
</clr-input-container>
<clr-checkbox-container>
<label>Is Required</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="uiComponent.isRequired" name="isRequired" />
<label>Required</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!uiComponent.componentName">
{{ isEdit ? 'Update' : 'Save' }}
</button>
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,12 @@
.ui-component-form {
padding: 20px;
background-color: #f9f9f9;
border-radius: 4px;
margin-bottom: 20px;
}
.form-actions {
margin-top: 20px;
display: flex;
gap: 10px;
}

View File

@@ -0,0 +1,33 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { UiComponent } from '../chart-config-manager.component';
@Component({
selector: 'app-ui-component-form',
templateUrl: './ui-component-form.component.html',
styleUrls: ['./ui-component-form.component.scss']
})
export class UiComponentFormComponent {
@Input() uiComponent: Partial<UiComponent> = {};
@Input() isEdit = false;
@Output() save = new EventEmitter<Partial<UiComponent>>();
@Output() cancel = new EventEmitter<void>();
componentTypes = [
'input',
'select',
'checkbox',
'radio',
'textarea',
'datepicker',
'slider',
'toggle'
];
onSubmit(): void {
this.save.emit(this.uiComponent);
}
onCancel(): void {
this.cancel.emit();
}
}

View File

@@ -0,0 +1,47 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiRequestService } from 'src/app/services/api/api-request.service';
import { UiComponent } from './chart-config-manager.component';
@Injectable({
providedIn: 'root'
})
export class UiComponentService {
private uiComponentsUrl = 'api/ui-components';
constructor(private apiRequest: ApiRequestService) { }
// Get all UI components for a chart type
getUiComponentsByChartType(chartTypeId: number): Observable<UiComponent[]> {
const url = `${this.uiComponentsUrl}/chart-type/${chartTypeId}`;
console.log(`Fetching UI components for chart type ${chartTypeId} from ${url}`);
return this.apiRequest.get(url);
}
// Get UI component by ID
getUiComponentById(id: number): Observable<UiComponent> {
const url = `${this.uiComponentsUrl}/${id}`;
console.log(`Fetching UI component ${id} from ${url}`);
return this.apiRequest.get(url);
}
// Create new UI component
createUiComponent(uiComponent: Partial<UiComponent>): Observable<UiComponent> {
console.log('Creating UI component:', uiComponent);
return this.apiRequest.post(this.uiComponentsUrl, uiComponent);
}
// Update UI component
updateUiComponent(id: number, uiComponent: UiComponent): Observable<UiComponent> {
const url = `${this.uiComponentsUrl}/${id}`;
console.log(`Updating UI component ${id}:`, uiComponent);
return this.apiRequest.put(url, uiComponent);
}
// Delete UI component
deleteUiComponent(id: number): Observable<void> {
const url = `${this.uiComponentsUrl}/${id}`;
console.log(`Deleting UI component ${id}`);
return this.apiRequest.delete(url);
}
}

View File

@@ -0,0 +1,76 @@
<div class="add-chart-type-page">
<div class="header">
<h2>Add New Chart Type</h2>
</div>
<!-- Error and Success Messages -->
<div class="alert alert-danger" *ngIf="errorMessage">
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ errorMessage }}</span>
</div>
</div>
<div class="alert alert-success" *ngIf="successMessage">
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ successMessage }}</span>
</div>
</div>
<div class="card">
<div class="card-block">
<form clrForm (ngSubmit)="createChartType()" #addChartTypeForm="ngForm">
<clr-input-container>
<label>Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="chartType.name" name="chartTypeName" required />
<clr-control-helper>Enter a unique name for the chart type (e.g., "line-chart", "bar-chart")</clr-control-helper>
</clr-input-container>
<clr-input-container>
<label>Display Name</label>
<input clrInput type="text" [(ngModel)]="chartType.displayName" name="chartTypeDisplayName" />
<clr-control-helper>This is the user-friendly name shown in the UI</clr-control-helper>
</clr-input-container>
<clr-textarea-container>
<label>Description</label>
<textarea clrTextarea [(ngModel)]="chartType.description" name="chartTypeDescription" rows="3"></textarea>
<clr-control-helper>Provide a detailed description of this chart type and when to use it</clr-control-helper>
</clr-textarea-container>
<clr-checkbox-container>
<label>Is Active</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="chartType.isActive" name="chartTypeIsActive" />
<label>Active</label>
</clr-checkbox-wrapper>
<clr-control-helper>Deactivate chart types that should not be available for selection</clr-control-helper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!chartType.name || loadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
Create Chart Type
</button>
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
</div>
</form>
</div>
</div>
<div class="info-section">
<h4>About Chart Types</h4>
<p>Chart types define the different visualization options available in the dashboard builder. Each chart type can have:</p>
<ul>
<li>Associated UI components that define the configuration form</li>
<li>Templates that define how the chart is rendered</li>
<li>Dynamic fields that capture specific configuration parameters</li>
</ul>
<p>After creating a chart type, you can configure its components, templates, and fields in the chart configuration manager.</p>
</div>
</div>

View File

@@ -0,0 +1,65 @@
.add-chart-type-page {
padding: 20px;
max-width: 800px;
margin: 0 auto;
.header {
margin-bottom: 20px;
h2 {
color: #0079b8;
font-weight: 300;
}
}
.card {
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.card-block {
padding: 20px;
}
}
.form-actions {
margin-top: 20px;
display: flex;
gap: 10px;
}
.info-section {
background-color: #f6f6f6;
border-left: 4px solid #0079b8;
padding: 15px;
border-radius: 4px;
h4 {
margin-top: 0;
color: #0079b8;
}
ul {
padding-left: 20px;
li {
margin-bottom: 8px;
}
}
p {
margin: 10px 0;
}
}
@media (max-width: 768px) {
padding: 10px;
.form-actions {
flex-direction: column;
button {
width: 100%;
margin-bottom: 10px;
}
}
}
}

View File

@@ -0,0 +1,70 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ClrLoadingState } from '@clr/angular';
import { ChartType, ChartTypeService } from './chart-type.service';
@Component({
selector: 'app-add-chart-type',
templateUrl: './add-chart-type.component.html',
styleUrls: ['./add-chart-type.component.scss']
})
export class AddChartTypeComponent {
chartType: Partial<ChartType> = {
isActive: true
};
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
errorMessage: string | null = null;
successMessage: string | null = null;
// Make ClrLoadingState available to template
readonly ClrLoadingState = ClrLoadingState;
constructor(
private chartTypeService: ChartTypeService,
private router: Router
) { }
// Show error message
private showError(message: string): void {
this.errorMessage = message;
setTimeout(() => {
this.errorMessage = null;
}, 5000);
}
// Show success message
private showSuccess(message: string): void {
this.successMessage = message;
setTimeout(() => {
this.successMessage = null;
}, 3000);
}
createChartType(): void {
if (!this.chartType.name) {
this.showError('Chart type name is required');
return;
}
this.loadingState = ClrLoadingState.LOADING;
this.chartTypeService.createChartType(this.chartType).subscribe({
next: (data) => {
this.showSuccess('Chart type created successfully');
this.loadingState = ClrLoadingState.SUCCESS;
// Redirect to chart types list after a short delay
setTimeout(() => {
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
}, 1500);
},
error: (error) => {
console.error('Error creating chart type:', error);
this.showError('Error creating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
onCancel(): void {
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
}
}

View File

@@ -0,0 +1,222 @@
<div class="chart-type-fields-page">
<div class="header">
<h2>
<button class="btn btn-link back-button" (click)="goBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
</button>
Dynamic Fields for {{ chartType?.name || 'Chart Type' }}
</h2>
</div>
<!-- Error and Success Messages -->
<div class="alert alert-danger" *ngIf="errorMessage">
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ errorMessage }}</span>
</div>
</div>
<div class="alert alert-success" *ngIf="successMessage">
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ successMessage }}</span>
</div>
</div>
<!-- Add Dynamic Field Form -->
<div class="card" *ngIf="showAddForm">
<div class="card-header">
<h3>Add New Dynamic Field</h3>
</div>
<div class="card-block">
<form clrForm (ngSubmit)="createDynamicField()" #addDynamicFieldForm="ngForm">
<clr-input-container>
<label>Field Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldName" name="fieldName" required />
<clr-control-helper>Enter a unique name for the dynamic field (e.g., "chart-title", "x-axis-label")</clr-control-helper>
</clr-input-container>
<clr-input-container>
<label>Field Label</label>
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldLabel" name="fieldLabel" />
<clr-control-helper>User-friendly label shown in the configuration form</clr-control-helper>
</clr-input-container>
<clr-input-container>
<label>Field Type</label>
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldType" name="fieldType" />
<clr-control-helper>Type of the field (e.g., "string", "number", "boolean")</clr-control-helper>
</clr-input-container>
<clr-textarea-container>
<label>Field Options</label>
<textarea clrTextarea [(ngModel)]="newDynamicField.fieldOptions" name="fieldOptions" rows="3"></textarea>
<clr-control-helper>JSON options for the field (for select fields, etc.)</clr-control-helper>
</clr-textarea-container>
<clr-input-container>
<label>Sort Order</label>
<input clrInput type="number" [(ngModel)]="newDynamicField.sortOrder" name="fieldSortOrder" />
<clr-control-helper>Order in which fields appear in the form</clr-control-helper>
</clr-input-container>
<clr-checkbox-container>
<label>Is Required</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.isRequired" name="fieldIsRequired" />
<label>Required</label>
</clr-checkbox-wrapper>
<clr-control-helper>Mark as required if this field must be filled</clr-control-helper>
</clr-checkbox-container>
<clr-checkbox-container>
<label>Show in UI</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.showInUi" name="fieldShowInUi" />
<label>Visible</label>
</clr-checkbox-wrapper>
<clr-control-helper>Uncheck to hide this field in the configuration form</clr-control-helper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!newDynamicField.fieldName || loadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
Create Dynamic Field
</button>
<button class="btn" type="button" (click)="showAddForm = false">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Dynamic Fields Grid -->
<div class="card">
<div class="card-header">
<div class="clr-row">
<div class="clr-col">
<h3>Dynamic Fields</h3>
</div>
<div class="clr-col" style="text-align: right;">
<button class="btn btn-primary" (click)="showAddForm = true">
<cds-icon shape="plus"></cds-icon> Add Dynamic Field
</button>
</div>
</div>
</div>
<div class="card-block">
<clr-datagrid [clrDgLoading]="loadingState === ClrLoadingState.LOADING">
<clr-dg-column>Field Name</clr-dg-column>
<clr-dg-column>Field Label</clr-dg-column>
<clr-dg-column>Field Type</clr-dg-column>
<clr-dg-column>Required</clr-dg-column>
<clr-dg-column>Visible</clr-dg-column>
<clr-dg-column>Sort Order</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let dynamicField of dynamicFields" [clrDgItem]="dynamicField">
<clr-dg-cell>{{dynamicField.fieldName}}</clr-dg-cell>
<clr-dg-cell>{{dynamicField.fieldLabel}}</clr-dg-cell>
<clr-dg-cell>{{dynamicField.fieldType}}</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="dynamicField.isRequired" [class.label-danger]="!dynamicField.isRequired">
{{dynamicField.isRequired ? 'Yes' : 'No'}}
</span>
</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="dynamicField.showInUi" [class.label-danger]="!dynamicField.showInUi">
{{dynamicField.showInUi ? 'Yes' : 'No'}}
</span>
</clr-dg-cell>
<clr-dg-cell>{{dynamicField.sortOrder}}</clr-dg-cell>
<clr-dg-cell>
<button class="btn btn-sm btn-icon" (click)="selectDynamicFieldForEdit(dynamicField)" title="Edit">
<cds-icon shape="pencil"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteDynamicField(dynamicField.id)" title="Delete">
<cds-icon shape="trash"></cds-icon>
</button>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{dynamicFields.length}} dynamic field(s)
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<!-- Edit Dynamic Field Form -->
<div class="card" *ngIf="selectedDynamicField">
<div class="card-header">
<h3>Edit Dynamic Field</h3>
</div>
<div class="card-block">
<form clrForm (ngSubmit)="updateDynamicField()" #editDynamicFieldForm="ngForm">
<clr-input-container>
<label>Field Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldName" name="editFieldName" required />
</clr-input-container>
<clr-input-container>
<label>Field Label</label>
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldLabel" name="editFieldLabel" />
</clr-input-container>
<clr-input-container>
<label>Field Type</label>
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldType" name="editFieldType" />
</clr-input-container>
<clr-textarea-container>
<label>Field Options</label>
<textarea clrTextarea [(ngModel)]="selectedDynamicField.fieldOptions" name="editFieldOptions" rows="3"></textarea>
</clr-textarea-container>
<clr-input-container>
<label>Sort Order</label>
<input clrInput type="number" [(ngModel)]="selectedDynamicField.sortOrder" name="editFieldSortOrder" />
</clr-input-container>
<clr-checkbox-container>
<label>Is Required</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.isRequired" name="editFieldIsRequired" />
<label>Required</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<clr-checkbox-container>
<label>Show in UI</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.showInUi" name="editFieldShowInUi" />
<label>Visible</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!selectedDynamicField.fieldName || loadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
Update Dynamic Field
</button>
<button class="btn" type="button" (click)="selectedDynamicField = null">Cancel</button>
</div>
</form>
</div>
</div>
<div class="info-section">
<h4>About Dynamic Fields</h4>
<p>Dynamic fields define the configurable parameters for a chart type. Each field represents:</p>
<ul>
<li>A configuration option that can be set when creating or editing a chart</li>
<li>Metadata like type, label, and validation rules</li>
<li>Visibility and requirement settings</li>
</ul>
<p>Dynamic fields allow you to create flexible chart configurations that can be customized per instance.</p>
</div>
</div>

View File

@@ -0,0 +1,128 @@
.chart-type-fields-page {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
.header {
margin-bottom: 20px;
display: flex;
align-items: center;
h2 {
color: #0079b8;
font-weight: 300;
display: flex;
align-items: center;
gap: 10px;
}
.back-button {
padding: 0;
cds-icon {
width: 24px;
height: 24px;
}
}
}
.card {
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.card-block {
padding: 20px;
}
.card-header {
h3 {
margin: 0;
color: #0079b8;
}
}
}
.form-actions {
margin-top: 20px;
display: flex;
gap: 10px;
}
.label {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
&.label-success {
background-color: #3d9970;
color: white;
}
&.label-danger {
background-color: #d32f2f;
color: white;
}
}
clr-datagrid {
margin-top: 10px;
clr-dg-cell {
&:first-child {
font-weight: 500;
}
}
}
.btn-icon {
margin-right: 5px;
}
.info-section {
background-color: #f6f6f6;
border-left: 4px solid #0079b8;
padding: 15px;
border-radius: 4px;
h4 {
margin-top: 0;
color: #0079b8;
}
ul {
padding-left: 20px;
li {
margin-bottom: 8px;
}
}
p {
margin: 10px 0;
}
}
@media (max-width: 768px) {
padding: 10px;
.form-actions {
flex-direction: column;
button {
width: 100%;
margin-bottom: 10px;
}
}
.card-header {
.clr-row {
flex-direction: column;
.clr-col {
text-align: left !important;
margin-bottom: 10px;
}
}
}
}
}

View File

@@ -0,0 +1,180 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ClrLoadingState } from '@clr/angular';
import { ChartType, ChartTypeService } from './chart-type.service';
import { DynamicField } from '../chart-config/chart-config-manager.component';
import { DynamicFieldService } from '../chart-config/dynamic-field.service';
@Component({
selector: 'app-chart-type-fields',
templateUrl: './chart-type-fields.component.html',
styleUrls: ['./chart-type-fields.component.scss']
})
export class ChartTypeFieldsComponent implements OnInit {
chartType: ChartType | null = null;
dynamicFields: DynamicField[] = [];
newDynamicField: Partial<DynamicField> = {
isRequired: false,
showInUi: true
};
selectedDynamicField: DynamicField | null = null;
showAddForm = false;
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
errorMessage: string | null = null;
successMessage: string | null = null;
// Make ClrLoadingState available to template
readonly ClrLoadingState = ClrLoadingState;
constructor(
private chartTypeService: ChartTypeService,
private dynamicFieldService: DynamicFieldService,
private route: ActivatedRoute,
private router: Router
) { }
ngOnInit(): void {
const chartTypeId = Number(this.route.snapshot.paramMap.get('id'));
if (chartTypeId) {
this.loadChartType(chartTypeId);
this.loadDynamicFields(chartTypeId);
} else {
this.showError('Invalid chart type ID');
}
}
// Show error message
private showError(message: string): void {
this.errorMessage = message;
setTimeout(() => {
this.errorMessage = null;
}, 5000);
}
// Show success message
private showSuccess(message: string): void {
this.successMessage = message;
setTimeout(() => {
this.successMessage = null;
}, 3000);
}
loadChartType(id: number): void {
this.chartTypeService.getChartTypeById(id).subscribe({
next: (data) => {
this.chartType = data;
},
error: (error) => {
console.error('Error loading chart type:', error);
this.showError('Error loading chart type: ' + (error.error?.message || error.message || 'Unknown error'));
}
});
}
loadDynamicFields(chartTypeId: number): void {
this.loadingState = ClrLoadingState.LOADING;
this.dynamicFieldService.getDynamicFieldsByChartType(chartTypeId).subscribe({
next: (data) => {
this.dynamicFields = data;
this.loadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading dynamic fields:', error);
this.showError('Error loading dynamic fields: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
createDynamicField(): void {
if (!this.chartType || !this.newDynamicField.fieldName) {
this.showError('Field name is required');
return;
}
// Create a copy without the chartType property
const fieldData: Partial<DynamicField> = {
fieldName: this.newDynamicField.fieldName,
fieldLabel: this.newDynamicField.fieldLabel,
fieldType: this.newDynamicField.fieldType,
fieldOptions: this.newDynamicField.fieldOptions,
isRequired: this.newDynamicField.isRequired,
showInUi: this.newDynamicField.showInUi,
sortOrder: this.newDynamicField.sortOrder
};
this.loadingState = ClrLoadingState.LOADING;
this.dynamicFieldService.createDynamicField(fieldData, this.chartType.id).subscribe({
next: (data) => {
this.dynamicFields.push(data);
this.newDynamicField = { isRequired: false, showInUi: true };
this.showAddForm = false;
this.showSuccess('Dynamic field created successfully');
this.loadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating dynamic field:', error);
this.showError('Error creating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
updateDynamicField(): void {
if (!this.selectedDynamicField || !this.selectedDynamicField.fieldName) {
this.showError('Field name is required');
return;
}
this.loadingState = ClrLoadingState.LOADING;
this.dynamicFieldService.updateDynamicField(this.selectedDynamicField.id, this.selectedDynamicField).subscribe({
next: (data) => {
const index = this.dynamicFields.findIndex(df => df.id === data.id);
if (index !== -1) {
this.dynamicFields[index] = data;
}
this.selectedDynamicField = null;
this.showSuccess('Dynamic field updated successfully');
this.loadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating dynamic field:', error);
this.showError('Error updating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
deleteDynamicField(id: number): void {
if (!confirm('Are you sure you want to delete this dynamic field?')) {
return;
}
this.loadingState = ClrLoadingState.LOADING;
this.dynamicFieldService.deleteDynamicField(id).subscribe({
next: () => {
this.dynamicFields = this.dynamicFields.filter(df => df.id !== id);
this.showSuccess('Dynamic field deleted successfully');
this.loadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting dynamic field:', error);
this.showError('Error deleting dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
selectDynamicFieldForEdit(dynamicField: DynamicField): void {
this.selectedDynamicField = { ...dynamicField };
}
goBack(): void {
if (this.chartType) {
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types/edit', this.chartType.id]);
} else {
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
}
}
}

View File

@@ -0,0 +1,75 @@
<div class="chart-type-manager">
<h2>Chart Type Management</h2>
<!-- Error and Success Messages -->
<div class="alert alert-danger" *ngIf="errorMessage">
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ errorMessage }}</span>
</div>
</div>
<div class="alert alert-success" *ngIf="successMessage">
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ successMessage }}</span>
</div>
</div>
<!-- Chart Types Grid -->
<div class="card">
<div class="card-header">
<div class="clr-row">
<div class="clr-col">
<h3>Chart Types</h3>
</div>
<div class="clr-col" style="text-align: right;">
<button class="btn btn-primary" [routerLink]="['/cns-portal/dashboardbuilder/chart-types/add']">
<cds-icon shape="plus"></cds-icon> Add Chart Type
</button>
</div>
</div>
</div>
<div class="card-block">
<clr-datagrid [clrDgLoading]="chartTypeLoadingState === ClrLoadingState.LOADING">
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Display Name</clr-dg-column>
<clr-dg-column>Description</clr-dg-column>
<clr-dg-column>Status</clr-dg-column>
<clr-dg-column>Created At</clr-dg-column>
<clr-dg-column>Updated At</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let chartType of chartTypes" [clrDgItem]="chartType">
<clr-dg-cell>{{chartType.name}}</clr-dg-cell>
<clr-dg-cell>{{chartType.displayName}}</clr-dg-cell>
<clr-dg-cell>{{chartType.description}}</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="chartType.isActive" [class.label-danger]="!chartType.isActive">
{{chartType.isActive ? 'Active' : 'Inactive'}}
</span>
</clr-dg-cell>
<clr-dg-cell>{{chartType.createdAt ? (chartType.createdAt | date:'short') : 'N/A'}}</clr-dg-cell>
<clr-dg-cell>{{chartType.updatedAt ? (chartType.updatedAt | date:'short') : 'N/A'}}</clr-dg-cell>
<clr-dg-cell>
<button class="btn btn-sm btn-icon" [routerLink]="['/cns-portal/dashboardbuilder/chart-types/edit', chartType.id]" title="Edit">
<cds-icon shape="pencil"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteChartType(chartType.id)" title="Delete">
<cds-icon shape="trash"></cds-icon>
</button>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{chartTypes.length}} chart type(s)
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
</div>

View File

@@ -0,0 +1,71 @@
.chart-type-manager {
padding: 20px;
h2 {
color: #0079b8;
font-weight: 300;
margin-bottom: 20px;
}
.card {
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.card-header {
h3 {
margin: 0;
color: #0079b8;
}
}
.card-block {
padding: 20px;
}
}
.label {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
&.label-success {
background-color: #3d9970;
color: white;
}
&.label-danger {
background-color: #d32f2f;
color: white;
}
}
clr-datagrid {
margin-top: 10px;
clr-dg-cell {
&:first-child {
font-weight: 500;
}
}
}
.btn-icon {
margin-right: 5px;
}
@media (max-width: 768px) {
padding: 10px;
.card-header {
.clr-row {
flex-direction: column;
.clr-col {
text-align: left !important;
margin-bottom: 10px;
}
}
}
}
}

View File

@@ -0,0 +1,115 @@
import { Component, OnInit } from '@angular/core';
import { ClrLoadingState } from '@clr/angular';
import { ChartType, ChartTypeService } from './chart-type.service';
@Component({
selector: 'app-chart-type-manager',
templateUrl: './chart-type-manager.component.html',
styleUrls: ['./chart-type-manager.component.scss']
})
export class ChartTypeManagerComponent implements OnInit {
chartTypes: ChartType[] = [];
chartTypeLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
// Make ClrLoadingState available to template
readonly ClrLoadingState = ClrLoadingState;
// Error handling
errorMessage: string | null = null;
successMessage: string | null = null;
constructor(private chartTypeService: ChartTypeService) { }
ngOnInit(): void {
this.loadChartTypes();
}
// Show error message
private showError(message: string): void {
this.errorMessage = message;
setTimeout(() => {
this.errorMessage = null;
}, 5000);
}
// Show success message
private showSuccess(message: string): void {
this.successMessage = message;
setTimeout(() => {
this.successMessage = null;
}, 3000);
}
// Chart Type Methods
loadChartTypes(): void {
this.chartTypeLoadingState = ClrLoadingState.LOADING;
this.chartTypeService.getAllChartTypes().subscribe({
next: (data) => {
// Process the data to ensure dates are properly formatted
this.chartTypes = data.map(chartType => ({
...chartType,
createdAt: this.formatDate(chartType.createdAt),
updatedAt: this.formatDate(chartType.updatedAt)
}));
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading chart types:', error);
this.showError('Error loading chart types: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTypeLoadingState = ClrLoadingState.ERROR;
}
});
}
// Format date to handle both string and object formats
private formatDate(date: any): string {
if (!date) return '';
// If it's already a string, return as is
if (typeof date === 'string') {
return date;
}
// If it's an object, try to convert to string
if (typeof date === 'object') {
// Handle various date object formats
if (date instanceof Date) {
return date.toISOString();
}
// Handle ISO string within object
if (date.date) {
return date.date;
}
// Handle other object formats
try {
return new Date(date).toISOString();
} catch {
return '';
}
}
return '';
}
deleteChartType(id: number): void {
if (!confirm('Are you sure you want to delete this chart type?')) {
return;
}
this.chartTypeLoadingState = ClrLoadingState.LOADING;
this.chartTypeService.deleteChartType(id).subscribe({
next: () => {
this.chartTypes = this.chartTypes.filter(ct => ct.id !== id);
this.showSuccess('Chart type deleted successfully');
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting chart type:', error);
this.showError('Error deleting chart type: ' + (error.error?.message || error.message || 'Unknown error'));
this.chartTypeLoadingState = ClrLoadingState.ERROR;
}
});
}
}

View File

@@ -0,0 +1,22 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-chart-type-page',
template: `
<div class="chart-type-page">
<app-chart-type-manager></app-chart-type-manager>
</div>
`,
styles: [`
.chart-type-page {
padding: 20px;
}
@media (max-width: 768px) {
.chart-type-page {
padding: 10px;
}
}
`]
})
export class ChartTypePageComponent { }

View File

@@ -0,0 +1,175 @@
<div class="chart-type-templates-page">
<div class="header">
<h2>
<button class="btn btn-link back-button" (click)="goBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
</button>
Chart Templates for {{ chartType?.name || 'Chart Type' }}
</h2>
</div>
<!-- Error and Success Messages -->
<div class="alert alert-danger" *ngIf="errorMessage">
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ errorMessage }}</span>
</div>
</div>
<div class="alert alert-success" *ngIf="successMessage">
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ successMessage }}</span>
</div>
</div>
<!-- Add Chart Template Form -->
<div class="card" *ngIf="showAddForm">
<div class="card-header">
<h3>Add New Chart Template</h3>
</div>
<div class="card-block">
<form clrForm (ngSubmit)="createChartTemplate()" #addChartTemplateForm="ngForm">
<clr-input-container>
<label>Template Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="newChartTemplate.templateName" name="templateName" required />
<clr-control-helper>Enter a unique name for the chart template</clr-control-helper>
</clr-input-container>
<clr-textarea-container>
<label>HTML Template</label>
<textarea clrTextarea [(ngModel)]="newChartTemplate.templateHtml" name="templateHtml" rows="5"></textarea>
<clr-control-helper>HTML structure for rendering the chart</clr-control-helper>
</clr-textarea-container>
<clr-textarea-container>
<label>CSS Template</label>
<textarea clrTextarea [(ngModel)]="newChartTemplate.templateCss" name="templateCss" rows="5"></textarea>
<clr-control-helper>CSS styling for the chart template</clr-control-helper>
</clr-textarea-container>
<clr-checkbox-container>
<label>Is Default</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="newChartTemplate.isDefault" name="isDefault" />
<label>Default Template</label>
</clr-checkbox-wrapper>
<clr-control-helper>Mark as default template for this chart type</clr-control-helper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!newChartTemplate.templateName || loadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
Create Chart Template
</button>
<button class="btn" type="button" (click)="showAddForm = false">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Chart Templates Grid -->
<div class="card">
<div class="card-header">
<div class="clr-row">
<div class="clr-col">
<h3>Chart Templates</h3>
</div>
<div class="clr-col" style="text-align: right;">
<button class="btn btn-primary" (click)="showAddForm = true">
<cds-icon shape="plus"></cds-icon> Add Chart Template
</button>
</div>
</div>
</div>
<div class="card-block">
<clr-datagrid [clrDgLoading]="loadingState === ClrLoadingState.LOADING">
<clr-dg-column>Template Name</clr-dg-column>
<clr-dg-column>Default</clr-dg-column>
<clr-dg-column>Created At</clr-dg-column>
<clr-dg-column>Updated At</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let chartTemplate of chartTemplates" [clrDgItem]="chartTemplate">
<clr-dg-cell>{{chartTemplate.templateName}}</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="chartTemplate.isDefault" [class.label-danger]="!chartTemplate.isDefault">
{{chartTemplate.isDefault ? 'Yes' : 'No'}}
</span>
</clr-dg-cell>
<clr-dg-cell>{{chartTemplate.createdAt | date:'short'}}</clr-dg-cell>
<clr-dg-cell>{{chartTemplate.updatedAt | date:'short'}}</clr-dg-cell>
<clr-dg-cell>
<button class="btn btn-sm btn-icon" (click)="selectChartTemplateForEdit(chartTemplate)" title="Edit">
<cds-icon shape="pencil"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteChartTemplate(chartTemplate.id)" title="Delete">
<cds-icon shape="trash"></cds-icon>
</button>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{chartTemplates.length}} chart template(s)
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<!-- Edit Chart Template Form -->
<div class="card" *ngIf="selectedChartTemplate">
<div class="card-header">
<h3>Edit Chart Template</h3>
</div>
<div class="card-block">
<form clrForm (ngSubmit)="updateChartTemplate()" #editChartTemplateForm="ngForm">
<clr-input-container>
<label>Template Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="selectedChartTemplate.templateName" name="editTemplateName" required />
</clr-input-container>
<clr-textarea-container>
<label>HTML Template</label>
<textarea clrTextarea [(ngModel)]="selectedChartTemplate.templateHtml" name="editTemplateHtml" rows="5"></textarea>
</clr-textarea-container>
<clr-textarea-container>
<label>CSS Template</label>
<textarea clrTextarea [(ngModel)]="selectedChartTemplate.templateCss" name="editTemplateCss" rows="5"></textarea>
</clr-textarea-container>
<clr-checkbox-container>
<label>Is Default</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="selectedChartTemplate.isDefault" name="editIsDefault" />
<label>Default Template</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!selectedChartTemplate.templateName || loadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
Update Chart Template
</button>
<button class="btn" type="button" (click)="selectedChartTemplate = null">Cancel</button>
</div>
</form>
</div>
</div>
<div class="info-section">
<h4>About Chart Templates</h4>
<p>Chart templates define how a chart of this type is rendered. Each template includes:</p>
<ul>
<li>HTML structure that defines the chart layout</li>
<li>CSS styling that controls the appearance</li>
<li>A default flag to indicate the primary template</li>
</ul>
<p>Templates allow you to have multiple visual representations for the same chart type.</p>
</div>
</div>

View File

@@ -0,0 +1,128 @@
.chart-type-templates-page {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
.header {
margin-bottom: 20px;
display: flex;
align-items: center;
h2 {
color: #0079b8;
font-weight: 300;
display: flex;
align-items: center;
gap: 10px;
}
.back-button {
padding: 0;
cds-icon {
width: 24px;
height: 24px;
}
}
}
.card {
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.card-block {
padding: 20px;
}
.card-header {
h3 {
margin: 0;
color: #0079b8;
}
}
}
.form-actions {
margin-top: 20px;
display: flex;
gap: 10px;
}
.label {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
&.label-success {
background-color: #3d9970;
color: white;
}
&.label-danger {
background-color: #d32f2f;
color: white;
}
}
clr-datagrid {
margin-top: 10px;
clr-dg-cell {
&:first-child {
font-weight: 500;
}
}
}
.btn-icon {
margin-right: 5px;
}
.info-section {
background-color: #f6f6f6;
border-left: 4px solid #0079b8;
padding: 15px;
border-radius: 4px;
h4 {
margin-top: 0;
color: #0079b8;
}
ul {
padding-left: 20px;
li {
margin-bottom: 8px;
}
}
p {
margin: 10px 0;
}
}
@media (max-width: 768px) {
padding: 10px;
.form-actions {
flex-direction: column;
button {
width: 100%;
margin-bottom: 10px;
}
}
.card-header {
.clr-row {
flex-direction: column;
.clr-col {
text-align: left !important;
margin-bottom: 10px;
}
}
}
}
}

View File

@@ -0,0 +1,176 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ClrLoadingState } from '@clr/angular';
import { ChartType, ChartTypeService } from './chart-type.service';
import { ChartTemplateService } from '../chart-config/chart-template.service';
import { ChartTemplate } from '../chart-config/chart-config-manager.component';
@Component({
selector: 'app-chart-type-templates',
templateUrl: './chart-type-templates.component.html',
styleUrls: ['./chart-type-templates.component.scss']
})
export class ChartTypeTemplatesComponent implements OnInit {
chartType: ChartType | null = null;
chartTemplates: ChartTemplate[] = [];
newChartTemplate: Partial<ChartTemplate> = {
isDefault: false
};
selectedChartTemplate: ChartTemplate | null = null;
showAddForm = false;
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
errorMessage: string | null = null;
successMessage: string | null = null;
// Make ClrLoadingState available to template
readonly ClrLoadingState = ClrLoadingState;
constructor(
private chartTypeService: ChartTypeService,
private chartTemplateService: ChartTemplateService,
private route: ActivatedRoute,
private router: Router
) { }
ngOnInit(): void {
const chartTypeId = Number(this.route.snapshot.paramMap.get('id'));
if (chartTypeId) {
this.loadChartType(chartTypeId);
this.loadChartTemplates(chartTypeId);
} else {
this.showError('Invalid chart type ID');
}
}
// Show error message
private showError(message: string): void {
this.errorMessage = message;
setTimeout(() => {
this.errorMessage = null;
}, 5000);
}
// Show success message
private showSuccess(message: string): void {
this.successMessage = message;
setTimeout(() => {
this.successMessage = null;
}, 3000);
}
loadChartType(id: number): void {
this.chartTypeService.getChartTypeById(id).subscribe({
next: (data) => {
this.chartType = data;
},
error: (error) => {
console.error('Error loading chart type:', error);
this.showError('Error loading chart type: ' + (error.error?.message || error.message || 'Unknown error'));
}
});
}
loadChartTemplates(chartTypeId: number): void {
this.loadingState = ClrLoadingState.LOADING;
this.chartTemplateService.getChartTemplatesByChartType(chartTypeId).subscribe({
next: (data) => {
this.chartTemplates = data;
this.loadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading chart templates:', error);
this.showError('Error loading chart templates: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
createChartTemplate(): void {
if (!this.chartType || !this.newChartTemplate.templateName) {
this.showError('Template name is required');
return;
}
// Create a copy without the chartType property
const templateData: Partial<ChartTemplate> = {
templateName: this.newChartTemplate.templateName,
templateHtml: this.newChartTemplate.templateHtml,
templateCss: this.newChartTemplate.templateCss,
isDefault: this.newChartTemplate.isDefault
};
this.loadingState = ClrLoadingState.LOADING;
this.chartTemplateService.createChartTemplate(templateData, this.chartType.id).subscribe({
next: (data) => {
this.chartTemplates.push(data);
this.newChartTemplate = { isDefault: false };
this.showAddForm = false;
this.showSuccess('Chart template created successfully');
this.loadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating chart template:', error);
this.showError('Error creating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
updateChartTemplate(): void {
if (!this.selectedChartTemplate || !this.selectedChartTemplate.templateName) {
this.showError('Template name is required');
return;
}
this.loadingState = ClrLoadingState.LOADING;
this.chartTemplateService.updateChartTemplate(this.selectedChartTemplate.id, this.selectedChartTemplate).subscribe({
next: (data) => {
const index = this.chartTemplates.findIndex(ct => ct.id === data.id);
if (index !== -1) {
this.chartTemplates[index] = data;
}
this.selectedChartTemplate = null;
this.showSuccess('Chart template updated successfully');
this.loadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating chart template:', error);
this.showError('Error updating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
deleteChartTemplate(id: number): void {
if (!confirm('Are you sure you want to delete this chart template?')) {
return;
}
this.loadingState = ClrLoadingState.LOADING;
this.chartTemplateService.deleteChartTemplate(id).subscribe({
next: () => {
this.chartTemplates = this.chartTemplates.filter(ct => ct.id !== id);
this.showSuccess('Chart template deleted successfully');
this.loadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting chart template:', error);
this.showError('Error deleting chart template: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
selectChartTemplateForEdit(chartTemplate: ChartTemplate): void {
this.selectedChartTemplate = { ...chartTemplate };
}
goBack(): void {
if (this.chartType) {
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types/edit', this.chartType.id]);
} else {
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
}
}
}

View File

@@ -0,0 +1,312 @@
<div class="chart-type-ui-components-page">
<div class="header">
<h2>
<button class="btn btn-link back-button" (click)="goBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
</button>
UI Components for {{ chartType?.name || 'Chart Type' }}
</h2>
</div>
<!-- Error and Success Messages -->
<div class="alert alert-danger" *ngIf="errorMessage">
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ errorMessage }}</span>
</div>
</div>
<div class="alert alert-success" *ngIf="successMessage">
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ successMessage }}</span>
</div>
</div>
<!-- Add UI Component Form -->
<div class="card" *ngIf="showAddForm">
<div class="card-header">
<h3>Add New UI Component</h3>
</div>
<div class="card-block">
<form clrForm (ngSubmit)="createUiComponent()" #addUiComponentForm="ngForm">
<clr-input-container>
<label>Component Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="newUiComponent.componentName" name="componentName" required />
<clr-control-helper>Enter a unique name for the UI component (e.g., "title-config", "axis-config")</clr-control-helper>
</clr-input-container>
<clr-input-container>
<label>Component Type</label>
<input clrInput type="text" [(ngModel)]="newUiComponent.componentType" name="componentType" />
<clr-control-helper>Type of the component (e.g., "input", "select", "checkbox")</clr-control-helper>
</clr-input-container>
<clr-input-container>
<label>Display Label</label>
<input clrInput type="text" [(ngModel)]="newUiComponent.displayLabel" name="displayLabel" />
<clr-control-helper>User-friendly label shown in the configuration form</clr-control-helper>
</clr-input-container>
<clr-input-container>
<label>Placeholder</label>
<input clrInput type="text" [(ngModel)]="newUiComponent.placeholder" name="placeholder" />
<clr-control-helper>Placeholder text for input fields</clr-control-helper>
</clr-input-container>
<clr-input-container>
<label>Sort Order</label>
<input clrInput type="number" [(ngModel)]="newUiComponent.sortOrder" name="sortOrder" />
<clr-control-helper>Order in which components appear in the form</clr-control-helper>
</clr-input-container>
<clr-checkbox-container>
<label>Is Required</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="newUiComponent.isRequired" name="isRequired" />
<label>Required</label>
</clr-checkbox-wrapper>
<clr-control-helper>Mark as required if this component must be filled</clr-control-helper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!newUiComponent.componentName || loadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
Create UI Component
</button>
<button class="btn" type="button" (click)="showAddForm = false">Cancel</button>
</div>
</form>
</div>
</div>
<!-- UI Components Grid -->
<div class="card">
<div class="card-header">
<div class="clr-row">
<div class="clr-col">
<h3>UI Components</h3>
</div>
<div class="clr-col" style="text-align: right;">
<button class="btn btn-primary" (click)="showAddForm = true">
<cds-icon shape="plus"></cds-icon> Add UI Component
</button>
</div>
</div>
</div>
<div class="card-block">
<clr-datagrid [clrDgLoading]="loadingState === ClrLoadingState.LOADING">
<clr-dg-column>Component Name</clr-dg-column>
<clr-dg-column>Component Type</clr-dg-column>
<clr-dg-column>Display Label</clr-dg-column>
<clr-dg-column>Required</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let uiComponent of uiComponents" [clrDgItem]="uiComponent">
<clr-dg-cell>{{uiComponent.componentName}}</clr-dg-cell>
<clr-dg-cell>{{uiComponent.componentType}}</clr-dg-cell>
<clr-dg-cell>{{uiComponent.displayLabel}}</clr-dg-cell>
<clr-dg-cell>
<span class="label" [class.label-success]="uiComponent.isRequired" [class.label-danger]="!uiComponent.isRequired">
{{uiComponent.isRequired ? 'Yes' : 'No'}}
</span>
</clr-dg-cell>
<clr-dg-cell class="action-cell">
<div class="action-buttons">
<button class="btn btn-sm btn-icon" (click)="selectUiComponentForEdit(uiComponent)" title="Edit" [disabled]="loadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="loadingState !== ClrLoadingState.LOADING" shape="pencil" aria-label="Edit"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteUiComponent(uiComponent.id)" title="Delete" [disabled]="loadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="loadingState !== ClrLoadingState.LOADING" shape="trash" aria-label="Delete"></cds-icon>
</button>
<button class="btn btn-sm btn-icon btn-primary" (click)="onViewComponentProperties(uiComponent)" title="View Properties" [disabled]="loadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="loadingState !== ClrLoadingState.LOADING" shape="eye" aria-label="View Properties"></cds-icon>
</button>
</div>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{uiComponents.length}} UI component(s)
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<!-- Edit UI Component Form -->
<div class="card" *ngIf="selectedUiComponent && !showAddComponentPropertyForm && !selectedComponentProperty">
<div class="card-header">
<h3>Edit UI Component</h3>
</div>
<div class="card-block">
<form clrForm (ngSubmit)="updateUiComponent()" #editUiComponentForm="ngForm">
<clr-input-container>
<label>Component Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="selectedUiComponent.componentName" name="editComponentName" required />
</clr-input-container>
<clr-input-container>
<label>Component Type</label>
<input clrInput type="text" [(ngModel)]="selectedUiComponent.componentType" name="editComponentType" />
</clr-input-container>
<clr-input-container>
<label>Display Label</label>
<input clrInput type="text" [(ngModel)]="selectedUiComponent.displayLabel" name="editDisplayLabel" />
</clr-input-container>
<clr-input-container>
<label>Placeholder</label>
<input clrInput type="text" [(ngModel)]="selectedUiComponent.placeholder" name="editPlaceholder" />
</clr-input-container>
<clr-input-container>
<label>Sort Order</label>
<input clrInput type="number" [(ngModel)]="selectedUiComponent.sortOrder" name="editSortOrder" />
</clr-input-container>
<clr-checkbox-container>
<label>Is Required</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="selectedUiComponent.isRequired" name="editIsRequired" />
<label>Required</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!selectedUiComponent.componentName || loadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
Update UI Component
</button>
<button class="btn" type="button" (click)="selectedUiComponent = null">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Component Properties Section -->
<div class="card" *ngIf="selectedUiComponent">
<div class="card-header">
<div class="clr-row">
<div class="clr-col">
<h3>Properties for {{selectedUiComponent?.componentName}}</h3>
</div>
<div class="clr-col" style="text-align: right;">
<button class="btn btn-primary" (click)="showAddComponentPropertyForm = true">
<cds-icon shape="plus"></cds-icon> Add Property
</button>
</div>
</div>
</div>
<!-- Add Component Property Form -->
<div class="card-block" *ngIf="showAddComponentPropertyForm">
<form clrForm (ngSubmit)="createComponentProperty()" #addComponentPropertyForm="ngForm">
<clr-input-container>
<label>Property Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyName" name="propertyName" required />
</clr-input-container>
<clr-input-container>
<label>Property Value</label>
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyValue" name="propertyValue" />
</clr-input-container>
<clr-input-container>
<label>Property Type</label>
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyType" name="propertyType" />
</clr-input-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!newComponentProperty.propertyName || componentPropertyLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
Create Property
</button>
<button class="btn" type="button" (click)="showAddComponentPropertyForm = false">Cancel</button>
</div>
</form>
</div>
<!-- Component Properties Table -->
<div class="card-block">
<clr-datagrid [clrDgLoading]="componentPropertyLoadingState === ClrLoadingState.LOADING">
<clr-dg-column>Property Name</clr-dg-column>
<clr-dg-column>Property Value</clr-dg-column>
<clr-dg-column>Property Type</clr-dg-column>
<clr-dg-column>Created At</clr-dg-column>
<clr-dg-column>Updated At</clr-dg-column>
<clr-dg-column>Actions</clr-dg-column>
<clr-dg-row *clrDgItems="let property of componentProperties" [clrDgItem]="property">
<clr-dg-cell>{{property.propertyName}}</clr-dg-cell>
<clr-dg-cell>{{property.propertyValue}}</clr-dg-cell>
<clr-dg-cell>{{property.propertyType}}</clr-dg-cell>
<clr-dg-cell>{{property.createdAt | date:'short'}}</clr-dg-cell>
<clr-dg-cell>{{property.updatedAt | date:'short'}}</clr-dg-cell>
<clr-dg-cell>
<button class="btn btn-sm btn-icon" (click)="selectComponentPropertyForEdit(property)" title="Edit" [disabled]="componentPropertyLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="componentPropertyLoadingState !== ClrLoadingState.LOADING" shape="pencil"></cds-icon>
</button>
<button class="btn btn-sm btn-icon" (click)="deleteComponentProperty(property.id)" title="Delete" [disabled]="componentPropertyLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
<cds-icon *ngIf="componentPropertyLoadingState !== ClrLoadingState.LOADING" shape="trash"></cds-icon>
</button>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{componentProperties.length}} propertie(s)
</clr-dg-footer>
</clr-datagrid>
</div>
<!-- Edit Component Property Form -->
<div class="card-block" *ngIf="selectedComponentProperty">
<h4>Edit Component Property</h4>
<form clrForm (ngSubmit)="updateComponentProperty()" #editComponentPropertyForm="ngForm">
<clr-input-container>
<label>Property Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyName" name="editPropertyName" required />
</clr-input-container>
<clr-input-container>
<label>Property Value</label>
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyValue" name="editPropertyValue" />
</clr-input-container>
<clr-input-container>
<label>Property Type</label>
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyType" name="editPropertyType" />
</clr-input-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!selectedComponentProperty.propertyName || componentPropertyLoadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
Update Property
</button>
<button class="btn" type="button" (click)="selectedComponentProperty = null">Cancel</button>
</div>
</form>
</div>
</div>
<div class="info-section">
<h4>About UI Components</h4>
<p>UI components define the configuration form elements for a chart type. Each component represents:</p>
<ul>
<li>A form field that appears when configuring a chart of this type</li>
<li>Metadata like label, placeholder, and validation rules</li>
<li>An order in which they appear in the configuration form</li>
</ul>
<p>After creating UI components, you can define their properties using the "View Properties" button.</p>
</div>
</div>

View File

@@ -0,0 +1,156 @@
.chart-type-ui-components-page {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
.header {
margin-bottom: 20px;
display: flex;
align-items: center;
h2 {
color: #0079b8;
font-weight: 300;
display: flex;
align-items: center;
gap: 10px;
}
.back-button {
padding: 0;
cds-icon {
width: 24px;
height: 24px;
}
}
}
.card {
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.card-block {
padding: 20px;
}
.card-header {
h3 {
margin: 0;
color: #0079b8;
}
}
}
.form-actions {
margin-top: 20px;
display: flex;
gap: 10px;
}
.label {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
&.label-success {
background-color: #3d9970;
color: white;
}
&.label-danger {
background-color: #d32f2f;
color: white;
}
}
clr-datagrid {
margin-top: 10px;
clr-dg-cell {
&:first-child {
font-weight: 500;
}
}
}
.btn-icon {
margin-right: 5px;
}
.info-section {
background-color: #f6f6f6;
border-left: 4px solid #0079b8;
padding: 15px;
border-radius: 4px;
h4 {
margin-top: 0;
color: #0079b8;
}
ul {
padding-left: 20px;
li {
margin-bottom: 8px;
}
}
p {
margin: 10px 0;
}
}
// Component Properties Section Styles
.action-cell {
min-width: 120px;
}
.action-buttons {
display: flex;
gap: 5px;
flex-wrap: nowrap;
align-items: center;
}
.action-buttons .btn-icon {
min-width: 36px;
display: flex;
align-items: center;
justify-content: center;
}
// Ensure icons are visible
cds-icon {
display: inline-block !important;
}
clr-spinner {
margin: 0;
}
@media (max-width: 768px) {
padding: 10px;
.form-actions {
flex-direction: column;
button {
width: 100%;
margin-bottom: 10px;
}
}
.card-header {
.clr-row {
flex-direction: column;
.clr-col {
text-align: left !important;
margin-bottom: 10px;
}
}
}
}
}

View File

@@ -0,0 +1,309 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ClrLoadingState } from '@clr/angular';
import { ChartType, ChartTypeService } from './chart-type.service';
import { UiComponentService } from '../chart-config/ui-component.service';
import { ComponentPropertyService } from '../chart-config/component-property.service';
import { UiComponent, ComponentProperty } from '../chart-config/chart-config-manager.component';
@Component({
selector: 'app-chart-type-ui-components',
templateUrl: './chart-type-ui-components.component.html',
styleUrls: ['./chart-type-ui-components.component.scss']
})
export class ChartTypeUiComponentsComponent implements OnInit {
chartType: ChartType | null = null;
uiComponents: UiComponent[] = [];
newUiComponent: Partial<UiComponent> = {
isRequired: false
};
selectedUiComponent: UiComponent | null = null;
showAddForm = false;
// Component Properties
componentProperties: ComponentProperty[] = [];
selectedComponentProperty: ComponentProperty | null = null;
newComponentProperty: Partial<ComponentProperty> = {};
showAddComponentPropertyForm = false;
componentPropertyLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
errorMessage: string | null = null;
successMessage: string | null = null;
// Make ClrLoadingState available to template
readonly ClrLoadingState = ClrLoadingState;
constructor(
private chartTypeService: ChartTypeService,
private uiComponentService: UiComponentService,
private componentPropertyService: ComponentPropertyService,
private route: ActivatedRoute,
private router: Router
) { }
ngOnInit(): void {
const chartTypeId = Number(this.route.snapshot.paramMap.get('id'));
if (chartTypeId) {
this.loadChartType(chartTypeId);
this.loadUiComponents(chartTypeId);
} else {
this.showError('Invalid chart type ID');
}
}
// Show error message
private showError(message: string): void {
this.errorMessage = message;
setTimeout(() => {
this.errorMessage = null;
}, 5000);
}
// Show success message
private showSuccess(message: string): void {
this.successMessage = message;
setTimeout(() => {
this.successMessage = null;
}, 3000);
}
loadChartType(id: number): void {
this.chartTypeService.getChartTypeById(id).subscribe({
next: (data) => {
this.chartType = data;
},
error: (error) => {
console.error('Error loading chart type:', error);
this.showError('Error loading chart type: ' + (error.error?.message || error.message || 'Unknown error'));
}
});
}
loadUiComponents(chartTypeId: number): void {
this.loadingState = ClrLoadingState.LOADING;
this.uiComponentService.getUiComponentsByChartType(chartTypeId).subscribe({
next: (data) => {
this.uiComponents = data;
this.loadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading UI components:', error);
this.showError('Error loading UI components: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
createUiComponent(): void {
if (!this.chartType || !this.newUiComponent.componentName) {
this.showError('Component name is required');
return;
}
this.loadingState = ClrLoadingState.LOADING;
// Create a complete chartType object with only the ID (following the pattern in chart-config-manager)
const chartTypeWithId = {
id: this.chartType.id,
name: '',
displayName: '',
description: '',
isActive: true,
createdAt: '',
updatedAt: ''
};
const componentData = {
...this.newUiComponent,
chartType: chartTypeWithId
};
this.uiComponentService.createUiComponent(componentData).subscribe({
next: (data) => {
this.uiComponents.push(data);
this.newUiComponent = { isRequired: false };
this.showAddForm = false;
this.showSuccess('UI component created successfully');
this.loadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating UI component:', error);
this.showError('Error creating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
updateUiComponent(): void {
if (!this.selectedUiComponent || !this.selectedUiComponent.componentName) {
this.showError('Component name is required');
return;
}
this.loadingState = ClrLoadingState.LOADING;
this.uiComponentService.updateUiComponent(this.selectedUiComponent.id, this.selectedUiComponent).subscribe({
next: (data) => {
const index = this.uiComponents.findIndex(uc => uc.id === data.id);
if (index !== -1) {
this.uiComponents[index] = data;
}
this.selectedUiComponent = null;
this.showSuccess('UI component updated successfully');
this.loadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating UI component:', error);
this.showError('Error updating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
deleteUiComponent(id: number): void {
if (!confirm('Are you sure you want to delete this UI component?')) {
return;
}
this.loadingState = ClrLoadingState.LOADING;
this.uiComponentService.deleteUiComponent(id).subscribe({
next: () => {
this.uiComponents = this.uiComponents.filter(uc => uc.id !== id);
this.showSuccess('UI component deleted successfully');
this.loadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting UI component:', error);
this.showError('Error deleting UI component: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
selectUiComponentForEdit(uiComponent: UiComponent): void {
this.selectedUiComponent = { ...uiComponent };
}
// Component Property Methods
loadComponentProperties(componentId: number): void {
if (!componentId) {
this.componentProperties = [];
return;
}
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
this.componentPropertyService.getComponentPropertiesByComponent(componentId).subscribe({
next: (data) => {
this.componentProperties = data;
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading component properties:', error);
this.showError('Error loading component properties: ' + (error.error?.message || error.message || 'Unknown error'));
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
}
});
}
createComponentProperty(): void {
if (!this.selectedUiComponent) {
this.showError('Please select a UI component first');
return;
}
if (!this.newComponentProperty.propertyName) {
this.showError('Property name is required');
return;
}
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
// Create a complete component object with only the ID
const componentWithId = {
id: this.selectedUiComponent.id
} as UiComponent;
const componentPropertyData = {
...this.newComponentProperty,
component: componentWithId
};
this.componentPropertyService.createComponentProperty(componentPropertyData).subscribe({
next: (data) => {
this.componentProperties.push(data);
this.newComponentProperty = {};
this.showAddComponentPropertyForm = false;
this.showSuccess('Component property created successfully');
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error creating component property:', error);
this.showError('Error creating component property: ' + (error.error?.message || error.message || 'Unknown error'));
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
}
});
}
updateComponentProperty(): void {
if (!this.selectedComponentProperty || !this.selectedComponentProperty.propertyName) {
this.showError('Property name is required');
return;
}
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
this.componentPropertyService.updateComponentProperty(this.selectedComponentProperty.id, this.selectedComponentProperty).subscribe({
next: (data) => {
const index = this.componentProperties.findIndex(cp => cp.id === data.id);
if (index !== -1) {
this.componentProperties[index] = data;
}
this.selectedComponentProperty = null;
this.showSuccess('Component property updated successfully');
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error updating component property:', error);
this.showError('Error updating component property: ' + (error.error?.message || error.message || 'Unknown error'));
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
}
});
}
deleteComponentProperty(id: number): void {
if (!confirm('Are you sure you want to delete this component property?')) {
return;
}
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
this.componentPropertyService.deleteComponentProperty(id).subscribe({
next: () => {
this.componentProperties = this.componentProperties.filter(cp => cp.id !== id);
this.showSuccess('Component property deleted successfully');
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error deleting component property:', error);
this.showError('Error deleting component property: ' + (error.error?.message || error.message || 'Unknown error'));
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
}
});
}
selectComponentPropertyForEdit(componentProperty: ComponentProperty): void {
this.selectedComponentProperty = { ...componentProperty };
}
// Helper method to view properties for a UI component
onViewComponentProperties(uiComponent: UiComponent): void {
this.selectedUiComponent = uiComponent;
this.loadComponentProperties(uiComponent.id);
}
goBack(): void {
if (this.chartType) {
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types/edit', this.chartType.id]);
} else {
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
}
}
}

View File

@@ -0,0 +1,55 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiRequestService } from 'src/app/services/api/api-request.service';
export interface ChartType {
id: number;
name: string;
displayName: string;
description: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
@Injectable({
providedIn: 'root'
})
export class ChartTypeService {
private chartTypesUrl = 'api/chart-types';
constructor(private apiRequest: ApiRequestService) { }
// Get all chart types
getAllChartTypes(): Observable<ChartType[]> {
console.log('Fetching all chart types from', this.chartTypesUrl);
return this.apiRequest.get(this.chartTypesUrl);
}
// Get chart type by ID
getChartTypeById(id: number): Observable<ChartType> {
const url = `${this.chartTypesUrl}/${id}`;
console.log(`Fetching chart type ${id} from ${url}`);
return this.apiRequest.get(url);
}
// Create new chart type
createChartType(chartType: Partial<ChartType>): Observable<ChartType> {
console.log('Creating chart type:', chartType);
return this.apiRequest.post(this.chartTypesUrl, chartType);
}
// Update chart type
updateChartType(id: number, chartType: ChartType): Observable<ChartType> {
const url = `${this.chartTypesUrl}/${id}`;
console.log(`Updating chart type ${id}:`, chartType);
return this.apiRequest.put(url, chartType);
}
// Delete chart type
deleteChartType(id: number): Observable<void> {
const url = `${this.chartTypesUrl}/${id}`;
console.log(`Deleting chart type ${id}`);
return this.apiRequest.delete(url);
}
}

View File

@@ -0,0 +1,160 @@
<div class="edit-chart-type-page">
<div class="header">
<h2>Edit Chart Type</h2>
</div>
<!-- Loading State -->
<div *ngIf="loadingState === ClrLoadingState.LOADING" class="loading-spinner">
<clr-spinner clrMedium></clr-spinner>
<span>Loading chart type...</span>
</div>
<!-- Error and Success Messages -->
<div class="alert alert-danger" *ngIf="errorMessage">
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ errorMessage }}</span>
</div>
</div>
<div class="alert alert-success" *ngIf="successMessage">
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
<cds-icon shape="close"></cds-icon>
</button>
<div class="alert-item">
<span class="alert-text">{{ successMessage }}</span>
</div>
</div>
<!-- Chart Type Form -->
<div class="card" *ngIf="chartType && loadingState !== ClrLoadingState.LOADING">
<div class="card-header">
<div class="chart-type-header">
<h3>{{ chartType.name }}</h3>
<span class="label" [class.label-success]="chartType.isActive" [class.label-danger]="!chartType.isActive">
{{ chartType.isActive ? 'Active' : 'Inactive' }}
</span>
</div>
</div>
<div class="card-block">
<form clrForm (ngSubmit)="updateChartType()" #editChartTypeForm="ngForm">
<clr-input-container>
<label>Name <span class="required">*</span></label>
<input clrInput type="text" [(ngModel)]="chartType.name" name="chartTypeName" required />
<clr-control-helper>Enter a unique name for the chart type (e.g., "line-chart", "bar-chart")</clr-control-helper>
</clr-input-container>
<clr-input-container>
<label>Display Name</label>
<input clrInput type="text" [(ngModel)]="chartType.displayName" name="chartTypeDisplayName" />
<clr-control-helper>This is the user-friendly name shown in the UI</clr-control-helper>
</clr-input-container>
<clr-textarea-container>
<label>Description</label>
<textarea clrTextarea [(ngModel)]="chartType.description" name="chartTypeDescription" rows="3"></textarea>
<clr-control-helper>Provide a detailed description of this chart type and when to use it</clr-control-helper>
</clr-textarea-container>
<clr-checkbox-container>
<label>Is Active</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="chartType.isActive" name="chartTypeIsActive" />
<label>Active</label>
</clr-checkbox-wrapper>
<clr-control-helper>Deactivate chart types that should not be available for selection</clr-control-helper>
</clr-checkbox-container>
<div class="form-actions">
<button class="btn btn-primary" type="submit" [disabled]="!chartType.name || loadingState === ClrLoadingState.LOADING">
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
Update Chart Type
</button>
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
<button class="btn btn-danger" type="button" (click)="onDelete()" style="margin-left: auto;">
<cds-icon shape="trash"></cds-icon> Delete
</button>
</div>
</form>
</div>
</div>
<!-- Related Entities Management -->
<div class="card" *ngIf="chartType && loadingState !== ClrLoadingState.LOADING">
<div class="card-header">
<h3>Related Configuration</h3>
</div>
<div class="card-block">
<div class="related-entities-grid">
<div class="entity-card">
<div class="entity-header">
<cds-icon shape="view-list" size="24"></cds-icon>
<h4>UI Components</h4>
</div>
<p>Manage the UI components that define the configuration form for this chart type.</p>
<button class="btn btn-sm btn-link" [routerLink]="['/cns-portal/dashboardbuilder/chart-types', chartType.id, 'ui-components']">
Manage UI Components
</button>
</div>
<div class="entity-card">
<div class="entity-header">
<cds-icon shape="template" size="24"></cds-icon>
<h4>Chart Templates</h4>
</div>
<p>Manage the templates that define how this chart type is rendered.</p>
<button class="btn btn-sm btn-link" [routerLink]="['/cns-portal/dashboardbuilder/chart-types', chartType.id, 'templates']">
Manage Templates
</button>
</div>
<div class="entity-card">
<div class="entity-header">
<cds-icon shape="form" size="24"></cds-icon>
<h4>Dynamic Fields</h4>
</div>
<p>Manage the dynamic fields that capture specific configuration parameters.</p>
<button class="btn btn-sm btn-link" [routerLink]="['/cns-portal/dashboardbuilder/chart-types', chartType.id, 'fields']">
Manage Fields
</button>
</div>
</div>
</div>
</div>
<!-- Chart Type Details -->
<div class="card" *ngIf="chartType && loadingState !== ClrLoadingState.LOADING">
<div class="card-header">
<h3>Chart Type Details</h3>
</div>
<div class="card-block">
<div class="details-grid">
<div class="detail-item">
<label>ID:</label>
<span>{{ chartType.id }}</span>
</div>
<div class="detail-item">
<label>Created At:</label>
<span>{{ chartType.createdAt ? (chartType.createdAt | date:'medium') : 'N/A' }}</span>
</div>
<div class="detail-item">
<label>Updated At:</label>
<span>{{ chartType.updatedAt ? (chartType.updatedAt | date:'medium') : 'N/A' }}</span>
</div>
</div>
</div>
</div>
<div class="info-section">
<h4>About Chart Types</h4>
<p>Chart types define the different visualization options available in the dashboard builder. Each chart type can have:</p>
<ul>
<li>Associated UI components that define the configuration form</li>
<li>Templates that define how the chart is rendered</li>
<li>Dynamic fields that capture specific configuration parameters</li>
</ul>
<p>After creating a chart type, you can configure its components, templates, and fields using the management links above.</p>
</div>
</div>

View File

@@ -0,0 +1,163 @@
.edit-chart-type-page {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
.header {
margin-bottom: 20px;
h2 {
color: #0079b8;
font-weight: 300;
}
}
.loading-spinner {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
gap: 10px;
}
.card {
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.card-block {
padding: 20px;
}
.card-header {
.chart-type-header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin: 0;
}
}
}
}
.form-actions {
margin-top: 20px;
display: flex;
gap: 10px;
}
.related-entities-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
.entity-card {
border: 1px solid #ddd;
border-radius: 4px;
padding: 20px;
text-align: center;
background-color: #f9f9f9;
.entity-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 15px;
cds-icon {
color: #0079b8;
margin-bottom: 10px;
}
h4 {
margin: 0;
color: #0079b8;
}
}
p {
color: #666;
margin-bottom: 15px;
min-height: 60px;
}
.btn-link {
font-weight: 500;
}
}
}
.details-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
.detail-item {
display: flex;
flex-direction: column;
label {
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
span {
padding: 8px;
background-color: #f6f6f6;
border-radius: 4px;
}
}
}
.info-section {
background-color: #f6f6f6;
border-left: 4px solid #0079b8;
padding: 15px;
border-radius: 4px;
h4 {
margin-top: 0;
color: #0079b8;
}
ul {
padding-left: 20px;
li {
margin-bottom: 8px;
}
}
p {
margin: 10px 0;
}
}
@media (max-width: 768px) {
padding: 10px;
.form-actions {
flex-direction: column;
button {
width: 100%;
margin-bottom: 10px;
}
}
.chart-type-header {
flex-direction: column;
align-items: flex-start !important;
.label {
margin-top: 10px;
}
}
.related-entities-grid {
grid-template-columns: 1fr;
}
}
}

View File

@@ -0,0 +1,159 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ClrLoadingState } from '@clr/angular';
import { ChartType, ChartTypeService } from './chart-type.service';
@Component({
selector: 'app-edit-chart-type',
templateUrl: './edit-chart-type.component.html',
styleUrls: ['./edit-chart-type.component.scss']
})
export class EditChartTypeComponent implements OnInit {
chartType: ChartType | null = null;
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
errorMessage: string | null = null;
successMessage: string | null = null;
// Make ClrLoadingState available to template
readonly ClrLoadingState = ClrLoadingState;
constructor(
private chartTypeService: ChartTypeService,
private route: ActivatedRoute,
private router: Router
) { }
ngOnInit(): void {
const id = Number(this.route.snapshot.paramMap.get('id'));
if (id) {
this.loadChartType(id);
} else {
this.showError('Invalid chart type ID');
}
}
// Show error message
private showError(message: string): void {
this.errorMessage = message;
setTimeout(() => {
this.errorMessage = null;
}, 5000);
}
// Show success message
private showSuccess(message: string): void {
this.successMessage = message;
setTimeout(() => {
this.successMessage = null;
}, 3000);
}
loadChartType(id: number): void {
this.loadingState = ClrLoadingState.LOADING;
this.chartTypeService.getChartTypeById(id).subscribe({
next: (data) => {
// Process the data to ensure dates are properly formatted
this.chartType = {
...data,
createdAt: this.formatDate(data.createdAt),
updatedAt: this.formatDate(data.updatedAt)
};
this.loadingState = ClrLoadingState.SUCCESS;
},
error: (error) => {
console.error('Error loading chart type:', error);
this.showError('Error loading chart type: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
// Format date to handle both string and object formats
private formatDate(date: any): string {
if (!date) return '';
// If it's already a string, return as is
if (typeof date === 'string') {
return date;
}
// If it's an object, try to convert to string
if (typeof date === 'object') {
// Handle various date object formats
if (date instanceof Date) {
return date.toISOString();
}
// Handle ISO string within object
if (date.date) {
return date.date;
}
// Handle other object formats
try {
return new Date(date).toISOString();
} catch {
return '';
}
}
return '';
}
updateChartType(): void {
if (!this.chartType || !this.chartType.name) {
this.showError('Chart type name is required');
return;
}
this.loadingState = ClrLoadingState.LOADING;
this.chartTypeService.updateChartType(this.chartType.id, this.chartType).subscribe({
next: (data) => {
// Process the data to ensure dates are properly formatted
this.chartType = {
...data,
createdAt: this.formatDate(data.createdAt),
updatedAt: this.formatDate(data.updatedAt)
};
this.showSuccess('Chart type updated successfully');
this.loadingState = ClrLoadingState.SUCCESS;
// Redirect to chart types list after a short delay
setTimeout(() => {
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
}, 1500);
},
error: (error) => {
console.error('Error updating chart type:', error);
this.showError('Error updating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
onCancel(): void {
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
}
onDelete(): void {
if (!this.chartType) return;
if (confirm('Are you sure you want to delete this chart type? This action cannot be undone.')) {
this.loadingState = ClrLoadingState.LOADING;
this.chartTypeService.deleteChartType(this.chartType.id).subscribe({
next: () => {
this.showSuccess('Chart type deleted successfully');
this.loadingState = ClrLoadingState.SUCCESS;
// Redirect to chart types list after a short delay
setTimeout(() => {
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
}, 1500);
},
error: (error) => {
console.error('Error deleting chart type:', error);
this.showError('Error deleting chart type: ' + (error.error?.message || error.message || 'Unknown error'));
this.loadingState = ClrLoadingState.ERROR;
}
});
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
<div class="chart-config-modal">
<h3>Chart Configuration Manager</h3>
<app-chart-config-manager></app-chart-config-manager>
</div>

View File

@@ -0,0 +1,5 @@
.chart-config-modal {
padding: 20px;
max-height: 80vh;
overflow-y: auto;
}

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-chart-config-modal',
templateUrl: './chart-config-modal.component.html',
styleUrls: ['./chart-config-modal.component.scss']
})
export class ChartConfigModalComponent {
// This component will be used to display the chart configuration manager in a modal
}

View File

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

View File

@@ -1,32 +1,334 @@
<div style="display: block; height: 100%; width: 100%;">
<!-- No filter controls needed with the new simplified approach -->
<!-- Filters are now configured at the drilldown level -->
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Back to Main View
</button>
<div class="chart-container">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- 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>
<!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
No data available
<!-- 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>
<!-- Chart display -->
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
<canvas baseChart
[datasets]="barChartData"
[labels]="barChartLabels"
[type]="barChartType"
[options]="barChartOptions"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
</canvas>
<div class="chart-wrapper">
<div class="chart-content" [class.loading]="isLoading">
<div *ngIf="noDataAvailable" class="no-data-message">
No data available
</div>
<div *ngIf="!noDataAvailable" class="chart-display">
<canvas baseChart
[datasets]="barChartData"
[labels]="barChartLabels"
[type]="barChartType"
[options]="barChartOptions"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
</canvas>
</div>
<div class="loading-overlay" *ngIf="isLoading">
<div class="shimmer-bar"></div>
</div>
</div>
</div>
<!-- sheield dashboard -->
<!--
<div class="chart-container">
<div class="chart-header">
<h3>Deal Stage Wise Progress</h3>
</div>
<div class="chart-wrapper">
<div class="chart-content" [class.loading]="isLoading">
<canvas
baseChart
[data]="barChartData"
[options]="barChartOptions"
[type]="barChartType"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
<div class="loading-overlay" *ngIf="isLoading">
<div class="shimmer-bar"></div>
</div>
</div>
</div>
</div> -->
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,296 @@
<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>
<!-- Filter toggle icon -->
<div class="filter-toggle-icon" *ngIf="hasFilters()" (click)="toggleFilters()">
<clr-icon shape="filter" size="24"
[style.color]="showFilters ? '#007cba' : '#666'"
title="Toggle Filters">
</clr-icon>
<span>
{{ showFilters ? 'Hide Filters' : 'Show Filters' }}
</span>
</div>
<!-- Render different chart types based on chartType input -->
<div class="chart-wrapper">
<!-- Dynamic Chart Container - uses extracted dynamic options and styles but static template -->
<div *ngIf="dynamicTemplate || chartType" class="chart-canvas-container">
<canvas baseChart
[datasets]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="chartType || 'bar'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Fallback for when no chart type is specified -->
<div *ngIf="!dynamicTemplate && !chartType" class="chart-canvas-container">
<canvas baseChart
[datasets]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'bar'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
</div>
<!-- Collapsible Base Filters -->
<div class="filters-section" *ngIf="baseFilters && baseFilters.length > 0 && showFilters">
<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 || filter.type === 'text'" class="filter-text">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)"
class="form-control" placeholder="Enter {{ filter.field || 'value' }}">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<select [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)" class="form-control">
<option value="">Select {{ filter.field || 'value' }}</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 || 'Filter ' + (i + 1) }}</label>
<div class="multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span *ngIf="!filter.value || (Array.isArray(filter.value) && filter.value.length === 0)">Select {{ filter.field || 'options' }}</span>
<span *ngIf="filter.value && !Array.isArray(filter.value)">
{{ filter.value }}
</span>
<span *ngIf="filter.value && Array.isArray(filter.value) && filter.value.length > 0">
{{ filter.value.length }} 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 || 'Filter ' + (i + 1) }}</label>
<div class="date-range-inputs">
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeInputChange(filter, 'start', $event)"
class="form-control" placeholder="Start Date">
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeInputChange(filter, 'end', $event)"
class="form-control" placeholder="End Date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-toggle">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</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 && showFilters">
<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 || filter.type === 'text'" class="filter-text">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)"
class="form-control" placeholder="Enter {{ filter.field || 'value' }}">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<select [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)" class="form-control">
<option value="">Select {{ filter.field || 'value' }}</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 || 'Filter ' + (i + 1) }}</label>
<div class="multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span *ngIf="!filter.value || (Array.isArray(filter.value) && filter.value.length === 0)">Select {{ filter.field || 'options' }}</span>
<span *ngIf="filter.value && !Array.isArray(filter.value)">
{{ filter.value }}
</span>
<span *ngIf="filter.value && Array.isArray(filter.value) && filter.value.length > 0">
{{ filter.value.length }} 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 || 'Filter ' + (i + 1) }}</label>
<div class="date-range-inputs">
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeInputChange(filter, 'start', $event)"
class="form-control" placeholder="Start Date">
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeInputChange(filter, 'end', $event)"
class="form-control" placeholder="End Date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-toggle">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</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() && showFilters">
<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 || filter.type === 'text'" class="filter-text">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)"
class="form-control" placeholder="Enter {{ filter.field || 'value' }}">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<select [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)" class="form-control">
<option value="">Select {{ filter.field || 'value' }}</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 || 'Filter ' + (i + 1) }}</label>
<div class="multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span *ngIf="!filter.value || (Array.isArray(filter.value) && filter.value.length === 0)">Select {{ filter.field || 'options' }}</span>
<span *ngIf="filter.value && !Array.isArray(filter.value)">
{{ filter.value }}
</span>
<span *ngIf="filter.value && Array.isArray(filter.value) && filter.value.length > 0">
{{ filter.value.length }} 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 || 'Filter ' + (i + 1) }}</label>
<div class="date-range-inputs">
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeInputChange(filter, 'start', $event)"
class="form-control" placeholder="Start Date">
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeInputChange(filter, 'end', $event)"
class="form-control" placeholder="End Date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-toggle">
<label>{{ filter.field || 'Filter ' + (i + 1) }}</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() && showFilters">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">
Clear All Filters
</button>
</div>
</div>
<!-- No Data Available Message -->
<div class="no-data-message" *ngIf="noDataAvailable && !isLoading">
<p>No data available for the selected filters.</p>
<button class="btn btn-sm btn-primary" (click)="fetchChartData()">Retry</button>
</div>
<!-- Loading Indicator -->
<div class="loading-indicator" *ngIf="isLoading">
<div class="spinner"></div>
<p>Loading chart data...</p>
</div>

View File

@@ -0,0 +1,307 @@
.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: 400px;
padding: 10px;
}
.chart-canvas-container {
position: relative;
height: 100%;
width: 100%;
padding: 15px;
box-sizing: border-box;
canvas {
display: block;
max-width: 100%;
max-height: 100%;
}
}
.filter-toggle-icon {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 5px;
cursor: pointer;
clr-icon {
transition: color 0.3s ease;
&:hover {
color: #007cba !important;
}
}
span {
margin-left: 5px;
font-size: 12px;
color: #666;
transition: color 0.3s ease;
}
&:hover span {
color: #007cba;
}
}
.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); }
}
@media (max-width: 768px) {
.filters-container {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.chart-wrapper {
min-height: 250px;
}
.chart-canvas-container {
padding: 10px;
}
}

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More