25 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
58 changed files with 7878 additions and 513 deletions

View File

@@ -1,13 +1,13 @@
<ol class="breadcrumb breadcrumb-arrow font-trirong"> <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://" [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> <li><a href="javascript://"><clr-icon shape="dashboard"></clr-icon> {{ 'Dashboard_builder' | translate }}</a></li>
</ol> </ol>
<br> <br>
<div class="dg-wrapper"> <div class="dg-wrapper">
<div class="clr-row"> <div class="clr-row">
<div class="clr-col-8"> <div class="clr-col-8">x
<h3>{{ 'Dashboard_builder' | translate }}</h3> <h3>{{ 'Dashboard_builder' | translate }}</h3>
</div> </div>
<div class="clr-col-4" style="text-align: right;"> <div class="clr-col-4" style="text-align: right;">
@@ -17,6 +17,10 @@
<button id="add" class="btn btn-primary" (click)="gotorunner()"> <button id="add" class="btn btn-primary" (click)="gotorunner()">
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }} <clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
</button> </button>
<!-- 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()"> <button class="btn btn-outline" (click)="onExport()">
<clr-icon shape="export"></clr-icon> {{ 'EXPORT_XLSX' | translate }} <clr-icon shape="export"></clr-icon> {{ 'EXPORT_XLSX' | translate }}
</button> </button>
@@ -27,8 +31,10 @@
</div> </div>
<clr-datagrid [clrDgLoading]="loading"> <clr-datagrid [clrDgLoading]="loading">
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{ 'Loading' | translate }} ...... </clr-spinner></ng-template> <clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{ 'Loading' | translate }} ......
<div *ngIf="error;else loadingSpinner">{{error}}</div></clr-dg-placeholder> </clr-spinner></ng-template>
<div *ngIf="error;else loadingSpinner">{{error}}</div>
</clr-dg-placeholder>
<clr-dg-column [clrDgField]="''"> <clr-dg-column [clrDgField]="''">
<ng-container *clrDgHideableColumn="{hidden: false}"> <ng-container *clrDgHideableColumn="{hidden: false}">
@@ -44,17 +50,17 @@
<ng-container *clrDgHideableColumn="{hidden: false}"> <ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Description' | translate }} {{ 'Description' | translate }}
</ng-container> </ng-container>
</clr-dg-column > </clr-dg-column>
<clr-dg-column [clrDgField]="'secuirity_profile'"> <clr-dg-column [clrDgField]="'secuirity_profile'">
<ng-container *clrDgHideableColumn="{hidden: false}"> <ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Security_Profile' | translate }} {{ 'Security_Profile' | translate }}
</ng-container> </ng-container>
</clr-dg-column > </clr-dg-column>
<clr-dg-column [clrDgField]="'add_to_home'"> <clr-dg-column [clrDgField]="'add_to_home'">
<ng-container *clrDgHideableColumn="{hidden: false}"> <ng-container *clrDgHideableColumn="{hidden: false}">
{{ 'Add_to_home' | translate }} {{ 'Add_to_home' | translate }}
</ng-container> </ng-container>
</clr-dg-column > </clr-dg-column>
<!-- <clr-dg-column [clrDgField]="'formType'"> <!-- <clr-dg-column [clrDgField]="'formType'">
<ng-container *clrDgHideableColumn="{hidden: false}"> <ng-container *clrDgHideableColumn="{hidden: false}">
Build Status Build Status
@@ -70,7 +76,8 @@
</ng-container></clr-dg-column> </ng-container></clr-dg-column>
<clr-dg-row *clrDgItems="let user of data?.slice()?.reverse();" [clrDgItem]="user"> <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><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.dashboard_name}}</clr-dg-cell>
<clr-dg-cell>{{user.description}}</clr-dg-cell> <clr-dg-cell>{{user.description}}</clr-dg-cell>
<clr-dg-cell>{{user.secuirity_profile}}</clr-dg-cell> <clr-dg-cell>{{user.secuirity_profile}}</clr-dg-cell>
@@ -80,13 +87,16 @@
<clr-dg-cell>{{user.testing}}</clr-dg-cell> --> <clr-dg-cell>{{user.testing}}</clr-dg-cell> -->
<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> --> <!-- <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"> <a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true"
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error" style="color: red;"></clr-icon></span> class="tooltip tooltip-sm tooltip-top-left">
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error"
style="color: red;"></clr-icon></span>
<span class="tooltip-content">{{ 'Delete' | translate }}</span> <span class="tooltip-content">{{ 'Delete' | translate }}</span>
</a> </a>
<clr-signpost> <clr-signpost>
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span> <span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success"
style="color: rgb(0, 130, 236);"></clr-icon></span>
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen> <clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
<h5 style="margin-top: 0">{{ 'Who_Column' | translate }}</h5> <h5 style="margin-top: 0">{{ 'Who_Column' | translate }}</h5>
<div>{{ 'Account_ID' | translate }}: <code class="clr-code">{{ user.accountId }}</code></div> <div>{{ 'Account_ID' | translate }}: <code class="clr-code">{{ user.accountId }}</code></div>
@@ -99,7 +109,8 @@
</clr-dg-cell> </clr-dg-cell>
<clr-dg-action-overflow> <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)="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> --> <!-- <button class="action-item" (click)="onDelete(user)">Delete<clr-icon shape="trash" class="is-error"></clr-icon></button> -->
</clr-dg-action-overflow> </clr-dg-action-overflow>
</clr-dg-row> </clr-dg-row>
@@ -112,16 +123,16 @@
</clr-dg-pagination> </clr-dg-pagination>
</clr-dg-footer> </clr-dg-footer>
</clr-datagrid> </clr-datagrid>
</div> </div>
<clr-modal [(clrModalOpen)]="addModall" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true"> <clr-modal [(clrModalOpen)]="addModall" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
<div class="modal-body"> <div class="modal-body">
<div class="s-order-dash-pg"> <div class="s-order-dash-pg">
<div class="chart-box" id="word1" (click)="gotoadd()"><br> <div class="chart-box" id="word1" (click)="gotoadd()"><br>
<img style="margin: auto; display: block;" src="/assets/images/fromscratch.png" height="90" width="90"> <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> <h5 class="center"> <b>{{ 'Start_from_scratch' | translate }}</b> </h5>
</div> </div>
<div class="chart-box" id="word1" ><br> <div class="chart-box" id="word1"><br>
<img style="margin: auto; display: block;" src="/assets/images/copytemplate.png" height="90" width="90"> <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> <h5 class="center"> <b>{{ 'Import_from_template' | translate }}</b> </h5>
</div> </div>
@@ -131,15 +142,15 @@
</div> </div>
</div> </div>
</div> </div>
</clr-modal> </clr-modal>
<clr-modal [(clrModalOpen)]="modalDelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true"> <clr-modal [(clrModalOpen)]="modalDelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
<div class="modal-body" *ngIf="rowSelected.id"> <div class="modal-body" *ngIf="rowSelected.id">
<h1 class="delete">{{ 'Are_you_sure_want_to_delete' | translate }}</h1> <h1 class="delete">{{ 'Are_you_sure_want_to_delete' | translate }}</h1>
<h2 class="heading">{{rowSelected.id}}</h2> <h2 class="heading">{{rowSelected.id}}</h2>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="modalDelete = false">{{ 'Cancel' | translate }}</button> <button type="button" class="btn btn-outline" (click)="modalDelete = false">{{ 'Cancel' | translate }}</button>
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary" >{{ 'Delete' | translate }}</button> <button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary">{{ 'Delete' | translate }}</button>
</div> </div>
</div> </div>
</clr-modal> </clr-modal>

View File

