This commit is contained in:
string 2025-10-25 21:46:54 +05:30
parent 98e0908920
commit c6ad8b5c2f
15 changed files with 1310 additions and 102 deletions

View File

@ -0,0 +1,113 @@
<!-- 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'">
<select [(ngModel)]="filterValue"
(ngModelChange)="onFilterValueChange($event)"
multiple
class="clr-select compact-multiselect">
<option *ngFor="let option of filterOptions" [value]="option">{{ option }}</option>
</select>
</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>

View File

@ -0,0 +1,189 @@
.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;
}
.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;
}
}
}
}

View File

@ -0,0 +1,218 @@
import { Component, OnInit, Input, Output, EventEmitter } 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 {
@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[] = [];
// 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 {
// 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];
}
});
// 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);
}
}
}
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
const currentState = this.filterService.getFilterValues();
this.filterValue = currentState[this.selectedFilter.id] || '';
}
}
}
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 });
}
}
onToggleChange(checked: boolean): void {
this.onFilterValueChange(checked);
}
onDateRangeChange(dateRange: { start: string | null, end: string | null }): void {
this.onFilterValueChange(dateRange);
}
// 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
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;
// 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.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) {
this.loadAvailableValues(this.filterKey);
}
// 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 {
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);
}
}
// 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);
}
}
}

View File

@ -1,3 +1,4 @@
export * from './filter.service';
export * from './common-filter.component';
export * from './chart-wrapper.component';
export * from './chart-wrapper.component';
export * from './compact-filter.component';

View File

