Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cea739e7f | ||
|
|
cf5461c8b0 | ||
|
|
285aeca0f8 | ||
|
|
5a89e7bc96 | ||
|
|
d8ad061ad7 | ||
|
|
c1da21b300 | ||
|
|
ef2dba3b92 | ||
|
|
cf0af07104 | ||
|
|
874d5dfed2 | ||
|
|
91af36866e | ||
|
|
534bc307a7 | ||
|
|
53424f0a7c | ||
|
|
49b5300887 | ||
|
|
c5d730ae22 | ||
|
|
40438fcc1b | ||
|
|
71be18b21f | ||
|
|
1263805d61 | ||
|
|
0e6e4899e5 | ||
|
|
29e50253af | ||
|
|
68b793ea60 | ||
|
|
5f0594c93c | ||
|
|
e7ee226b66 | ||
|
|
ff2f5a1c34 | ||
|
|
fc36794b22 | ||
|
|
75ccca8caf | ||
|
|
c4dfb0283c | ||
|
|
57acdb0300 | ||
|
|
aee28f604f | ||
|
|
8375e53fdf | ||
|
|
db7a9c727d | ||
|
|
6e91af551e | ||
|
|
de5479cc07 | ||
|
|
807058e40d | ||
|
|
c6022b0e22 | ||
|
|
9aed6e0d43 | ||
|
|
64b664e625 | ||
|
|
49f1a2fbf2 | ||
|
|
6bfe890cd9 | ||
|
|
3e70f85644 | ||
|
|
482805b5cf | ||
|
|
ffda17e6b1 | ||
|
|
7396843bc6 | ||
|
|
4f75ecb3e0 | ||
|
|
7c1a487114 | ||
|
|
47e9fb92e3 | ||
|
|
7f735dcada | ||
|
|
bd315f42a3 | ||
|
|
e8c1f46430 | ||
|
|
fa96ca81bd | ||
|
|
c384f44c0c | ||
|
|
50df914ca9 | ||
|
|
e0bd888c45 | ||
|
|
02b82fcaf8 | ||
|
|
557afc348f | ||
|
|
1dec787062 | ||
|
|
8853cf75cf | ||
|
|
ad57f11f8a | ||
|
|
9b775a8c63 | ||
|
|
cf4fc1be93 | ||
|
|
4c135c4901 | ||
|
|
0b738ca7ca | ||
|
|
1b17bb706d | ||
|
|
f740076d60 | ||
|
|
bedcc0822d | ||
|
|
87810acc9e | ||
|
|
96b90e5dbd | ||
|
|
ced99e0940 | ||
|
|
4f82ae8698 | ||
|
|
e6779e8f34 | ||
|
|
82425d5377 | ||
|
|
2995328ec1 | ||
|
|
afc2c1f8a1 | ||
|
|
418b02acd7 | ||
|
|
cdd752469c | ||
|
|
cdcf1e07c7 | ||
|
|
c6ad8b5c2f | ||
|
|
98e0908920 | ||
|
|
8266bfdc01 | ||
|
|
f2b6a4d145 | ||
|
|
02f37a1bc5 | ||
|
|
f24138cfbd |
@@ -1,8 +1,9 @@
|
||||
export class ReportBuilder {
|
||||
public report_id: number;
|
||||
public report_name:string;
|
||||
public description: string;
|
||||
public report_tags: string;
|
||||
public servicename:string;
|
||||
|
||||
public report_name: string;
|
||||
public description: string;
|
||||
public report_tags: string;
|
||||
public servicename: string;
|
||||
// Add SureConnect reference
|
||||
public sureConnectId: number | null;
|
||||
}
|
||||
@@ -23,8 +23,22 @@ export interface DashboardContentModel {
|
||||
component?: any;
|
||||
name: string;
|
||||
type?:string;
|
||||
// Common properties
|
||||
// Chart properties
|
||||
xAxis?: string;
|
||||
yAxis?: string | string[];
|
||||
chartType?: string;
|
||||
charttitle?: string;
|
||||
chartlegend?: boolean;
|
||||
showlabel?: boolean;
|
||||
chartcolor?: boolean;
|
||||
slices?: boolean;
|
||||
donut?: boolean;
|
||||
charturl?: string;
|
||||
chartparameter?: string;
|
||||
datastore?: string;
|
||||
table?: string;
|
||||
datasource?: string;
|
||||
fieldName?: string;
|
||||
connection?: string;
|
||||
baseFilters?: any[];
|
||||
// Common filter properties
|
||||
@@ -37,6 +51,11 @@ export interface DashboardContentModel {
|
||||
drilldownParameter?: string;
|
||||
drilldownFilters?: any[];
|
||||
drilldownLayers?: any[];
|
||||
// Compact filter properties
|
||||
filterKey?: string;
|
||||
filterType?: string;
|
||||
filterLabel?: string;
|
||||
filterOptions?: string[];
|
||||
}
|
||||
|
||||
export interface DashboardModel {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>gaurav</title>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>this is h1</h1>
|
||||
<h2>this is h1</h2>
|
||||
<h3>this is h1</h3>
|
||||
<h4>this is h1</h4>
|
||||
<p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsa fuga, asperiores mollitia iste vitae repellendus adipisci atque eum corrupti ad placeat unde voluptatum quia perferendis neque expedita, sequi iure quo. Ut error adipisci ex cum sint, suscipit, voluptatem repellat nemo dolorum unde dolores quasi aut. A earum quo mollitia voluptatibus!</p>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -71,13 +71,19 @@
|
||||
Calculated Fields
|
||||
</ng-container></clr-dg-column>
|
||||
|
||||
|
||||
<!-- Webhook Status Column -->
|
||||
<clr-dg-column>
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
Webhook Status
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
|
||||
<!-- who column -->
|
||||
<clr-dg-column>
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
<clr-icon shape="bars"></clr-icon> Action
|
||||
</ng-container></clr-dg-column>
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<!-- end -->
|
||||
|
||||
<clr-dg-row *clrDgItems="let user of product" [clrDgItem]="user">
|
||||
@@ -104,7 +110,7 @@
|
||||
<span
|
||||
style="margin-right: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-grow: 1;"
|
||||
title="{{user.url_endpoint}}" (click)="showFullUrl(user.url_endpoint)">
|
||||
{{user.url | slice:0:30}}{{user.url_endpoint.length > 30 ? '...' : ''}}
|
||||
{{user.url | slice:0:30}}{{user?.url_endpoint?.length > 30 ? '...' : ''}}
|
||||
</span>
|
||||
<button class="btn btn-icon btn-sm" (click)="copyToClipboard(user.url_endpoint)" title="Copy URL">
|
||||
<clr-icon shape="copy-to-clipboard"></clr-icon>
|
||||
@@ -138,39 +144,69 @@
|
||||
style="cursor: pointer; align-items: center;"><clr-icon shape="details"
|
||||
*ngIf="user.calculated_field_json"></clr-icon></clr-dg-cell>
|
||||
|
||||
|
||||
<!-- Webhook Status Cell -->
|
||||
<clr-dg-cell>
|
||||
<span *ngIf="user.webhook_url && user.webhook_url.trim() !== ''" class="webhook-enabled">Enabled</span>
|
||||
<span *ngIf="!user.webhook_url || user.webhook_url.trim() === ''" class="webhook-disabled">Disabled</span>
|
||||
</clr-dg-cell>
|
||||
|
||||
<!-- who column -->
|
||||
<clr-dg-cell>
|
||||
<div style="display: flex; align-items: center; gap: 1px; flex-wrap: nowrap;">
|
||||
<clr-signpost>
|
||||
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success"
|
||||
style="color: rgb(0, 130, 236);"></clr-icon></span>
|
||||
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
||||
<h5 style="margin-top: 0">Who Column</h5>
|
||||
<div>Account ID: <code class="clr-code">{{user.accountId}}</code></div>
|
||||
<div>Created At: <code class="clr-code">{{user.createdAt| date}}</code></div>
|
||||
<div>Created By: <code class="clr-code">{{user.createdBy}}</code></div>
|
||||
<div>Updated At: <code class="clr-code">{{user.updatedAt | date}}</code></div>
|
||||
<div>Updated By: <code class="clr-code">{{user.updatedBy}}</code></div>
|
||||
</clr-signpost-content>
|
||||
</clr-signpost>
|
||||
<clr-signpost>
|
||||
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success"
|
||||
style="color: rgb(0, 130, 236);"></clr-icon></span>
|
||||
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
||||
<h5 style="margin-top: 0">Who Column</h5>
|
||||
<div>Account ID: <code class="clr-code">{{user.accountId}}</code></div>
|
||||
<div>Created At: <code class="clr-code">{{user.createdAt| date}}</code></div>
|
||||
<div>Created By: <code class="clr-code">{{user.createdBy}}</code></div>
|
||||
<div>Updated At: <code class="clr-code">{{user.updatedAt | date}}</code></div>
|
||||
<div>Updated By: <code class="clr-code">{{user.updatedBy}}</code></div>
|
||||
</clr-signpost-content>
|
||||
</clr-signpost>
|
||||
|
||||
<!-- New JSON Update button -->
|
||||
<button class="btn btn-icon btn-sm" (click)="updateJson(user.id)" title="Update JSON">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</button>
|
||||
<!-- New JSON Update button -->
|
||||
<button class="btn btn-icon" (click)="updateJson(user.id)" title="Update JSON">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</button>
|
||||
|
||||
<!-- Calculated Field button -->
|
||||
<button class="btn btn-icon btn-sm" (click)="fetchAvailableKeys(user, false)" title="Create Calculated Field">
|
||||
<clr-icon shape="calculator"></clr-icon>
|
||||
</button>
|
||||
<!-- Calculated Field button -->
|
||||
<button class="btn btn-icon" (click)="fetchAvailableKeys(user)" title="Create Calculated Field">
|
||||
<clr-icon shape="calculator"></clr-icon>
|
||||
</button>
|
||||
|
||||
<!-- Group By button -->
|
||||
<button class="btn btn-icon btn-sm" (click)="openGroupByModal(user)" title="Group By Configuration">
|
||||
<clr-icon shape="group"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Group By button -->
|
||||
<button class="btn btn-icon" (click)="openGroupByModal(user)" title="Group By Configuration">
|
||||
<clr-icon shape="group"></clr-icon>
|
||||
</button>
|
||||
|
||||
<!-- Blending Keys button (only shown for blending type with non-empty blending_lakeids) -->
|
||||
<button class="btn btn-icon" (click)="fetchBlendingKeys(user)" title="Configure Blending Query"
|
||||
*ngIf="canShowBlendingAction(user)">
|
||||
<clr-icon shape="code"></clr-icon>
|
||||
</button>
|
||||
|
||||
<!-- Field Mapping button -->
|
||||
<button class="btn btn-icon" (click)="openFieldMappingModal(user)" title="Field Mapping"
|
||||
*ngIf="user.url && user.sure_connect_id">
|
||||
<clr-icon shape="map"></clr-icon>
|
||||
</button>
|
||||
|
||||
<!-- Scheduler button -->
|
||||
<button class="btn btn-icon" (click)="openSchedulerModal(user)" title="Scheduler">
|
||||
<clr-icon shape="clock"></clr-icon>
|
||||
</button>
|
||||
|
||||
<!-- Webhook toggle button -->
|
||||
<button class="btn btn-icon" (click)="toggleWebhook(user)" title="Toggle Webhook">
|
||||
<clr-icon shape="network-globe"></clr-icon>
|
||||
</button>
|
||||
|
||||
<!-- Copy Webhook URL button -->
|
||||
<button class="btn btn-icon" (click)="copyWebhookUrl(user.webhook_url)" title="Copy Webhook URL"
|
||||
*ngIf="user.webhook_url && user.webhook_url.trim() !== ''">
|
||||
<clr-icon shape="copy-to-clipboard"></clr-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
|
||||
<!-- who colmn -->
|
||||
@@ -409,16 +445,16 @@
|
||||
instanceId="edit-form-{{rowSelected.id}}"></app-cron-job-builder>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-12">
|
||||
<!-- <div class="clr-col-sm-12">
|
||||
<label> json</label>
|
||||
<input id="name" type="Text" class="form-control" style="border: none; outline: none; height:33px !important;"
|
||||
[(ngModel)]="rowSelected.json" name=" json " />
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-12">
|
||||
<label>Url</label>
|
||||
<label>Url Endpoint</label>
|
||||
<input class="clr-input" type="text" [(ngModel)]="rowSelected.url_endpoint" name="url_endpoint" />
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="clr-col-sm-12">
|
||||
<label>Batch Volume</label>
|
||||
@@ -434,8 +470,30 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Add ref_datalake_id dropdown -->
|
||||
<!-- Data Lake Type -->
|
||||
<div class="clr-col-sm-12">
|
||||
<label>Data Lake Type</label>
|
||||
<select class="clr-input" [(ngModel)]="rowSelected.datalake_type" name="datalake_type">
|
||||
<option *ngFor="let type of dataLakeTypes" [value]="type.value">{{ type.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Blending Data Lakes (only show when blending type is selected) -->
|
||||
<div class="clr-col-sm-12" *ngIf="rowSelected.datalake_type === 'blending'">
|
||||
<label>Blending Data Lakes</label>
|
||||
<div class="checkbox-container">
|
||||
<div *ngFor="let dataLake of dataLakeList" class="checkbox-item">
|
||||
<input type="checkbox" [value]="dataLake.id"
|
||||
(change)="onEditBlendingLakeCheckboxChange($event, dataLake.id)"
|
||||
[checked]="isEditBlendingLakeSelected(dataLake.id)">
|
||||
<label>{{ dataLake.name }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<small class="clr-subtext">Select data lakes to blend</small>
|
||||
</div>
|
||||
|
||||
<!-- Reference Data Lake (only show when normal type is selected) -->
|
||||
<div class="clr-col-sm-12" *ngIf="rowSelected.datalake_type === 'normal'">
|
||||
<label>Reference Data Lake</label>
|
||||
<select class="clr-input" [(ngModel)]="rowSelected.ref_datalake_id" name="ref_datalake_id">
|
||||
<option value="">Select Data Lake</option>
|
||||
@@ -538,16 +596,16 @@
|
||||
(cronExpressionChange)="onAddCronExpressionChange($event)" instanceId="add-form"></app-cron-job-builder>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-12">
|
||||
<!-- <div class="clr-col-sm-12">
|
||||
<label>json</label>
|
||||
<input class="form-control" type="Text" formControlName="json"
|
||||
style="border: none; outline: none; height:33px !important;" />
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="clr-col-sm-12">
|
||||
<!-- <div class="clr-col-sm-12">
|
||||
<label> Url Endpoint</label>
|
||||
<input class="clr-input" type="text" formControlName="url_endpoint" />
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="clr-col-sm-12">
|
||||
<label> Batch Volume</label>
|
||||
@@ -563,8 +621,29 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Add ref_datalake_id dropdown -->
|
||||
<!-- Data Lake Type -->
|
||||
<div class="clr-col-sm-12">
|
||||
<label>Data Lake Type</label>
|
||||
<select formControlName="datalake_type">
|
||||
<option *ngFor="let type of dataLakeTypes" [value]="type.value">{{ type.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Blending Data Lakes (only show when blending type is selected) -->
|
||||
<div class="clr-col-sm-12" *ngIf="entryForm.get('datalake_type')?.value === 'blending'">
|
||||
<label>Blending Data Lakes</label>
|
||||
<div class="checkbox-container">
|
||||
<div *ngFor="let dataLake of dataLakeList" class="checkbox-item">
|
||||
<input type="checkbox" [value]="dataLake.id" (change)="onBlendingLakeCheckboxChange($event, dataLake.id)"
|
||||
[checked]="isBlendingLakeSelected(dataLake.id)">
|
||||
<label>{{ dataLake.name }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<small class="clr-subtext">Select data lakes to blend</small>
|
||||
</div>
|
||||
|
||||
<!-- Reference Data Lake (only show when normal type is selected) -->
|
||||
<div class="clr-col-sm-12" *ngIf="entryForm.get('datalake_type')?.value === 'normal'">
|
||||
<label>Reference Data Lake</label>
|
||||
<select formControlName="ref_datalake_id">
|
||||
<option value="">Select Data Lake</option>
|
||||
@@ -916,4 +995,195 @@
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<!-- htmlpopup -->
|
||||
<!-- Blending Keys Panel -->
|
||||
<div class="blending-keys-panel" *ngIf="showBlendingKeys">
|
||||
<div class="panel-header">
|
||||
<h3>Blending Configuration</h3>
|
||||
<button class="btn btn-icon" (click)="closeBlendingKeys()" title="Close">
|
||||
<clr-icon shape="close"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-content">
|
||||
<!-- Display keys for each blending lake -->
|
||||
<div class="keys-container" *ngFor="let keyData of blendingKeysData">
|
||||
<h4>Data Lake {{ keyData.lakeId }} - Table: {{ keyData.tableName }}</h4>
|
||||
<div class="headers-container">
|
||||
<span class="header-tag" *ngFor="let header of keyData.headers">{{ header }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SQL Query Editor -->
|
||||
<div class="sql-editor-container">
|
||||
<h4>SQL Query Builder</h4>
|
||||
<textarea class="clr-textarea sql-textarea" [(ngModel)]="sqlQueryText"
|
||||
placeholder="Enter your SQL query here using the available headers..." rows="6"></textarea>
|
||||
<div class="editor-actions">
|
||||
<button class="btn btn-primary" (click)="updateSqlQuery()">Update SQL Query</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blending Configuration Modal -->
|
||||
<clr-modal [(clrModalOpen)]="showBlendingKeys" [clrModalSize]="'xl'" [clrModalStaticBackdrop]="true">
|
||||
<h3 class="modal-title">Blending Configuration</h3>
|
||||
<div class="modal-body">
|
||||
<div class="blending-config-container">
|
||||
<!-- Display keys for each blending lake -->
|
||||
<div class="keys-section" *ngIf="blendingKeysData && blendingKeysData.length > 0">
|
||||
<h4 class="section-title">
|
||||
<clr-icon shape="data-field" class="section-icon"></clr-icon>
|
||||
Available Data Sources
|
||||
</h4>
|
||||
<div class="keys-container">
|
||||
<div class="key-card" *ngFor="let keyData of blendingKeysData">
|
||||
<div class="key-header">
|
||||
<h5>Data Lake {{ keyData.lakeId }} - Table: {{ keyData.tableName }}</h5>
|
||||
</div>
|
||||
<div class="headers-container">
|
||||
<span class="header-tag" *ngFor="let header of keyData.headers" (click)="insertHeaderIntoQuery(header)">
|
||||
{{ header }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SQL Query Editor -->
|
||||
<div class="sql-editor-section">
|
||||
<h4 class="section-title">
|
||||
<clr-icon shape="code" class="section-icon"></clr-icon>
|
||||
SQL Query Builder
|
||||
</h4>
|
||||
<div class="sql-editor-container">
|
||||
<textarea class="clr-textarea sql-textarea" [(ngModel)]="sqlQueryText"
|
||||
placeholder="Enter your SQL query here using the available headers. Click on header names above to insert them into your query."
|
||||
rows="8"></textarea>
|
||||
<div class="editor-hint">
|
||||
<small class="clr-subtext">Tip: Click on header names above to automatically insert them into your
|
||||
query</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="closeBlendingKeys()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="updateSqlQuery()">Update SQL Query</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<!-- Field Mapping Modal -->
|
||||
<clr-modal [(clrModalOpen)]="showFieldMappingModal" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
||||
<h3 class="modal-title">Field Mapping Configuration</h3>
|
||||
<div class="modal-body">
|
||||
<div class="field-mapping-container">
|
||||
<div class="instructions">
|
||||
<p>Map original field names to new names. Leave mapping blank to exclude field from mapping.</p>
|
||||
</div>
|
||||
|
||||
<div class="mapping-table-container" *ngIf="fieldMappingData && fieldMappingData.length > 0">
|
||||
<table class="table mapping-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50%">Original Field Name</th>
|
||||
<th width="50%">Mapped Field Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let field of fieldMappingData; let i = index">
|
||||
<td class="original-field">
|
||||
<span class="field-tag">{{ field.original }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="clr-input mapping-input" [(ngModel)]="field.mapped"
|
||||
placeholder="Enter new field name" (ngModelChange)="updateFieldMapping(i, $event)">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="no-data-message" *ngIf="fieldMappingData && fieldMappingData.length === 0">
|
||||
<p>No fields available for mapping. Please check the data source configuration.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="closeFieldMappingModal()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="updateFieldMappings()"
|
||||
[disabled]="!fieldMappingData || fieldMappingData.length === 0">Update Field Mappings</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<!-- Scheduler Modal -->
|
||||
<clr-modal [(clrModalOpen)]="showSchedulerModal" [clrModalSize]="'md'" [clrModalStaticBackdrop]="true">
|
||||
<h3 class="modal-title">Job Scheduler</h3>
|
||||
<div class="modal-body">
|
||||
<div class="scheduler-container">
|
||||
<div class="data-lake-info">
|
||||
<h4>Data Lake: {{ selectedSchedulerItem?.name }}</h4>
|
||||
</div>
|
||||
|
||||
<!-- Job Information -->
|
||||
<div class="job-info-section" *ngIf="schedulerJob">
|
||||
<div class="job-header">
|
||||
<h5>Job Information</h5>
|
||||
<span class="badge" [ngClass]="getStatusBadgeClass(schedulerJob.status)">
|
||||
{{ schedulerJob.status }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="job-details">
|
||||
<div class="detail-row">
|
||||
<label>Job Name:</label>
|
||||
<span>{{ schedulerJob.name }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<label>Job Type:</label>
|
||||
<span>{{ schedulerJob.jobType }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<label>Description:</label>
|
||||
<span>{{ schedulerJob.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Actions -->
|
||||
<div class="job-actions">
|
||||
<button class="btn btn-warning" (click)="pauseJob()" *ngIf="schedulerJob.status === 'RUNNING'">
|
||||
<clr-icon shape="pause"></clr-icon> Pause Job
|
||||
</button>
|
||||
<button class="btn btn-success" (click)="resumeJob()" *ngIf="schedulerJob.status === 'PAUSED'">
|
||||
<clr-icon shape="play"></clr-icon> Resume Job
|
||||
</button>
|
||||
<button class="btn btn-danger" (click)="stopJob()" *ngIf="schedulerJob.status !== 'STOPPED'">
|
||||
<clr-icon shape="stop"></clr-icon> Stop Job
|
||||
</button>
|
||||
<button class="btn btn-primary" (click)="restartJob()" *ngIf="schedulerJob.status === 'STOPPED'">
|
||||
<clr-icon shape="play"></clr-icon> Restart Job
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Job Found -->
|
||||
<div class="no-job-section" *ngIf="schedulerJob === null">
|
||||
<div class="no-job-message">
|
||||
<p>No scheduled job found for this Data Lake.</p>
|
||||
<p>Would you like to create a new job?</p>
|
||||
</div>
|
||||
|
||||
<div class="create-job-actions">
|
||||
<button class="btn btn-primary" (click)="createJob()">
|
||||
<clr-icon shape="plus"></clr-icon> Create Job
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="closeSchedulerModal()">Close</button>
|
||||
</div>
|
||||
|
||||
</clr-modal>
|
||||
@@ -384,3 +384,494 @@
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Data Lake Type Styles */
|
||||
.clr-subtext {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
[data-multiple] {
|
||||
height: auto !important;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* Checkbox Styles for Blending Data Lakes */
|
||||
.checkbox-container {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.checkbox-item label {
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Blending Keys Panel Styles */
|
||||
.blending-keys-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
border-top: 1px solid #ccc;
|
||||
box-shadow: 0 -2px 4px rgba(0,0,0,0.1);
|
||||
z-index: 1000;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.keys-container {
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.keys-container h4 {
|
||||
margin-top: 0;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.headers-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.header-tag {
|
||||
display: inline-block;
|
||||
background-color: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 12px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.sql-editor-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.sql-textarea {
|
||||
width: 100%;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Blending Configuration Modal Styles */
|
||||
.blending-config-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #0072a0;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.keys-section {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.key-card {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.key-header h5 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.headers-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 20px;
|
||||
padding: 6px 12px;
|
||||
margin: 4px;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.header-tag:hover {
|
||||
background-color: #bbdefb;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.sql-editor-section {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.sql-editor-container {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.sql-textarea {
|
||||
width: 100%;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.editor-hint {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* Field Mapping Modal Styles */
|
||||
.field-mapping-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background-color: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.instructions p {
|
||||
margin: 0;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.mapping-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.mapping-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.mapping-table th {
|
||||
background-color: #f5f5f5;
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.mapping-table td {
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.original-field {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.field-tag {
|
||||
display: inline-block;
|
||||
background-color: #e0e0e0;
|
||||
border: 1px solid #bdbdbd;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mapping-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mapping-input:focus {
|
||||
outline: none;
|
||||
border-color: #0072a0;
|
||||
box-shadow: 0 0 0 2px rgba(0, 114, 160, 0.2);
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* Scheduler Modal Styles */
|
||||
.scheduler-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.data-lake-info h4 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.job-info-section {
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.job-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.job-header h5 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 5px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-light {
|
||||
background-color: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.job-details {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.detail-row label {
|
||||
font-weight: 500;
|
||||
width: 120px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.detail-row span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.job-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.job-actions .btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.no-job-section {
|
||||
text-align: center;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.no-job-message p {
|
||||
margin: 10px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.no-job-message p:first-child {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.create-job-actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.create-job-actions .btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* Webhook Status Styles */
|
||||
.webhook-enabled {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.webhook-disabled {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Webhook Status Styles */
|
||||
.webhook-status {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.webhook-status.enabled {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.webhook-status.disabled {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Webhook Modal Styles */
|
||||
.webhook-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.webhook-container .data-lake-info h4 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.webhook-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.webhook-info {
|
||||
background-color: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.webhook-info p {
|
||||
margin: 0;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { AlertService } from 'src/app/services/alert.service';
|
||||
import { Data_lakeservice } from './Data_lake.service';
|
||||
import { SchedulerService } from 'src/app/services/scheduler.service';
|
||||
import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators, ValidationErrors } from '@angular/forms';
|
||||
import { ExtensionService } from 'src/app/services/fnd/extension.service';
|
||||
import { DashboardContentModel2 } from 'src/app/models/builder/dashboard';
|
||||
@@ -10,6 +11,7 @@ import { UserInfoService } from 'src/app/services/user-info.service';
|
||||
import { SureconnectService } from '../sureconnect/sureconnect.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||
import { DataLakeSchedulerService } from './dataLakescheduler.service';
|
||||
declare var JsBarcode: any;
|
||||
@Component({
|
||||
selector: 'app-Data_lake',
|
||||
@@ -66,6 +68,12 @@ export class Data_lakeComponent implements OnInit {
|
||||
dataLakeList: any[] = [];
|
||||
selectedDataLake: any = null;
|
||||
|
||||
// New properties for data lake type
|
||||
dataLakeTypes = [
|
||||
{ value: 'normal', label: 'Normal' },
|
||||
{ value: 'blending', label: 'Blending' }
|
||||
];
|
||||
|
||||
// Calculated field properties
|
||||
calculatedFieldModalOpen = false;
|
||||
availableKeys: string[] = [];
|
||||
@@ -99,10 +107,36 @@ export class Data_lakeComponent implements OnInit {
|
||||
|
||||
selectedAggregationFields: { field: string; operation: string }[] = [];
|
||||
|
||||
// New property to store selected blending lake IDs as string
|
||||
selectedBlendingLakeIds: string[] = [];
|
||||
editSelectedBlendingLakeIds: string[] = [];
|
||||
|
||||
// New properties for blending functionality
|
||||
showBlendingKeys = false;
|
||||
blendingKeysData: any[] = [];
|
||||
sqlQueryText = '';
|
||||
selectedBlendingItem: any = null;
|
||||
|
||||
// New properties for field mapping
|
||||
showFieldMappingModal = false;
|
||||
fieldMappingData: any[] = [];
|
||||
fieldMappings: { [key: string]: string } = {};
|
||||
selectedMappingItem: any = null;
|
||||
|
||||
// New properties for scheduler functionality
|
||||
showSchedulerModal = false;
|
||||
schedulerJob: any = null;
|
||||
selectedSchedulerItem: any = null;
|
||||
|
||||
// New properties for webhook functionality
|
||||
showWebhookModal = false;
|
||||
selectedWebhookItem: any = null;
|
||||
|
||||
constructor(
|
||||
private extensionService: ExtensionService,
|
||||
private userInfoService: UserInfoService,
|
||||
private mainService: Data_lakeservice,
|
||||
private schedulerService: DataLakeSchedulerService,
|
||||
private alertService: AlertService,
|
||||
private toastr: ToastrService,
|
||||
private _fb: FormBuilder,
|
||||
@@ -124,23 +158,25 @@ export class Data_lakeComponent implements OnInit {
|
||||
this.getDataLakeList(); // Fetch Data Lake list for reference dropdown
|
||||
this.entryForm = this._fb.group({
|
||||
name: [null],
|
||||
|
||||
url: [null],
|
||||
|
||||
schedule: [null],
|
||||
|
||||
cron_job: [null],
|
||||
|
||||
json: [null],
|
||||
|
||||
url_endpoint: [null],
|
||||
|
||||
batch_volume: [null],
|
||||
sure_connect_id: [null],
|
||||
ref_datalake_id: [null],
|
||||
datalake_type: ['normal'], // Default to normal
|
||||
blending_lakeids: [[]], // Array for multiple selections
|
||||
webhook_url: [null] // Add webhook_url field
|
||||
});
|
||||
|
||||
sure_connect_id: [null], // Add SureConnect field
|
||||
ref_datalake_id: [null], // Add Data Lake reference field
|
||||
// Set default SureConnect value after form initialization if list is already loaded
|
||||
if (this.sureConnectList && this.sureConnectList.length > 0) {
|
||||
const defaultSureConnectId = this.sureConnectList[0].id;
|
||||
this.entryForm.get('sure_connect_id')?.setValue(defaultSureConnectId);
|
||||
}
|
||||
|
||||
}); // component_button200
|
||||
// form code start
|
||||
this.extensionService.getJsonObjectsByFormCodeList(this.formcode).subscribe(data => {
|
||||
console.log(data);
|
||||
@@ -232,6 +268,20 @@ export class Data_lakeComponent implements OnInit {
|
||||
this.sureConnectService.getAll().subscribe((data: any[]) => {
|
||||
this.sureConnectList = data;
|
||||
console.log('SureConnect List:', this.sureConnectList);
|
||||
|
||||
// Set default SureConnect value if list is not empty
|
||||
if (this.sureConnectList && this.sureConnectList.length > 0) {
|
||||
// Set the first SureConnect item as default
|
||||
const defaultSureConnectId = this.sureConnectList[0].id;
|
||||
|
||||
// Set default for ADD form if it exists
|
||||
if (this.entryForm && this.entryForm.get('sure_connect_id')) {
|
||||
// Only set default if no value is already selected
|
||||
if (!this.entryForm.get('sure_connect_id')?.value) {
|
||||
this.entryForm.get('sure_connect_id')?.setValue(defaultSureConnectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, (error) => {
|
||||
console.log('Error fetching SureConnect list:', error);
|
||||
});
|
||||
@@ -240,8 +290,7 @@ export class Data_lakeComponent implements OnInit {
|
||||
// Fetch Data Lake list for reference dropdown
|
||||
getDataLakeList() {
|
||||
this.mainService.getAll().subscribe((data: any[]) => {
|
||||
// Filter out the current item to avoid self-reference
|
||||
this.dataLakeList = data.filter(item => item.id !== (this.rowSelected?.id || 0));
|
||||
this.dataLakeList = data;
|
||||
console.log('Data Lake List:', this.dataLakeList);
|
||||
}, (error) => {
|
||||
console.log('Error fetching Data Lake list:', error);
|
||||
@@ -686,8 +735,22 @@ export class Data_lakeComponent implements OnInit {
|
||||
this.editCronExpression = row.cron_job || '';
|
||||
// Set the selected SureConnect for edit form
|
||||
this.selectedSureConnect = row.sure_connect_id || null;
|
||||
// Set the selected Data Lake for edit form
|
||||
this.selectedDataLake = row.ref_datalake_id || null;
|
||||
|
||||
// If no SureConnect is selected, set the first one as default
|
||||
if (!this.rowSelected.sure_connect_id && this.sureConnectList && this.sureConnectList.length > 0) {
|
||||
this.rowSelected.sure_connect_id = this.sureConnectList[0].id;
|
||||
this.selectedSureConnect = this.sureConnectList[0].id;
|
||||
}
|
||||
|
||||
// Initialize blending lake IDs for edit form from string
|
||||
if (row.blending_lakeids && typeof row.blending_lakeids === 'string') {
|
||||
this.editSelectedBlendingLakeIds = row.blending_lakeids.split(',').filter(id => id.trim() !== '');
|
||||
} else if (row.blending_lakeids && Array.isArray(row.blending_lakeids)) {
|
||||
this.editSelectedBlendingLakeIds = row.blending_lakeids.map(id => id.toString());
|
||||
} else {
|
||||
this.editSelectedBlendingLakeIds = [];
|
||||
}
|
||||
|
||||
// Use setTimeout to ensure the component has time to initialize
|
||||
setTimeout(() => {
|
||||
this.modalEdit = true;
|
||||
@@ -838,18 +901,33 @@ export class Data_lakeComponent implements OnInit {
|
||||
}
|
||||
|
||||
goToAdd(row) {
|
||||
this.modalAdd = true;
|
||||
this.modalAdd = false;
|
||||
this.addCronExpression = '';
|
||||
this.selectedSureConnect = null; // Reset SureConnect selection
|
||||
this.selectedDataLake = null; // Reset Data Lake selection
|
||||
this.submitted = false;
|
||||
// Reset blending lake IDs for add form
|
||||
this.selectedBlendingLakeIds = [];
|
||||
|
||||
// Reset the form control for cron_job and sure_connect_id
|
||||
if (this.entryForm) {
|
||||
if (this.entryForm.get('cron_job')) {
|
||||
this.entryForm.get('cron_job')?.setValue('');
|
||||
}
|
||||
// Reset blending_lakeids to empty string
|
||||
this.entryForm.get('blending_lakeids')?.setValue('');
|
||||
|
||||
// Set default SureConnect value if available
|
||||
if (this.sureConnectList && this.sureConnectList.length > 0) {
|
||||
const defaultSureConnectId = this.sureConnectList[0].id;
|
||||
// Only set default if no value is already selected
|
||||
if (!this.entryForm.get('sure_connect_id')?.value) {
|
||||
this.entryForm.get('sure_connect_id')?.setValue(defaultSureConnectId);
|
||||
}
|
||||
} else {
|
||||
this.entryForm.get('sure_connect_id')?.setValue('');
|
||||
}
|
||||
}
|
||||
this.modalAdd = true;
|
||||
}
|
||||
submitted = false;
|
||||
onSubmit() {
|
||||
@@ -1290,4 +1368,433 @@ export class Data_lakeComponent implements OnInit {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Method to check if blending action should be available
|
||||
canShowBlendingAction(item: any): boolean {
|
||||
return item.datalake_type === 'blending' &&
|
||||
item.blending_lakeids &&
|
||||
item.blending_lakeids.trim() !== '';
|
||||
}
|
||||
|
||||
// Method to insert header into SQL query at cursor position
|
||||
insertHeaderIntoQuery(header: string) {
|
||||
const textarea = document.querySelector('.sql-textarea') as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const text = textarea.value;
|
||||
const before = text.substring(0, start);
|
||||
const after = text.substring(end, text.length);
|
||||
|
||||
// Insert the header at cursor position
|
||||
this.sqlQueryText = before + header + after;
|
||||
|
||||
// Set cursor position after the inserted text
|
||||
setTimeout(() => {
|
||||
textarea.selectionStart = start + header.length;
|
||||
textarea.selectionEnd = start + header.length;
|
||||
textarea.focus();
|
||||
}, 0);
|
||||
} else {
|
||||
// Fallback: append to end of text
|
||||
this.sqlQueryText += header;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to fetch blending keys
|
||||
fetchBlendingKeys(item: any) {
|
||||
this.selectedBlendingItem = item;
|
||||
this.blendingKeysData = [];
|
||||
this.showBlendingKeys = true;
|
||||
|
||||
// Initialize SQL query text from existing data
|
||||
if (item.sqlquery_json) {
|
||||
this.sqlQueryText = item.sqlquery_json;
|
||||
} else {
|
||||
this.sqlQueryText = '';
|
||||
}
|
||||
|
||||
// Parse blending_lakeids string to array
|
||||
const lakeIds = item.blending_lakeids.split(',').map(id => id.trim()).filter(id => id !== '');
|
||||
|
||||
// Fetch keys for each lake ID
|
||||
lakeIds.forEach(lakeId => {
|
||||
this.mainService.fetchBlendingKeys(Number(lakeId)).subscribe(
|
||||
(data: any) => {
|
||||
this.blendingKeysData.push({
|
||||
lakeId: lakeId,
|
||||
tableName: data.tableName,
|
||||
headers: data.headers
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching keys for lake ID:', lakeId, error);
|
||||
this.toastr.error(`Failed to fetch keys for Data Lake ${lakeId}`);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Method to update SQL query
|
||||
updateSqlQuery() {
|
||||
if (!this.selectedBlendingItem) {
|
||||
this.toastr.error('No blending item selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the SQL query JSON field
|
||||
this.selectedBlendingItem.sqlquery_json = this.sqlQueryText;
|
||||
|
||||
// Call update service
|
||||
this.mainService.update(this.selectedBlendingItem.id, this.selectedBlendingItem).subscribe(
|
||||
(response) => {
|
||||
this.toastr.success('SQL query updated successfully');
|
||||
// Update the local data
|
||||
const index = this.product.findIndex(p => p.id === this.selectedBlendingItem.id);
|
||||
if (index !== -1) {
|
||||
this.product[index] = {...this.selectedBlendingItem};
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error updating SQL query:', error);
|
||||
this.toastr.error('Failed to update SQL query');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Method to close blending keys panel
|
||||
closeBlendingKeys() {
|
||||
this.showBlendingKeys = false;
|
||||
this.blendingKeysData = [];
|
||||
this.sqlQueryText = '';
|
||||
this.selectedBlendingItem = null;
|
||||
}
|
||||
|
||||
// Method to handle checkbox change for blending lakes (ADD form)
|
||||
onBlendingLakeCheckboxChange(event: any, lakeId: number) {
|
||||
const lakeIdStr = lakeId.toString();
|
||||
if (event.target.checked) {
|
||||
// Add to selected blending lake IDs
|
||||
if (!this.selectedBlendingLakeIds.includes(lakeIdStr)) {
|
||||
this.selectedBlendingLakeIds.push(lakeIdStr);
|
||||
}
|
||||
} else {
|
||||
// Remove from selected blending lake IDs
|
||||
const index = this.selectedBlendingLakeIds.indexOf(lakeIdStr);
|
||||
if (index > -1) {
|
||||
this.selectedBlendingLakeIds.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the form control value as a comma-separated string
|
||||
this.entryForm.get('blending_lakeids')?.setValue(this.selectedBlendingLakeIds.join(','));
|
||||
}
|
||||
|
||||
// Method to check if a blending lake is selected (ADD form)
|
||||
isBlendingLakeSelected(lakeId: number): boolean {
|
||||
return this.selectedBlendingLakeIds.includes(lakeId.toString());
|
||||
}
|
||||
|
||||
// Method to handle checkbox change for blending lakes (EDIT form)
|
||||
onEditBlendingLakeCheckboxChange(event: any, lakeId: number) {
|
||||
const lakeIdStr = lakeId.toString();
|
||||
if (event.target.checked) {
|
||||
// Add to selected blending lake IDs
|
||||
if (!this.editSelectedBlendingLakeIds.includes(lakeIdStr)) {
|
||||
this.editSelectedBlendingLakeIds.push(lakeIdStr);
|
||||
}
|
||||
} else {
|
||||
// Remove from selected blending lake IDs
|
||||
const index = this.editSelectedBlendingLakeIds.indexOf(lakeIdStr);
|
||||
if (index > -1) {
|
||||
this.editSelectedBlendingLakeIds.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the rowSelected value as a comma-separated string
|
||||
this.rowSelected.blending_lakeids = this.editSelectedBlendingLakeIds.join(',');
|
||||
}
|
||||
|
||||
// Method to check if a blending lake is selected (EDIT form)
|
||||
isEditBlendingLakeSelected(lakeId: number): boolean {
|
||||
return this.editSelectedBlendingLakeIds.includes(lakeId.toString());
|
||||
}
|
||||
|
||||
// Method to open field mapping modal
|
||||
openFieldMappingModal(item: any) {
|
||||
this.selectedMappingItem = item;
|
||||
this.fieldMappingData = [];
|
||||
this.fieldMappings = {};
|
||||
this.showFieldMappingModal = true;
|
||||
|
||||
// Fetch available keys using the same API as calculated fields
|
||||
this.mainService.fetchAvailableKeys(item.url, item.sure_connect_id).subscribe(
|
||||
(keys: string[]) => {
|
||||
// Initialize field mappings with empty values
|
||||
this.fieldMappingData = keys.map(key => ({
|
||||
original: key,
|
||||
mapped: ''
|
||||
}));
|
||||
|
||||
// If there's existing mapping data, populate it
|
||||
if (item.mapping_json) {
|
||||
try {
|
||||
const existingMappings = JSON.parse(item.mapping_json);
|
||||
this.fieldMappingData.forEach(field => {
|
||||
if (existingMappings[field.original]) {
|
||||
field.mapped = existingMappings[field.original];
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error parsing existing mapping JSON:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching keys for field mapping:', error);
|
||||
this.toastr.error('Failed to fetch available keys for field mapping');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Method to update field mappings
|
||||
updateFieldMappings() {
|
||||
if (!this.selectedMappingItem) {
|
||||
this.toastr.error('No item selected for mapping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create mapping object
|
||||
const mappings: { [key: string]: string } = {};
|
||||
this.fieldMappingData.forEach(field => {
|
||||
if (field.mapped && field.mapped.trim() !== '') {
|
||||
mappings[field.original] = field.mapped.trim();
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string
|
||||
const mappingJson = JSON.stringify(mappings);
|
||||
|
||||
// Update the mapping JSON field
|
||||
this.selectedMappingItem.mapping_json = mappingJson;
|
||||
|
||||
// Call update service
|
||||
this.mainService.update(this.selectedMappingItem.id, this.selectedMappingItem).subscribe(
|
||||
(response) => {
|
||||
this.toastr.success('Field mappings updated successfully');
|
||||
// Update the local data
|
||||
const index = this.product.findIndex(p => p.id === this.selectedMappingItem.id);
|
||||
if (index !== -1) {
|
||||
this.product[index] = {...this.selectedMappingItem};
|
||||
}
|
||||
this.closeFieldMappingModal();
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error updating field mappings:', error);
|
||||
this.toastr.error('Failed to update field mappings');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Method to close field mapping modal
|
||||
closeFieldMappingModal() {
|
||||
this.showFieldMappingModal = false;
|
||||
this.fieldMappingData = [];
|
||||
this.fieldMappings = {};
|
||||
this.selectedMappingItem = null;
|
||||
}
|
||||
|
||||
// Method to update individual field mapping
|
||||
updateFieldMapping(index: number, value: string) {
|
||||
if (index >= 0 && index < this.fieldMappingData.length) {
|
||||
this.fieldMappingData[index].mapped = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Method to open scheduler modal
|
||||
openSchedulerModal(item: any) {
|
||||
this.selectedSchedulerItem = item;
|
||||
this.schedulerJob = null;
|
||||
this.showSchedulerModal = true;
|
||||
|
||||
// Fetch job by lake ID
|
||||
this.schedulerService.getJobByLakeId(item.id).subscribe(
|
||||
(job: any) => {
|
||||
this.schedulerJob = job;
|
||||
},
|
||||
(error) => {
|
||||
// If job not found, it's expected - we'll show create option
|
||||
if (error.status === 404) {
|
||||
this.schedulerJob = null;
|
||||
} else {
|
||||
console.error('Error fetching scheduler job:', error);
|
||||
this.toastr.error('Failed to fetch scheduler job information');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Method to create a new job
|
||||
createJob() {
|
||||
if (!this.selectedSchedulerItem) {
|
||||
this.toastr.error('No data lake selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const jobData = {
|
||||
name: this.selectedSchedulerItem.name,
|
||||
status: 'RUNNING',
|
||||
description: `Scheduled job for ${this.selectedSchedulerItem.name}`,
|
||||
jobType: 'DATALAKE',
|
||||
lakeid: this.selectedSchedulerItem.id
|
||||
};
|
||||
|
||||
this.schedulerService.createJob(jobData).subscribe(
|
||||
(job: any) => {
|
||||
this.schedulerJob = job;
|
||||
this.toastr.success('Job created successfully');
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error creating job:', error);
|
||||
this.toastr.error('Failed to create job');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Method to pause a job
|
||||
pauseJob() {
|
||||
if (!this.schedulerJob || !this.schedulerJob.id) {
|
||||
this.toastr.error('No job selected');
|
||||
return;
|
||||
}
|
||||
|
||||
this.schedulerService.pauseJob(this.schedulerJob.id).subscribe(
|
||||
(response: any) => {
|
||||
this.schedulerJob.status = 'PAUSED';
|
||||
this.toastr.success('Job paused successfully');
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error pausing job:', error);
|
||||
this.toastr.error('Failed to pause job');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Method to resume a job
|
||||
resumeJob() {
|
||||
if (!this.schedulerJob || !this.schedulerJob.id) {
|
||||
this.toastr.error('No job selected');
|
||||
return;
|
||||
}
|
||||
|
||||
this.schedulerService.resumeJob(this.schedulerJob.id).subscribe(
|
||||
(response: any) => {
|
||||
this.schedulerJob.status = 'RUNNING';
|
||||
this.toastr.success('Job resumed successfully');
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error resuming job:', error);
|
||||
this.toastr.error('Failed to resume job');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Method to stop a job
|
||||
stopJob() {
|
||||
if (!this.schedulerJob || !this.schedulerJob.id) {
|
||||
this.toastr.error('No job selected');
|
||||
return;
|
||||
}
|
||||
|
||||
this.schedulerService.stopJob(this.schedulerJob.id).subscribe(
|
||||
(response: any) => {
|
||||
this.schedulerJob.status = 'STOPPED';
|
||||
this.toastr.success('Job stopped successfully');
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error stopping job:', error);
|
||||
this.toastr.error('Failed to stop job');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Method to restart a stopped job
|
||||
restartJob() {
|
||||
if (!this.schedulerJob || !this.schedulerJob.id) {
|
||||
this.toastr.error('No job selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// To restart a job, we need to create a new job with the same parameters
|
||||
const jobData = {
|
||||
name: this.schedulerJob.name,
|
||||
status: 'RUNNING',
|
||||
description: this.schedulerJob.description,
|
||||
jobType: this.schedulerJob.jobType,
|
||||
lakeid: this.schedulerJob.lakeid
|
||||
};
|
||||
|
||||
this.schedulerService.createJob(jobData).subscribe(
|
||||
(job: any) => {
|
||||
this.schedulerJob = job;
|
||||
this.toastr.success('Job restarted successfully');
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error restarting job:', error);
|
||||
this.toastr.error('Failed to restart job');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Method to close scheduler modal
|
||||
closeSchedulerModal() {
|
||||
this.showSchedulerModal = false;
|
||||
this.schedulerJob = null;
|
||||
this.selectedSchedulerItem = null;
|
||||
}
|
||||
|
||||
// Method to get status badge class
|
||||
getStatusBadgeClass(status: string): string {
|
||||
switch (status) {
|
||||
case 'RUNNING':
|
||||
return 'badge-success';
|
||||
case 'PAUSED':
|
||||
return 'badge-warning';
|
||||
case 'STOPPED':
|
||||
return 'badge-danger';
|
||||
default:
|
||||
return 'badge-light';
|
||||
}
|
||||
}
|
||||
|
||||
// Method to toggle webhook for a data lake item
|
||||
toggleWebhook(item: any) {
|
||||
// Call the enableWebhook service method
|
||||
this.mainService.enableWebhook(item.id).subscribe(
|
||||
(updatedItem: any) => {
|
||||
// Update the local data with the response from the server
|
||||
const index = this.product.findIndex(p => p.id === item.id);
|
||||
if (index !== -1) {
|
||||
this.product[index] = {...updatedItem};
|
||||
}
|
||||
|
||||
// Show success message based on the new webhook status
|
||||
if (updatedItem.webhook_url && updatedItem.webhook_url.trim() !== '') {
|
||||
this.toastr.success('Webhook enabled successfully');
|
||||
} else {
|
||||
this.toastr.success('Webhook disabled successfully');
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error toggling webhook:', error);
|
||||
this.toastr.error('Failed to toggle webhook');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Method to copy webhook URL to clipboard
|
||||
copyWebhookUrl(webhookUrl: string) {
|
||||
this.copyToClipboard(webhookUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,12 @@ export class Data_lakeservice{
|
||||
return this.apiRequest.get(apiUrl);
|
||||
}
|
||||
|
||||
// Method to fetch blending keys for a specific lake ID
|
||||
fetchBlendingKeys(lakeId: number): Observable<any> {
|
||||
const _http = `${this.baseURL}/keys/${lakeId}`;
|
||||
return this.apiRequest.get(_http);
|
||||
}
|
||||
|
||||
// Method to update calculated fields for a data lake item
|
||||
updateCalculatedFields(id: number, calculatedFieldJson: string, isCalculatedField: boolean): Observable<any> {
|
||||
const _http = this.baseURL + "/" + id;
|
||||
@@ -51,5 +57,11 @@ export class Data_lakeservice{
|
||||
};
|
||||
return this.apiRequest.put(_http, data);
|
||||
}
|
||||
|
||||
// Method to enable webhook for a data lake item
|
||||
enableWebhook(id: number): Observable<any> {
|
||||
const _http = `${this.baseURL}/webhook/${id}`;
|
||||
return this.apiRequest.get(_http);
|
||||
}
|
||||
// updateaction
|
||||
}
|
||||
@@ -1,6 +1,15 @@
|
||||
<div class="cron-job-builder">
|
||||
<h4>Cron Job Builder</h4>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||
<div class="form-group">
|
||||
<label>Second</label>
|
||||
<select class="form-control" [(ngModel)]="second" (change)="onFieldChange()">
|
||||
<option *ngFor="let option of secondOptions" [value]="option.value">{{ option.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||
<div class="form-group">
|
||||
<label>Minute</label>
|
||||
@@ -28,7 +37,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-lg-3 clr-col-md-6 clr-col-sm-6 clr-col-12">
|
||||
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||
<div class="form-group">
|
||||
<label>Month</label>
|
||||
<select class="form-control" [(ngModel)]="month" (change)="onFieldChange()">
|
||||
@@ -37,7 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-lg-3 clr-col-md-6 clr-col-sm-12 clr-col-12">
|
||||
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||
<div class="form-group">
|
||||
<label>Day of Week</label>
|
||||
<select class="form-control" [(ngModel)]="dayOfWeek" (change)="onFieldChange()">
|
||||
|
||||
@@ -10,7 +10,8 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
||||
@Input() instanceId: string = ''; // Unique identifier for each instance
|
||||
@Output() cronExpressionChange = new EventEmitter<string>();
|
||||
|
||||
// Cron job fields
|
||||
// Cron job fields (now starting with seconds)
|
||||
second: string = '*';
|
||||
minute: string = '*';
|
||||
hour: string = '*';
|
||||
dayOfMonth: string = '*';
|
||||
@@ -21,6 +22,7 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
||||
cronDescription: string = '';
|
||||
|
||||
// Options for selectors
|
||||
secondOptions: any[] = [];
|
||||
minuteOptions: any[] = [];
|
||||
hourOptions: any[] = [];
|
||||
dayOfMonthOptions: any[] = [];
|
||||
@@ -29,9 +31,10 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
||||
|
||||
ngOnInit() {
|
||||
// Initialize options for each instance
|
||||
this.minuteOptions = this.generateOptions(0, 59);
|
||||
this.hourOptions = this.generateOptions(0, 23);
|
||||
this.dayOfMonthOptions = this.generateOptions(1, 31);
|
||||
this.secondOptions = this.generateOptions(0, 59, 'second');
|
||||
this.minuteOptions = this.generateOptions(0, 59, 'minute');
|
||||
this.hourOptions = this.generateOptions(0, 23, 'hour');
|
||||
this.dayOfMonthOptions = this.generateOptions(1, 31, 'day');
|
||||
this.monthOptions = [
|
||||
{ value: '*', label: 'Every month' },
|
||||
{ value: '1', label: 'January' },
|
||||
@@ -73,8 +76,8 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
generateOptions(start: number, end: number): any[] {
|
||||
const options = [{ value: '*', label: `Every (${start === 0 ? '0' : start}-${end})` }];
|
||||
generateOptions(start: number, end: number, type: string): any[] {
|
||||
const options = [{ value: '*', label: `Every ${type} (${start === 0 ? '0' : start}-${end})` }];
|
||||
for (let i = start; i <= end; i++) {
|
||||
options.push({ value: i.toString(), label: i.toString() });
|
||||
}
|
||||
@@ -83,7 +86,16 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
||||
|
||||
parseCronExpression(expression: string) {
|
||||
const parts = expression.split(' ');
|
||||
if (parts.length >= 5) {
|
||||
if (parts.length >= 6) { // Now expecting 6 parts with seconds
|
||||
this.second = parts[0];
|
||||
this.minute = parts[1];
|
||||
this.hour = parts[2];
|
||||
this.dayOfMonth = parts[3];
|
||||
this.month = parts[4];
|
||||
this.dayOfWeek = parts[5];
|
||||
} else if (parts.length >= 5) { // For backward compatibility with 5-part cron expressions
|
||||
// Default seconds to 0 if not provided
|
||||
this.second = '0';
|
||||
this.minute = parts[0];
|
||||
this.hour = parts[1];
|
||||
this.dayOfMonth = parts[2];
|
||||
@@ -95,19 +107,33 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
||||
generateDescription() {
|
||||
let description = 'Runs ';
|
||||
|
||||
// Second description
|
||||
if (this.second === '*') {
|
||||
description += 'every second';
|
||||
} else if (this.second.includes('/')) {
|
||||
const interval = this.second.split('/')[1];
|
||||
description += `every ${interval} seconds`;
|
||||
} else {
|
||||
description += `at second ${this.second}`;
|
||||
}
|
||||
|
||||
// Minute description
|
||||
if (this.minute === '*') {
|
||||
description += 'every minute';
|
||||
if (this.second === '*') {
|
||||
description += '';
|
||||
} else {
|
||||
description += ' of every minute';
|
||||
}
|
||||
} else if (this.minute.includes('/')) {
|
||||
const interval = this.minute.split('/')[1];
|
||||
description += `every ${interval} minutes`;
|
||||
description += ` of every ${interval} minutes`;
|
||||
} else {
|
||||
description += `at minute ${this.minute}`;
|
||||
description += ` at minute ${this.minute}`;
|
||||
}
|
||||
|
||||
// Hour description
|
||||
if (this.hour === '*') {
|
||||
if (this.minute === '*') {
|
||||
if (this.minute === '*' && this.second === '*') {
|
||||
description += '';
|
||||
} else {
|
||||
description += ' past every hour';
|
||||
@@ -146,21 +172,23 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
// Special case for simple patterns
|
||||
if (this.minute === '0' && this.hour === '0' && this.dayOfMonth === '1' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
if (this.second === '0' && this.minute === '0' && this.hour === '0' && this.dayOfMonth === '1' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
description = 'Runs at midnight on the first day of every month';
|
||||
} else if (this.minute === '0' && this.hour === '0' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
} else if (this.second === '0' && this.minute === '0' && this.hour === '0' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
description = 'Runs at midnight every day';
|
||||
} else if (this.minute === '0' && this.hour === '*' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
} else if (this.second === '0' && this.minute === '0' && this.hour === '*' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
description = 'Runs at the top of every hour';
|
||||
} else if (this.minute === '*' && this.hour === '*' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
description = 'Runs every minute';
|
||||
} else if (this.second === '0' && this.minute === '*' && this.hour === '*' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
description = 'Runs at the top of every minute';
|
||||
} else if (this.second === '*' && this.minute === '*' && this.hour === '*' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
description = 'Runs every second';
|
||||
}
|
||||
|
||||
this.cronDescription = description;
|
||||
}
|
||||
|
||||
buildCronExpression() {
|
||||
const expression = `${this.minute} ${this.hour} ${this.dayOfMonth} ${this.month} ${this.dayOfWeek}`;
|
||||
const expression = `${this.second} ${this.minute} ${this.hour} ${this.dayOfMonth} ${this.month} ${this.dayOfWeek}`;
|
||||
this.cronExpressionChange.emit(expression);
|
||||
this.generateDescription(); // Update description when expression changes
|
||||
return expression;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from "rxjs";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ApiRequestService } from "src/app/services/api/api-request.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataLakeSchedulerService {
|
||||
private baseURL = "scheduler";
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private apiRequest: ApiRequestService,
|
||||
) { }
|
||||
|
||||
// Get job by lake ID
|
||||
getJobByLakeId(lakeId: number): Observable<any> {
|
||||
const _http = `${this.baseURL}/lake/${lakeId}`;
|
||||
return this.apiRequest.get(_http);
|
||||
}
|
||||
|
||||
// Create a new job
|
||||
createJob(jobData: any): Observable<any> {
|
||||
const _http = `${this.baseURL}/create`;
|
||||
return this.apiRequest.post(_http, jobData);
|
||||
}
|
||||
|
||||
// Pause a job
|
||||
pauseJob(jobId: number): Observable<any> {
|
||||
const _http = `${this.baseURL}/pause/${jobId}`;
|
||||
return this.apiRequest.post(_http, {});
|
||||
}
|
||||
|
||||
// Resume a job
|
||||
resumeJob(jobId: number): Observable<any> {
|
||||
const _http = `${this.baseURL}/resume/${jobId}`;
|
||||
return this.apiRequest.post(_http, {});
|
||||
}
|
||||
|
||||
// Stop a job
|
||||
stopJob(jobId: number): Observable<any> {
|
||||
const _http = `${this.baseURL}/stop/${jobId}`;
|
||||
return this.apiRequest.delete(_http);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,67 @@
|
||||
<ol class="breadcrumb breadcrumb-arrow font-trirong">
|
||||
<li><a href="javascript://" [routerLink]="['/cns-portal/dashboard/order']"><clr-icon shape="home"></clr-icon></a></li>
|
||||
<li><a href="javascript://"><clr-icon shape="dashboard"></clr-icon> {{ 'Dashboard_builder' | translate }}</a></li>
|
||||
</ol>
|
||||
<li><a href="javascript://" [routerLink]="['/cns-portal/dashboard/order']"><clr-icon shape="home"></clr-icon></a></li>
|
||||
<li><a href="javascript://"><clr-icon shape="dashboard"></clr-icon> {{ 'Dashboard_builder' | translate }}</a></li>
|
||||
</ol>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div class="dg-wrapper">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<h3>{{ 'Dashboard_builder' | translate }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-4" style="text-align: right;">
|
||||
<button id="add" class="btn btn-primary" (click)="gotorunner()">
|
||||
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
|
||||
</button>
|
||||
<button class="btn btn-outline" (click)="onExport()">
|
||||
<clr-icon shape="export"></clr-icon> {{ 'EXPORT_XLSX' | translate }}
|
||||
</button>
|
||||
<button id="add" class="btn btn-primary" (click)="gotoadd()">
|
||||
<clr-icon shape="plus"></clr-icon> {{ 'ADD' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dg-wrapper">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">x
|
||||
<h3>{{ 'Dashboard_builder' | translate }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-4" style="text-align: right;">
|
||||
<button class="btn btn-success" [routerLink]="['/cns-portal/shield-dashboard']">
|
||||
<clr-icon shape="shield"></clr-icon>Shield Dashboard
|
||||
</button>
|
||||
<button id="add" class="btn btn-primary" (click)="gotorunner()">
|
||||
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
|
||||
</button>
|
||||
<!-- Add Chart Config button -->
|
||||
<button class="btn btn-primary" (click)="openChartConfig()">
|
||||
<clr-icon shape="cog"></clr-icon> Chart Config
|
||||
</button>
|
||||
<button class="btn btn-outline" (click)="onExport()">
|
||||
<clr-icon shape="export"></clr-icon> {{ 'EXPORT_XLSX' | translate }}
|
||||
</button>
|
||||
<button id="add" class="btn btn-primary" (click)="gotoadd()">
|
||||
<clr-icon shape="plus"></clr-icon> {{ 'ADD' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<clr-datagrid [clrDgLoading]="loading">
|
||||
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{ 'Loading' | translate }} ...... </clr-spinner></ng-template>
|
||||
<div *ngIf="error;else loadingSpinner">{{error}}</div></clr-dg-placeholder>
|
||||
<clr-datagrid [clrDgLoading]="loading">
|
||||
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{ 'Loading' | translate }} ......
|
||||
</clr-spinner></ng-template>
|
||||
<div *ngIf="error;else loadingSpinner">{{error}}</div>
|
||||
</clr-dg-placeholder>
|
||||
|
||||
<clr-dg-column [clrDgField]="''">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Go_to' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'dashboard_name'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Dashboard_Name' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'description'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Description' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column >
|
||||
<clr-dg-column [clrDgField]="'secuirity_profile'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Security_Profile' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column >
|
||||
<clr-dg-column [clrDgField]="'add_to_home'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Add_to_home' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column >
|
||||
<!-- <clr-dg-column [clrDgField]="'formType'">
|
||||
<clr-dg-column [clrDgField]="''">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Go_to' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'dashboard_name'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Dashboard_Name' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'description'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Description' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'secuirity_profile'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Security_Profile' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'add_to_home'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Add_to_home' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<!-- <clr-dg-column [clrDgField]="'formType'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
Build Status
|
||||
</ng-container>
|
||||
@@ -62,84 +71,86 @@
|
||||
Testing
|
||||
</ng-container>
|
||||
</clr-dg-column > -->
|
||||
<clr-dg-column [clrDgField]="'action'"> <ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Action' | translate }}
|
||||
</ng-container></clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'action'"> <ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Action' | translate }}
|
||||
</ng-container></clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let user of data?.slice()?.reverse();" [clrDgItem]="user">
|
||||
<clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;" (click)="goToEdit(user.id)"> {{ 'SET_UP' | translate }}</span></clr-dg-cell>
|
||||
<clr-dg-cell>{{user.dashboard_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.description}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.secuirity_profile}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.add_to_home}}</clr-dg-cell>
|
||||
<clr-dg-row *clrDgItems="let user of data?.slice()?.reverse();" [clrDgItem]="user">
|
||||
<clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;"
|
||||
(click)="goToEdit(user.id)"> {{ 'SET_UP' | translate }}</span></clr-dg-cell>
|
||||
<clr-dg-cell>{{user.dashboard_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.description}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.secuirity_profile}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.add_to_home}}</clr-dg-cell>
|
||||
|
||||
<!-- <clr-dg-cell><input type="radio" id="cb1" class="dots" [ngStyle]="{'background-color': user.build == true ? 'green' : 'red'}"></clr-dg-cell>
|
||||
<!-- <clr-dg-cell><input type="radio" id="cb1" class="dots" [ngStyle]="{'background-color': user.build == true ? 'green' : 'red'}"></clr-dg-cell>
|
||||
<clr-dg-cell>{{user.testing}}</clr-dg-cell> -->
|
||||
<clr-dg-cell>
|
||||
<!-- <span style="cursor: pointer; padding: 10px; "><clr-icon shape="form" (click)="goToEdit(user.id)" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span> -->
|
||||
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true" class="tooltip tooltip-sm tooltip-top-left">
|
||||
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error" style="color: red;"></clr-icon></span>
|
||||
<span class="tooltip-content">{{ 'Delete' | translate }}</span>
|
||||
</a>
|
||||
<clr-dg-cell>
|
||||
<!-- <span style="cursor: pointer; padding: 10px; "><clr-icon shape="form" (click)="goToEdit(user.id)" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span> -->
|
||||
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true"
|
||||
class="tooltip tooltip-sm tooltip-top-left">
|
||||
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error"
|
||||
style="color: red;"></clr-icon></span>
|
||||
<span class="tooltip-content">{{ 'Delete' | translate }}</span>
|
||||
</a>
|
||||
|
||||
<clr-signpost>
|
||||
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span>
|
||||
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
||||
<h5 style="margin-top: 0">{{ 'Who_Column' | translate }}</h5>
|
||||
<div>{{ 'Account_ID' | translate }}: <code class="clr-code">{{ user.accountId }}</code></div>
|
||||
<div>{{ 'Created_At' | translate }}: <code class="clr-code">{{ user.createdAt | date }}</code></div>
|
||||
<div>{{ 'Created_By' | translate }}: <code class="clr-code">{{ user.createdBy }}</code></div>
|
||||
<div>{{ 'Updated_At' | translate }}: <code class="clr-code">{{ user.updatedAt | date }}</code></div>
|
||||
<div>{{ 'Updated_By' | translate }}: <code class="clr-code">{{ user.updatedBy }}</code></div>
|
||||
</clr-signpost-content>
|
||||
</clr-signpost>
|
||||
<clr-signpost>
|
||||
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success"
|
||||
style="color: rgb(0, 130, 236);"></clr-icon></span>
|
||||
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
||||
<h5 style="margin-top: 0">{{ 'Who_Column' | translate }}</h5>
|
||||
<div>{{ 'Account_ID' | translate }}: <code class="clr-code">{{ user.accountId }}</code></div>
|
||||
<div>{{ 'Created_At' | translate }}: <code class="clr-code">{{ user.createdAt | date }}</code></div>
|
||||
<div>{{ 'Created_By' | translate }}: <code class="clr-code">{{ user.createdBy }}</code></div>
|
||||
<div>{{ 'Updated_At' | translate }}: <code class="clr-code">{{ user.updatedAt | date }}</code></div>
|
||||
<div>{{ 'Updated_By' | translate }}: <code class="clr-code">{{ user.updatedBy }}</code></div>
|
||||
</clr-signpost-content>
|
||||
</clr-signpost>
|
||||
|
||||
</clr-dg-cell>
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="goToEditData(user.id)">{{ 'Edit' | translate }} <clr-icon shape="edit" class="is-error"></clr-icon></button>
|
||||
<!-- <button class="action-item" (click)="onDelete(user)">Delete<clr-icon shape="trash" class="is-error"></clr-icon></button> -->
|
||||
</clr-dg-action-overflow>
|
||||
</clr-dg-row>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="goToEditData(user.id)">{{ 'Edit' | translate }} <clr-icon shape="edit"
|
||||
class="is-error"></clr-icon></button>
|
||||
<!-- <button class="action-item" (click)="onDelete(user)">Delete<clr-icon shape="trash" class="is-error"></clr-icon></button> -->
|
||||
</clr-dg-action-overflow>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">{{ 'Users_per_page' | translate }}</clr-dg-page-size>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
||||
{{ 'of' | translate }} {{pagination.totalItems}} {{ 'users' | translate }}
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">{{ 'Users_per_page' | translate }}</clr-dg-page-size>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
||||
{{ 'of' | translate }} {{pagination.totalItems}} {{ 'users' | translate }}
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<clr-modal [(clrModalOpen)]="addModall" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
||||
<div class="modal-body">
|
||||
<div class="s-order-dash-pg">
|
||||
<div class="chart-box" id="word1" (click)="gotoadd()"><br>
|
||||
<img style="margin: auto; display: block;" src="/assets/images/fromscratch.png" height="90" width="90">
|
||||
<h5 class="center"> <b>{{ 'Start_from_scratch' | translate }}</b> </h5>
|
||||
</div>
|
||||
<div class="chart-box" id="word1"><br>
|
||||
<img style="margin: auto; display: block;" src="/assets/images/copytemplate.png" height="90" width="90">
|
||||
<h5 class="center"> <b>{{ 'Import_from_template' | translate }}</b> </h5>
|
||||
</div>
|
||||
<div class="chart-box" id="word1"><br>
|
||||
<img style="margin: auto; display: block;" src="/assets/images/database.png" height="90" width="90">
|
||||
<h5 class="center"> <b>{{ 'Import_from_public_project' | translate }}</b> </h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<clr-modal [(clrModalOpen)]="addModall" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
||||
<div class="modal-body">
|
||||
<div class="s-order-dash-pg">
|
||||
<div class="chart-box" id="word1" (click)="gotoadd()"><br>
|
||||
<img style="margin: auto; display: block;" src="/assets/images/fromscratch.png" height="90" width="90">
|
||||
<h5 class="center"> <b>{{ 'Start_from_scratch' | translate }}</b> </h5>
|
||||
</div>
|
||||
<div class="chart-box" id="word1" ><br>
|
||||
<img style="margin: auto; display: block;" src="/assets/images/copytemplate.png" height="90" width="90">
|
||||
<h5 class="center"> <b>{{ 'Import_from_template' | translate }}</b> </h5>
|
||||
</div>
|
||||
<div class="chart-box" id="word1"><br>
|
||||
<img style="margin: auto; display: block;" src="/assets/images/database.png" height="90" width="90">
|
||||
<h5 class="center"> <b>{{ 'Import_from_public_project' | translate }}</b> </h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<clr-modal [(clrModalOpen)]="modalDelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
||||
<div class="modal-body" *ngIf="rowSelected.id">
|
||||
<h1 class="delete">{{ 'Are_you_sure_want_to_delete' | translate }}</h1>
|
||||
<h2 class="heading">{{rowSelected.id}}</h2>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="modalDelete = false">{{ 'Cancel' | translate }}</button>
|
||||
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary" >{{ 'Delete' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
|
||||
|
||||
<clr-modal [(clrModalOpen)]="modalDelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
||||
<div class="modal-body" *ngIf="rowSelected.id">
|
||||
<h1 class="delete">{{ 'Are_you_sure_want_to_delete' | translate }}</h1>
|
||||
<h2 class="heading">{{rowSelected.id}}</h2>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="modalDelete = false">{{ 'Cancel' | translate }}</button>
|
||||
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary">{{ 'Delete' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</clr-modal>
|
||||
@@ -24,6 +24,7 @@ export class AllnewdashComponent implements OnInit {
|
||||
projectname;
|
||||
projectId;
|
||||
error;
|
||||
chartConfigManagerOpen = false;
|
||||
constructor(
|
||||
private router : Router,
|
||||
private route: ActivatedRoute,private dashboardService : DashboardService,
|
||||
@@ -121,4 +122,9 @@ export class AllnewdashComponent implements OnInit {
|
||||
// this.router.navigate(['../editdashn'],{relativeTo:this.route});
|
||||
// }
|
||||
|
||||
// Add method to open chart configuration manager
|
||||
openChartConfig(): void {
|
||||
this.router.navigate(['../chart-types'],{relativeTo:this.route});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
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;
|
||||
}
|
||||
|
||||
console.log('Updating dynamic field:', this.selectedDynamicField);
|
||||
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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<!-- Configuration Mode -->
|
||||
<div class="compact-filter-config" *ngIf="isConfigMode">
|
||||
<div class="config-header">
|
||||
<h5>Compact Filter Configuration</h5>
|
||||
<button class="btn btn-sm btn-link" (click)="cancelConfiguration()">
|
||||
<clr-icon shape="close"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="config-form">
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">API URL</label>
|
||||
<input type="text" [(ngModel)]="configApiUrl" (ngModelChange)="onApiUrlChange($event)" placeholder="Enter API URL" class="clr-input">
|
||||
</div>
|
||||
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">Filter Key</label>
|
||||
<select [(ngModel)]="configFilterKey" (ngModelChange)="onFilterKeyChange($event)" class="clr-select">
|
||||
<option value="">Select a key</option>
|
||||
<option *ngFor="let key of availableKeys" [value]="key">{{ key }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">Filter Type</label>
|
||||
<select [(ngModel)]="configFilterType" (ngModelChange)="onFilterTypeChange($event)" class="clr-select">
|
||||
<option value="text">Text</option>
|
||||
<option value="dropdown">Dropdown</option>
|
||||
<option value="multiselect">Multi-Select</option>
|
||||
<option value="date-range">Date Range</option>
|
||||
<option value="toggle">Toggle</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Options will be automatically populated for dropdown/multiselect based on API data -->
|
||||
<div class="clr-form-control" *ngIf="configFilterType === 'dropdown' || configFilterType === 'multiselect'">
|
||||
<label class="clr-control-label">Available Values (comma separated)</label>
|
||||
<div class="available-values">
|
||||
{{ availableValues.join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="cancelConfiguration()">Cancel</button>
|
||||
<button class="btn btn-sm btn-primary" (click)="saveConfiguration()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Mode -->
|
||||
<div class="compact-filter" *ngIf="!isConfigMode">
|
||||
<div class="filter-header">
|
||||
<span class="filter-label" *ngIf="filterLabel">{{ filterLabel }}</span>
|
||||
<span class="filter-key" *ngIf="!filterLabel && filterKey">{{ filterKey }}</span>
|
||||
<span class="filter-type">({{ filterType }})</span>
|
||||
<button class="btn btn-icon btn-sm" (click)="toggleConfigMode()">
|
||||
<clr-icon shape="cog"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div class="filter-control" *ngIf="filterType === 'text'">
|
||||
<input type="text"
|
||||
[(ngModel)]="filterValue"
|
||||
(ngModelChange)="onFilterValueChange($event)"
|
||||
[placeholder]="filterLabel || filterKey"
|
||||
class="clr-input compact-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div class="filter-control" *ngIf="filterType === 'dropdown'">
|
||||
<select [(ngModel)]="filterValue"
|
||||
(ngModelChange)="onFilterValueChange($event)"
|
||||
class="clr-select compact-select">
|
||||
<option value="">{{ filterLabel || filterKey }}</option>
|
||||
<option *ngFor="let option of filterOptions" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter -->
|
||||
<div class="filter-control" *ngIf="filterType === 'multiselect'">
|
||||
<div class="compact-multiselect-display" (click)="toggleMultiselectDropdown()" style="padding: 5px; border: 1px solid #ddd; cursor: pointer; background-color: #f8f8f8;">
|
||||
<span *ngIf="filterValue && filterValue.length > 0">{{ filterValue.length }} selected</span>
|
||||
<span *ngIf="!filterValue || filterValue.length === 0">{{ filterLabel || filterKey || 'Select options' }}</span>
|
||||
<clr-icon shape="caret down" style="float: right; margin-top: 3px;"></clr-icon>
|
||||
</div>
|
||||
<div class="compact-multiselect-dropdown" *ngIf="showMultiselectDropdown" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; border-top: none; padding: 10px; background-color: white; position: absolute; z-index: 1000; width: calc(100% - 2px); box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div *ngFor="let option of filterOptions" class="clr-checkbox-wrapper" style="margin-bottom: 5px;">
|
||||
<input type="checkbox"
|
||||
[id]="'multiselect-' + option"
|
||||
[value]="option"
|
||||
[checked]="isOptionSelected(option)"
|
||||
(change)="onMultiselectOptionChange($event, option)">
|
||||
<label [for]="'multiselect-' + option" class="clr-control-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div class="filter-control date-range" *ngIf="filterType === 'date-range'">
|
||||
<input type="date"
|
||||
[(ngModel)]="filterValue.start"
|
||||
(ngModelChange)="onDateRangeChange({ start: $event, end: filterValue.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input compact-date">
|
||||
<input type="date"
|
||||
[(ngModel)]="filterValue.end"
|
||||
(ngModelChange)="onDateRangeChange({ start: filterValue.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input compact-date">
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div class="filter-control toggle" *ngIf="filterType === 'toggle'">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filterValue"
|
||||
(ngModelChange)="onToggleChange($event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filterLabel || filterKey }}</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,224 @@
|
||||
.compact-filter {
|
||||
display: inline-block;
|
||||
min-width: 120px;
|
||||
max-width: 200px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.filter-label, .filter-key {
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.filter-type {
|
||||
font-size: 10px;
|
||||
color: #6c757d;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 2px;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
&.date-range {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.toggle {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.compact-input,
|
||||
.compact-select,
|
||||
.compact-multiselect,
|
||||
.compact-date {
|
||||
width: 100%;
|
||||
padding: 4px 6px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.compact-select,
|
||||
.compact-multiselect {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.compact-multiselect {
|
||||
height: auto;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.clr-checkbox {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.clr-toggle {
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
min-width: 100px;
|
||||
max-width: 150px;
|
||||
|
||||
.compact-input,
|
||||
.compact-select,
|
||||
.compact-multiselect,
|
||||
.compact-date {
|
||||
font-size: 11px;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 11px;
|
||||
max-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
min-width: 80px;
|
||||
max-width: 120px;
|
||||
|
||||
.compact-input,
|
||||
.compact-select,
|
||||
.compact-multiselect,
|
||||
.compact-date {
|
||||
font-size: 10px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 10px;
|
||||
max-width: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.compact-filter-config {
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
padding: 2px;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.config-form {
|
||||
.clr-form-control {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.clr-control-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.clr-input,
|
||||
.clr-select,
|
||||
.clr-textarea {
|
||||
font-size: 12px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.clr-textarea {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.available-values {
|
||||
background: #e9ecef;
|
||||
border-radius: 3px;
|
||||
padding: 6px;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 5px;
|
||||
margin-top: 10px;
|
||||
|
||||
.btn {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnDestroy } from '@angular/core';
|
||||
import { FilterService, Filter } from './filter.service';
|
||||
import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-compact-filter',
|
||||
templateUrl: './compact-filter.component.html',
|
||||
styleUrls: ['./compact-filter.component.scss']
|
||||
})
|
||||
export class CompactFilterComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() filterKey: string = '';
|
||||
@Input() filterType: string = 'text';
|
||||
@Input() filterOptions: string[] = [];
|
||||
@Input() filterLabel: string = '';
|
||||
@Input() apiUrl: string = '';
|
||||
@Input() connectionId: number | undefined;
|
||||
@Output() filterChange = new EventEmitter<any>();
|
||||
@Output() configChange = new EventEmitter<any>();
|
||||
|
||||
selectedFilter: Filter | null = null;
|
||||
filterValue: any = '';
|
||||
availableFilters: Filter[] = [];
|
||||
availableKeys: string[] = [];
|
||||
availableValues: string[] = [];
|
||||
|
||||
// Multiselect dropdown state
|
||||
showMultiselectDropdown: boolean = false;
|
||||
|
||||
// Configuration properties
|
||||
isConfigMode: boolean = false;
|
||||
configFilterKey: string = '';
|
||||
configFilterType: string = 'text';
|
||||
configFilterOptions: string = '';
|
||||
configFilterLabel: string = '';
|
||||
configApiUrl: string = '';
|
||||
configConnectionId: number | undefined;
|
||||
|
||||
constructor(
|
||||
private filterService: FilterService,
|
||||
private alertService: AlertsService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Initialize configuration from inputs
|
||||
this.configFilterKey = this.filterKey;
|
||||
this.configFilterType = this.filterType;
|
||||
this.configFilterLabel = this.filterLabel;
|
||||
this.configFilterOptions = this.filterOptions.join(',');
|
||||
this.configApiUrl = this.apiUrl;
|
||||
this.configConnectionId = this.connectionId;
|
||||
|
||||
// Load available keys and values if API URL and filter key are provided
|
||||
if (this.apiUrl) {
|
||||
this.loadAvailableKeys();
|
||||
// Load available values for the current filter key if it's a dropdown or multiselect
|
||||
if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) {
|
||||
this.loadAvailableValues(this.filterKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Register this filter with the filter service
|
||||
this.registerFilter();
|
||||
|
||||
// Subscribe to filter definitions to get available filters
|
||||
this.filterService.filters$.subscribe(filters => {
|
||||
this.availableFilters = filters;
|
||||
this.updateSelectedFilter();
|
||||
});
|
||||
|
||||
// Subscribe to filter state changes
|
||||
this.filterService.filterState$.subscribe(state => {
|
||||
if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) {
|
||||
this.filterValue = state[this.selectedFilter.id];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// If filterKey changes, clear the previous filter value and remove old filter from service
|
||||
if (changes.filterKey) {
|
||||
// Clear the previous filter value
|
||||
this.filterValue = '';
|
||||
|
||||
// Clear filter options
|
||||
this.filterOptions = [];
|
||||
|
||||
// Clear available values
|
||||
this.availableValues = [];
|
||||
|
||||
// If we had a previous selected filter, clear its value in the service
|
||||
if (this.selectedFilter && changes.filterKey.previousValue) {
|
||||
const oldFilterId = changes.filterKey.previousValue;
|
||||
this.filterService.updateFilterValue(oldFilterId, '');
|
||||
}
|
||||
}
|
||||
|
||||
// If filterKey or filterType changes, re-register the filter
|
||||
if (changes.filterKey || changes.filterType) {
|
||||
// Load available values for the current filter key if it's a dropdown or multiselect
|
||||
if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) {
|
||||
this.loadAvailableValues(this.filterKey);
|
||||
}
|
||||
this.registerFilter();
|
||||
}
|
||||
|
||||
// Handle API URL changes
|
||||
if (changes.apiUrl && !changes.apiUrl.firstChange) {
|
||||
if (this.apiUrl) {
|
||||
this.loadAvailableKeys();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register this filter with the filter service
|
||||
registerFilter(): void {
|
||||
if (this.filterKey) {
|
||||
// Get current filter values from the service
|
||||
const currentFilterValues = this.filterService.getFilterValues();
|
||||
|
||||
// Create a filter definition for this compact filter
|
||||
const filterDef: Filter = {
|
||||
id: `${this.filterKey}`,
|
||||
field: this.filterKey,
|
||||
label: this.filterLabel || this.filterKey,
|
||||
type: this.filterType as any,
|
||||
options: this.filterOptions,
|
||||
value: this.filterValue // Use the current filter value
|
||||
};
|
||||
|
||||
// Get current filters
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
|
||||
// Check if this filter is already registered
|
||||
const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id);
|
||||
|
||||
if (existingFilterIndex >= 0) {
|
||||
// Preserve the existing filter configuration
|
||||
const existingFilter = currentFilters[existingFilterIndex];
|
||||
|
||||
// Preserve the existing filter value if it exists in the service
|
||||
if (currentFilterValues.hasOwnProperty(existingFilter.id)) {
|
||||
filterDef.value = currentFilterValues[existingFilter.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
} else if (existingFilter.value !== undefined) {
|
||||
// Fallback to existing filter's value if no service value
|
||||
filterDef.value = existingFilter.value;
|
||||
this.filterValue = filterDef.value;
|
||||
}
|
||||
|
||||
// Preserve other configuration properties
|
||||
filterDef.label = existingFilter.label;
|
||||
filterDef.options = existingFilter.options || this.filterOptions;
|
||||
|
||||
// Update existing filter
|
||||
currentFilters[existingFilterIndex] = filterDef;
|
||||
} else {
|
||||
// For new filters, check if there's already a value in the service
|
||||
if (currentFilterValues.hasOwnProperty(filterDef.id)) {
|
||||
filterDef.value = currentFilterValues[filterDef.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
}
|
||||
|
||||
// Add new filter
|
||||
currentFilters.push(filterDef);
|
||||
}
|
||||
|
||||
// Update the filter service with the new filter list
|
||||
this.filterService.setFilters(currentFilters);
|
||||
|
||||
// Update the selected filter reference
|
||||
this.selectedFilter = filterDef;
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedFilter(): void {
|
||||
if (this.filterKey && this.availableFilters.length > 0) {
|
||||
this.selectedFilter = this.availableFilters.find(f => f.field === this.filterKey) || null;
|
||||
if (this.selectedFilter) {
|
||||
// Get current value for this filter from the service
|
||||
const currentState = this.filterService.getFilterValues();
|
||||
const filterValue = currentState[this.selectedFilter.id];
|
||||
if (filterValue !== undefined) {
|
||||
this.filterValue = filterValue;
|
||||
} else if (this.selectedFilter.value !== undefined) {
|
||||
// Use the filter's default value if no service value
|
||||
this.filterValue = this.selectedFilter.value;
|
||||
} else {
|
||||
// Use the current filter value as fallback
|
||||
this.filterValue = this.filterValue || '';
|
||||
}
|
||||
|
||||
// Also update configuration properties from the selected filter
|
||||
this.configFilterKey = this.selectedFilter.field;
|
||||
this.configFilterType = this.selectedFilter.type;
|
||||
this.configFilterLabel = this.selectedFilter.label;
|
||||
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onFilterValueChange(value: any): void {
|
||||
if (this.selectedFilter) {
|
||||
this.filterValue = value;
|
||||
this.filterService.updateFilterValue(this.selectedFilter.id, value);
|
||||
this.filterChange.emit({ filterId: this.selectedFilter.id, value: value });
|
||||
|
||||
// Update the filter definition in the service to reflect the new value
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
const filterIndex = currentFilters.findIndex(f => f.id === this.selectedFilter.id);
|
||||
if (filterIndex >= 0) {
|
||||
currentFilters[filterIndex].value = value;
|
||||
this.filterService.setFilters(currentFilters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onToggleChange(checked: boolean): void {
|
||||
this.onFilterValueChange(checked);
|
||||
}
|
||||
|
||||
onDateRangeChange(dateRange: { start: string | null, end: string | null }): void {
|
||||
this.onFilterValueChange(dateRange);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Component cleanup - remove this filter from the filter service
|
||||
if (this.selectedFilter) {
|
||||
// Use the proper removeFilter method which handles both filter definition and state
|
||||
this.filterService.removeFilter(this.selectedFilter.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Load available keys from API
|
||||
loadAvailableKeys(): void {
|
||||
if (this.apiUrl) {
|
||||
this.alertService.getColumnfromurl(this.apiUrl, this.connectionId).subscribe(
|
||||
(keys: string[]) => {
|
||||
this.availableKeys = keys;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available keys:', error);
|
||||
this.availableKeys = [];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load available values for a specific key
|
||||
loadAvailableValues(key: string): void {
|
||||
if (this.apiUrl && key) {
|
||||
this.alertService.getValuesFromUrl(this.apiUrl, this.connectionId, key).subscribe(
|
||||
(values: string[]) => {
|
||||
this.availableValues = values;
|
||||
// Update filter options if this is a dropdown or multiselect
|
||||
if (this.filterType === 'dropdown' || this.filterType === 'multiselect') {
|
||||
this.filterOptions = values;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available values:', error);
|
||||
this.availableValues = [];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration methods
|
||||
toggleConfigMode(): void {
|
||||
this.isConfigMode = !this.isConfigMode;
|
||||
if (this.isConfigMode) {
|
||||
// Initialize config values from current filter if available
|
||||
if (this.selectedFilter) {
|
||||
this.configFilterKey = this.selectedFilter.field;
|
||||
this.configFilterType = this.selectedFilter.type;
|
||||
this.configFilterLabel = this.selectedFilter.label;
|
||||
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
|
||||
} else {
|
||||
// Fallback to current properties
|
||||
this.configFilterKey = this.filterKey;
|
||||
this.configFilterType = this.filterType;
|
||||
this.configFilterLabel = this.filterLabel;
|
||||
this.configFilterOptions = this.filterOptions.join(',');
|
||||
}
|
||||
this.configApiUrl = this.apiUrl;
|
||||
this.configConnectionId = this.connectionId;
|
||||
}
|
||||
}
|
||||
|
||||
saveConfiguration(): void {
|
||||
const config = {
|
||||
filterKey: this.configFilterKey,
|
||||
filterType: this.configFilterType,
|
||||
filterLabel: this.configFilterLabel,
|
||||
filterOptions: this.configFilterOptions.split(',').map(opt => opt.trim()).filter(opt => opt),
|
||||
apiUrl: this.configApiUrl,
|
||||
connectionId: this.configConnectionId
|
||||
};
|
||||
|
||||
// Emit configuration change
|
||||
this.configChange.emit(config);
|
||||
|
||||
// Update local properties
|
||||
this.filterKey = config.filterKey;
|
||||
this.filterType = config.filterType;
|
||||
this.filterLabel = config.filterLabel;
|
||||
this.filterOptions = config.filterOptions;
|
||||
this.apiUrl = config.apiUrl;
|
||||
this.connectionId = config.connectionId;
|
||||
|
||||
// Clear filter value when changing configuration
|
||||
this.filterValue = '';
|
||||
|
||||
// Load available keys if API URL is provided
|
||||
if (this.apiUrl) {
|
||||
this.loadAvailableKeys();
|
||||
}
|
||||
|
||||
// Load available values for the selected key if it's a dropdown or multiselect
|
||||
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && this.configFilterKey) {
|
||||
this.loadAvailableValues(this.configFilterKey);
|
||||
}
|
||||
|
||||
// Register the updated filter with the filter service
|
||||
this.registerFilter();
|
||||
|
||||
// Update selected filter
|
||||
this.updateSelectedFilter();
|
||||
|
||||
// Exit config mode
|
||||
this.isConfigMode = false;
|
||||
}
|
||||
|
||||
cancelConfiguration(): void {
|
||||
this.isConfigMode = false;
|
||||
}
|
||||
|
||||
// Handle filter key change in configuration
|
||||
onFilterKeyChange(key: string): void {
|
||||
// Clear the previous filter value when changing keys
|
||||
this.filterValue = '';
|
||||
|
||||
// Clear filter options until new values are loaded
|
||||
this.filterOptions = [];
|
||||
|
||||
this.configFilterKey = key;
|
||||
|
||||
// Load available values for the selected key if it's a dropdown or multiselect
|
||||
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && key) {
|
||||
this.loadAvailableValues(key);
|
||||
}
|
||||
|
||||
// Clear the filter service value for the previous filter key
|
||||
if (this.selectedFilter) {
|
||||
this.filterService.updateFilterValue(this.selectedFilter.id, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle API URL change in configuration
|
||||
onApiUrlChange(url: string): void {
|
||||
this.configApiUrl = url;
|
||||
// Load available keys when API URL changes
|
||||
if (url) {
|
||||
this.loadAvailableKeys();
|
||||
// Also clear available values since the API has changed
|
||||
this.availableValues = [];
|
||||
this.filterOptions = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle filter type change in configuration
|
||||
onFilterTypeChange(type: string): void {
|
||||
this.configFilterType = type;
|
||||
// If changing to dropdown or multiselect and we have a key selected, load values
|
||||
if ((type === 'dropdown' || type === 'multiselect') && this.configFilterKey) {
|
||||
this.loadAvailableValues(this.configFilterKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to check if an option is selected for checkboxes
|
||||
isOptionSelected(option: string): boolean {
|
||||
if (!this.filterValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure filterValue is an array for multiselect
|
||||
if (!Array.isArray(this.filterValue)) {
|
||||
this.filterValue = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.filterValue.includes(option);
|
||||
}
|
||||
// need to check this
|
||||
// Add method to handle multiselect option change
|
||||
onMultiselectOptionChange(event: any, option: string): void {
|
||||
// Initialize filterValue array if it doesn't exist
|
||||
if (!this.filterValue) {
|
||||
this.filterValue = [];
|
||||
}
|
||||
|
||||
// Ensure filterValue is an array
|
||||
if (!Array.isArray(this.filterValue)) {
|
||||
this.filterValue = [];
|
||||
}
|
||||
|
||||
if (event.target.checked) {
|
||||
// Add option if not already in array
|
||||
if (!this.filterValue.includes(option)) {
|
||||
this.filterValue.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
const index = this.filterValue.indexOf(option);
|
||||
if (index > -1) {
|
||||
this.filterValue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit the change event
|
||||
this.onFilterValueChange(this.filterValue);
|
||||
}
|
||||
|
||||
// Add method to toggle multiselect dropdown visibility
|
||||
toggleMultiselectDropdown(): void {
|
||||
this.showMultiselectDropdown = !this.showMultiselectDropdown;
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
if (this.showMultiselectDropdown) {
|
||||
setTimeout(() => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.compact-multiselect-display') && !target.closest('.compact-multiselect-dropdown')) {
|
||||
this.showMultiselectDropdown = false;
|
||||
document.removeEventListener('click', handleClick);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleClick);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './filter.service';
|
||||
export * from './common-filter.component';
|
||||
export * from './chart-wrapper.component';
|
||||
export * from './compact-filter.component';
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="chart-config-modal">
|
||||
<h3>Chart Configuration Manager</h3>
|
||||
<app-chart-config-manager></app-chart-config-manager>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
.chart-config-modal {
|
||||
padding: 20px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,32 +1,334 @@
|
||||
<div style="display: block; height: 100%; width: 100%;">
|
||||
<!-- No filter controls needed with the new simplified approach -->
|
||||
<!-- Filters are now configured at the drilldown level -->
|
||||
<div class="chart-container">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
||||
No data available
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="chart-header">
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Bar Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart display -->
|
||||
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
||||
<canvas baseChart
|
||||
[datasets]="barChartData"
|
||||
[labels]="barChartLabels"
|
||||
[type]="barChartType"
|
||||
[options]="barChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-content" [class.loading]="isLoading">
|
||||
|
||||
<div *ngIf="noDataAvailable" class="no-data-message">
|
||||
No data available
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="!noDataAvailable" class="chart-display">
|
||||
<canvas baseChart
|
||||
[datasets]="barChartData"
|
||||
[labels]="barChartLabels"
|
||||
[type]="barChartType"
|
||||
[options]="barChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
</div>
|
||||
|
||||
<div class="loading-overlay" *ngIf="isLoading">
|
||||
<div class="shimmer-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- sheield dashboard -->
|
||||
<!--
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>Deal Stage Wise Progress</h3>
|
||||
</div>
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-content" [class.loading]="isLoading">
|
||||
<canvas
|
||||
baseChart
|
||||
[data]="barChartData"
|
||||
[options]="barChartOptions"
|
||||
[type]="barChartType"
|
||||
(chartClick)="chartClicked($event)"
|
||||
(chartHover)="chartHovered($event)">
|
||||
</canvas>
|
||||
<div class="loading-overlay" *ngIf="isLoading">
|
||||
<div class="shimmer-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
|
||||
</div>
|
||||
@@ -1,31 +1,278 @@
|
||||
// Bar Chart Component Styles
|
||||
:host {
|
||||
display: block;
|
||||
// Chart container structure
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// Filter section styling
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// Chart header styling
|
||||
.chart-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chart wrapper and content
|
||||
.chart-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.chart-content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 300px; // Ensure minimum height for chart
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
|
||||
.chart-display {
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.chart-display {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
|
||||
.shimmer-bar {
|
||||
width: 80%;
|
||||
height: 20px;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar-chart-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
// Responsive design for chart container
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.bar-chart-container {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
.chart-container {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.bar-chart-container {
|
||||
height: 250px;
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
min-height: 250px; // Adjust for mobile
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,21 +57,46 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
maxRotation: 45,
|
||||
minRotation: 45
|
||||
minRotation: 45,
|
||||
padding: 15,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top',
|
||||
labels: {
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
bottom: 60,
|
||||
left: 15,
|
||||
right: 15,
|
||||
top: 15
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -84,12 +109,20 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// No data state
|
||||
noDataAvailable: boolean = false;
|
||||
|
||||
// Loading state
|
||||
isLoading: boolean = false;
|
||||
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
@@ -111,6 +144,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('BarChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -141,15 +180,329 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set loading state
|
||||
this.isLoading = true;
|
||||
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
|
||||
// If we're in drilldown mode, fetch the appropriate drilldown data
|
||||
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
|
||||
this.fetchDrilldownData();
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -173,73 +526,41 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
console.log('Bar chart data URL:', url);
|
||||
|
||||
// Convert baseFilters to filter parameters
|
||||
let filterParams = '';
|
||||
const filterObj = {};
|
||||
|
||||
// Add base filters
|
||||
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||
const filterObj = {};
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filters:', this.baseFilters);
|
||||
console.log('Base filter params:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
// Add common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
console.log('Filter definitions:', filterDefinitions);
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
console.log(`Processing filter ID: ${filterId}, Value:`, filterValue);
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
console.log(`Filter definition for ${filterId}:`, filterDef);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
console.log(`Mapping filter ID ${filterId} to field name: ${fieldName}`);
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
console.log(`Added to merged filters: ${fieldName} =`, filterValue);
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
console.log(`No field name found for filter ID ${filterId}, using ID as field name`);
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
console.log(`Added to merged filters: ${filterId} =`, filterValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('Final merged filter object:', filterParams);
|
||||
console.log('Final filter object:', filterObj);
|
||||
// Fetch data from the dashboard service with parameter field and value
|
||||
// For base level, we pass empty parameter and value, but now also pass filters
|
||||
const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
|
||||
@@ -251,8 +572,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.noDataAvailable = true;
|
||||
this.barChartLabels = [];
|
||||
this.barChartData = [];
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -281,8 +603,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.barChartLabels = [];
|
||||
this.barChartData = [];
|
||||
}
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('=== BAR CHART ERROR ===');
|
||||
@@ -290,8 +613,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.noDataAvailable = true;
|
||||
this.barChartLabels = [];
|
||||
this.barChartData = [];
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
// Keep default data in case of error
|
||||
}
|
||||
);
|
||||
@@ -303,8 +627,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.noDataAvailable = true;
|
||||
this.barChartLabels = [];
|
||||
this.barChartData = [];
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,61 +719,47 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
console.log('Drilldown data URL:', url);
|
||||
|
||||
// Convert drilldown layer filters to filter parameters (if applicable)
|
||||
let filterParams = '';
|
||||
const filterObj = {};
|
||||
|
||||
// Add drilldown layer filters
|
||||
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
|
||||
const filterObj = {};
|
||||
drilldownConfig.filters.forEach((filter: any) => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Drilldown layer filter parameters:', filterParams);
|
||||
|
||||
// Convert drilldownFilters to filter parameters for drilldown level
|
||||
let drilldownFilterParams = '';
|
||||
// Add drilldownFilters
|
||||
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
|
||||
const filterObj = {};
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
// Add common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Add drilldown filters first
|
||||
if (drilldownFilterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(drilldownFilterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
// Convert to JSON string for API call
|
||||
let drilldownFilterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('Drilldown filter parameters:', drilldownFilterParams);
|
||||
@@ -480,17 +791,23 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Trigger change detection
|
||||
// this.barChartData = [...this.barChartData];
|
||||
console.log('Updated bar chart with drilldown data:', { labels: this.barChartLabels, data: this.barChartData });
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.barChartLabels = data.labels;
|
||||
this.barChartData = data.datasets;
|
||||
console.log('Updated bar chart with drilldown legacy data format:', { labels: this.barChartLabels, data: this.barChartData });
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
} else {
|
||||
console.warn('Drilldown received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.barChartLabels = [];
|
||||
this.barChartData = [];
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
@@ -498,12 +815,17 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.noDataAvailable = true;
|
||||
this.barChartLabels = [];
|
||||
this.barChartData = [];
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
// Keep current data in case of error
|
||||
}
|
||||
);
|
||||
|
||||
// Add subscription to array for cleanup
|
||||
this.subscriptions.push(subscription);
|
||||
|
||||
// Set loading state
|
||||
this.isLoading = true;
|
||||
}
|
||||
|
||||
// Reset to original data (go back to base level)
|
||||
@@ -707,6 +1029,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.originalBarChartLabels = [];
|
||||
this.originalBarChartData = [];
|
||||
|
||||
// Clear multiselect tracking
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Remove document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
|
||||
console.log('BarChartComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,310 @@
|
||||
<div style="display:block">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<div style="display:block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
||||
No data available
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Bubble Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart display -->
|
||||
<div *ngIf="!noDataAvailable">
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart container -->
|
||||
<div style="position: relative; height: calc(100% - 80px); width: 100%; padding: 0 10px 30px 10px;">
|
||||
<!-- Loading indicator -->
|
||||
<div *ngIf="!dataLoaded" style="text-align: center; padding: 20px; color: #666; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; width: 100%;">
|
||||
Loading data...
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
<div *ngIf="dataLoaded && (noDataAvailable || !isChartDataValid())" style="text-align: center; padding: 20px; color: #666; font-style: italic; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; width: 100%;">
|
||||
No data available
|
||||
</div>
|
||||
|
||||
<!-- Chart display - Always render the canvas but conditionally show/hide with CSS -->
|
||||
<canvas baseChart
|
||||
[datasets]="bubbleChartData"
|
||||
[type]="bubbleChartType"
|
||||
[options]="bubbleChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
(chartClick)="chartClicked($event)"
|
||||
[style.visibility]="dataLoaded && !noDataAvailable && isChartDataValid() ? 'visible' : 'hidden'"
|
||||
[style.position]="'absolute'"
|
||||
[style.top]="'0'"
|
||||
[style.left]="'0'"
|
||||
[style.height]="'100%'"
|
||||
[style.width]="'100%'"
|
||||
[style.padding]="'0 10px 20px 10px'">
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,44 +1,315 @@
|
||||
<div class="doughnut-chart-container">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<div class="chart-container">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="chart-header">
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||
<div class="chart-wrapper">
|
||||
<!-- Show loading indicator -->
|
||||
<div class="loading-indicator" *ngIf="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading chart data...</p>
|
||||
</div>
|
||||
<div class="chart-content" [class.loading]="isLoading">
|
||||
<!-- Show no data message -->
|
||||
<div class="no-data-message" *ngIf="noDataAvailable && doughnutChartLabels.length === 0">
|
||||
<p>No chart data available</p>
|
||||
</div>
|
||||
|
||||
<!-- Show no data message -->
|
||||
<div class="no-data-message" *ngIf="noDataAvailable">
|
||||
<p>No chart data available</p>
|
||||
</div>
|
||||
<!-- Show chart when data is available -->
|
||||
<canvas baseChart
|
||||
[datasets]="doughnutChartData"
|
||||
[labels]="doughnutChartLabels"
|
||||
[type]="doughnutChartType"
|
||||
[options]="doughnutChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
|
||||
<!-- Show chart when data is available -->
|
||||
<canvas baseChart
|
||||
*ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0"
|
||||
[data]="doughnutChartData"
|
||||
[labels]="doughnutChartLabels"
|
||||
[type]="doughnutChartType"
|
||||
[options]="doughnutChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
<!-- Loading overlay -->
|
||||
<div class="loading-overlay" *ngIf="isLoading">
|
||||
<div class="shimmer-donut"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-legend" *ngIf="!noDataAvailable && showlabel && doughnutChartLabels && doughnutChartLabels.length > 0">
|
||||
|
||||
<div class="chart-legend" *ngIf="showlabel && doughnutChartLabels && doughnutChartLabels.length > 0">
|
||||
<div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index">
|
||||
<span class="legend-color" [style.background-color]="getLegendColor(i)"></span>
|
||||
<span class="legend-label">{{ label }}</span>
|
||||
<span class="legend-value">{{ doughnutChartData && doughnutChartData[i] !== undefined ? doughnutChartData[i] : 0 }}</span>
|
||||
<span class="legend-value">{{ doughnutChartData && doughnutChartData[0] && doughnutChartData[0].data && doughnutChartData[0].data[i] !== undefined ? doughnutChartData[0].data[i] : 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,184 +1,343 @@
|
||||
.doughnut-chart-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 400px;
|
||||
min-height: 400px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.doughnut-chart-container:hover {
|
||||
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.chart-title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #3498db;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 250px;
|
||||
margin: 15px 0;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-wrapper canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
|
||||
}
|
||||
|
||||
.chart-wrapper canvas:hover {
|
||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
|
||||
transform: scale(1.02);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #eaeaea;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.legend-item:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
border-color: #3498db;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
display: inline-block;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-right: 15px;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.legend-value {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #3498db;
|
||||
background: linear-gradient(135deg, #e9ecef 0%, #dde1e5 100%);
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-indicator, .no-data-message {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
font-style: italic;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// Chart container structure - simplified to match shield dashboard
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
}
|
||||
min-height: 400px; // Ensure minimum height
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.loading-indicator p, .no-data-message p {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.doughnut-chart-container {
|
||||
// Filter section styling
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 20px;
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
min-height: 200px;
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
justify-content: space-between;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
font-size: 16px;
|
||||
padding: 20px;
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// Chart header styling
|
||||
.chart-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a192f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chart wrapper and content - simplified to match shield dashboard
|
||||
.chart-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.chart-content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
|
||||
canvas {
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
max-height: calc(100% - 40px); // Leave space for legend
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
|
||||
.shimmer-donut {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chart legend - simplified
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e9ecef;
|
||||
min-width: 120px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.legend-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #3498db;
|
||||
min-width: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.chart-container {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.legend-item {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
min-height: 250px;
|
||||
|
||||
canvas {
|
||||
max-height: calc(100% - 60px); // More space for legend on mobile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
|
||||
templateUrl: './doughnut-chart.component.html',
|
||||
styleUrls: ['./doughnut-chart.component.scss']
|
||||
})
|
||||
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
|
||||
@Input() xAxis: string;
|
||||
@Input() yAxis: string | string[];
|
||||
@Input() table: string;
|
||||
@@ -36,7 +36,21 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
public doughnutChartLabels: string[] = ["Category A", "Category B", "Category C"];
|
||||
public doughnutChartData: number[] = [30, 50, 20];
|
||||
public doughnutChartData: any[] = [
|
||||
{
|
||||
data: [30, 50, 20],
|
||||
backgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
],
|
||||
hoverBackgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
]
|
||||
}
|
||||
];
|
||||
public doughnutChartType: string = "doughnut";
|
||||
public doughnutChartOptions: any = {
|
||||
responsive: true,
|
||||
@@ -72,6 +86,14 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -96,12 +118,20 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
// No data state
|
||||
noDataAvailable: boolean = false;
|
||||
|
||||
// Loading state
|
||||
isLoading: boolean = false;
|
||||
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
@@ -118,7 +148,10 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
|
||||
// Validate initial data
|
||||
this.validateChartData();
|
||||
this.fetchChartData();
|
||||
// Only fetch data if we have the required inputs, otherwise show default data
|
||||
if (this.table && this.xAxis && this.yAxis) {
|
||||
this.fetchChartData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,17 +171,33 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
|
||||
// Add default data to ensure chart visibility
|
||||
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
|
||||
this.doughnutChartData = [30, 50, 20];
|
||||
this.doughnutChartData = [
|
||||
{
|
||||
data: [30, 50, 20],
|
||||
backgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
],
|
||||
hoverBackgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Ensure we have matching arrays
|
||||
if (this.doughnutChartLabels.length !== this.doughnutChartData.length) {
|
||||
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length);
|
||||
if (this.doughnutChartLabels.length !== (this.doughnutChartData[0]?.data?.length || 0)) {
|
||||
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData[0]?.data?.length || 0);
|
||||
while (this.doughnutChartLabels.length < maxLength) {
|
||||
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
|
||||
}
|
||||
while (this.doughnutChartData.length < maxLength) {
|
||||
this.doughnutChartData.push(0);
|
||||
if (this.doughnutChartData[0]) {
|
||||
while (this.doughnutChartData[0].data.length < maxLength) {
|
||||
this.doughnutChartData[0].data.push(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +213,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('DoughnutChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -185,6 +240,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
console.log('Chart configuration changed, fetching new data');
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// If we have the required inputs and haven't fetched data yet, fetch it
|
||||
if ((xAxisChanged || yAxisChanged || tableChanged) && this.table && this.xAxis && this.yAxis && !this.isFetchingData) {
|
||||
console.log('Required inputs available, fetching data');
|
||||
this.fetchChartData();
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewChecked() {
|
||||
@@ -198,6 +259,318 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
@@ -206,14 +579,18 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set loading state
|
||||
this.isLoading = true;
|
||||
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
|
||||
// If we're in drilldown mode, fetch the appropriate drilldown data
|
||||
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
|
||||
this.fetchDrilldownData();
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -283,7 +660,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Doughnut chart data URL:', url);
|
||||
console.log('Chart data URL:', url);
|
||||
|
||||
// Fetch data from the dashboard service with parameter field and value
|
||||
// For base level, we pass empty parameter and value, but now also pass filters
|
||||
@@ -291,89 +668,113 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
(data: any) => {
|
||||
console.log('Received doughnut chart data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Doughnut chart API returned null data. Check if the API endpoint is working correctly.');
|
||||
console.warn('API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For doughnut charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the doughnut chart
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.doughnutChartLabels = data.chartLabels || [];
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.doughnutChartData = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
this.doughnutChartLabels = data.chartLabels;
|
||||
|
||||
// Handle different data structures
|
||||
let chartDataValues;
|
||||
if (Array.isArray(data.chartData)) {
|
||||
// If chartData is already an array of values
|
||||
if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
|
||||
chartDataValues = data.chartData;
|
||||
}
|
||||
// If chartData is an array with one object containing the data
|
||||
else if (data.chartData.length > 0 && data.chartData[0].data) {
|
||||
chartDataValues = data.chartData[0].data;
|
||||
}
|
||||
// Default case
|
||||
else {
|
||||
chartDataValues = data.chartData;
|
||||
}
|
||||
} else {
|
||||
this.doughnutChartData = [];
|
||||
chartDataValues = [data.chartData];
|
||||
}
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
// Trigger change detection
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
|
||||
this.doughnutChartData = [
|
||||
{
|
||||
data: chartDataValues,
|
||||
backgroundColor: this.chartColors.slice(0, chartDataValues.length),
|
||||
hoverBackgroundColor: this.chartColors.slice(0, chartDataValues.length)
|
||||
}
|
||||
];
|
||||
console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.doughnutChartLabels = data.labels || [];
|
||||
this.doughnutChartData = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
// Trigger change detection
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
this.doughnutChartLabels = data.labels;
|
||||
this.doughnutChartData = data.datasets;
|
||||
console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||
} else {
|
||||
console.warn('Doughnut chart received data does not have expected structure', data);
|
||||
// Reset to default data
|
||||
console.warn('Received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Keep default data instead of empty arrays
|
||||
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
|
||||
this.doughnutChartData = [
|
||||
{
|
||||
data: [30, 50, 20],
|
||||
backgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
],
|
||||
hoverBackgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
// Reset flag after fetching
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching doughnut chart data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Reset flag after fetching
|
||||
// Keep default data in case of error
|
||||
this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
|
||||
this.doughnutChartData = [
|
||||
{
|
||||
data: [30, 50, 20],
|
||||
backgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
],
|
||||
hoverBackgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56'
|
||||
]
|
||||
}
|
||||
];
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('Missing required data for doughnut chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
// Don't set noDataAvailable to true when there's no required data
|
||||
// This allows static data to be displayed
|
||||
this.noDataAvailable = false;
|
||||
// Validate the chart data to ensure we have some data to display
|
||||
this.validateChartData();
|
||||
// Force a redraw to ensure the chart displays
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
// Reset flag after fetching
|
||||
console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Reset flags after fetching
|
||||
this.isFetchingData = false;
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,6 +870,35 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
@@ -488,59 +918,66 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For doughnut charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the doughnut chart
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.doughnutChartLabels = data.chartLabels || [];
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.doughnutChartData = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
this.doughnutChartLabels = data.chartLabels;
|
||||
|
||||
// Handle different data structures
|
||||
let chartDataValues;
|
||||
if (Array.isArray(data.chartData)) {
|
||||
// If chartData is already an array of values
|
||||
if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
|
||||
chartDataValues = data.chartData;
|
||||
}
|
||||
// If chartData is an array with one object containing the data
|
||||
else if (data.chartData.length > 0 && data.chartData[0].data) {
|
||||
chartDataValues = data.chartData[0].data;
|
||||
}
|
||||
// Default case
|
||||
else {
|
||||
chartDataValues = data.chartData;
|
||||
}
|
||||
} else {
|
||||
this.doughnutChartData = [];
|
||||
chartDataValues = [data.chartData];
|
||||
}
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
// Trigger change detection
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
|
||||
this.doughnutChartData = [
|
||||
{
|
||||
data: chartDataValues,
|
||||
backgroundColor: this.chartColors.slice(0, chartDataValues.length),
|
||||
hoverBackgroundColor: this.chartColors.slice(0, chartDataValues.length)
|
||||
}
|
||||
];
|
||||
console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.doughnutChartLabels = data.labels || [];
|
||||
this.doughnutChartData = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
// Trigger change detection
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
this.doughnutChartLabels = data.labels;
|
||||
this.doughnutChartData = data.datasets;
|
||||
console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
} else {
|
||||
console.warn('Drilldown received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
// Keep current data instead of empty arrays
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching drilldown data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Keep current data in case of error
|
||||
// Set loading state to false
|
||||
this.isLoading = false;
|
||||
}
|
||||
);
|
||||
|
||||
// Set loading state
|
||||
this.isLoading = true;
|
||||
}
|
||||
|
||||
// Reset to original data (go back to base level)
|
||||
@@ -557,7 +994,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
console.log('Restored original labels');
|
||||
}
|
||||
if (this.originalDoughnutChartData.length > 0) {
|
||||
this.doughnutChartData = [...this.originalDoughnutChartData];
|
||||
this.doughnutChartData = JSON.parse(JSON.stringify(this.originalDoughnutChartData));
|
||||
console.log('Restored original data');
|
||||
}
|
||||
|
||||
@@ -599,44 +1036,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for legend item
|
||||
* @param index Index of the legend item
|
||||
*/
|
||||
public getLegendColor(index: number): string {
|
||||
// Get legend color for a specific index
|
||||
getLegendColor(index: number): string {
|
||||
return this.chartColors[index % this.chartColors.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure labels and data arrays have the same length
|
||||
*/
|
||||
private syncLabelAndDataArrays(): void {
|
||||
// Handle empty arrays
|
||||
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length);
|
||||
|
||||
// Pad the shorter array with default values
|
||||
while (this.doughnutChartLabels.length < maxLength) {
|
||||
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
|
||||
}
|
||||
|
||||
while (this.doughnutChartData.length < maxLength) {
|
||||
this.doughnutChartData.push(0);
|
||||
}
|
||||
|
||||
// Truncate the longer array if needed
|
||||
if (this.doughnutChartLabels.length > maxLength) {
|
||||
this.doughnutChartLabels = this.doughnutChartLabels.slice(0, maxLength);
|
||||
}
|
||||
|
||||
if (this.doughnutChartData.length > maxLength) {
|
||||
this.doughnutChartData = this.doughnutChartData.slice(0, maxLength);
|
||||
}
|
||||
}
|
||||
|
||||
// events
|
||||
public chartClicked(e: any): void {
|
||||
console.log('Doughnut chart clicked:', e);
|
||||
@@ -723,6 +1127,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Doughnut chart hovered:', e);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,285 @@
|
||||
<div class="dynamic-chart-container">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Dynamic Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing content -->
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { Component, OnInit, ViewChild, Input, OnChanges, SimpleChanges } from '@
|
||||
import { ChartConfiguration, ChartData, ChartDataset } from 'chart.js';
|
||||
import { BaseChartDirective } from 'ng2-charts';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dynamic-chart',
|
||||
@@ -37,9 +39,20 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
|
||||
@ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -47,6 +60,12 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('DynamicChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -107,6 +126,14 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -139,7 +166,49 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/dynamic?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -504,4 +573,322 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
this.dynamicChartData = _dynamicChartData;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
|
||||
// Remove document click handler if it exists
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,285 @@
|
||||
<div class="financial-chart-container">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Financial Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing content -->
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
|
||||
@@ -1,108 +1,192 @@
|
||||
.financial-chart-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 400px;
|
||||
min-height: 400px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.financial-chart-container:hover {
|
||||
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #3498db;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 250px;
|
||||
margin: 15px 0;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.chart-wrapper canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
|
||||
}
|
||||
|
||||
.chart-wrapper canvas:hover {
|
||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
|
||||
transform: scale(1.02);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-indicator, .no-data-message {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
font-style: italic;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-indicator p, .no-data-message p {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.financial-chart-container {
|
||||
padding: 15px;
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
font-size: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
font-size: 16px;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-financial-chart',
|
||||
@@ -33,9 +36,21 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService,
|
||||
private alertService: AlertsService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -43,6 +58,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('FinancialChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
// Load filter options for dropdown/multiselect filters
|
||||
this.loadFilterOptions();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -86,6 +109,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -118,7 +149,49 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/financial?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -496,4 +569,520 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
// Ensure filter has required properties
|
||||
if (!filter.type) filter.type = 'text';
|
||||
if (!filter.options) filter.options = '';
|
||||
if (!filter.availableValues) filter.availableValues = '';
|
||||
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
// Ensure filter has required properties
|
||||
if (!filter.type) filter.type = 'text';
|
||||
if (!filter.options) filter.options = '';
|
||||
if (!filter.availableValues) filter.availableValues = '';
|
||||
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
// Ensure filter has required properties
|
||||
if (!filter.type) filter.type = 'text';
|
||||
if (!filter.options) filter.options = '';
|
||||
if (!filter.availableValues) filter.availableValues = '';
|
||||
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
|
||||
// Remove document click handler if it exists
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
|
||||
// Load filter options for dropdown and multiselect filters
|
||||
private loadFilterOptions(): void {
|
||||
console.log('Loading filter options');
|
||||
|
||||
// Load options for base filters
|
||||
if (this.baseFilters && this.table) {
|
||||
this.baseFilters.forEach((filter, index) => {
|
||||
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
|
||||
this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load options for drilldown filters
|
||||
if (this.drilldownFilters && this.drilldownApiUrl) {
|
||||
this.drilldownFilters.forEach((filter, index) => {
|
||||
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
|
||||
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load options for layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach((layer, layerIndex) => {
|
||||
if (layer.filters && layer.apiUrl) {
|
||||
layer.filters.forEach((filter, filterIndex) => {
|
||||
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
|
||||
this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load filter values for a specific field
|
||||
private loadFilterValuesForField(
|
||||
apiUrl: string,
|
||||
connectionId: number | undefined,
|
||||
field: string,
|
||||
filterIndex: number,
|
||||
filterType: 'base' | 'drilldown' | 'layer',
|
||||
layerIndex?: number
|
||||
): void {
|
||||
if (apiUrl && field) {
|
||||
this.alertService.getValuesFromUrl(apiUrl, connectionId, field).subscribe(
|
||||
(values: string[]) => {
|
||||
console.log(`Loaded filter values for ${filterType} filter ${field}:`, values);
|
||||
|
||||
// Update the filter with available values
|
||||
if (filterType === 'base') {
|
||||
const filter = this.baseFilters[filterIndex];
|
||||
if (filter) {
|
||||
filter.availableValues = values.join(', ');
|
||||
// For dropdown/multiselect types, also update the options
|
||||
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
|
||||
filter.options = filter.availableValues;
|
||||
}
|
||||
}
|
||||
} else if (filterType === 'drilldown') {
|
||||
const filter = this.drilldownFilters[filterIndex];
|
||||
if (filter) {
|
||||
filter.availableValues = values.join(', ');
|
||||
// For dropdown/multiselect types, also update the options
|
||||
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
|
||||
filter.options = filter.availableValues;
|
||||
}
|
||||
}
|
||||
} else if (filterType === 'layer' && layerIndex !== undefined) {
|
||||
const layer = this.drilldownLayers[layerIndex];
|
||||
if (layer && layer.filters) {
|
||||
const filter = layer.filters[filterIndex];
|
||||
if (filter) {
|
||||
filter.availableValues = values.join(', ');
|
||||
// For dropdown/multiselect types, also update the options
|
||||
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
|
||||
filter.options = filter.availableValues;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available values for field:', field, error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle base filter field change
|
||||
onBaseFilterFieldChange(index: number, field: string): void {
|
||||
const filter = this.baseFilters[index];
|
||||
if (filter) {
|
||||
filter.field = field;
|
||||
// If field changes, reset value and options
|
||||
filter.value = '';
|
||||
filter.options = '';
|
||||
filter.availableValues = '';
|
||||
|
||||
// If we have a field and table URL, load available values
|
||||
if (field && this.table && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
|
||||
this.loadFilterValuesForField(this.table, this.connection, field, index, 'base');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle base filter type change
|
||||
onBaseFilterTypeChange(index: number, type: string): void {
|
||||
const filter = this.baseFilters[index];
|
||||
if (filter) {
|
||||
filter.type = type;
|
||||
// If type changes to dropdown/multiselect and we have a field, load available values
|
||||
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.table) {
|
||||
this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle drilldown filter field change
|
||||
onDrilldownFilterFieldChange(index: number, field: string): void {
|
||||
const filter = this.drilldownFilters[index];
|
||||
if (filter) {
|
||||
filter.field = field;
|
||||
// If field changes, reset value and options
|
||||
filter.value = '';
|
||||
filter.options = '';
|
||||
filter.availableValues = '';
|
||||
|
||||
// If we have a field and drilldown API URL, load available values
|
||||
if (field && this.drilldownApiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
|
||||
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, field, index, 'drilldown');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle drilldown filter type change
|
||||
onDrilldownFilterTypeChange(index: number, type: string): void {
|
||||
const filter = this.drilldownFilters[index];
|
||||
if (filter) {
|
||||
filter.type = type;
|
||||
// If type changes to dropdown/multiselect and we have a field, load available values
|
||||
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.drilldownApiUrl) {
|
||||
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle layer filter field change
|
||||
onLayerFilterFieldChange(layerIndex: number, filterIndex: number, field: string): void {
|
||||
const layer = this.drilldownLayers[layerIndex];
|
||||
if (layer && layer.filters) {
|
||||
const filter = layer.filters[filterIndex];
|
||||
if (filter) {
|
||||
filter.field = field;
|
||||
// If field changes, reset value and options
|
||||
filter.value = '';
|
||||
filter.options = '';
|
||||
filter.availableValues = '';
|
||||
|
||||
// If we have a field and layer API URL, load available values
|
||||
if (field && layer.apiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
|
||||
this.loadFilterValuesForField(layer.apiUrl, this.connection, field, filterIndex, 'layer', layerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle layer filter type change
|
||||
onLayerFilterTypeChange(layerIndex: number, filterIndex: number, type: string): void {
|
||||
const layer = this.drilldownLayers[layerIndex];
|
||||
if (layer && layer.filters) {
|
||||
const filter = layer.filters[filterIndex];
|
||||
if (filter) {
|
||||
filter.type = type;
|
||||
// If type changes to dropdown/multiselect and we have a field, load available values
|
||||
if ((type === 'dropdown' || type === 'multiselect') && filter.field && layer.apiUrl) {
|
||||
this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,289 @@
|
||||
<div style="display: block;">
|
||||
<div class="dg-wrapper">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<h3>{{charttitle || 'Data Grid'}}</h3>
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row">
|
||||
<!-- <div class="clr-col-8">
|
||||
<h3>{{charttitle || 'Data Grid'}}</h3>
|
||||
</div> -->
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<div class="clr-col-4" *ngIf="drilldownEnabled && drilldownStack.length > 0" style="text-align: right;">
|
||||
<button class="btn btn-sm btn-link" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedKey}} = {{drilldownStack[drilldownStack.length - 1].clickedValue}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<clr-datagrid [clrDgLoading]="loading">
|
||||
<clr-dg-placeholder> <ng-template #loadingSpinner><clr-spinner>Loading ... </clr-spinner></ng-template>
|
||||
<clr-dg-placeholder>
|
||||
<ng-template #loadingSpinner>
|
||||
<clr-spinner>Loading ... </clr-spinner>
|
||||
</ng-template>
|
||||
<div *ngIf="error;else loadingSpinner">{{error}}</div>
|
||||
</clr-dg-placeholder>
|
||||
|
||||
@@ -19,7 +296,7 @@
|
||||
|
||||
<clr-dg-row *clrDgItems="let item of givendata" [clrDgItem]="item">
|
||||
<!-- Dynamic cells based on response keys -->
|
||||
<clr-dg-cell *ngFor="let header of dynamicHeaders">
|
||||
<clr-dg-cell *ngFor="let header of dynamicHeaders" (click)="onRowClick(item, header.key)">
|
||||
{{item[header.key]}}
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
@@ -1,12 +1,180 @@
|
||||
@import '../../../../../../../styles1.scss';
|
||||
input.ng-invalid.ng-touched {
|
||||
border-color: red;
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.error_mess {
|
||||
color: red;
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
clr-datagrid{
|
||||
height: 400px; /* Adjust the height as needed */
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.dg-wrapper {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
clr-datagrid {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { UsergrpmaintainceService } from 'src/app/services/admin/usergrpmaintaince.service';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
// Add FilterService import
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
// Add Subscription import
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-grid-view',
|
||||
templateUrl: './grid-view.component.html',
|
||||
styleUrls: ['./grid-view.component.scss']
|
||||
})
|
||||
export class GridViewComponent implements OnInit, OnChanges {
|
||||
export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() xAxis: string;
|
||||
@Input() yAxis: string | string[];
|
||||
@Input() table: string;
|
||||
@@ -23,6 +27,16 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
@Input() datasource: string;
|
||||
@Input() fieldName: string;
|
||||
@Input() connection: number; // Add connection input
|
||||
// Drilldown configuration inputs
|
||||
@Input() drilldownEnabled: boolean = false;
|
||||
@Input() drilldownApiUrl: string;
|
||||
@Input() drilldownXAxis: string;
|
||||
@Input() drilldownYAxis: string;
|
||||
@Input() drilldownParameter: string; // Add drilldown parameter input
|
||||
@Input() baseFilters: any[] = []; // Add base filters input
|
||||
@Input() drilldownFilters: any[] = []; // Add drilldown filters input
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
loading = false;
|
||||
givendata: any[] = [];
|
||||
@@ -38,13 +52,46 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
submitted = false;
|
||||
dynamicHeaders: any[] = [];
|
||||
|
||||
constructor(
|
||||
// Multi-layer drilldown state tracking
|
||||
drilldownStack: any[] = []; // Stack to track drilldown navigation history
|
||||
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
|
||||
originalGridData: any[] = [];
|
||||
|
||||
// No data state
|
||||
noDataAvailable: boolean = false;
|
||||
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Add subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add a flag to track if filters have been initialized
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Add properties to track open multiselect dropdowns
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
|
||||
// Add property to track document click handler
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
|
||||
constructor(
|
||||
private mainservice: UsergrpmaintainceService,
|
||||
private dashboardService: Dashboard3Service,
|
||||
// Add FilterService to constructor
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the grid data
|
||||
console.log('GridView: Filter state changed:', filters);
|
||||
this.fetchGridData();
|
||||
})
|
||||
);
|
||||
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
@@ -55,33 +102,193 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
const tableChanged = changes.table && !changes.table.firstChange;
|
||||
const connectionChanged = changes.connection && !changes.connection.firstChange; // Add connection change detection
|
||||
const connectionChanged = changes.connection && !changes.connection.firstChange;
|
||||
const baseFiltersChanged = changes.baseFilters && !changes.baseFilters.firstChange;
|
||||
// Drilldown configuration changes
|
||||
const drilldownEnabledChanged = changes.drilldownEnabled && !changes.drilldownEnabled.firstChange;
|
||||
const drilldownApiUrlChanged = changes.drilldownApiUrl && !changes.drilldownApiUrl.firstChange;
|
||||
const drilldownXAxisChanged = changes.drilldownXAxis && !changes.drilldownXAxis.firstChange;
|
||||
const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange;
|
||||
const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.firstChange;
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Respond to input changes
|
||||
if (xAxisChanged || yAxisChanged || tableChanged || connectionChanged) {
|
||||
console.log('X or Y axis or table or connection changed, fetching new data');
|
||||
// Only fetch data if xAxis, yAxis, table, or connection has changed (and it's not the first change)
|
||||
if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
|
||||
drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
|
||||
drilldownLayersChanged)) {
|
||||
console.log('X or Y axis or table or connection or base filters or drilldown config changed, fetching new data');
|
||||
// Only fetch data if xAxis, yAxis, table, connection, baseFilters or drilldown config has changed (and it's not the first change)
|
||||
this.fetchGridData();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Dynamic headers for the grid
|
||||
|
||||
fetchGridData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
|
||||
// If we're in drilldown mode, fetch the appropriate drilldown data
|
||||
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
|
||||
this.fetchDrilldownData();
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have the necessary data, fetch grid data from the service
|
||||
if (this.table && this.xAxis) {
|
||||
console.log('Fetching grid data for:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
// if (this.table && this.xAxis) {
|
||||
if (this.table) {
|
||||
|
||||
console.log('=== GRID VIEW DEBUG INFO ===');
|
||||
console.log('Table:', this.table);
|
||||
console.log('X-Axis:', this.xAxis);
|
||||
console.log('Y-Axis:', this.yAxis);
|
||||
console.log('Connection:', this.connection);
|
||||
|
||||
// Convert yAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
|
||||
|
||||
// Get the parameter value from the drilldown stack for base level (should be empty)
|
||||
let parameterValue = '';
|
||||
|
||||
// Log the URL that will be called
|
||||
let url = `chart/getdashjson/grid?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Grid data URL:', url);
|
||||
|
||||
// Get filter parameters from base filters
|
||||
const filterObj = {};
|
||||
|
||||
// Add base filters
|
||||
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add common filters directly as key-value pairs
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
|
||||
// Add common filters using the field name as the key
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('GridView: Final filter object to send to API:', filterObj);
|
||||
|
||||
// Fetch data from the dashboard service, similar to other chart components
|
||||
this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection).subscribe(
|
||||
this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
|
||||
(data: any) => {
|
||||
console.log('=== GRID VIEW DATA RESPONSE ===');
|
||||
console.log('Received grid data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Grid API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.error = "No data Available";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,49 +297,387 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
this.givendata = data.chartData;
|
||||
this.extractDynamicHeaders(data.chartData);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with data:', this.givendata);
|
||||
} else if (data && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
this.givendata = data.data;
|
||||
this.extractDynamicHeaders(data.data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with legacy data format:', this.givendata);
|
||||
} else if (Array.isArray(data)) {
|
||||
// Handle case where data is directly an array
|
||||
this.givendata = data;
|
||||
this.extractDynamicHeaders(data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with array data:', this.givendata);
|
||||
} else {
|
||||
console.warn('Grid received data does not have expected structure', data);
|
||||
this.error = "No valid data received";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
}
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}, (error) => {
|
||||
console.log('Error fetching grid data:', error);
|
||||
this.error = "Server Error";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
});
|
||||
} else if (this.table) {
|
||||
console.log('Missing xAxis, falling back to default data fetching');
|
||||
// Fallback to default data fetching when only table is provided
|
||||
// Convert yAxis to string if it's an array
|
||||
// Convert yAxis to string if it's an array
|
||||
const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
|
||||
|
||||
// Fetch data from the dashboard service, similar to other chart components
|
||||
this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection).subscribe(
|
||||
(data: any) => {
|
||||
// this.mainservice.getAll().subscribe((data: any) => {
|
||||
console.log('recv data ', data);
|
||||
this.givendata = Array.isArray(data) ? data : [];
|
||||
this.extractDynamicHeaders(data);
|
||||
this.error = this.givendata && this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
}, (error) => {
|
||||
console.log(error);
|
||||
this.error = "Server Error";
|
||||
});
|
||||
// this.mainservice.getAll().subscribe((data: any) => {
|
||||
console.log('recv data ', data);
|
||||
this.givendata = Array.isArray(data) ? data : [];
|
||||
this.extractDynamicHeaders(data);
|
||||
this.error = this.givendata && this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata && this.givendata.length === 0;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}, (error) => {
|
||||
console.log(error);
|
||||
this.error = "Server Error";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
});
|
||||
} else {
|
||||
console.log('Missing required data for grid:', { table: this.table });
|
||||
this.error = "Table name is required";
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch drilldown data based on current drilldown level
|
||||
fetchDrilldownData(): void {
|
||||
console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
|
||||
console.log('Drilldown stack:', this.drilldownStack);
|
||||
|
||||
// Get the current drilldown configuration based on the current level
|
||||
let drilldownConfig;
|
||||
if (this.currentDrilldownLevel === 1) {
|
||||
// Base drilldown level
|
||||
drilldownConfig = {
|
||||
apiUrl: this.drilldownApiUrl,
|
||||
xAxis: this.drilldownXAxis,
|
||||
yAxis: this.drilldownYAxis,
|
||||
parameter: this.drilldownParameter
|
||||
};
|
||||
} else {
|
||||
// Multi-layer drilldown level
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
|
||||
drilldownConfig = this.drilldownLayers[layerIndex];
|
||||
} else {
|
||||
console.warn('Invalid drilldown layer index:', layerIndex);
|
||||
this.error = "Invalid drilldown configuration";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Drilldown config for level', this.currentDrilldownLevel, ':', drilldownConfig);
|
||||
|
||||
// Check if we have valid drilldown configuration
|
||||
if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
|
||||
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
|
||||
this.error = "Missing drilldown configuration";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the parameter value from the drilldown stack
|
||||
let parameterValue = '';
|
||||
if (this.drilldownStack.length > 0) {
|
||||
const lastEntry = this.drilldownStack[this.drilldownStack.length - 1];
|
||||
parameterValue = lastEntry.clickedValue || '';
|
||||
console.log('Parameter value from last click:', parameterValue);
|
||||
}
|
||||
|
||||
// Get the parameter field from drilldown config
|
||||
const parameterField = drilldownConfig.parameter || '';
|
||||
console.log('Parameter field:', parameterField);
|
||||
|
||||
console.log('Fetching drilldown data for level:', this.currentDrilldownLevel, {
|
||||
apiUrl: drilldownConfig.apiUrl,
|
||||
xAxis: drilldownConfig.xAxis,
|
||||
yAxis: drilldownConfig.yAxis,
|
||||
parameterField: parameterField,
|
||||
parameterValue: parameterValue,
|
||||
connection: this.connection
|
||||
});
|
||||
|
||||
// Build the actual API URL with parameter replacement
|
||||
let actualApiUrl = drilldownConfig.apiUrl;
|
||||
console.log('Original API URL:', actualApiUrl);
|
||||
console.log('Parameter value to use:', parameterValue);
|
||||
console.log('Parameter field:', parameterField);
|
||||
|
||||
// Check if the URL contains angle brackets for parameter replacement
|
||||
const hasAngleBrackets = /<[^>]+>/.test(actualApiUrl);
|
||||
|
||||
if (hasAngleBrackets && parameterValue) {
|
||||
// Replace angle brackets placeholder with actual value
|
||||
console.log('Replacing angle brackets with parameter value');
|
||||
const encodedValue = encodeURIComponent(parameterValue);
|
||||
actualApiUrl = actualApiUrl.replace(/<[^>]+>/g, encodedValue);
|
||||
console.log('URL after angle bracket replacement:', actualApiUrl);
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
let url = `chart/getdashjson/grid?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
if (parameterField && parameterValue) {
|
||||
url += `¶meter=${encodeURIComponent(parameterField)}¶meterValue=${encodeURIComponent(parameterValue)}`;
|
||||
}
|
||||
console.log('Drilldown data URL:', url);
|
||||
|
||||
// Convert drilldown layer filters to filter parameters (if applicable)
|
||||
const filterObj = {};
|
||||
|
||||
// Add drilldown layer filters
|
||||
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
|
||||
drilldownConfig.filters.forEach((filter: any) => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add drilldownFilters
|
||||
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
const filterDefinitions = this.filterService.getFilters();
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
filterObj[fieldName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to JSON string for API call
|
||||
let drilldownFilterParams = '';
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
|
||||
console.log('Drilldown filter parameters:', drilldownFilterParams);
|
||||
|
||||
// For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
|
||||
this.dashboardService.getChartData(
|
||||
drilldownConfig.apiUrl, 'grid',
|
||||
drilldownConfig.xAxis, drilldownConfig.yAxis,
|
||||
this.connection,
|
||||
parameterField, parameterValue,
|
||||
drilldownFilterParams
|
||||
).subscribe(
|
||||
(data: any) => {
|
||||
console.log('Received drilldown data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.error = "No data Available";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartData) {
|
||||
this.givendata = data.chartData;
|
||||
this.extractDynamicHeaders(data.chartData);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with drilldown data:', this.givendata);
|
||||
} else if (data && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
this.givendata = data.data;
|
||||
this.extractDynamicHeaders(data.data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with drilldown legacy data format:', this.givendata);
|
||||
} else if (Array.isArray(data)) {
|
||||
// Handle case where data is directly an array
|
||||
this.givendata = data;
|
||||
this.extractDynamicHeaders(data);
|
||||
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
|
||||
this.noDataAvailable = this.givendata.length === 0;
|
||||
console.log('Updated grid with drilldown array data:', this.givendata);
|
||||
} else {
|
||||
console.warn('Drilldown received data does not have expected structure', data);
|
||||
this.error = "No valid data received";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching drilldown data:', error);
|
||||
this.error = "Server Error";
|
||||
this.givendata = [];
|
||||
this.noDataAvailable = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Reset to original data (go back to base level)
|
||||
resetToOriginalData(): void {
|
||||
console.log('Resetting to original data');
|
||||
console.log('Current stack before reset:', this.drilldownStack);
|
||||
console.log('Current level before reset:', this.currentDrilldownLevel);
|
||||
|
||||
this.currentDrilldownLevel = 0;
|
||||
this.drilldownStack = [];
|
||||
|
||||
if (this.originalGridData.length > 0) {
|
||||
// Create a deep copy to avoid reference issues
|
||||
this.givendata = JSON.parse(JSON.stringify(this.originalGridData));
|
||||
this.extractDynamicHeaders(this.givendata);
|
||||
console.log('Restored original data');
|
||||
}
|
||||
|
||||
console.log('After reset - data:', this.givendata);
|
||||
|
||||
// Re-fetch original data
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Navigate back to previous drilldown level
|
||||
navigateBack(): void {
|
||||
console.log('Navigating back, current stack:', this.drilldownStack);
|
||||
console.log('Current level:', this.currentDrilldownLevel);
|
||||
|
||||
if (this.drilldownStack.length > 0) {
|
||||
// Remove the last entry from the stack
|
||||
const removedEntry = this.drilldownStack.pop();
|
||||
console.log('Removed entry from stack:', removedEntry);
|
||||
|
||||
// Update the current drilldown level
|
||||
this.currentDrilldownLevel = this.drilldownStack.length;
|
||||
console.log('New level after pop:', this.currentDrilldownLevel);
|
||||
console.log('Stack after pop:', this.drilldownStack);
|
||||
|
||||
if (this.drilldownStack.length > 0) {
|
||||
// Fetch data for the previous level
|
||||
console.log('Fetching data for previous level');
|
||||
this.fetchDrilldownData();
|
||||
} else {
|
||||
// Back to base level
|
||||
console.log('Back to base level, resetting to original data');
|
||||
this.resetToOriginalData();
|
||||
}
|
||||
} else {
|
||||
// Already at base level, reset to original data
|
||||
console.log('Already at base level, resetting to original data');
|
||||
this.resetToOriginalData();
|
||||
}
|
||||
}
|
||||
|
||||
// Method to handle grid row clicks for drilldown
|
||||
onRowClick(item: any, key: string): void {
|
||||
console.log('Grid row clicked:', { item, key });
|
||||
|
||||
// If drilldown is enabled
|
||||
if (this.drilldownEnabled) {
|
||||
// Get the value for the clicked key
|
||||
const clickedValue = item[key];
|
||||
|
||||
console.log('Clicked on row value:', { key, value: clickedValue });
|
||||
|
||||
// If we're not at the base level, store original data
|
||||
if (this.currentDrilldownLevel === 0) {
|
||||
// Store original data before entering drilldown mode
|
||||
// Create a deep copy to avoid reference issues
|
||||
this.originalGridData = JSON.parse(JSON.stringify(this.givendata));
|
||||
console.log('Stored original data for drilldown');
|
||||
}
|
||||
|
||||
// Determine the next drilldown level
|
||||
const nextDrilldownLevel = this.currentDrilldownLevel + 1;
|
||||
|
||||
console.log('Next drilldown level will be:', nextDrilldownLevel);
|
||||
|
||||
// Check if there's a drilldown configuration for this level
|
||||
let hasDrilldownConfig = false;
|
||||
let drilldownConfig;
|
||||
|
||||
if (nextDrilldownLevel === 1) {
|
||||
// Base drilldown level
|
||||
drilldownConfig = {
|
||||
apiUrl: this.drilldownApiUrl,
|
||||
xAxis: this.drilldownXAxis,
|
||||
yAxis: this.drilldownYAxis,
|
||||
parameter: this.drilldownParameter
|
||||
};
|
||||
hasDrilldownConfig = !!this.drilldownApiUrl && !!this.drilldownXAxis && !!this.drilldownYAxis;
|
||||
} else {
|
||||
// Multi-layer drilldown level
|
||||
const layerIndex = nextDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length) {
|
||||
drilldownConfig = this.drilldownLayers[layerIndex];
|
||||
hasDrilldownConfig = drilldownConfig.enabled &&
|
||||
!!drilldownConfig.apiUrl &&
|
||||
!!drilldownConfig.xAxis &&
|
||||
!!drilldownConfig.yAxis;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Drilldown config for next level:', drilldownConfig);
|
||||
console.log('Has drilldown config:', hasDrilldownConfig);
|
||||
|
||||
// If there's a drilldown configuration for the next level, proceed
|
||||
if (hasDrilldownConfig) {
|
||||
// Add this click to the drilldown stack
|
||||
const stackEntry = {
|
||||
level: nextDrilldownLevel,
|
||||
clickedKey: key,
|
||||
clickedValue: clickedValue
|
||||
};
|
||||
|
||||
this.drilldownStack.push(stackEntry);
|
||||
|
||||
console.log('Added to drilldown stack:', stackEntry);
|
||||
console.log('Current drilldown stack:', this.drilldownStack);
|
||||
|
||||
// Update the current drilldown level
|
||||
this.currentDrilldownLevel = nextDrilldownLevel;
|
||||
|
||||
console.log('Entering drilldown level:', this.currentDrilldownLevel);
|
||||
|
||||
// Fetch drilldown data for the new level
|
||||
this.fetchDrilldownData();
|
||||
} else {
|
||||
console.log('No drilldown configuration for level:', nextDrilldownLevel);
|
||||
}
|
||||
} else {
|
||||
console.log('Drilldown not enabled');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,4 +720,261 @@ export class GridViewComponent implements OnInit, OnChanges {
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, str => str.toUpperCase());
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchGridData();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
console.log('GridViewComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||
this.subscriptions.forEach(subscription => {
|
||||
if (subscription && !subscription.closed) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
this.subscriptions = [];
|
||||
|
||||
// Clear data to help with garbage collection
|
||||
this.givendata = [];
|
||||
this.dynamicHeaders = [];
|
||||
this.drilldownStack = [];
|
||||
this.originalGridData = [];
|
||||
|
||||
// Clear multiselect tracking
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Remove document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
|
||||
console.log('GridViewComponent destroyed and cleaned up');
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,282 @@
|
||||
<div style="display: block">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<div style="display: block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Line Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
@@ -16,12 +285,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Chart display -->
|
||||
<div *ngIf="!noDataAvailable">
|
||||
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
||||
<canvas baseChart
|
||||
[datasets]="lineChartData"
|
||||
[labels]="lineChartLabels"
|
||||
[options]="lineChartOptions"
|
||||
|
||||
[legend]="lineChartLegend"
|
||||
[type]="lineChartType"
|
||||
(chartHover)="chartHovered($event)"
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
|
||||
templateUrl: './line-chart.component.html',
|
||||
styleUrls: ['./line-chart.component.scss']
|
||||
})
|
||||
export class LineChartComponent implements OnInit, OnChanges {
|
||||
export class LineChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() xAxis: string;
|
||||
@Input() yAxis: string | string[];
|
||||
@Input() table: string;
|
||||
@@ -88,6 +88,11 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
@@ -109,6 +114,12 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('LineChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -140,6 +151,318 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
|
||||
@@ -1,19 +1,287 @@
|
||||
<div class="pie-chart-container">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||
<div class="chart-wrapper">
|
||||
<!-- Show loading indicator -->
|
||||
<div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable">
|
||||
<div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable && !isFetchingData">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading chart data...</p>
|
||||
</div>
|
||||
@@ -23,15 +291,15 @@
|
||||
<p>No chart data available</p>
|
||||
</div>
|
||||
|
||||
<!-- Show chart when data is available -->
|
||||
<!-- Show chart when data is available or show default data -->
|
||||
<canvas baseChart
|
||||
*ngIf="pieChartLabels.length > 0 && pieChartData.length > 0"
|
||||
[data]="pieChartData"
|
||||
[datasets]="pieChartDatasets"
|
||||
[labels]="pieChartLabels"
|
||||
[type]="pieChartType"
|
||||
[options]="pieChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
(chartClick)="chartClicked($event)"
|
||||
[style.display]="shouldShowChart() ? 'block' : 'none'">
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="chart-legend" *ngIf="showlabel && pieChartLabels && pieChartLabels.length > 0">
|
||||
|
||||
@@ -149,10 +149,192 @@
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
// Filter section styles
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.pie-chart-container {
|
||||
padding: 15px;
|
||||
height: auto;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
@@ -179,4 +361,18 @@
|
||||
font-size: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
|
||||
templateUrl: './pie-chart.component.html',
|
||||
styleUrls: ['./pie-chart.component.scss']
|
||||
})
|
||||
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
|
||||
@Input() xAxis: string;
|
||||
@Input() yAxis: string | string[];
|
||||
@Input() table: string;
|
||||
@@ -37,6 +37,12 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
|
||||
public pieChartLabels: string[] = ['Category A', 'Category B', 'Category C'];
|
||||
public pieChartData: number[] = [30, 50, 20];
|
||||
public pieChartDatasets: any[] = [
|
||||
{
|
||||
data: [30, 50, 20],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
public pieChartType: string = 'pie';
|
||||
public pieChartOptions: any = {
|
||||
responsive: true,
|
||||
@@ -96,11 +102,16 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
noDataAvailable: boolean = false;
|
||||
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
isFetchingData: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
@@ -127,12 +138,28 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
// Validate initial data
|
||||
this.validateChartData();
|
||||
this.fetchChartData();
|
||||
// Initialize datasets with default data
|
||||
this.pieChartDatasets = [
|
||||
{
|
||||
data: this.pieChartData,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
// Only fetch data if we have the required inputs, otherwise show default data
|
||||
if (this.table && this.xAxis && this.yAxis) {
|
||||
this.fetchChartData();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('PieChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -154,10 +181,328 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
console.log('Chart configuration changed, fetching new data');
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// If we have the required inputs and haven't fetched data yet, fetch it
|
||||
if ((xAxisChanged || yAxisChanged || tableChanged) && this.table && this.xAxis && this.yAxis && !this.isFetchingData) {
|
||||
console.log('Required inputs available, fetching data');
|
||||
this.fetchChartData();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
@@ -243,7 +588,7 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Pie chart data URL:', url);
|
||||
console.log('Chart data URL:', url);
|
||||
|
||||
// Fetch data from the dashboard service with parameter field and value
|
||||
// For base level, we pass empty parameter and value, but now also pass filters
|
||||
@@ -251,12 +596,8 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
(data: any) => {
|
||||
console.log('Received pie chart data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Pie chart API returned null data. Check if the API endpoint is working correctly.');
|
||||
console.warn('API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
@@ -264,50 +605,57 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For pie charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the pie chart
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.pieChartLabels = data.chartLabels || [];
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.pieChartData = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
this.pieChartLabels = data.chartLabels;
|
||||
|
||||
// Extract the actual data values from the chartData array
|
||||
// chartData is an array with one object containing the data
|
||||
if (data.chartData.length > 0 && data.chartData[0].data) {
|
||||
this.pieChartData = data.chartData[0].data;
|
||||
} else {
|
||||
this.pieChartData = [];
|
||||
}
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
|
||||
// Trigger change detection
|
||||
this.pieChartLabels = [...this.pieChartLabels];
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
this.pieChartDatasets = [
|
||||
{
|
||||
data: this.pieChartData,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.pieChartLabels = data.labels || [];
|
||||
this.pieChartData = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
this.pieChartLabels = data.labels;
|
||||
this.pieChartData = data.datasets[0]?.data || [];
|
||||
// Trigger change detection
|
||||
this.pieChartLabels = [...this.pieChartLabels];
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
this.pieChartDatasets = [
|
||||
{
|
||||
data: this.pieChartData,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
} else {
|
||||
console.warn('Pie chart received data does not have expected structure', data);
|
||||
// Reset to default data
|
||||
console.warn('Received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Keep default data if no data is available
|
||||
if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
|
||||
this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
|
||||
this.pieChartData = [30, 50, 20];
|
||||
this.pieChartDatasets = [
|
||||
{
|
||||
data: this.pieChartData,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
@@ -315,23 +663,13 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
(error) => {
|
||||
console.error('Error fetching pie chart data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('Missing required data for pie chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
// Don't set noDataAvailable to true when there's no required data
|
||||
// This allows static data to be displayed
|
||||
this.noDataAvailable = false;
|
||||
// Validate the chart data to ensure we have some data to display
|
||||
this.validateChartData();
|
||||
// Force a redraw to ensure the chart displays
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
this.noDataAvailable = true;
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}
|
||||
@@ -360,8 +698,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
} else {
|
||||
console.warn('Invalid drilldown layer index:', layerIndex);
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -372,8 +708,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
|
||||
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -470,64 +804,57 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
if (data === null) {
|
||||
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For pie charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the pie chart
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.pieChartLabels = data.chartLabels || [];
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.pieChartData = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
this.pieChartLabels = data.chartLabels;
|
||||
|
||||
// Extract the actual data values from the chartData array
|
||||
// chartData is an array with one object containing the data
|
||||
if (data.chartData.length > 0 && data.chartData[0].data) {
|
||||
this.pieChartData = data.chartData[0].data;
|
||||
} else {
|
||||
this.pieChartData = [];
|
||||
}
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
|
||||
// Trigger change detection
|
||||
this.pieChartLabels = [...this.pieChartLabels];
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
this.pieChartDatasets = [
|
||||
{
|
||||
data: this.pieChartData,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.pieChartLabels = data.labels || [];
|
||||
this.pieChartData = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
this.pieChartLabels = data.labels;
|
||||
this.pieChartData = data.datasets[0]?.data || [];
|
||||
// Trigger change detection
|
||||
this.pieChartLabels = [...this.pieChartLabels];
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
this.pieChartDatasets = [
|
||||
{
|
||||
data: this.pieChartData,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
} else {
|
||||
console.warn('Drilldown received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
// Keep current data if no data is available
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching drilldown data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Keep current data in case of error
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -588,82 +915,52 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for legend item
|
||||
* @param index Index of the legend item
|
||||
*/
|
||||
public getLegendColor(index: number): string {
|
||||
// Validate chart data to ensure labels and data arrays have the same length
|
||||
private validateChartData(): void {
|
||||
if (this.pieChartLabels && this.pieChartData) {
|
||||
// For pie charts, we need to ensure labels and data arrays have the same length
|
||||
const labelCount = this.pieChartLabels.length;
|
||||
const dataCount = this.pieChartData.length;
|
||||
|
||||
if (labelCount !== dataCount) {
|
||||
console.warn('Pie chart labels and data arrays have different lengths:', { labels: labelCount, data: dataCount });
|
||||
// Pad or truncate data array to match label count
|
||||
if (dataCount < labelCount) {
|
||||
// Pad with zeros
|
||||
while (this.pieChartData.length < labelCount) {
|
||||
this.pieChartData.push(0);
|
||||
}
|
||||
} else if (dataCount > labelCount) {
|
||||
// Truncate data array
|
||||
this.pieChartData = this.pieChartData.slice(0, labelCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get legend color for a specific index
|
||||
getLegendColor(index: number): string {
|
||||
return this.chartColors[index % this.chartColors.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure labels and data arrays have the same length
|
||||
*/
|
||||
private syncLabelAndDataArrays(): void {
|
||||
// Ensure we have matching arrays
|
||||
if (this.pieChartLabels.length !== this.pieChartData.length) {
|
||||
const maxLength = Math.max(this.pieChartLabels.length, this.pieChartData.length);
|
||||
while (this.pieChartLabels.length < maxLength) {
|
||||
this.pieChartLabels.push(`Label ${this.pieChartLabels.length + 1}`);
|
||||
}
|
||||
while (this.pieChartData.length < maxLength) {
|
||||
this.pieChartData.push(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and sanitize chart data
|
||||
*/
|
||||
private validateChartData(): void {
|
||||
console.log('Validating chart data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
|
||||
// Ensure we have valid arrays
|
||||
if (!Array.isArray(this.pieChartLabels)) {
|
||||
this.pieChartLabels = [];
|
||||
// Method to determine if chart should be displayed
|
||||
shouldShowChart(): boolean {
|
||||
// Show chart if we have data
|
||||
if (this.pieChartLabels.length > 0 && this.pieChartData.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Array.isArray(this.pieChartData)) {
|
||||
this.pieChartData = [];
|
||||
// Show chart if we're still fetching data
|
||||
if (this.isFetchingData) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure we have some data to display
|
||||
if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
|
||||
// Add default data to ensure chart visibility
|
||||
this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
|
||||
this.pieChartData = [30, 50, 20];
|
||||
console.log('Added default data for chart display');
|
||||
// Show chart if we have default data
|
||||
if (this.pieChartLabels.length > 0 && this.originalPieChartLabels.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
|
||||
// Ensure all data values are numbers
|
||||
this.pieChartData = this.pieChartData.map(value => {
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
|
||||
console.log('After validation:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
}
|
||||
|
||||
ngAfterViewChecked() {
|
||||
// Debugging: Log component state after view checks
|
||||
console.log('PieChartComponent state:', {
|
||||
labels: this.pieChartLabels,
|
||||
data: this.pieChartData,
|
||||
hasData: this.pieChartLabels.length > 0 && this.pieChartData.length > 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if chart data is valid and ready to display
|
||||
*/
|
||||
public isChartDataValid(): boolean {
|
||||
return this.pieChartLabels && this.pieChartData &&
|
||||
Array.isArray(this.pieChartLabels) && Array.isArray(this.pieChartData) &&
|
||||
this.pieChartLabels.length > 0 && this.pieChartData.length > 0 &&
|
||||
this.pieChartLabels.length === this.pieChartData.length;
|
||||
return false;
|
||||
}
|
||||
|
||||
// events
|
||||
@@ -752,6 +1049,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Pie chart hovered:', e);
|
||||
}
|
||||
|
||||
ngAfterViewChecked(): void {
|
||||
// This lifecycle hook can be used if needed for post-render operations
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,292 @@
|
||||
<div style="display: block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<div style="display: block">
|
||||
<canvas baseChart
|
||||
[datasets]="polarAreaChartData"
|
||||
[labels]="polarAreaChartLabels"
|
||||
[type]="polarAreaChartType"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Polar Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position: relative; height: calc(100% - 80px); padding: 0 10px 30px 10px;">
|
||||
<canvas baseChart
|
||||
[datasets]="polarAreaChartData"
|
||||
[labels]="polarAreaChartLabels"
|
||||
[options]="polarAreaChartOptions"
|
||||
[type]="polarAreaChartType"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,18 +1,192 @@
|
||||
// Polar Chart Component Styles
|
||||
div[style*="display: block"] {
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the chart container has proper sizing
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-polar-chart',
|
||||
@@ -33,9 +35,20 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -43,6 +56,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('PolarChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -71,6 +90,32 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
{ data: [ 300, 500, 100, 40, 120 ], label: 'Series 1'}
|
||||
];
|
||||
|
||||
public polarAreaChartOptions: any = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 10,
|
||||
bottom: 30
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
r: {
|
||||
ticks: {
|
||||
backdropColor: 'rgba(0, 0, 0, 0)'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public polarAreaChartType: string = 'polarArea';
|
||||
|
||||
// Multi-layer drilldown state tracking
|
||||
@@ -85,6 +130,324 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -117,7 +480,49 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -132,7 +537,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.warn('Polar chart API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
@@ -145,32 +555,54 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.polarAreaChartLabels = data.chartLabels;
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.polarAreaChartData = data.chartData[0].data.map(value => {
|
||||
// Convert the data to the expected format for polar area charts
|
||||
const chartValues = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
return isNaN(Number(value)) ? 0 : Number(value);
|
||||
});
|
||||
// Assign data in the correct format (array of objects with data property)
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: chartValues,
|
||||
label: data.chartData[0].label || 'Dataset 1'
|
||||
}
|
||||
];
|
||||
} else {
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
}
|
||||
// Trigger change detection
|
||||
this.polarAreaChartData = [...this.polarAreaChartData];
|
||||
console.log('Updated polar chart with data:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.polarAreaChartLabels = data.labels;
|
||||
this.polarAreaChartData = data.data.map(value => {
|
||||
// Convert the data to the expected format for polar area charts
|
||||
const chartValues = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
return isNaN(Number(value)) ? 0 : Number(value);
|
||||
});
|
||||
// Trigger change detection
|
||||
this.polarAreaChartData = [...this.polarAreaChartData];
|
||||
// Assign data in the correct format (array of objects with data property)
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: chartValues,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
console.log('Updated polar chart with legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
|
||||
} else {
|
||||
console.warn('Polar chart received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
}
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
@@ -179,7 +611,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.error('Error fetching polar chart data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
// Keep default data in case of error
|
||||
@@ -189,7 +626,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.log('Missing required data for polar chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}
|
||||
@@ -219,7 +661,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.warn('Invalid drilldown layer index:', layerIndex);
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -231,7 +678,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -287,6 +739,35 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
@@ -300,23 +781,40 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For polar charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the polar chart
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.polarAreaChartLabels = data.chartLabels;
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.polarAreaChartData = data.chartData[0].data.map(value => {
|
||||
// Convert the data to the expected format for polar area charts
|
||||
const chartValues = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
return isNaN(Number(value)) ? 0 : Number(value);
|
||||
});
|
||||
// Assign data in the correct format (array of objects with data property)
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: chartValues,
|
||||
label: data.chartData[0].label || 'Dataset 1'
|
||||
}
|
||||
];
|
||||
} else {
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
}
|
||||
// Trigger change detection
|
||||
this.polarAreaChartData = [...this.polarAreaChartData];
|
||||
@@ -325,10 +823,18 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
// Handle the original expected format as fallback
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.polarAreaChartLabels = data.labels;
|
||||
this.polarAreaChartData = data.data.map(value => {
|
||||
// Convert the data to the expected format for polar area charts
|
||||
const chartValues = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
return isNaN(Number(value)) ? 0 : Number(value);
|
||||
});
|
||||
// Assign data in the correct format (array of objects with data property)
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: chartValues,
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
// Trigger change detection
|
||||
this.polarAreaChartData = [...this.polarAreaChartData];
|
||||
console.log('Updated polar chart with drilldown legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
|
||||
@@ -336,14 +842,24 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
console.warn('Drilldown received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching drilldown data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.polarAreaChartLabels = [];
|
||||
this.polarAreaChartData = [];
|
||||
this.polarAreaChartData = [
|
||||
{
|
||||
data: [],
|
||||
label: 'Dataset 1'
|
||||
}
|
||||
];
|
||||
// Keep current data in case of error
|
||||
}
|
||||
);
|
||||
@@ -417,13 +933,13 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
// Get the label of the clicked element
|
||||
const clickedLabel = this.polarAreaChartLabels[clickedIndex];
|
||||
|
||||
console.log('Clicked on polar area:', { index: clickedIndex, label: clickedLabel });
|
||||
console.log('Clicked on polar point:', { index: clickedIndex, label: clickedLabel });
|
||||
|
||||
// If we're not at the base level, store original data
|
||||
if (this.currentDrilldownLevel === 0) {
|
||||
// Store original data before entering drilldown mode
|
||||
this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels];
|
||||
this.originalPolarAreaChartData = [...this.polarAreaChartData];
|
||||
this.originalPolarAreaChartData = JSON.parse(JSON.stringify(this.polarAreaChartData));
|
||||
console.log('Stored original data for drilldown');
|
||||
}
|
||||
|
||||
@@ -491,6 +1007,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Polar chart hovered:', e);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,282 @@
|
||||
<div style="display: block">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<div style="display: block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Radar Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
@@ -16,7 +285,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Chart display -->
|
||||
<div *ngIf="!noDataAvailable">
|
||||
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
||||
<canvas baseChart
|
||||
[datasets]="radarChartData"
|
||||
[labels]="radarChartLabels"
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user