@@ -24,6 +24,7 @@ export class AllnewdashComponent implements OnInit {
projectname; projectname;
projectId; projectId;
error; error;
chartConfigManagerOpen = false;
constructor( constructor(
private router : Router, private router : Router,
private route: ActivatedRoute,private dashboardService : DashboardService, private route: ActivatedRoute,private dashboardService : DashboardService,
@@ -121,4 +122,9 @@ export class AllnewdashComponent implements OnInit {
// this.router.navigate(['../editdashn'],{relativeTo:this.route}); // 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

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

@@ -17,6 +17,9 @@
<div style="display: inline; float: right;"> <div style="display: inline; float: right;">
<!-- <button class="btn btn-primary">Build</button> <!-- <button class="btn btn-primary">Build</button>
<button class="btn btn-primary" (click)="onSchedule()">Schedule</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> </div>

View File

@@ -30,6 +30,8 @@ import { CompactFilterComponent } from '../common-filter';
import { FilterService } from '../common-filter/filter.service'; import { FilterService } from '../common-filter/filter.service';
// Add the UnifiedChartComponent import // Add the UnifiedChartComponent import
import { UnifiedChartComponent } from '../gadgets/unified-chart'; import { UnifiedChartComponent } from '../gadgets/unified-chart';
// Add the DynamicChartLoaderService import
import { DynamicChartLoaderService } from '../chart-config/dynamic-chart-loader.service';
function isNullArray(arr) { function isNullArray(arr) {
return !Array.isArray(arr) || arr.length === 0; return !Array.isArray(arr) || arr.length === 0;
@@ -58,6 +60,7 @@ export class EditnewdashComponent implements OnInit {
// Add availableKeys property for compact filter // Add availableKeys property for compact filter
availableKeys: string[] = []; availableKeys: string[] = [];
// Initialize with default widgets and update dynamically
WidgetsMock: WidgetModel[] = [ WidgetsMock: WidgetModel[] = [
{ {
name: 'Common Filter', name: 'Common Filter',
@@ -75,26 +78,26 @@ export class EditnewdashComponent implements OnInit {
name: 'Line Chart', name: 'Line Chart',
identifier: 'line_chart' identifier: 'line_chart'
}, },
{ // {
name: 'Bar Chart', // name: 'Bar Chart',
identifier: 'bar_chart' // identifier: 'bar_chart'
}, // },
{ // {
name: 'Pie Chart', // name: 'Pie Chart',
identifier: 'pie_chart' // identifier: 'pie_chart'
}, // },
{ // {
name: 'Polar Area Chart', // name: 'Polar Area Chart',
identifier: 'polar_area_chart' // identifier: 'polar_area_chart'
}, // },
{ // {
name: 'Bubble Chart', // name: 'Bubble Chart',
identifier: 'bubble_chart' // identifier: 'bubble_chart'
}, // },
{ // {
name: 'Scatter Chart', // name: 'Scatter Chart',
identifier: 'scatter_chart' // identifier: 'scatter_chart'
}, // },
{ {
name: 'Dynamic Chart', name: 'Dynamic Chart',
identifier: 'dynamic_chart' identifier: 'dynamic_chart'
@@ -131,16 +134,16 @@ export class EditnewdashComponent implements OnInit {
protected componentCollection = [ protected componentCollection = [
{ name: "Common Filter", componentInstance: CommonFilterComponent }, { name: "Common Filter", componentInstance: CommonFilterComponent },
{ name: "Line Chart", componentInstance: LineChartComponent }, { name: "Line Chart", componentInstance: UnifiedChartComponent },
{ name: "Doughnut Chart", componentInstance: DoughnutChartComponent }, { name: "Doughnut Chart", componentInstance: UnifiedChartComponent },
{ name: "Radar Chart", componentInstance: RadarChartComponent }, { name: "Radar Chart", componentInstance: UnifiedChartComponent },
{ name: "Bar Chart", componentInstance: BarChartComponent }, { name: "Bar Chart", componentInstance: UnifiedChartComponent },
{ name: "Pie Chart", componentInstance: PieChartComponent }, { name: "Pie Chart", componentInstance: UnifiedChartComponent },
{ name: "Polar Area Chart", componentInstance: PolarChartComponent }, { name: "Polar Area Chart", componentInstance: UnifiedChartComponent },
{ name: "Bubble Chart", componentInstance: BubbleChartComponent }, { name: "Bubble Chart", componentInstance: UnifiedChartComponent },
{ name: "Scatter Chart", componentInstance: ScatterChartComponent }, { name: "Scatter Chart", componentInstance: UnifiedChartComponent },
{ name: "Dynamic Chart", componentInstance: DynamicChartComponent }, { name: "Dynamic Chart", componentInstance: UnifiedChartComponent },
{ name: "Financial Chart", componentInstance: FinancialChartComponent }, { name: "Financial Chart", componentInstance: UnifiedChartComponent },
{ name: "To Do Chart", componentInstance: ToDoChartComponent }, { name: "To Do Chart", componentInstance: ToDoChartComponent },
{ name: "Grid View", componentInstance: GridViewComponent }, { name: "Grid View", componentInstance: GridViewComponent },
{ name: "Compact Filter", componentInstance: CompactFilterComponent }, { name: "Compact Filter", componentInstance: CompactFilterComponent },
@@ -206,6 +209,9 @@ export class EditnewdashComponent implements OnInit {
// Add drilldown column data property // Add drilldown column data property
drilldownColumnData = []; // Add drilldown column data property drilldownColumnData = []; // Add drilldown column data property
// Add chart types property for dynamic chart selection
chartTypes: any[] = [];
constructor(private route: ActivatedRoute, constructor(private route: ActivatedRoute,
private router: Router, private router: Router,
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
@@ -214,7 +220,8 @@ export class EditnewdashComponent implements OnInit {
private datastoreService: DatastoreService, private datastoreService: DatastoreService,
private alertService: AlertsService, private alertService: AlertsService,
private sureconnectService: SureconnectService, private sureconnectService: SureconnectService,
private filterService: FilterService) { } // Add SureconnectService and FilterService to constructor private filterService: FilterService,
private dynamicChartLoader: DynamicChartLoaderService) { } // Add SureconnectService, FilterService, and DynamicChartLoaderService to constructor
// Add property to track if coming from dashboard runner // Add property to track if coming from dashboard runner
fromRunner: boolean = false; fromRunner: boolean = false;
@@ -299,6 +306,9 @@ export class EditnewdashComponent implements OnInit {
apiUrl: [''] apiUrl: ['']
}); });
// Load chart types for dynamic chart selection
this.loadChartTypesForSelection();
// Load sureconnect data first, then load dashboard data // Load sureconnect data first, then load dashboard data
this.loadSureconnectData(); this.loadSureconnectData();
@@ -306,6 +316,37 @@ export class EditnewdashComponent implements OnInit {
this.loadCommonFilterData(); this.loadCommonFilterData();
} }
// Add method to load all chart types for dynamic selection
loadChartTypesForSelection() {
console.log('Loading chart types for selection');
this.dynamicChartLoader.loadActiveChartTypes().subscribe({
next: (chartTypes) => {
console.log('Loaded chart types for selection:', chartTypes);
this.chartTypes = chartTypes;
// Convert each chart type to a WidgetModel
const newWidgets = chartTypes.map(ct => ({
name: ct.displayName || ct.name,
// identifier: ct.name.toLowerCase().replace(/\s+/g, '_')
identifier: `${ct.name.toLowerCase().replace(/\s+/g, '_')}_chart`
}));
// Filter out duplicates by identifier
const existingIds = new Set(this.WidgetsMock.map(w => w.identifier));
const uniqueNewWidgets = newWidgets.filter(w => !existingIds.has(w.identifier));
// Append unique new widgets to WidgetsMock
this.WidgetsMock = [...this.WidgetsMock, ...uniqueNewWidgets];
console.log('Updated WidgetsMock:', this.WidgetsMock);
},
error: (error) => {
console.error('Error loading chart types for selection:', error);
}
});
}
// Add method to load sureconnect data // Add method to load sureconnect data
loadSureconnectData() { loadSureconnectData() {
this.sureconnectService.getAll().subscribe((data: any[]) => { this.sureconnectService.getAll().subscribe((data: any[]) => {
@@ -393,6 +434,27 @@ export class EditnewdashComponent implements OnInit {
} }
}); });
// Map chart names to unified chart types
const chartTypeMap = {
'Radar Chart': 'radar',
'Line Chart': 'line',
'Doughnut Chart': 'doughnut',
'Bar Chart': 'bar',
'Pie Chart': 'pie',
'Polar Area Chart': 'polar',
'Bubble Chart': 'bubble',
'Scatter Chart': 'scatter',
'Dynamic Chart': 'line',
'Financial Chart': 'line'
};
// If this is a chart, set the chartType property
if (chartTypeMap.hasOwnProperty(dashboard.name)) {
dashboard.chartType = chartTypeMap[dashboard.name];
// Keep the original name instead of changing it to "Unified Chart"
// dashboard.name = "Unified Chart";
}
// Ensure compact filter configuration properties are properly initialized // Ensure compact filter configuration properties are properly initialized
if (dashboard.component === 'Compact Filter' || dashboard.name === 'Compact Filter') { if (dashboard.component === 'Compact Filter' || dashboard.name === 'Compact Filter') {
// Make sure all compact filter properties exist // Make sure all compact filter properties exist
@@ -417,6 +479,29 @@ export class EditnewdashComponent implements OnInit {
} }
}); });
// Map unified chart types back to chart names for serialization
const chartNameMap = {
'radar': 'Radar Chart',
'line': 'Line Chart',
'doughnut': 'Doughnut Chart',
'bar': 'Bar Chart',
'pie': 'Pie Chart',
'polar': 'Polar Area Chart',
'bubble': 'Bubble Chart',
'scatter': 'Scatter Chart'
};
// If this is a unified chart, set the name back to the appropriate chart name
if (dashboard.name === 'Unified Chart' && dashboard.chartType && chartNameMap.hasOwnProperty(dashboard.chartType)) {
dashboard.name = chartNameMap[dashboard.chartType];
}
// Also handle the case where the chart already has the correct name
else if (dashboard.chartType && chartNameMap.hasOwnProperty(dashboard.chartType) &&
dashboard.name === chartNameMap[dashboard.chartType]) {
// The name is already correct, no need to change it
dashboard.component = "Unified Chart";
}
// Ensure compact filter configuration properties are preserved // Ensure compact filter configuration properties are preserved
if (dashboard.name === 'Compact Filter') { if (dashboard.name === 'Compact Filter') {
// Make sure all compact filter properties exist // Make sure all compact filter properties exist
@@ -428,6 +513,7 @@ export class EditnewdashComponent implements OnInit {
} }
}); });
} }
// Add method to get available fields for a filter dropdown (excluding already selected fields) // Add method to get available fields for a filter dropdown (excluding already selected fields)
getAvailableFields(filters: any[], currentIndex: number, allFields: string[]): string[] { getAvailableFields(filters: any[], currentIndex: number, allFields: string[]): string[] {
if (!filters || !allFields) { if (!filters || !allFields) {
@@ -473,6 +559,7 @@ export class EditnewdashComponent implements OnInit {
} }
} }
switch (componentType) { switch (componentType) {
// Handle all chart types by converting them to unified charts
case "radar_chart": case "radar_chart":
return this.dashboardArray.push({ return this.dashboardArray.push({
cols: 5, cols: 5,
@@ -480,8 +567,13 @@ export class EditnewdashComponent implements OnInit {
x: 0, x: 0,
y: 0, y: 0,
chartid: maxChartId + 1, chartid: maxChartId + 1,
component: RadarChartComponent, component: UnifiedChartComponent,
name: "Radar Chart" name: "Radar Chart",
chartType: 'radar',
xAxis: '',
yAxis: '',
table: '',
connection: undefined
}); });
case "line_chart": case "line_chart":
return this.dashboardArray.push({ return this.dashboardArray.push({
@@ -490,8 +582,13 @@ export class EditnewdashComponent implements OnInit {
x: 0, x: 0,
y: 0, y: 0,
chartid: maxChartId + 1, chartid: maxChartId + 1,
component: LineChartComponent, component: UnifiedChartComponent,
name: "Line Chart" name: "Line Chart",
chartType: 'line',
xAxis: '',
yAxis: '',
table: '',
connection: undefined
}); });
case "doughnut_chart": case "doughnut_chart":
return this.dashboardArray.push({ return this.dashboardArray.push({
@@ -500,19 +597,17 @@ export class EditnewdashComponent implements OnInit {
x: 0, x: 0,
y: 0, y: 0,
chartid: maxChartId + 1, chartid: maxChartId + 1,
component: DoughnutChartComponent, component: UnifiedChartComponent,
name: "Doughnut Chart" name: "Doughnut Chart",
chartType: 'doughnut',
xAxis: '',
yAxis: '',
table: '',
connection: undefined
}); });
case "bar_chart": case "bar_chart":
return this.dashboardArray.push({ // Use dynamic chart creation for bar charts
cols: 5, return this.createDynamicChart('bar', maxChartId);
rows: 6,
x: 0,
y: 0,
chartid: maxChartId + 1,
component: BarChartComponent,
name: "Bar Chart"
});
case "pie_chart": case "pie_chart":
return this.dashboardArray.push({ return this.dashboardArray.push({
cols: 5, cols: 5,
@@ -520,8 +615,13 @@ export class EditnewdashComponent implements OnInit {
x: 0, x: 0,
y: 0, y: 0,
chartid: maxChartId + 1, chartid: maxChartId + 1,
component: PieChartComponent, component: UnifiedChartComponent,
name: "Pie Chart" name: "Pie Chart",
chartType: 'pie',
xAxis: '',
yAxis: '',
table: '',
connection: undefined
}); });
case "polar_area_chart": case "polar_area_chart":
return this.dashboardArray.push({ return this.dashboardArray.push({
@@ -530,8 +630,13 @@ export class EditnewdashComponent implements OnInit {
x: 0, x: 0,
y: 0, y: 0,
chartid: maxChartId + 1, chartid: maxChartId + 1,
component: PolarChartComponent, component: UnifiedChartComponent,
name: "Polar Area Chart" name: "Polar Area Chart",
chartType: 'polar',
xAxis: '',
yAxis: '',
table: '',
connection: undefined
}); });
case "bubble_chart": case "bubble_chart":
return this.dashboardArray.push({ return this.dashboardArray.push({
@@ -540,8 +645,13 @@ export class EditnewdashComponent implements OnInit {
x: 0, x: 0,
y: 0, y: 0,
chartid: maxChartId + 1, chartid: maxChartId + 1,
component: BubbleChartComponent, component: UnifiedChartComponent,
name: "Bubble Chart" name: "Bubble Chart",
chartType: 'bubble',
xAxis: '',
yAxis: '',
table: '',
connection: undefined
}); });
case "scatter_chart": case "scatter_chart":
return this.dashboardArray.push({ return this.dashboardArray.push({
@@ -550,8 +660,13 @@ export class EditnewdashComponent implements OnInit {
x: 0, x: 0,
y: 0, y: 0,
chartid: maxChartId + 1, chartid: maxChartId + 1,
component: ScatterChartComponent, component: UnifiedChartComponent,
name: "Scatter Chart" name: "Scatter Chart",
chartType: 'scatter',
xAxis: '',
yAxis: '',
table: '',
connection: undefined
}); });
case "dynamic_chart": case "dynamic_chart":
return this.dashboardArray.push({ return this.dashboardArray.push({
@@ -560,8 +675,13 @@ export class EditnewdashComponent implements OnInit {
x: 0, x: 0,
y: 0, y: 0,
chartid: maxChartId + 1, chartid: maxChartId + 1,
component: DynamicChartComponent, component: UnifiedChartComponent,
name: "Dynamic Chart" name: "Dynamic Chart",
chartType: 'line', // Default to line for dynamic chart
xAxis: '',
yAxis: '',
table: '',
connection: undefined
}); });
case "financial_chart": case "financial_chart":
return this.dashboardArray.push({ return this.dashboardArray.push({
@@ -570,8 +690,13 @@ export class EditnewdashComponent implements OnInit {
x: 0, x: 0,
y: 0, y: 0,
chartid: maxChartId + 1, chartid: maxChartId + 1,
component: FinancialChartComponent, component: UnifiedChartComponent,
name: "Financial Chart" name: "Financial Chart",
chartType: 'line', // Default to line for financial chart
xAxis: '',
yAxis: '',
table: '',
connection: undefined
}); });
case "to_do_chart": case "to_do_chart":
return this.dashboardArray.push({ return this.dashboardArray.push({
@@ -634,6 +759,27 @@ export class EditnewdashComponent implements OnInit {
table: '', table: '',
connection: undefined connection: undefined
}); });
default:
// Handle any other chart types dynamically
// Extract chart type name from identifier (e.g., "heatmap_chart" -> "heatmap")
const chartTypeName = componentType.replace('_chart', '');
const displayName = chartTypeName.charAt(0).toUpperCase() + chartTypeName.slice(1) + ' Chart';
// Create a unified chart with the dynamic chart type
return this.dashboardArray.push({
cols: 5,
rows: 6,
x: 0,
y: 0,
chartid: maxChartId + 1,
component: UnifiedChartComponent,
name: displayName,
chartType: chartTypeName,
xAxis: '',
yAxis: '',
table: '',
connection: undefined
});
} }
} }
removeItem(item) { removeItem(item) {
@@ -1047,7 +1193,14 @@ export class EditnewdashComponent implements OnInit {
} }
// For UnifiedChartComponent, pass chart properties with chartType // For UnifiedChartComponent, pass chart properties with chartType
if (item.name === 'Unified Chart') { // Check for specific chart names that use UnifiedChartComponent
const unifiedChartNames = [
'Radar Chart', 'Line Chart', 'Doughnut Chart', 'Bar Chart',
'Pie Chart', 'Polar Area Chart', 'Bubble Chart', 'Scatter Chart',
'Dynamic Chart', 'Financial Chart', 'Unified Chart'
];
if (unifiedChartNames.includes(item.name)) {
const unifiedChartInputs = { const unifiedChartInputs = {
chartType: item.chartType || 'bar', chartType: item.chartType || 'bar',
xAxis: item.xAxis, xAxis: item.xAxis,
@@ -1942,4 +2095,257 @@ export class EditnewdashComponent implements OnInit {
} }
} }
// Add method to apply dynamic template to a chart
applyDynamicTemplate(chartItem: any, template: any) {
console.log('Applying dynamic template to chart:', chartItem, template);
// Apply HTML template
if (template.templateHtml) {
// In a real implementation, you would dynamically render the HTML template
// For now, we'll just log it
console.log('HTML Template:', template.templateHtml);
}
// Apply CSS styles
if (template.templateCss) {
// In a real implementation, you would dynamically apply the CSS styles
// For now, we'll just log it
console.log('CSS Template:', template.templateCss);
}
// Return the chart item with template applied
return {
...chartItem,
template: template
};
}
// Add method to test dynamic chart creation
testDynamicChartCreation() {
console.log('Testing dynamic chart creation');
// Show a success message to the user
alert('Dynamic chart test started. Check the browser console for detailed output.');
// Load all chart types
this.dynamicChartLoader.loadAllChartConfigurations().subscribe({
next: (chartTypes) => {
console.log('Loaded chart types:', chartTypes);
// Find bar chart type
const barChartType = chartTypes.find((ct: any) => ct.name === 'bar');
if (barChartType) {
console.log('Found bar chart type:', barChartType);
// Load configuration for bar chart
this.dynamicChartLoader.loadChartConfiguration(barChartType.id).subscribe({
next: (config) => {
console.log('Loaded bar chart configuration:', config);
// Create a test chart item
const chartItem = {
cols: 5,
rows: 6,
x: 0,
y: 0,
chartid: 100,
component: UnifiedChartComponent,
name: 'Test Dynamic Bar Chart',
chartType: 'bar',
xAxis: '',
yAxis: '',
table: '',
connection: undefined,
// Add dynamic fields from configuration
dynamicFields: config.dynamicFields || []
};
console.log('Created test chart item:', chartItem);
// If we have templates, apply the default one
if (config.templates && config.templates.length > 0) {
const defaultTemplate = config.templates.find((t: any) => t.isDefault) || config.templates[0];
if (defaultTemplate) {
console.log('Applying default template:', defaultTemplate);
const chartWithTemplate = this.applyDynamicTemplate(chartItem, defaultTemplate);
console.log('Chart with template:', chartWithTemplate);
// Show success message
alert('Dynamic chart test completed successfully! Check console for details.');
}
} else {
// Show success message even without templates
alert('Dynamic chart test completed successfully! No templates found. Check console for details.');
}
},
error: (error) => {
console.error('Error loading bar chart configuration:', error);
alert('Error loading bar chart configuration. Check console for details.');
}
});
} else {
console.warn('Bar chart type not found');
alert('Bar chart type not found in the database.');
}
},
error: (error) => {
console.error('Error loading chart types:', error);
alert('Error loading chart types. Check console for details.');
}
});
}
// Add method to load dynamic chart configuration
loadDynamicChartConfiguration(chartTypeId: number) {
console.log(`Loading dynamic chart configuration for chart type ${chartTypeId}`);
this.dynamicChartLoader.loadChartConfiguration(chartTypeId).subscribe({
next: (config) => {
console.log('Loaded dynamic chart configuration:', config);
// Here you would apply the configuration to the UI
// For example, populate form fields, set up templates, etc.
},
error: (error) => {
console.error('Error loading dynamic chart configuration:', error);
}
});
}
// Add method to create a dynamic chart with configuration from database
createDynamicChart(chartTypeName: string, maxChartId: number) {
console.log(`Creating dynamic chart of type: ${chartTypeName}`);
// First, get the chart type by name
this.dynamicChartLoader.getChartTypeByName(chartTypeName).subscribe({
next: (chartType) => {
if (chartType) {
console.log(`Found chart type:`, chartType);
// Load the complete configuration for this chart type
this.dynamicChartLoader.loadChartConfiguration(chartType.id).subscribe({
next: (config) => {
console.log(`Loaded configuration for ${chartTypeName}:`, config);
// Create the chart item with dynamic configuration
const chartItem = {
cols: 5,
rows: 6,
x: 0,
y: 0,
chartid: maxChartId + 1,
component: UnifiedChartComponent,
name: chartType.displayName || chartTypeName,
chartType: chartType.name,
xAxis: '',
yAxis: '',
table: '',
connection: undefined,
// Add any dynamic fields from the configuration
dynamicFields: config.dynamicFields || []
};
// Add UI components as configuration properties
if (config.uiComponents && config.uiComponents.length > 0) {
config.uiComponents.forEach(component => {
chartItem[component.componentName] = '';
});
}
this.dashboardArray.push(chartItem);
console.log(`Created dynamic chart:`, chartItem);
},
error: (error) => {
console.error(`Error loading configuration for ${chartTypeName}:`, error);
// Fallback to default configuration
// this.createDefaultChart(chartTypeName, maxChartId);
}
});
} else {
console.warn(`Chart type ${chartTypeName} not found, using default configuration`);
// this.createDefaultChart(chartTypeName, maxChartId);
}
},
error: (error) => {
console.error('Error loading configuration for chart type:', error);
// Fallback to default configuration
// this.createDefaultChart(chartType.name, chartType.displayName || chartType.name);
}
});
}
// Fallback method to create default chart configuration
createDefaultChart(chartTypeName: string, chartDisplayName: string) {
console.log(`Creating default chart for ${chartTypeName}`);
// Map chart type names to chart types
const chartTypeMap = {
'bar': 'bar',
'line': 'line',
'pie': 'pie',
'doughnut': 'doughnut',
'radar': 'radar',
'polar': 'polar',
'bubble': 'bubble',
'scatter': 'scatter'
};
// Get the chart type from the name
const chartType = chartTypeMap[chartTypeName.toLowerCase()] || 'bar';
// Safely calculate maxChartId, handling cases where chartid might be NaN or missing
let maxChartId = 0;
if (this.dashboardArray && this.dashboardArray.length > 0) {
const validChartIds = this.dashboardArray
.map(item => item.chartid)
.filter(chartid => typeof chartid === 'number' && !isNaN(chartid));
if (validChartIds.length > 0) {
maxChartId = Math.max(...validChartIds);
}
}
const chartItem = {
cols: 5,
rows: 6,
x: 0,
y: 0,
chartid: maxChartId + 1,
component: UnifiedChartComponent,
name: chartDisplayName,
chartType: chartType,
xAxis: '',
yAxis: '',
table: '',
connection: undefined
};
this.dashboardArray.push(chartItem);
console.log('Created default chart:', chartItem);
// Update the dashboard collection
this.dashboardCollection.dashboard = this.dashboardArray.slice();
// Force gridster to refresh
if (this.options && this.options.api) {
this.options.api.optionsChanged();
}
}
// Helper method to get display name for chart type
getChartDisplayName(chartTypeName: string): string {
const displayNameMap = {
'bar': 'Bar Chart',
'line': 'Line Chart',
'pie': 'Pie Chart',
'doughnut': 'Doughnut Chart',
'radar': 'Radar Chart',
'polar': 'Polar Area Chart',
'bubble': 'Bubble Chart',
'scatter': 'Scatter Chart'
};
return displayNameMap[chartTypeName.toLowerCase()] || chartTypeName;
}
} }

View File

@@ -13,114 +13,36 @@
<h4>{{ charttitle }}</h4> <h4>{{ charttitle }}</h4>
</div> </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 --> <!-- Render different chart types based on chartType input -->
<div class="chart-wrapper"> <div class="chart-wrapper">
<!-- Bar Chart --> <!-- Dynamic Chart Container - uses extracted dynamic options and styles but static template -->
<div *ngIf="chartType === 'bar'"> <div *ngIf="dynamicTemplate || chartType" class="chart-canvas-container">
<canvas baseChart
[data]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'bar'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Line Chart -->
<div *ngIf="chartType === 'line'">
<canvas baseChart
[data]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'line'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Pie Chart -->
<div *ngIf="chartType === 'pie'">
<canvas baseChart
[data]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'pie'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Doughnut Chart -->
<div *ngIf="chartType === 'doughnut'">
<canvas baseChart
[data]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'doughnut'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Bubble Chart -->
<div *ngIf="chartType === 'bubble'">
<canvas baseChart
[datasets]="bubbleChartData"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'bubble'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Radar Chart -->
<div *ngIf="chartType === 'radar'">
<canvas baseChart
[data]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'radar'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Polar Area Chart -->
<div *ngIf="chartType === 'polar'">
<canvas baseChart
[data]="chartData"
[labels]="chartLabels"
[options]="chartOptions"
[legend]="chartLegend"
[chartType]="'polarArea'"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
</div>
<!-- Scatter Chart -->
<div *ngIf="chartType === 'scatter'">
<canvas baseChart <canvas baseChart
[datasets]="chartData" [datasets]="chartData"
[labels]="chartLabels"
[options]="chartOptions" [options]="chartOptions"
[legend]="chartLegend" [legend]="chartLegend"
[chartType]="'scatter'" [chartType]="chartType || 'bar'"
(chartClick)="chartClicked($event)" (chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)"> (chartHover)="chartHovered($event)">
</canvas> </canvas>
</div> </div>
<!-- Default/Unknown Chart Type --> <!-- Fallback for when no chart type is specified -->
<div *ngIf="!['bar', 'line', 'pie', 'doughnut', 'bubble', 'radar', 'polar', 'scatter'].includes(chartType)"> <div *ngIf="!dynamicTemplate && !chartType" class="chart-canvas-container">
<canvas baseChart <canvas baseChart
[data]="chartData" [datasets]="chartData"
[labels]="chartLabels" [labels]="chartLabels"
[options]="chartOptions" [options]="chartOptions"
[legend]="chartLegend" [legend]="chartLegend"
@@ -131,23 +53,23 @@
</div> </div>
</div> </div>
<!-- Base Filters --> <!-- Collapsible Base Filters -->
<div class="filters-section" *ngIf="baseFilters && baseFilters.length > 0"> <div class="filters-section" *ngIf="baseFilters && baseFilters.length > 0 && showFilters">
<h5>Filters</h5> <h5>Filters</h5>
<div class="filters-container"> <div class="filters-container">
<div class="filter-item" *ngFor="let filter of baseFilters; let i = index"> <div class="filter-item" *ngFor="let filter of baseFilters; let i = index">
<!-- Text Filter --> <!-- Text Filter -->
<div *ngIf="filter.type === 'text'" class="filter-text"> <div *ngIf="!filter.type || filter.type === 'text'" class="filter-text">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)" <input type="text" [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)"
class="form-control" placeholder="Enter {{ filter.field }}"> class="form-control" placeholder="Enter {{ filter.field || 'value' }}">
</div> </div>
<!-- Dropdown Filter --> <!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown"> <div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<select [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)" class="form-control"> <select [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)" class="form-control">
<option value="">Select {{ filter.field }}</option> <option value="">Select {{ filter.field || 'value' }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option"> <option *ngFor="let option of getFilterOptions(filter)" [value]="option">
{{ option }} {{ option }}
</option> </option>
@@ -156,12 +78,15 @@
<!-- Multiselect Filter --> <!-- Multiselect Filter -->
<div *ngIf="filter.type === 'multiselect'" class="filter-multiselect"> <div *ngIf="filter.type === 'multiselect'" class="filter-multiselect">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="multiselect-container"> <div class="multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')"> <div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span *ngIf="getSelectedOptionsCount(filter) === 0">Select {{ filter.field }}</span> <span *ngIf="!filter.value || (Array.isArray(filter.value) && filter.value.length === 0)">Select {{ filter.field || 'options' }}</span>
<span *ngIf="getSelectedOptionsCount(filter) > 0"> <span *ngIf="filter.value && !Array.isArray(filter.value)">
{{ getSelectedOptionsCount(filter) }} selected {{ filter.value }}
</span>
<span *ngIf="filter.value && Array.isArray(filter.value) && filter.value.length > 0">
{{ filter.value.length }} selected
</span> </span>
</div> </div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')"> <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
@@ -178,18 +103,18 @@
<!-- Date Range Filter --> <!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-date-range"> <div *ngIf="filter.type === 'date-range'" class="filter-date-range">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="date-range-inputs"> <div class="date-range-inputs">
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeChange(filter, filter.value)" <input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeInputChange(filter, 'start', $event)"
class="form-control" placeholder="Start Date"> class="form-control" placeholder="Start Date">
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeChange(filter, filter.value)" <input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeInputChange(filter, 'end', $event)"
class="form-control" placeholder="End Date"> class="form-control" placeholder="End Date">
</div> </div>
</div> </div>
<!-- Toggle Filter --> <!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-toggle"> <div *ngIf="filter.type === 'toggle'" class="filter-toggle">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="toggle-switch"> <div class="toggle-switch">
<input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)" <input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"
id="toggle-{{ filter.field }}"> id="toggle-{{ filter.field }}">
@@ -203,22 +128,22 @@
</div> </div>
<!-- Drilldown Filters --> <!-- Drilldown Filters -->
<div class="filters-section" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0"> <div class="filters-section" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0 && showFilters">
<h5>Drilldown Filters</h5> <h5>Drilldown Filters</h5>
<div class="filters-container"> <div class="filters-container">
<div class="filter-item" *ngFor="let filter of drilldownFilters; let i = index"> <div class="filter-item" *ngFor="let filter of drilldownFilters; let i = index">
<!-- Text Filter --> <!-- Text Filter -->
<div *ngIf="filter.type === 'text'" class="filter-text"> <div *ngIf="!filter.type || filter.type === 'text'" class="filter-text">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)" <input type="text" [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)"
class="form-control" placeholder="Enter {{ filter.field }}"> class="form-control" placeholder="Enter {{ filter.field || 'value' }}">
</div> </div>
<!-- Dropdown Filter --> <!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown"> <div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<select [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)" class="form-control"> <select [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)" class="form-control">
<option value="">Select {{ filter.field }}</option> <option value="">Select {{ filter.field || 'value' }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option"> <option *ngFor="let option of getFilterOptions(filter)" [value]="option">
{{ option }} {{ option }}
</option> </option>
@@ -227,12 +152,15 @@
<!-- Multiselect Filter --> <!-- Multiselect Filter -->
<div *ngIf="filter.type === 'multiselect'" class="filter-multiselect"> <div *ngIf="filter.type === 'multiselect'" class="filter-multiselect">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="multiselect-container"> <div class="multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')"> <div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span *ngIf="getSelectedOptionsCount(filter) === 0">Select {{ filter.field }}</span> <span *ngIf="!filter.value || (Array.isArray(filter.value) && filter.value.length === 0)">Select {{ filter.field || 'options' }}</span>
<span *ngIf="getSelectedOptionsCount(filter) > 0"> <span *ngIf="filter.value && !Array.isArray(filter.value)">
{{ getSelectedOptionsCount(filter) }} selected {{ filter.value }}
</span>
<span *ngIf="filter.value && Array.isArray(filter.value) && filter.value.length > 0">
{{ filter.value.length }} selected
</span> </span>
</div> </div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')"> <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
@@ -249,18 +177,18 @@
<!-- Date Range Filter --> <!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-date-range"> <div *ngIf="filter.type === 'date-range'" class="filter-date-range">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="date-range-inputs"> <div class="date-range-inputs">
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeChange(filter, filter.value)" <input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeInputChange(filter, 'start', $event)"
class="form-control" placeholder="Start Date"> class="form-control" placeholder="Start Date">
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeChange(filter, filter.value)" <input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeInputChange(filter, 'end', $event)"
class="form-control" placeholder="End Date"> class="form-control" placeholder="End Date">
</div> </div>
</div> </div>
<!-- Toggle Filter --> <!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-toggle"> <div *ngIf="filter.type === 'toggle'" class="filter-toggle">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="toggle-switch"> <div class="toggle-switch">
<input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)" <input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"
id="drilldown-toggle-{{ filter.field }}"> id="drilldown-toggle-{{ filter.field }}">
@@ -274,22 +202,22 @@
</div> </div>
<!-- Layer Filters --> <!-- Layer Filters -->
<div class="filters-section" *ngIf="hasActiveLayerFilters()"> <div class="filters-section" *ngIf="hasActiveLayerFilters() && showFilters">
<h5>Layer Filters</h5> <h5>Layer Filters</h5>
<div class="filters-container"> <div class="filters-container">
<div class="filter-item" *ngFor="let filter of getActiveLayerFilters(); let i = index"> <div class="filter-item" *ngFor="let filter of getActiveLayerFilters(); let i = index">
<!-- Text Filter --> <!-- Text Filter -->
<div *ngIf="filter.type === 'text'" class="filter-text"> <div *ngIf="!filter.type || filter.type === 'text'" class="filter-text">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)" <input type="text" [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)"
class="form-control" placeholder="Enter {{ filter.field }}"> class="form-control" placeholder="Enter {{ filter.field || 'value' }}">
</div> </div>
<!-- Dropdown Filter --> <!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown"> <div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<select [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)" class="form-control"> <select [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)" class="form-control">
<option value="">Select {{ filter.field }}</option> <option value="">Select {{ filter.field || 'value' }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option"> <option *ngFor="let option of getFilterOptions(filter)" [value]="option">
{{ option }} {{ option }}
</option> </option>
@@ -298,12 +226,15 @@
<!-- Multiselect Filter --> <!-- Multiselect Filter -->
<div *ngIf="filter.type === 'multiselect'" class="filter-multiselect"> <div *ngIf="filter.type === 'multiselect'" class="filter-multiselect">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="multiselect-container"> <div class="multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')"> <div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span *ngIf="getSelectedOptionsCount(filter) === 0">Select {{ filter.field }}</span> <span *ngIf="!filter.value || (Array.isArray(filter.value) && filter.value.length === 0)">Select {{ filter.field || 'options' }}</span>
<span *ngIf="getSelectedOptionsCount(filter) > 0"> <span *ngIf="filter.value && !Array.isArray(filter.value)">
{{ getSelectedOptionsCount(filter) }} selected {{ filter.value }}
</span>
<span *ngIf="filter.value && Array.isArray(filter.value) && filter.value.length > 0">
{{ filter.value.length }} selected
</span> </span>
</div> </div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')"> <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
@@ -320,18 +251,18 @@
<!-- Date Range Filter --> <!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-date-range"> <div *ngIf="filter.type === 'date-range'" class="filter-date-range">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="date-range-inputs"> <div class="date-range-inputs">
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeChange(filter, filter.value)" <input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeInputChange(filter, 'start', $event)"
class="form-control" placeholder="Start Date"> class="form-control" placeholder="Start Date">
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeChange(filter, filter.value)" <input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeInputChange(filter, 'end', $event)"
class="form-control" placeholder="End Date"> class="form-control" placeholder="End Date">
</div> </div>
</div> </div>
<!-- Toggle Filter --> <!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-toggle"> <div *ngIf="filter.type === 'toggle'" class="filter-toggle">
<label>{{ filter.field }}</label> <label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
<div class="toggle-switch"> <div class="toggle-switch">
<input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)" <input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"
id="layer-toggle-{{ filter.field }}"> id="layer-toggle-{{ filter.field }}">
@@ -345,7 +276,7 @@
</div> </div>
<!-- Clear Filters Button --> <!-- Clear Filters Button -->
<div class="clear-filters" *ngIf="hasActiveFilters()"> <div class="clear-filters" *ngIf="hasActiveFilters() && showFilters">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()"> <button class="btn btn-sm btn-outline" (click)="clearAllFilters()">
Clear All Filters Clear All Filters
</button> </button>

View File

@@ -30,7 +30,49 @@
.chart-wrapper { .chart-wrapper {
position: relative; position: relative;
height: calc(100% - 100px); height: calc(100% - 100px);
min-height: 300px; 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 { .filters-section {
@@ -246,7 +288,6 @@
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
// Responsive adjustments
@media (max-width: 768px) { @media (max-width: 768px) {
.filters-container { .filters-container {
flex-direction: column; flex-direction: column;
@@ -259,4 +300,8 @@
.chart-wrapper { .chart-wrapper {
min-height: 250px; min-height: 250px;
} }
.chart-canvas-container {
padding: 10px;
}
} }

View File

@@ -1,9 +1,10 @@
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core'; import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, ViewChild, ElementRef, Renderer2 } from '@angular/core';
import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service'; import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service'; import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { BaseChartDirective } from 'ng2-charts'; import { BaseChartDirective } from 'ng2-charts';
import { ChartConfiguration, ChartDataset } from 'chart.js'; import { ChartConfiguration, ChartDataset } from 'chart.js';
import { DynamicChartLoaderService } from '../../chart-config/dynamic-chart-loader.service';
@Component({ @Component({
selector: 'app-unified-chart', selector: 'app-unified-chart',
@@ -61,6 +62,9 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
currentDrilldownLevel: number = 0; currentDrilldownLevel: number = 0;
originalChartData: any = {}; originalChartData: any = {};
// Filter visibility toggle
showFilters: boolean = false;
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
@@ -72,9 +76,151 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
private documentClickHandler: ((event: MouseEvent) => void) | null = null; private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false; private filtersInitialized: boolean = false;
// Dynamic template properties
dynamicTemplate: string = '';
dynamicStyles: string = '';
dynamicOptions: any = null;
// Properties to hold extracted values from dynamic template
extractedChartType: string = '';
extractedDatasetsBinding: string = '';
extractedLabelsBinding: string = '';
extractedOptionsBinding: string = '';
extractedLegendBinding: string = '';
extractedChartClickBinding: string = '';
extractedChartHoverBinding: string = '';
// Add setter to log when dynamicTemplate changes
setDynamicTemplate(value: string) {
console.log('Setting dynamic template:', value);
this.dynamicTemplate = value;
// Extract values from the dynamic template
this.extractTemplateValues(value);
// Apply dynamic options if they were extracted
if (this.dynamicOptions) {
this.mergeDynamicOptions();
}
// Apply dynamic styles if they were extracted
if (this.dynamicStyles) {
this.applyDynamicStyles();
}
// Trigger change detection to ensure the template is rendered
setTimeout(() => {
console.log('Dynamic template updated in DOM');
// Check if the dynamic template container exists
const dynamicContainer = this.el.nativeElement.querySelector('.dynamic-template-container');
console.log('Dynamic template container:', dynamicContainer);
if (dynamicContainer) {
console.log('Dynamic container innerHTML:', dynamicContainer.innerHTML);
}
// Check if the canvas element exists in the DOM
const canvasElements = this.el.nativeElement.querySelectorAll('canvas');
console.log('Canvas elements found in DOM:', canvasElements.length);
if (canvasElements.length > 0) {
console.log('First canvas element:', canvasElements[0]);
// Check if it has the baseChart directive processed
const firstCanvas = canvasElements[0];
console.log('Canvas has baseChart directive processed:', firstCanvas.classList.contains('chartjs-render-monitor'));
} else {
console.log('No canvas elements found - checking if template was inserted but not processed');
// Check if there's HTML content in the dynamic container
if (dynamicContainer) {
const htmlContent = dynamicContainer.innerHTML;
console.log('HTML content in dynamic container:', htmlContent);
if (htmlContent && htmlContent.includes('canvas')) {
console.log('Canvas tag found in HTML but not processed by Angular');
}
}
}
}, 100);
}
// Extract values from dynamic template HTML
private extractTemplateValues(template: string): void {
console.log('Extracting values from template:', template);
// Reset extracted values
this.extractedChartType = this.chartType || '';
this.extractedDatasetsBinding = 'chartData';
this.extractedLabelsBinding = 'chartLabels';
this.extractedOptionsBinding = 'chartOptions';
this.extractedLegendBinding = 'chartLegend';
this.extractedChartClickBinding = 'chartClicked($event)';
this.extractedChartHoverBinding = 'chartHovered($event)';
if (!template) {
console.log('No template to extract values from');
return;
}
// Parse the template to extract bindings
// Look for [chartType] binding
const chartTypeMatch = template.match(/\[chartType\]="([^"]+)"/);
if (chartTypeMatch && chartTypeMatch[1]) {
this.extractedChartType = chartTypeMatch[1];
console.log('Extracted chartType binding:', this.extractedChartType);
}
// Look for [datasets] binding
const datasetsMatch = template.match(/\[datasets\]="([^"]+)"/);
if (datasetsMatch && datasetsMatch[1]) {
this.extractedDatasetsBinding = datasetsMatch[1];
console.log('Extracted datasets binding:', this.extractedDatasetsBinding);
}
// Look for [labels] binding
const labelsMatch = template.match(/\[labels\]="([^"]+)"/);
if (labelsMatch && labelsMatch[1]) {
this.extractedLabelsBinding = labelsMatch[1];
console.log('Extracted labels binding:', this.extractedLabelsBinding);
}
// Look for [options] binding
const optionsMatch = template.match(/\[options\]="([^"]+)"/);
if (optionsMatch && optionsMatch[1]) {
this.extractedOptionsBinding = optionsMatch[1];
console.log('Extracted options binding:', this.extractedOptionsBinding);
}
// Look for [legend] binding
const legendMatch = template.match(/\[legend\]="([^"]+)"/);
if (legendMatch && legendMatch[1]) {
this.extractedLegendBinding = legendMatch[1];
console.log('Extracted legend binding:', this.extractedLegendBinding);
}
// Look for (chartClick) binding
const chartClickMatch = template.match(/\(chartClick\)="([^"]+)"/);
if (chartClickMatch && chartClickMatch[1]) {
this.extractedChartClickBinding = chartClickMatch[1];
console.log('Extracted chartClick binding:', this.extractedChartClickBinding);
}
// Look for (chartHover) binding
const chartHoverMatch = template.match(/\(chartHover\)="([^"]+)"/);
if (chartHoverMatch && chartHoverMatch[1]) {
this.extractedChartHoverBinding = chartHoverMatch[1];
console.log('Extracted chartHover binding:', this.extractedChartHoverBinding);
}
// Extract CSS styles if present in the template
const styleMatch = template.match(/<style[^>]*>([\s\S]*?)<\/style>/i);
if (styleMatch && styleMatch[1]) {
this.dynamicStyles = styleMatch[1];
console.log('Extracted CSS styles:', this.dynamicStyles);
}
}
constructor( constructor(
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
private filterService: FilterService private filterService: FilterService,
private dynamicChartLoader: DynamicChartLoaderService,
private renderer: Renderer2,
private el: ElementRef
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
@@ -85,13 +231,54 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
}) })
); );
// Log initial input values for debugging
console.log('UnifiedChartComponent ngOnInit - initial input values:', {
chartType: this.chartType,
xAxis: this.xAxis,
yAxis: this.yAxis,
table: this.table,
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
// Check if filters are available
console.log('Has filters in ngOnInit:', this.hasFilters());
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Initialize chart options with default structure
this.initializeChartOptions(); this.initializeChartOptions();
// Load dynamic template and options for this chart type
this.loadDynamicChartConfig();
this.fetchChartData(); this.fetchChartData();
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('UnifiedChartComponent input changes:', changes); console.log('UnifiedChartComponent input changes:', changes);
// Log chartType changes specifically
if (changes.chartType) {
console.log('Chart type changed from', changes.chartType.previousValue, 'to', changes.chartType.currentValue);
}
// Log all input values for debugging
console.log('Current input values:', {
chartType: this.chartType,
xAxis: this.xAxis,
yAxis: this.yAxis,
table: this.table,
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
// Initialize filter values if they haven't been initialized yet // Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues(); this.initializeFilterValues();
@@ -114,6 +301,41 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange; const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange;
const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.firstChange; const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.firstChange;
// Log base filters changes for debugging
if (baseFiltersChanged) {
console.log('Base filters changed:', changes.baseFilters);
console.log('Current base filters:', this.baseFilters);
// Log detailed information about each filter
if (this.baseFilters && Array.isArray(this.baseFilters)) {
this.baseFilters.forEach((filter, index) => {
console.log(`Base filter ${index} details:`, {
field: filter.field,
value: filter.value,
type: filter.type,
options: filter.options
});
});
}
}
// Also log when baseFilters is not changed but we still have filters
if (!baseFiltersChanged && this.baseFilters && this.baseFilters.length > 0) {
console.log('Base filters present but not changed, logging current state:');
this.baseFilters.forEach((filter, index) => {
console.log(`Base filter ${index} details:`, {
field: filter.field,
value: filter.value,
type: filter.type,
options: filter.options
});
});
}
// Load dynamic template and options if chart type changed
if (chartTypeChanged) {
this.loadDynamicChartConfig();
}
// Only fetch data if the actual chart configuration changed and we're not already fetching // Only fetch data if the actual chart configuration changed and we're not already fetching
if (!this.isFetchingData && (chartTypeChanged || xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || drilldownFiltersChanged || if (!this.isFetchingData && (chartTypeChanged || xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || drilldownFiltersChanged ||
drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged || drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
@@ -126,7 +348,18 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
// Update legend visibility if it changed // Update legend visibility if it changed
if (changes.chartlegend !== undefined) { if (changes.chartlegend !== undefined) {
this.chartLegend = changes.chartlegend.currentValue; this.chartLegend = changes.chartlegend.currentValue;
// Ensure chartOptions and required structures exist before accessing legend
if (!this.chartOptions) {
this.chartOptions = {};
}
if (!this.chartOptions.plugins) {
this.chartOptions.plugins = {};
}
if (!this.chartOptions.plugins.legend) {
this.chartOptions.plugins.legend = { display: this.chartLegend };
} else {
this.chartOptions.plugins.legend.display = this.chartLegend; this.chartOptions.plugins.legend.display = this.chartLegend;
}
console.log('Chart legend changed to:', this.chartLegend); console.log('Chart legend changed to:', this.chartLegend);
} }
} }
@@ -141,13 +374,199 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
} }
} }
// Load dynamic chart configuration (template, styles, and options) for the current chart type
private loadDynamicChartConfig(): void {
if (!this.chartType) {
console.log('No chart type specified, skipping dynamic chart config loading');
return;
}
console.log(`Loading dynamic chart configuration for chart type: ${this.chartType}`);
console.log('Current dynamic template:', this.dynamicTemplate);
// Get chart type by name and load its configuration
console.log('Calling getChartTypeByName with:', this.chartType);
this.dynamicChartLoader.getChartTypeByName(this.chartType).subscribe({
next: (chartType) => {
console.log('Received chart type by name :', chartType);
if (chartType) {
console.log('Found chart type:', chartType);
// Load the complete configuration for this chart type
console.log('Loading chart configuration for chart type ID:', chartType.id);
this.dynamicChartLoader.loadChartConfiguration(chartType.id).subscribe({
next: (config) => {
console.log('Received chart configuration:', config);
console.log('Loaded chart configuration:', config);
// Apply the first template if available (for CSS styles)
if (config.templates && config.templates.length > 0) {
const defaultTemplate = config.templates.find(t => t.isDefault) || config.templates[0];
if (defaultTemplate) {
const templateHtml = defaultTemplate.templateHtml || '';
console.log('Template HTML to be set:', templateHtml);
this.setDynamicTemplate(templateHtml);
this.dynamicStyles = defaultTemplate.templateCss || '';
// Apply styles to the component
this.applyDynamicStyles();
console.log('Applied dynamic template and styles', {
template: this.dynamicTemplate,
styles: this.dynamicStyles
});
}
} else {
console.log('No templates found for chart type:', this.chartType);
}
// Apply dynamic options if available
console.log('Checking for dynamic fields:', config.dynamicFields);
if (config.dynamicFields && config.dynamicFields.length > 0) {
// Find the field that contains chart options
const optionsField = config.dynamicFields.find(field =>
field.fieldName === 'chartOptions' || field.fieldName.toLowerCase().includes('options'));
if (!optionsField) {
console.log('No chartOptions field found in dynamic fields');
}
if (optionsField && optionsField.fieldOptions) {
try {
this.dynamicOptions = JSON.parse(optionsField.fieldOptions);
console.log('Applied dynamic chart options:', this.dynamicOptions);
// Merge dynamic options with current chart options
this.mergeDynamicOptions();
} catch (e) {
console.error('Error parsing dynamic chart options:', e);
}
}
} else {
console.log('No dynamic fields found for chart type:', this.chartType);
}
},
error: (error) => {
console.error('Error loading chart configuration:', error);
}
});
} else {
console.log(`Chart type ${this.chartType} not found in database`);
// Log available chart types for debugging
console.log('Available chart types in database:');
this.dynamicChartLoader.loadAllChartConfigurations().subscribe({
next: (chartTypes) => {
console.log('All chart types:', chartTypes);
},
error: (error) => {
console.error('Error loading chart types:', error);
}
});
}
},
error: (error) => {
console.error('Error loading chart type:', error);
}
});
}
// Merge dynamic options with current chart options
private mergeDynamicOptions(): void {
if (this.dynamicOptions) {
console.log('Merging dynamic options with existing chart options:', {
existing: this.chartOptions,
dynamic: this.dynamicOptions
});
// Deep merge dynamic options with existing chart options
this.chartOptions = this.deepMerge(this.chartOptions, this.dynamicOptions);
console.log('Merged chart options:', this.chartOptions);
// If we have a chart instance, update it
if (this.chart) {
this.chart.options = this.chartOptions;
this.chart.render();
}
}
}
// Helper method for deep merging objects
private deepMerge(target: any, source: any): any {
const result = { ...target };
for (const key in source) {
if (source.hasOwnProperty(key)) {
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
// Recursively merge objects
result[key] = this.deepMerge(result[key] || {}, source[key]);
} else {
// Override with source value
result[key] = source[key];
}
}
}
return result;
}
// Apply dynamic styles to the component
private applyDynamicStyles(): void {
// Remove any previously applied dynamic styles
const existingStyles = this.el.nativeElement.querySelectorAll('.dynamic-chart-styles');
existingStyles.forEach((style: HTMLElement) => {
style.remove();
});
// Apply new styles if available
if (this.dynamicStyles) {
const styleElement = this.renderer.createElement('style');
this.renderer.setAttribute(styleElement, 'class', 'dynamic-chart-styles');
this.renderer.setProperty(styleElement, 'textContent', this.dynamicStyles);
this.renderer.appendChild(this.el.nativeElement, styleElement);
console.log('Applied dynamic styles to component');
}
}
// Initialize chart after dynamic template is rendered
private initializeDynamicChart(): void {
// This is a complex issue - Angular directives in dynamically inserted HTML
// don't get processed automatically. We would need to use a different approach
// such as creating components dynamically or using a different template mechanism.
console.log('Initializing dynamic chart - this is where we would handle chart initialization');
// NOTE: The baseChart directive in dynamically inserted HTML via [innerHTML]
// will not be processed by Angular. This is a limitation of Angular's change detection.
// Possible solutions:
// 1. Use Angular's dynamic component creation API
// 2. Modify the approach to use a different template mechanism
// 3. Keep the canvas element in the static template and only load options dynamically
}
// Check if filters are available
hasFilters(): boolean {
const hasBaseFilters = this.baseFilters && this.baseFilters.length > 0;
console.log('Checking for filters - baseFilters:', this.baseFilters, 'hasBaseFilters:', hasBaseFilters);
return hasBaseFilters;
}
// Toggle filter visibility
toggleFilters(): void {
console.log('Toggling filters. Current state:', this.showFilters);
console.log('Base filters available:', this.hasFilters());
this.showFilters = !this.showFilters;
console.log('New state:', this.showFilters);
}
// Initialize filter values with proper default values based on type // Initialize filter values with proper default values based on type
private initializeFilterValues(): void { private initializeFilterValues(): void {
console.log('Initializing filter values'); console.log('Initializing filter values');
console.log('Base filters before initialization:', this.baseFilters);
// Initialize base filters // Initialize base filters
if (this.baseFilters) { if (this.baseFilters && Array.isArray(this.baseFilters)) {
this.baseFilters.forEach(filter => { this.baseFilters.forEach((filter, index) => {
console.log(`Processing base filter ${index}:`, filter);
if (filter.value === undefined || filter.value === null) { if (filter.value === undefined || filter.value === null) {
switch (filter.type) { switch (filter.type) {
case 'multiselect': case 'multiselect':
@@ -162,13 +581,22 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
default: default:
filter.value = ''; filter.value = '';
} }
console.log(`Initialized base filter ${index} value to:`, filter.value);
} else {
console.log(`Base filter ${index} already has value:`, filter.value);
} }
}); });
} else {
// Initialize as empty array if not provided
this.baseFilters = [];
} }
console.log('Base filters after initialization:', this.baseFilters);
// Initialize drilldown filters // Initialize drilldown filters
if (this.drilldownFilters) { if (this.drilldownFilters && Array.isArray(this.drilldownFilters)) {
this.drilldownFilters.forEach(filter => { this.drilldownFilters.forEach((filter, index) => {
console.log(`Processing drilldown filter ${index}:`, filter);
if (filter.value === undefined || filter.value === null) { if (filter.value === undefined || filter.value === null) {
switch (filter.type) { switch (filter.type) {
case 'multiselect': case 'multiselect':
@@ -183,15 +611,23 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
default: default:
filter.value = ''; filter.value = '';
} }
console.log(`Initialized drilldown filter ${index} value to:`, filter.value);
} else {
console.log(`Drilldown filter ${index} already has value:`, filter.value);
} }
}); });
} else {
// Initialize as empty array if not provided
this.drilldownFilters = [];
} }
// Initialize layer filters // Initialize layer filters
if (this.drilldownLayers) { if (this.drilldownLayers && Array.isArray(this.drilldownLayers)) {
this.drilldownLayers.forEach(layer => { this.drilldownLayers.forEach((layer, layerIndex) => {
if (layer.filters) { console.log(`Processing drilldown layer ${layerIndex}:`, layer);
layer.filters.forEach((filter: any) => { if (layer.filters && Array.isArray(layer.filters)) {
layer.filters.forEach((filter, filterIndex) => {
console.log(`Processing layer ${layerIndex} filter ${filterIndex}:`, filter);
if (filter.value === undefined || filter.value === null) { if (filter.value === undefined || filter.value === null) {
switch (filter.type) { switch (filter.type) {
case 'multiselect': case 'multiselect':
@@ -206,10 +642,16 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
default: default:
filter.value = ''; filter.value = '';
} }
console.log(`Initialized layer ${layerIndex} filter ${filterIndex} value to:`, filter.value);
} else {
console.log(`Layer ${layerIndex} filter ${filterIndex} already has value:`, filter.value);
} }
}); });
} }
}); });
} else {
// Initialize as empty array if not provided
this.drilldownLayers = [];
} }
console.log('Filter values initialized:', { console.log('Filter values initialized:', {
@@ -221,6 +663,24 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
// Initialize chart options based on chart type // Initialize chart options based on chart type
private initializeChartOptions(): void { private initializeChartOptions(): void {
// Initialize with default structure to ensure plugins.legend exists
this.chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
}
}
};
// If we have dynamic options, use them instead of the default ones
if (this.dynamicOptions) {
this.mergeDynamicOptions();
return;
}
switch (this.chartType) { switch (this.chartType) {
case 'bar': case 'bar':
this.initializeBarChartOptions(); this.initializeBarChartOptions();
@@ -255,6 +715,8 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
this.chartOptions = { this.chartOptions = {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
barPercentage: 0.6, // Reduced from 0.8 to create more spacing between bars
categoryPercentage: 0.8, // Reduced from 0.9 to create more spacing between categories
scales: { scales: {
x: { x: {
ticks: { ticks: {
@@ -276,7 +738,9 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
font: { font: {
size: 12 size: 12
} }
} },
// Add some padding to the y-axis to prevent bars from touching the top
suggestedMax: 10
} }
}, },
plugins: { plugins: {
@@ -286,11 +750,21 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
labels: { labels: {
font: { font: {
size: 12 size: 12
} },
// Add padding to legend items
padding: 20
} }
}, },
tooltip: { tooltip: {
enabled: true enabled: true,
// Improve tooltip appearance
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleFont: {
size: 14
},
bodyFont: {
size: 12
}
} }
}, },
layout: { layout: {
@@ -300,6 +774,21 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
right: 15, right: 15,
top: 15 top: 15
} }
},
// Add bar chart specific options
indexAxis: 'x', // Horizontal bars
elements: {
bar: {
borderWidth: 2, // Increased border width for better visibility
borderSkipped: false, // Show all borders for better separation
// Add border color to make bars more distinct
borderColor: 'rgba(255, 255, 255, 0.8)' // White border for better separation
}
},
// Animation settings for smoother transitions
animation: {
duration: 1000,
easing: 'easeInOutQuart'
} }
}; };
} }
@@ -434,7 +923,7 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
mode: 'point', mode: 'point',
intersect: false, intersect: false,
callbacks: { callbacks: {
label: function(context: any) { label: function (context: any) {
const point: any = context.raw; const point: any = context.raw;
if (point && point.hasOwnProperty('y') && point.hasOwnProperty('r')) { if (point && point.hasOwnProperty('y') && point.hasOwnProperty('r')) {
const yValue = parseFloat(point.y); const yValue = parseFloat(point.y);
@@ -544,6 +1033,11 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
this.isLoading = true; this.isLoading = true;
this.noDataAvailable = false; this.noDataAvailable = false;
// Ensure chart options are initialized
if (!this.chartOptions) {
this.initializeChartOptions();
}
console.log('Starting fetchChartData for chart type:', this.chartType); console.log('Starting fetchChartData for chart type:', this.chartType);
// If we're in drilldown mode, fetch the appropriate drilldown data // If we're in drilldown mode, fetch the appropriate drilldown data
@@ -1115,17 +1609,17 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
const hue2rgb = (p: number, q: number, t: number) => { const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1; if (t < 0) t += 1;
if (t > 1) t -= 1; if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1/2) return q; if (t < 1 / 2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p; return p;
}; };
const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q; const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3); r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h); g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3); b = hue2rgb(p, q, h - 1 / 3);
} }
return { return {
@@ -1291,7 +1785,7 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
} }
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log('Chart hovered:', e); // console.log('Chart hovered:', e);
} }
// Method to check if chart data is valid // Method to check if chart data is valid
@@ -1346,4 +1840,257 @@ export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy {
return false; return false;
} }
} }
// 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, event: any): void {
// For date range filters, we need to handle the change differently
// since we're binding to individual start/end properties
if (!filter.value) {
filter.value = { start: null, end: null };
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range input changes for start/end dates
onDateRangeInputChange(filter: any, dateType: string, event: any): void {
// Initialize filter.value if it doesn't exist
if (!filter.value) {
filter.value = { start: null, end: null };
}
// Update the specific date type (start or end)
filter.value[dateType] = event.target.value;
// 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

@@ -126,6 +126,7 @@ import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.c
import { QueryComponent } from './superadmin/query/query.component'; import { QueryComponent } from './superadmin/query/query.component';
import { QueryaddComponent } from './superadmin/queryadd/queryadd.component'; import { QueryaddComponent } from './superadmin/queryadd/queryadd.component';
import { QueryeditComponent } from './superadmin/queryedit/queryedit.component'; import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
import { ChartTypePageComponent } from './builder/dashboardnew/chart-type-manager/chart-type-page.component';
@@ -222,6 +223,7 @@ const routes: Routes = [
{ path: 'editdata/:id', component: EditformnewdashComponent }, { path: 'editdata/:id', component: EditformnewdashComponent },
{ path: 'editdashn/:id', component: EditnewdashComponent }, { path: 'editdashn/:id', component: EditnewdashComponent },
{ path: 'schedule/:id', component: ScheduleComponent }, { path: 'schedule/:id', component: ScheduleComponent },
{ path: 'chart-types', component: ChartTypePageComponent },
] ]
}, },

View File

@@ -162,6 +162,14 @@ import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.c
// Import Shield Dashboard Module // Import Shield Dashboard Module
import { ShieldDashboardModule } from './builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.module'; import { ShieldDashboardModule } from './builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.module';
// Import UnifiedChartComponent
import { UnifiedChartComponent } from './builder/dashboardnew/gadgets/unified-chart/unified-chart.component';
// Import ChartConfigManagerComponent
import { ChartConfigManagerComponent } from './builder/dashboardnew/chart-config-manager/chart-config-manager.component';
// Import ChartTypeManagerComponent
import { ChartTypeManagerComponent } from './builder/dashboardnew/chart-type-manager/chart-type-manager.component';
// Import ChartTypePageComponent
import { ChartTypePageComponent } from './builder/dashboardnew/chart-type-manager/chart-type-page.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -173,6 +181,14 @@ import { ShieldDashboardModule } from './builder/dashboardnew/gadgets/shield-das
SubmenuComponent, ModulesComponent, SessionloggerComponent, SubmenuComponent, ModulesComponent, SessionloggerComponent,
DashboardnewComponent, EditformnewdashComponent, EditnewdashComponent, ScheduleComponent, DashboardnewComponent, EditformnewdashComponent, EditnewdashComponent, ScheduleComponent,
CommonFilterComponent, ChartWrapperComponent, CompactFilterComponent, DoughnutChartComponent, LineChartComponent, RadarChartComponent, BarChartComponent, BubbleChartComponent, DynamicChartComponent, ScatterChartComponent, PolarChartComponent, PieChartComponent, FinancialChartComponent, ToDoChartComponent, GridViewComponent, CommonFilterComponent, ChartWrapperComponent, CompactFilterComponent, DoughnutChartComponent, LineChartComponent, RadarChartComponent, BarChartComponent, BubbleChartComponent, DynamicChartComponent, ScatterChartComponent, PolarChartComponent, PieChartComponent, FinancialChartComponent, ToDoChartComponent, GridViewComponent,
// Add UnifiedChartComponent to declarations
UnifiedChartComponent,
// Add ChartConfigManagerComponent to declarations
ChartConfigManagerComponent,
// Add ChartTypeManagerComponent to declarations
ChartTypeManagerComponent,
// Add ChartTypePageComponent to declarations
ChartTypePageComponent,
DashrunnerlineComponent, BarRunnerComponent, LineRunnerComponent, DoughnutRunnerComponent, GridRunnerComponent, PieRunnerComponent, PolarRunnerComponent, RadarRunnerComponent, ScatterRunnerComponent, TodoRunnerComponent, BubbleRunnerComponent, DashrunnerlineComponent, BarRunnerComponent, LineRunnerComponent, DoughnutRunnerComponent, GridRunnerComponent, PieRunnerComponent, PolarRunnerComponent, RadarRunnerComponent, ScatterRunnerComponent, TodoRunnerComponent, BubbleRunnerComponent,
// Add CompactFilterRunnerComponent to declarations // Add CompactFilterRunnerComponent to declarations
CompactFilterRunnerComponent, CompactFilterRunnerComponent,