@ -24,6 +24,8 @@ import { isArray } from 'highcharts';
import { SureconnectService } from '../sureconnect/sureconnect.service';
// Add the CommonFilterComponent import
import { CommonFilterComponent } from '../common-filter/common-filter.component';
// Add the CompactFilterComponent import
import { CompactFilterComponent } from '../common-filter';
function isNullArray(arr) {
return !Array.isArray(arr) || arr.length === 0;
@ -98,6 +100,10 @@ export class EditnewdashComponent implements OnInit {
{
name: 'Grid View',
identifier: 'grid_view'
},
{
name: 'Compact Filter',
identifier: 'compact_filter'
}
]
@ -123,6 +129,7 @@ export class EditnewdashComponent implements OnInit {
{ name: "Financial Chart", componentInstance: FinancialChartComponent },
{ name: "To Do Chart", componentInstance: ToDoChartComponent },
{ name: "Grid View", componentInstance: GridViewComponent },
{ name: "Compact Filter", componentInstance: CompactFilterComponent }, // Add this line
];
model: any;
linesdata: any;
@ -168,7 +175,12 @@ export class EditnewdashComponent implements OnInit {
drilldownLayers: [] as any[],
// Common filter properties
commonFilterEnabled: false,
commonFilterEnabledDrilldown: false
commonFilterEnabledDrilldown: false,
// Compact filter properties
filterKey: '',
filterType: 'text',
filterLabel: '',
filterOptions: [] as string[]
};
// Add sureconnect data property
@ -519,6 +531,21 @@ export class EditnewdashComponent implements OnInit {
component: CommonFilterComponent,
name: "Common Filter"
});
case "compact_filter":
return this.dashboardArray.push({
cols: 3,
rows: 2,
x: 0,
y: 0,
chartid: maxChartId + 1,
component: CompactFilterComponent,
name: "Compact Filter",
// Add default configuration for compact filter
filterKey: '',
filterType: 'text',
filterLabel: '',
filterOptions: []
});
case "grid_view":
return this.dashboardArray.push({
cols: 5,
@ -561,6 +588,19 @@ export class EditnewdashComponent implements OnInit {
if (item['commonFilterEnabledDrilldown'] === undefined) {
this.gadgetsEditdata['commonFilterEnabledDrilldown'] = false;
}
// Initialize compact filter properties if not present
if (item['filterKey'] === undefined) {
this.gadgetsEditdata['filterKey'] = '';
}
if (item['filterType'] === undefined) {
this.gadgetsEditdata['filterType'] = 'text';
}
if (item['filterLabel'] === undefined) {
this.gadgetsEditdata['filterLabel'] = '';
}
if (item['filterOptions'] === undefined) {
this.gadgetsEditdata['filterOptions'] = [];
}
this.getStores();
// Set default connection if none is set and we have connections
@ -736,6 +776,14 @@ export class EditnewdashComponent implements OnInit {
xyz.drilldownLayers = this.gadgetsEditdata.drilldownLayers;
xyz.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
// For compact filter, preserve filter configuration properties
if (item.component && item.component.name === 'CompactFilterComponent') {
xyz.filterKey = this.gadgetsEditdata.filterKey || '';
xyz.filterType = this.gadgetsEditdata.filterType || 'text';
xyz.filterLabel = this.gadgetsEditdata.filterLabel || '';
xyz.filterOptions = this.gadgetsEditdata.filterOptions || [];
}
console.log(xyz);
return xyz;
}
@ -809,6 +857,16 @@ export class EditnewdashComponent implements OnInit {
chartInputs['connection'] = item['connection'] || undefined;
}
// For CompactFilterComponent, pass filter configuration properties
if (item.component && item.component.name === 'CompactFilterComponent') {
chartInputs['filterKey'] = item['filterKey'] || '';
chartInputs['filterType'] = item['filterType'] || 'text';
chartInputs['filterLabel'] = item['filterLabel'] || '';
chartInputs['filterOptions'] = item['filterOptions'] || [];
chartInputs['apiUrl'] = item['table'] || ''; // Use table as API URL
chartInputs['connectionId'] = item['connection'] ? parseInt(item['connection'], 10) : undefined;
}
// Remove undefined properties to avoid passing unnecessary data
Object.keys(chartInputs).forEach(key => {
if (chartInputs[key] === undefined) {
@ -863,6 +921,16 @@ export class EditnewdashComponent implements OnInit {
updatedItem.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
updatedItem.commonFilterEnabledDrilldown = this.gadgetsEditdata.commonFilterEnabledDrilldown; // Add drilldown common filter property
// For compact filter, preserve filter configuration properties
if (item.component && item.component.name === 'CompactFilterComponent') {
updatedItem.filterKey = this.gadgetsEditdata.filterKey || '';
updatedItem.filterType = this.gadgetsEditdata.filterType || 'text';
updatedItem.filterLabel = this.gadgetsEditdata.filterLabel || '';
updatedItem.filterOptions = this.gadgetsEditdata.filterOptions || [];
updatedItem.table = this.gadgetsEditdata.table || ''; // API URL
updatedItem.connection = this.gadgetsEditdata.connection || undefined; // Connection ID
}
console.log('Updated item:', updatedItem);
return updatedItem;
}

View File

@ -1,32 +1,50 @@
<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">
<!-- Compact Filters -->
<div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0">
<app-compact-filter
*ngFor="let filter of baseFilters"
[filterKey]="filter.field"
(filterChange)="onFilterChange($event)">
</app-compact-filter>
</div>
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" 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;">
<div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator">
<span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span>
<button class="btn btn-secondary btn-sm" (click)="navigateBack()">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
<button class="btn btn-danger btn-sm" (click)="resetToOriginalData()">
Back to Main View
</button>
</div>
<!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
No data available
<div class="chart-header">
<h3>{{ charttitle || 'Bar Chart' }}</h3>
</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]="barChartData.length === 0 && barChartLabels.length === 0 && !noDataAvailable">
<!-- No data message -->
<div class="no-data-message" *ngIf="noDataAvailable">
<p>No data available</p>
</div>
<!-- Chart display -->
<canvas baseChart
*ngIf="!noDataAvailable"
[datasets]="barChartData"
[labels]="barChartLabels"
[type]="barChartType"
[options]="barChartOptions"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
</canvas>
<!-- Loading overlay -->
<div class="loading-overlay" *ngIf="barChartData.length === 0 && barChartLabels.length === 0 && !noDataAvailable">
<div class="shimmer-bar"></div>
</div>
</div>
</div>
</div>

View File

@ -5,27 +5,214 @@
width: 100%;
}
.bar-chart-container {
position: relative;
.chart-container {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
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;
padding: 20px;
}
canvas {
display: block;
max-width: 100%;
max-height: 100%;
.chart-container:hover {
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
transform: translateY(-2px);
}
.compact-filters-container {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 10px;
padding: 5px;
background: #ffffff;
border: 1px solid #e9ecef;
border-radius: 6px;
min-height: 40px;
}
.drilldown-indicator {
background-color: #e0e0e0;
padding: 10px;
margin-bottom: 15px;
border-radius: 8px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.drilldown-text {
font-weight: bold;
color: #333;
font-size: 16px;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn-secondary {
background-color: #007cba;
color: white;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.chart-header {
margin-bottom: 20px;
h3 {
font-size: 22px;
font-weight: 600;
color: #0a192f;
margin: 0;
text-align: center;
padding-bottom: 10px;
border-bottom: 2px solid #3498db;
}
}
.chart-wrapper {
flex: 1;
position: relative;
.chart-content {
position: relative;
height: 100%;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
&.loading {
opacity: 0.7;
canvas {
filter: blur(2px);
}
}
canvas {
max-width: 100%;
max-height: 100%;
transition: filter 0.3s ease;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
}
canvas:hover {
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
transform: scale(1.02);
transition: all 0.3s ease;
}
.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%;
width: 100%;
}
.no-data-message p {
margin: 0;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.8);
.shimmer-bar {
width: 80%;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
}
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
// Responsive design for chart container
@media (max-width: 768px) {
.bar-chart-container {
height: 300px;
.chart-container {
padding: 15px;
}
.chart-header h3 {
font-size: 18px;
}
.drilldown-indicator {
flex-direction: column;
gap: 5px;
}
.drilldown-text {
font-size: 14px;
}
.compact-filters-container {
flex-wrap: wrap;
}
}
@media (max-width: 480px) {
.bar-chart-container {
height: 250px;
.chart-container {
padding: 10px;
}
.chart-header h3 {
font-size: 16px;
}
}

View File

@ -140,6 +140,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
console.log('Chart legend changed to:', this.barChartLegend);
}
}
// Handle filter changes from compact filters
onFilterChange(event: { filterId: string, value: any }): void {
console.log('Compact filter changed:', event);
// The filter service will automatically trigger chart updates through the subscription
}
fetchChartData(): void {
// Set flag to prevent recursive calls

View File

@ -1,39 +1,53 @@
<div class="doughnut-chart-container">
<!-- Compact Filters -->
<div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0">
<app-compact-filter
*ngFor="let filter of baseFilters"
[filterKey]="filter.field"
(filterChange)="onFilterChange($event)">
</app-compact-filter>
</div>
<!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" 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;">
<div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator">
<span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span>
<button class="btn btn-secondary btn-sm" (click)="navigateBack()">
Back to Level {{currentDrilldownLevel - 1}}
</button>
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
<button class="btn btn-danger btn-sm" (click)="resetToOriginalData()">
Back to Main View
</button>
</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>
<!-- 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
*ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0"
[data]="doughnutChartData"
[labels]="doughnutChartLabels"
[type]="doughnutChartType"
[options]="doughnutChartOptions"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
</canvas>
<div class="chart-header">
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
</div>
<div class="chart-wrapper">
<div class="chart-content" [class.loading]="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
<!-- 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
*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="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
<div class="shimmer-donut"></div>
</div>
</div>
</div>
<div class="chart-legend" *ngIf="!noDataAvailable && 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>

View File

@ -17,17 +17,78 @@
transform: translateY(-2px);
}
.compact-filters-container {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 10px;
padding: 5px;
background: #ffffff;
border: 1px solid #e9ecef;
border-radius: 6px;
min-height: 40px;
}
.chart-title {
font-size: 26px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 20px;
.drilldown-indicator {
background-color: #e0e0e0;
padding: 10px;
margin-bottom: 15px;
border-radius: 8px;
text-align: center;
padding-bottom: 15px;
border-bottom: 2px solid #3498db;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.drilldown-text {
font-weight: bold;
color: #333;
font-size: 16px;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn-secondary {
background-color: #007cba;
color: white;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.chart-header {
margin-bottom: 20px;
.chart-title {
font-size: 22px;
font-weight: 600;
color: #0a192f;
margin: 0;
text-align: center;
padding-bottom: 10px;
border-bottom: 2px solid #3498db;
}
}
.chart-wrapper {
@ -56,6 +117,62 @@
transition: all 0.3s ease;
}
.chart-content {
position: relative;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
&.loading {
opacity: 0.7;
canvas {
filter: blur(2px);
}
}
.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%;
width: 100%;
}
.no-data-message p {
margin: 0;
}
.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 {
display: flex;
flex-wrap: wrap;
@ -119,36 +236,13 @@
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;
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); }
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Responsive design */
@ -157,9 +251,17 @@
padding: 15px;
}
.chart-title {
font-size: 20px;
margin-bottom: 15px;
.chart-header .chart-title {
font-size: 18px;
}
.drilldown-indicator {
flex-direction: column;
gap: 5px;
}
.drilldown-text {
font-size: 14px;
}
.chart-wrapper {
@ -181,4 +283,8 @@
font-size: 16px;
padding: 20px;
}
.compact-filters-container {
flex-wrap: wrap;
}
}

View File

@ -200,6 +200,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
this.subscriptions.forEach(sub => sub.unsubscribe());
}
// Handle filter changes from compact filters
onFilterChange(event: { filterId: string, value: any }): void {
console.log('Compact filter changed:', event);
// The filter service will automatically trigger chart updates through the subscription
}
// Public method to refresh data when filters change
refreshData(): void {
this.fetchChartData();

View File

@ -0,0 +1,25 @@
<div class="sidebar-filters">
<!-- Component Palette Button -->
<div class="component-palette-section">
<button class="component-palette-button" (click)="toggleComponentPalette()">
<clr-icon shape="plus-circle"></clr-icon>
Add Components
</button>
<!-- Component Palette (hidden by default) -->
<div class="component-palette" *ngIf="showComponentPalette">
<h3 class="palette-title">Available Components</h3>
<div class="component-list">
<div
*ngFor="let component of availableComponents"
class="component-item"
draggable="true"
(dragstart)="onComponentDragStart($event, component)"
(dragend)="onComponentDragEnd($event)">
<clr-icon shape="drag-handle" class="drag-icon"></clr-icon>
{{ component.name }}
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { DashboardFilterService } from '../../services/dashboard-filter.service';
@Component({
selector: 'app-shield-sidebar-filters',
templateUrl: './sidebar-filters.component.html',
styleUrls: ['./sidebar-filters.component.scss']
})
export class SidebarFiltersComponent implements OnInit {
constructor(private filterService: DashboardFilterService) { }
ngOnInit(): void {
// Component initialization
}
}

View File

@ -74,6 +74,10 @@ export class ShieldDashboardComponent implements OnInit {
{
name: 'Quarterwise Flow',
identifier: 'line_chart'
},
{
name: 'Compact Filter',
identifier: 'compact_filter'
}
];
@ -300,6 +304,20 @@ export class ShieldDashboardComponent implements OnInit {
drilldownLayers: []
};
break;
case "compact_filter":
newItem = {
cols: 3,
rows: 2,
y: 0,
x: 0,
chartType: 'compact-filter',
name: 'Compact Filter',
id: newId,
baseFilters: [],
drilldownEnabled: false,
drilldownLayers: []
};
break;
default:
newItem = {
cols: 5,

View File

@ -0,0 +1,223 @@
import { Component, OnInit } from '@angular/core';
import { GridsterConfig, GridsterItem } from 'angular-gridster2';
interface ShieldDashboardItem extends GridsterItem {
chartType: string;
name: string;
id: number;
component?: any;
}
@Component({
selector: 'app-shield-dashboard',
templateUrl: './shield-dashboard.component.html',
styleUrls: ['./shield-dashboard.component.scss']
})
export class ShieldDashboardComponent implements OnInit {
options: GridsterConfig;
dashboard: Array<ShieldDashboardItem>;
// Keep track of deleted items
deletedItems: Array<ShieldDashboardItem> = [];
constructor() { }
ngOnInit(): void {
this.options = {
gridType: 'fit',
enableEmptyCellDrop: true,
emptyCellDropCallback: this.onDrop,
pushItems: true,
swap: true,
pushDirections: { north: true, east: true, south: true, west: true },
resizable: { enabled: true },
itemChangeCallback: this.itemChange.bind(this),
draggable: {
enabled: true,
ignoreContent: true,
dropOverItems: true,
dragHandleClass: 'drag-handler',
ignoreContentClass: 'no-drag',
},
displayGrid: 'always',
minCols: 10,
minRows: 10,
itemResizeCallback: this.itemResize.bind(this)
};
// Initialize the dashboard with empty canvas
this.dashboard = [];
}
onDrop = (event: any) => {
// Handle dropping new components onto the dashboard
console.log('Item dropped:', event);
// Get the component identifier from the drag event
const componentType = event.dataTransfer ? event.dataTransfer.getData('widgetIdentifier') : '';
console.log('Component type dropped:', componentType);
if (componentType) {
this.addComponentToDashboard(componentType);
} else {
console.log('No component type found in drag data');
}
}
// Add a new component to the dashboard
addComponentToDashboard(componentType: string) {
// Generate a new ID for the component
const newId = this.dashboard.length > 0 ? Math.max(...this.dashboard.map(item => item.id), 0) + 1 : 1;
let newItem: ShieldDashboardItem;
switch (componentType) {
case "bar_chart":
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'bar-chart',
name: 'Bar Chart',
id: newId
};
break;
case "doughnut_chart":
// For doughnut charts, we'll need to determine which one based on existing items
const donutCount = this.dashboard.filter(item => item.chartType === 'donut-chart').length;
if (donutCount % 2 === 0) {
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'donut-chart',
name: 'End Customer Donut',
id: newId
};
} else {
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'donut-chart',
name: 'Segment Penetration Donut',
id: newId
};
}
break;
case "map_chart":
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'map-chart',
name: 'Map Chart',
id: newId
};
break;
case "grid_view":
newItem = {
cols: 10,
rows: 6,
y: 0,
x: 0,
chartType: 'data-table',
name: 'Data Table',
id: newId
};
break;
case "to_do_chart":
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'deal-details',
name: 'Deal Details',
id: newId
};
break;
case "line_chart":
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: 'quarterwise-flow',
name: 'Quarterwise Flow',
id: newId
};
break;
default:
newItem = {
cols: 5,
rows: 6,
y: 0,
x: 0,
chartType: componentType,
name: componentType,
id: newId
};
}
// Add the new item to the dashboard
this.dashboard.push(newItem);
}
removeItem(item: ShieldDashboardItem) {
// Add the item to deleted items list before removing
this.deletedItems.push({...item});
// Remove the item from the dashboard
this.dashboard.splice(this.dashboard.indexOf(item), 1);
}
// Restore a deleted item
restoreItem(item: ShieldDashboardItem) {
// Remove from deleted items
this.deletedItems.splice(this.deletedItems.indexOf(item), 1);
// Add back to dashboard
this.dashboard.push(item);
}
// Clear all deleted items
clearDeletedItems() {
this.deletedItems = [];
}
itemChange() {
console.log('Item changed:', this.dashboard);
}
itemResize(item: any, itemComponent: any) {
console.log('Item resized:', item);
// Trigger a window resize event to notify charts to resize
window.dispatchEvent(new Event('resize'));
}
/**
* Extract only the relevant chart configuration properties to pass to chart components
* This prevents errors when trying to set properties that don't exist on the components
*/
getChartInputs(item: any): any {
// Only pass properties that are relevant to chart components
const chartInputs = {
chartType: item.chartType,
name: item.name
};
// Remove undefined properties to avoid passing unnecessary data
Object.keys(chartInputs).forEach(key => {
if (chartInputs[key] === undefined) {
delete chartInputs[key];
}
});
return chartInputs;
}
}