Commit c81ea227 by Ooh-Ao

kpi

parent 1fc00b6e
/**
* Simple KPI Widget Configuration Examples
*
* This file demonstrates how to configure the Simple KPI Widget using the
* comprehensive configuration system with various options and use cases.
*/
import { Component, OnInit } from '@angular/core';
import { WidgetConfigService } from '../services/widget-config.service';
import { WidgetPreviewDataService } from '../services/widget-preview-data.service';
@Component({
selector: 'app-simple-kpi-config-examples',
template: `
<div class="simple-kpi-examples">
<h1>Simple KPI Widget Configuration Examples</h1>
<!-- Example 1: Basic KPI Configuration -->
<div class="example-section">
<h2>Example 1: Basic KPI Configuration</h2>
<div class="example-content">
<h3>Configuration</h3>
<pre>{{ basicConfig | json }}</pre>
<h3>Preview Data</h3>
<pre>{{ basicPreviewData | json }}</pre>
</div>
</div>
<!-- Example 2: Styled KPI with Trend -->
<div class="example-section">
<h2>Example 2: Styled KPI with Trend</h2>
<div class="example-content">
<h3>Configuration</h3>
<pre>{{ styledConfig | json }}</pre>
</div>
</div>
<!-- Example 3: Advanced Configuration -->
<div class="example-section">
<h2>Example 3: Advanced Configuration</h2>
<div class="example-content">
<h3>Configuration</h3>
<pre>{{ advancedConfig | json }}</pre>
</div>
</div>
<!-- Example 4: Custom CSS Configuration -->
<div class="example-section">
<h2>Example 4: Custom CSS Configuration</h2>
<div class="example-content">
<h3>Configuration</h3>
<pre>{{ customCSSConfig | json }}</pre>
</div>
</div>
<!-- Example 5: Different Aggregation Types -->
<div class="example-section">
<h2>Example 5: Different Aggregation Types</h2>
<div class="example-content">
<h3>Sum Aggregation</h3>
<pre>{{ sumConfig | json }}</pre>
<h3>Average Aggregation</h3>
<pre>{{ averageConfig | json }}</pre>
<h3>Count Aggregation</h3>
<pre>{{ countConfig | json }}</pre>
<h3>Max/Min Aggregation</h3>
<pre>{{ maxMinConfig | json }}</pre>
</div>
</div>
<!-- Example 6: Trend Configuration -->
<div class="example-section">
<h2>Example 6: Trend Configuration</h2>
<div class="example-content">
<h3>Percentage Trend</h3>
<pre>{{ percentageTrendConfig | json }}</pre>
<h3>Absolute Trend</h3>
<pre>{{ absoluteTrendConfig | json }}</pre>
<h3>Ratio Trend</h3>
<pre>{{ ratioTrendConfig | json }}</pre>
</div>
</div>
</div>
`,
styles: [`
.simple-kpi-examples {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.example-section {
margin-bottom: 3rem;
padding: 2rem;
background: #f8f9fa;
border-radius: 12px;
border: 1px solid #e9ecef;
}
.example-content {
margin-top: 1rem;
}
.example-content h3 {
color: #495057;
margin-top: 1.5rem;
margin-bottom: 0.5rem;
}
pre {
background: #2d3748;
color: #e2e8f0;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
font-size: 0.85rem;
line-height: 1.5;
}
`]
})
export class SimpleKpiConfigExamplesComponent implements OnInit {
// Configuration examples
basicConfig: any;
styledConfig: any;
advancedConfig: any;
customCSSConfig: any;
sumConfig: any;
averageConfig: any;
countConfig: any;
maxMinConfig: any;
percentageTrendConfig: any;
absoluteTrendConfig: any;
ratioTrendConfig: any;
// Preview data
basicPreviewData: any[];
constructor(
private widgetConfigService: WidgetConfigService,
private previewDataService: WidgetPreviewDataService
) {}
ngOnInit(): void {
this.initializeConfigurations();
this.loadPreviewData();
}
private initializeConfigurations(): void {
// Example 1: Basic KPI Configuration
this.basicConfig = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Total Sales',
valueField: 'revenue',
unit: '$',
aggregation: 'sum'
},
style: {
backgroundColor: '#e8f5e8',
textColor: '#2d5a2d',
accentColor: '#28a745',
borderRadius: 8,
padding: 16
}
};
// Example 2: Styled KPI with Trend
this.styledConfig = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Customer Satisfaction',
valueField: 'satisfaction',
unit: '%',
aggregation: 'average',
showTrend: true,
trendField: 'satisfaction',
trendType: 'percentage'
},
style: {
backgroundColor: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
textColor: '#ffffff',
accentColor: '#ffffff',
borderColor: '#ffffff',
borderRadius: 16,
padding: 24,
fontSize: 18,
fontWeight: 'bold',
fontFamily: 'Inter, system-ui, sans-serif'
},
animation: {
enableAnimations: true,
animationType: 'fade',
animationDuration: 500,
hoverEffects: true
}
};
// Example 3: Advanced Configuration
this.advancedConfig = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Revenue Growth',
valueField: 'revenue',
unit: '%',
aggregation: 'average',
decimalPlaces: 2,
showTrend: true,
trendField: 'growth',
trendType: 'percentage'
},
style: {
backgroundColor: '#f8f9fa',
textColor: '#495057',
accentColor: '#007bff',
borderColor: '#007bff',
borderWidth: 3,
borderRadius: 20,
padding: 32,
margin: 16,
fontSize: 20,
fontWeight: 'bold',
fontFamily: 'system-ui, -apple-system, sans-serif'
},
layout: {
width: 350,
height: 200,
responsive: true
},
interaction: {
enableTooltip: true,
enableClick: true,
clickAction: 'drill_down'
},
filters: {
category: 'sales',
period: 'last_30_days'
},
dataMapping: [
{ sourceField: 'total_revenue', targetField: 'revenue', transformation: 'format_number' },
{ sourceField: 'growth_rate', targetField: 'growth', transformation: 'none' }
]
};
// Example 4: Custom CSS Configuration
this.customCSSConfig = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Custom Styled KPI',
valueField: 'value',
unit: 'items'
},
style: {
backgroundColor: '#1a202c',
textColor: '#e2e8f0',
accentColor: '#00ff88',
borderRadius: 12,
padding: 20,
customCSS: `
.simple-kpi-widget {
position: relative;
overflow: hidden;
}
.simple-kpi-widget::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent 30%, rgba(0, 255, 136, 0.1) 50%, transparent 70%);
animation: shimmer 2s infinite;
}
.widget-header {
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
border-bottom: 2px solid #00ff88;
}
.kpi-value {
background: linear-gradient(135deg, #00ff88 0%, #00cc6a 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
filter: drop-shadow(0 0 10px rgba(0, 255, 136, 0.3));
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.simple-kpi-widget:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 20px 40px rgba(0, 255, 136, 0.2);
}
`
}
};
// Example 5: Different Aggregation Types
this.sumConfig = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Total Revenue',
valueField: 'amount',
unit: '$',
aggregation: 'sum'
}
};
this.averageConfig = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Average Order Value',
valueField: 'orderValue',
unit: '$',
aggregation: 'average',
decimalPlaces: 2
}
};
this.countConfig = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Total Orders',
aggregation: 'count',
unit: 'orders'
}
};
this.maxMinConfig = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Highest Sale',
valueField: 'amount',
unit: '$',
aggregation: 'max'
}
};
// Example 6: Trend Configuration
this.percentageTrendConfig = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Sales Growth',
valueField: 'current',
unit: '%',
showTrend: true,
trendField: 'growth',
trendType: 'percentage'
},
style: {
backgroundColor: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
textColor: '#ffffff',
accentColor: '#ffffff'
}
};
this.absoluteTrendConfig = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Revenue Change',
valueField: 'current',
unit: '$',
showTrend: true,
trendField: 'change',
trendType: 'absolute'
},
style: {
backgroundColor: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
textColor: '#ffffff',
accentColor: '#ffffff'
}
};
this.ratioTrendConfig = {
widgetType: 'SimpleKpiWidgetComponent',
config: {
title: 'Performance Ratio',
valueField: 'current',
unit: 'x',
showTrend: true,
trendField: 'ratio',
trendType: 'ratio'
},
style: {
backgroundColor: 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)',
textColor: '#ffffff',
accentColor: '#ffffff'
}
};
}
private loadPreviewData(): void {
// Generate sample data for KPI widgets
this.basicPreviewData = this.previewDataService.generateSimpleKpiPreviewData({
count: 5,
categories: ['sales', 'marketing', 'operations']
});
}
// Example methods for programmatic configuration
/**
* Create a basic KPI widget configuration
*/
createBasicKpiConfig(): void {
const config = this.widgetConfigService.createWidgetConfig('SimpleKpiWidgetComponent', {
config: {
title: 'My KPI',
valueField: 'value',
unit: '$',
aggregation: 'sum'
},
style: {
backgroundColor: '#e8f5e8',
textColor: '#2d5a2d',
accentColor: '#28a745'
}
});
console.log('Created basic KPI config:', config);
}
/**
* Create a styled KPI widget configuration
*/
createStyledKpiConfig(): void {
const config = this.widgetConfigService.createWidgetConfig('SimpleKpiWidgetComponent', {
config: {
title: 'Styled KPI',
valueField: 'value',
unit: '%',
showTrend: true,
trendType: 'percentage'
},
style: {
backgroundColor: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
textColor: '#ffffff',
accentColor: '#ffffff',
borderRadius: 16,
padding: 24,
fontSize: 18,
fontWeight: 'bold'
},
animation: {
enableAnimations: true,
animationType: 'fade',
animationDuration: 500,
hoverEffects: true
}
});
console.log('Created styled KPI config:', config);
}
/**
* Create a KPI widget with custom CSS
*/
createCustomCSSKpiConfig(): void {
const config = this.widgetConfigService.createWidgetConfig('SimpleKpiWidgetComponent', {
config: {
title: 'Custom CSS KPI',
valueField: 'value',
unit: 'items'
},
style: {
backgroundColor: '#1a202c',
textColor: '#e2e8f0',
accentColor: '#00ff88',
customCSS: `
.simple-kpi-widget {
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
border: 2px solid #00ff88;
box-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
}
.kpi-value {
background: linear-gradient(135deg, #00ff88 0%, #00cc6a 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
`
}
});
console.log('Created custom CSS KPI config:', config);
}
/**
* Update an existing KPI widget configuration
*/
updateKpiConfig(widgetId: string, updates: any): void {
const updated = this.widgetConfigService.updateWidgetConfig(widgetId, {
config: {
...updates.config
},
style: {
...updates.style
}
});
console.log('Updated KPI config:', updated);
}
/**
* Get KPI widget configurations by aggregation type
*/
getKpiConfigsByAggregation(aggregation: string): void {
const allConfigs = this.widgetConfigService.getWidgetConfigsByType('SimpleKpiWidgetComponent');
const filteredConfigs = allConfigs.filter(config =>
config.config.aggregation === aggregation
);
console.log(`KPI configs with ${aggregation} aggregation:`, filteredConfigs);
}
}
......@@ -558,7 +558,7 @@
</div>
</div>
<div *ngIf="widgetType === 'SlicerWidgetComponent'">
<mat-form-field appearance="fill">
......@@ -601,45 +601,421 @@
</div>
<div *ngIf="widgetType === 'SimpleKpiWidgetComponent'">
<mat-form-field appearance="fill">
<mat-label>Title</mat-label>
<input matInput [(ngModel)]="currentConfig.title" name="title">
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Value Field</mat-label>
<mat-select [(ngModel)]="currentConfig.valueField" name="valueField">
<mat-option *ngFor="let col of availableColumns" [value]="col">{{ col }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Aggregation</mat-label>
<mat-select [(ngModel)]="currentConfig.aggregation" name="aggregation">
<mat-option value="count">Count</mat-option>
<mat-option value="sum">Sum</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Unit</mat-label>
<input matInput [(ngModel)]="currentConfig.unit" name="unit">
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Icon (e.g., 'person-fill')</mat-label>
<input matInput [(ngModel)]="currentConfig.icon" name="icon">
<mat-hint>Find icons at icons.getbootstrap.com</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Icon Color</mat-label>
<input matInput type="color" [(ngModel)]="currentConfig.iconColor" name="iconColor" class="h-[40px]">
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Background Color (Header)</mat-label>
<input matInput [(ngModel)]="currentConfig.backgroundColor" name="backgroundColor">
<mat-hint>e.g., 'red', '#FF0000', 'linear-gradient(to right, red, yellow)'</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Border Color (Card)</mat-label>
<input matInput type="color" [(ngModel)]="currentConfig.borderColor" name="borderColor" class="h-[40px]">
</mat-form-field>
<!-- Basic Configuration -->
<div class="config-section border p-4 rounded-lg mb-4">
<h3 class="text-lg font-semibold mb-3 text-blue-600">Basic Configuration</h3>
<mat-form-field appearance="fill" class="w-full">
<mat-label>Title</mat-label>
<input matInput [(ngModel)]="currentConfig.title" name="title" aria-label="Widget title">
<mat-hint>Widget title displayed in header</mat-hint>
</mat-form-field>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Value Field</mat-label>
<mat-select [(ngModel)]="currentConfig.valueField" name="valueField">
<mat-option *ngFor="let col of availableColumns" [value]="col">{{ col }}</mat-option>
</mat-select>
<mat-hint>Field containing the KPI value</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Label Field</mat-label>
<mat-select [(ngModel)]="currentConfig.labelField" name="labelField">
<mat-option *ngFor="let col of availableColumns" [value]="col">{{ col }}</mat-option>
</mat-select>
<mat-hint>Field containing the KPI label</mat-hint>
</mat-form-field>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Aggregation</mat-label>
<mat-select [(ngModel)]="currentConfig.aggregation" name="aggregation">
<mat-option value="count">Count</mat-option>
<mat-option value="sum">Sum</mat-option>
<mat-option value="average">Average</mat-option>
<mat-option value="max">Maximum</mat-option>
<mat-option value="min">Minimum</mat-option>
</mat-select>
<mat-hint>How to aggregate the data</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Unit</mat-label>
<input matInput [(ngModel)]="currentConfig.unit" name="unit" placeholder="e.g., $, %, items" aria-label="Unit">
<mat-hint>Unit to display after the value</mat-hint>
</mat-form-field>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Icon (Bootstrap Icons)</mat-label>
<input matInput [(ngModel)]="currentConfig.icon" name="icon" placeholder="e.g., person-fill, building" aria-label="Icon">
<mat-hint>Find icons at icons.getbootstrap.com</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Decimal Places</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.decimalPlaces" name="decimalPlaces" min="0" max="10" aria-label="Decimal places">
<mat-hint>Number of decimal places to show</mat-hint>
</mat-form-field>
</div>
</div>
<!-- Trend Configuration -->
<div class="config-section border p-4 rounded-lg mb-4">
<h3 class="text-lg font-semibold mb-3 text-green-600">Trend Settings</h3>
<div class="flex items-center mb-3">
<mat-checkbox [(ngModel)]="currentConfig.showTrend" name="showTrend" class="mr-2">
Show Trend Indicator
</mat-checkbox>
</div>
<div *ngIf="currentConfig.showTrend" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Trend Field</mat-label>
<mat-select [(ngModel)]="currentConfig.trendField" name="trendField">
<mat-option *ngFor="let col of availableColumns" [value]="col">{{ col }}</mat-option>
</mat-select>
<mat-hint>Field containing trend data</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Trend Type</mat-label>
<mat-select [(ngModel)]="currentConfig.trendType" name="trendType">
<mat-option value="percentage">Percentage Change</mat-option>
<mat-option value="absolute">Absolute Change</mat-option>
<mat-option value="ratio">Ratio</mat-option>
</mat-select>
<mat-hint>How to calculate trend</mat-hint>
</mat-form-field>
</div>
</div>
<!-- Style Configuration -->
<div class="config-section border p-4 rounded-lg mb-4">
<h3 class="text-lg font-semibold mb-3 text-purple-600">Style & Colors</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Background Color</mat-label>
<input matInput [(ngModel)]="currentConfig.backgroundColor" name="backgroundColor" placeholder="#FF0000 or linear-gradient(...)" aria-label="Background color">
<mat-hint>Header background color</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Text Color</mat-label>
<input matInput type="color" [(ngModel)]="currentConfig.textColor" name="textColor" class="h-[40px]" aria-label="Text color">
<mat-hint>Text color for labels</mat-hint>
</mat-form-field>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Accent Color</mat-label>
<input matInput type="color" [(ngModel)]="currentConfig.accentColor" name="accentColor" class="h-[40px]" aria-label="Accent color">
<mat-hint>Color for the main KPI value</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Border Color</mat-label>
<input matInput type="color" [(ngModel)]="currentConfig.borderColor" name="borderColor" class="h-[40px]" aria-label="Border color">
<mat-hint>Card border color</mat-hint>
</mat-form-field>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Icon Color</mat-label>
<input matInput type="color" [(ngModel)]="currentConfig.iconColor" name="iconColor" class="h-[40px]" aria-label="Icon color">
<mat-hint>Icon color</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Border Radius (px)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.borderRadius" name="borderRadius" min="0" max="50" aria-label="Border radius">
<mat-hint>Roundness of widget corners</mat-hint>
</mat-form-field>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<mat-form-field appearance="fill">
<mat-label>Padding (px)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.padding" name="padding" min="0" max="100" aria-label="Padding">
<mat-hint>Internal spacing</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Font Size (px)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.fontSize" name="fontSize" min="10" max="48" aria-label="Font size">
<mat-hint>Font size for values</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Font Weight</mat-label>
<mat-select [(ngModel)]="currentConfig.fontWeight" name="fontWeight">
<mat-option value="normal">Normal</mat-option>
<mat-option value="bold">Bold</mat-option>
<mat-option value="lighter">Lighter</mat-option>
<mat-option value="bolder">Bolder</mat-option>
</mat-select>
<mat-hint>Font weight</mat-hint>
</mat-form-field>
</div>
<mat-form-field appearance="fill" class="w-full">
<mat-label>Font Family</mat-label>
<mat-select [(ngModel)]="currentConfig.fontFamily" name="fontFamily">
<mat-option value="system-ui, -apple-system, sans-serif">System Font</mat-option>
<mat-option value="Arial, sans-serif">Arial</mat-option>
<mat-option value="Helvetica, sans-serif">Helvetica</mat-option>
<mat-option value="Georgia, serif">Georgia</mat-option>
<mat-option value="Times New Roman, serif">Times New Roman</mat-option>
<mat-option value="Courier New, monospace">Courier New</mat-option>
</mat-select>
<mat-hint>Font family for the widget</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="w-full">
<mat-label>Custom CSS</mat-label>
<textarea matInput [(ngModel)]="currentConfig.customCSS" name="customCSS" rows="4"
placeholder="/* Add custom CSS rules here */&#10;.simple-kpi-widget:hover {&#10; transform: translateY(-4px);&#10;}"></textarea>
<mat-hint>Add custom CSS for advanced styling</mat-hint>
</mat-form-field>
</div>
<!-- Animation Configuration -->
<div class="config-section border p-4 rounded-lg mb-4">
<h3 class="text-lg font-semibold mb-3 text-orange-600">Animation Settings</h3>
<div class="flex items-center mb-3">
<mat-checkbox [(ngModel)]="currentConfig.enableAnimations" name="enableAnimations" class="mr-2">
Enable Animations
</mat-checkbox>
</div>
<div *ngIf="currentConfig.enableAnimations" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Animation Type</mat-label>
<mat-select [(ngModel)]="currentConfig.animationType" name="animationType">
<mat-option value="fade">Fade</mat-option>
<mat-option value="slide">Slide</mat-option>
<mat-option value="bounce">Bounce</mat-option>
<mat-option value="pulse">Pulse</mat-option>
<mat-option value="none">None</mat-option>
</mat-select>
<mat-hint>Animation effect type</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Animation Duration (ms)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.animationDuration" name="animationDuration" min="100" max="2000" step="100" aria-label="Animation duration">
<mat-hint>Duration of animation</mat-hint>
</mat-form-field>
</div>
<div *ngIf="currentConfig.enableAnimations" class="flex items-center">
<mat-checkbox [(ngModel)]="currentConfig.hoverEffects" name="hoverEffects" class="mr-2">
Enable Hover Effects
</mat-checkbox>
</div>
</div>
<!-- Interaction Configuration -->
<div class="config-section border p-4 rounded-lg mb-4">
<h3 class="text-lg font-semibold mb-3 text-indigo-600">Interaction Settings</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-center">
<mat-checkbox [(ngModel)]="currentConfig.enableTooltip" name="enableTooltip" class="mr-2">
Enable Tooltips
</mat-checkbox>
</div>
<div class="flex items-center">
<mat-checkbox [(ngModel)]="currentConfig.enableClick" name="enableClick" class="mr-2">
Enable Click Events
</mat-checkbox>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-center">
<mat-checkbox [(ngModel)]="currentConfig.enableHover" name="enableHover" class="mr-2">
Enable Hover Effects
</mat-checkbox>
</div>
<div class="flex items-center">
<mat-checkbox [(ngModel)]="currentConfig.enableExport" name="enableExport" class="mr-2">
Enable Export
</mat-checkbox>
</div>
</div>
<mat-form-field appearance="fill" class="w-full">
<mat-label>Click Action</mat-label>
<mat-select [(ngModel)]="currentConfig.clickAction" name="clickAction">
<mat-option value="none">None</mat-option>
<mat-option value="drill_down">Drill Down</mat-option>
<mat-option value="open_modal">Open Modal</mat-option>
<mat-option value="navigate">Navigate</mat-option>
<mat-option value="custom">Custom</mat-option>
</mat-select>
<mat-hint>Action when widget is clicked</mat-hint>
</mat-form-field>
<mat-form-field *ngIf="currentConfig.clickAction === 'custom'" appearance="fill" class="w-full">
<mat-label>Custom Click Handler</mat-label>
<textarea matInput [(ngModel)]="currentConfig.customClickHandler" name="customClickHandler" rows="3"
placeholder="function(event) {&#10; console.log('Widget clicked:', event);&#10;}"></textarea>
<mat-hint>Custom JavaScript function for click events</mat-hint>
</mat-form-field>
</div>
<!-- Layout Configuration -->
<div class="config-section border p-4 rounded-lg mb-4">
<h3 class="text-lg font-semibold mb-3 text-teal-600">Layout Settings</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Width (px)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.width" name="width" min="100" max="800" aria-label="Widget width">
<mat-hint>Widget width</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Height (px)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.height" name="height" min="100" max="600" aria-label="Widget height">
<mat-hint>Widget height</mat-hint>
</mat-form-field>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Minimum Width (px)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.minWidth" name="minWidth" min="50" max="400" aria-label="Minimum width">
<mat-hint>Minimum widget width</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Minimum Height (px)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.minHeight" name="minHeight" min="50" max="300" aria-label="Minimum height">
<mat-hint>Minimum widget height</mat-hint>
</mat-form-field>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Aspect Ratio</mat-label>
<mat-select [(ngModel)]="currentConfig.aspectRatio" name="aspectRatio">
<mat-option value="auto">Auto</mat-option>
<mat-option value="16:9">16:9</mat-option>
<mat-option value="4:3">4:3</mat-option>
<mat-option value="1:1">1:1 (Square)</mat-option>
<mat-option value="3:2">3:2</mat-option>
</mat-select>
<mat-hint>Widget aspect ratio</mat-hint>
</mat-form-field>
<div class="flex items-center">
<mat-checkbox [(ngModel)]="currentConfig.responsive" name="responsive" class="mr-2">
Responsive Layout
</mat-checkbox>
</div>
</div>
</div>
<!-- Data Configuration -->
<div class="config-section border p-4 rounded-lg mb-4">
<h3 class="text-lg font-semibold mb-3 text-red-600">Data Settings</h3>
<mat-form-field appearance="fill" class="w-full">
<mat-label>Data Source</mat-label>
<mat-select [(ngModel)]="currentConfig.dataSource" name="dataSource">
<mat-option value="static">Static Data</mat-option>
<mat-option value="api">API Endpoint</mat-option>
<mat-option value="websocket">WebSocket</mat-option>
<mat-option value="file">File Upload</mat-option>
</mat-select>
<mat-hint>Data source type</mat-hint>
</mat-form-field>
<mat-form-field *ngIf="currentConfig.dataSource === 'api'" appearance="fill" class="w-full">
<mat-label>API Endpoint</mat-label>
<input matInput [(ngModel)]="currentConfig.apiEndpoint" name="apiEndpoint" placeholder="/api/kpi-data" aria-label="API endpoint">
<mat-hint>API endpoint URL</mat-hint>
</mat-form-field>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Refresh Interval (ms)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.refreshInterval" name="refreshInterval" min="0" step="1000" aria-label="Refresh interval">
<mat-hint>0 = No auto-refresh</mat-hint>
</mat-form-field>
<div class="flex items-center">
<mat-checkbox [(ngModel)]="currentConfig.cacheEnabled" name="cacheEnabled" class="mr-2">
Enable Data Caching
</mat-checkbox>
</div>
</div>
<mat-form-field *ngIf="currentConfig.cacheEnabled" appearance="fill" class="w-full">
<mat-label>Cache Duration (seconds)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.cacheDuration" name="cacheDuration" min="1" max="3600" aria-label="Cache duration">
<mat-hint>How long to cache data</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="w-full">
<mat-label>Data Transform Function</mat-label>
<textarea matInput [(ngModel)]="currentConfig.dataTransform" name="dataTransform" rows="3"
placeholder="data => data.map(item => ({ ...item, formattedValue: formatCurrency(item.value) }))"></textarea>
<mat-hint>JavaScript function to transform data</mat-hint>
</mat-form-field>
</div>
<!-- Security Configuration -->
<div class="config-section border p-4 rounded-lg mb-4">
<h3 class="text-lg font-semibold mb-3 text-gray-600">Security Settings</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-center">
<mat-checkbox [(ngModel)]="currentConfig.requireAuth" name="requireAuth" class="mr-2">
Require Authentication
</mat-checkbox>
</div>
<div class="flex items-center">
<mat-checkbox [(ngModel)]="currentConfig.dataEncryption" name="dataEncryption" class="mr-2">
Enable Data Encryption
</mat-checkbox>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-center">
<mat-checkbox [(ngModel)]="currentConfig.auditLog" name="auditLog" class="mr-2">
Enable Audit Logging
</mat-checkbox>
</div>
<mat-form-field appearance="fill">
<mat-label>Rate Limit (requests/min)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.rateLimit" name="rateLimit" min="0" aria-label="Rate limit">
<mat-hint>0 = No limit</mat-hint>
</mat-form-field>
</div>
<mat-form-field appearance="fill" class="w-full">
<mat-label>Allowed Roles (comma-separated)</mat-label>
<input matInput [(ngModel)]="currentConfig.allowedRoles" name="allowedRoles" placeholder="admin, analyst, manager" aria-label="Allowed roles">
<mat-hint>Roles that can access this widget</mat-hint>
</mat-form-field>
</div>
</div>
<div *ngIf="widgetType === 'PieChartWidgetComponent' || widgetType === 'BarChartWidgetComponent' || widgetType === 'AreaChartWidgetComponent' || widgetType === 'DoughnutChartWidgetComponent' || widgetType === 'FunnelChartWidgetComponent'">
......
......@@ -86,6 +86,80 @@ export class WidgetConfigComponent implements OnInit {
if (!this.currentConfig.yFields) this.currentConfig.yFields = [];
if (!this.currentConfig.series) this.currentConfig.series = [];
}
// Initialize Simple KPI Widget default values
if (this.widgetType === 'SimpleKpiWidgetComponent') {
// Basic configuration defaults
if (!this.currentConfig.title) this.currentConfig.title = 'KPI Widget';
if (!this.currentConfig.valueField) this.currentConfig.valueField = 'value';
if (!this.currentConfig.labelField) this.currentConfig.labelField = 'label';
if (!this.currentConfig.aggregation) this.currentConfig.aggregation = 'sum';
if (!this.currentConfig.unit) this.currentConfig.unit = '';
if (!this.currentConfig.icon) this.currentConfig.icon = 'info';
if (this.currentConfig.decimalPlaces === undefined) this.currentConfig.decimalPlaces = 0;
// Trend configuration defaults
if (this.currentConfig.showTrend === undefined) this.currentConfig.showTrend = false;
if (!this.currentConfig.trendField) this.currentConfig.trendField = 'trend';
if (!this.currentConfig.trendType) this.currentConfig.trendType = 'percentage';
// Style configuration defaults
if (!this.currentConfig.backgroundColor) this.currentConfig.backgroundColor = 'linear-gradient(to top right, #3366FF, #00CCFF)';
if (!this.currentConfig.textColor) this.currentConfig.textColor = '#FFFFFF';
if (!this.currentConfig.accentColor) this.currentConfig.accentColor = '#FFFFFF';
if (!this.currentConfig.borderColor) this.currentConfig.borderColor = '#FFFFFF';
if (!this.currentConfig.iconColor) this.currentConfig.iconColor = '#FFFFFF';
if (this.currentConfig.borderRadius === undefined) this.currentConfig.borderRadius = 8;
if (this.currentConfig.padding === undefined) this.currentConfig.padding = 16;
if (this.currentConfig.margin === undefined) this.currentConfig.margin = 8;
if (this.currentConfig.borderWidth === undefined) this.currentConfig.borderWidth = 1;
if (this.currentConfig.fontSize === undefined) this.currentConfig.fontSize = 16;
if (!this.currentConfig.fontWeight) this.currentConfig.fontWeight = 'normal';
if (!this.currentConfig.fontFamily) this.currentConfig.fontFamily = 'system-ui, -apple-system, sans-serif';
if (!this.currentConfig.customCSS) this.currentConfig.customCSS = '';
// Animation configuration defaults
if (this.currentConfig.enableAnimations === undefined) this.currentConfig.enableAnimations = true;
if (!this.currentConfig.animationType) this.currentConfig.animationType = 'fade';
if (this.currentConfig.animationDuration === undefined) this.currentConfig.animationDuration = 300;
if (this.currentConfig.animationDelay === undefined) this.currentConfig.animationDelay = 0;
if (this.currentConfig.hoverEffects === undefined) this.currentConfig.hoverEffects = true;
// Interaction configuration defaults
if (this.currentConfig.enableTooltip === undefined) this.currentConfig.enableTooltip = true;
if (this.currentConfig.enableClick === undefined) this.currentConfig.enableClick = true;
if (this.currentConfig.enableHover === undefined) this.currentConfig.enableHover = true;
if (this.currentConfig.enableSelection === undefined) this.currentConfig.enableSelection = false;
if (this.currentConfig.enableExport === undefined) this.currentConfig.enableExport = false;
if (this.currentConfig.enableRefresh === undefined) this.currentConfig.enableRefresh = true;
if (!this.currentConfig.clickAction) this.currentConfig.clickAction = 'none';
if (!this.currentConfig.customClickHandler) this.currentConfig.customClickHandler = '';
// Layout configuration defaults
if (this.currentConfig.width === undefined) this.currentConfig.width = 300;
if (this.currentConfig.height === undefined) this.currentConfig.height = 200;
if (this.currentConfig.minWidth === undefined) this.currentConfig.minWidth = 200;
if (this.currentConfig.minHeight === undefined) this.currentConfig.minHeight = 150;
if (this.currentConfig.maxWidth === undefined) this.currentConfig.maxWidth = 600;
if (this.currentConfig.maxHeight === undefined) this.currentConfig.maxHeight = 400;
if (!this.currentConfig.aspectRatio) this.currentConfig.aspectRatio = 'auto';
if (this.currentConfig.responsive === undefined) this.currentConfig.responsive = true;
// Data configuration defaults
if (!this.currentConfig.dataSource) this.currentConfig.dataSource = 'static';
if (!this.currentConfig.apiEndpoint) this.currentConfig.apiEndpoint = '';
if (this.currentConfig.refreshInterval === undefined) this.currentConfig.refreshInterval = 0;
if (this.currentConfig.cacheEnabled === undefined) this.currentConfig.cacheEnabled = false;
if (this.currentConfig.cacheDuration === undefined) this.currentConfig.cacheDuration = 300;
if (!this.currentConfig.dataTransform) this.currentConfig.dataTransform = '';
// Security configuration defaults
if (this.currentConfig.requireAuth === undefined) this.currentConfig.requireAuth = false;
if (!this.currentConfig.allowedRoles) this.currentConfig.allowedRoles = '';
if (this.currentConfig.dataEncryption === undefined) this.currentConfig.dataEncryption = false;
if (this.currentConfig.auditLog === undefined) this.currentConfig.auditLog = false;
if (this.currentConfig.rateLimit === undefined) this.currentConfig.rateLimit = 0;
}
}
resetConfig(): void {
......
......@@ -21,7 +21,7 @@
<!-- Content -->
<div *ngIf="!isLoading && !hasError" class="grid grid-cols-1 sm:grid-cols-3 gap-4 h-full">
<!-- Present -->
<div class="flex flex-col items-center justify-center p-4 bg-green-50 rounded-lg">
<i class="bi bi-person-check-fill text-3xl text-green-500"></i>
......
<!-- simple-kpi-widget.component.html -->
<div [style.border-color]="borderColor" class="relative flex flex-col h-full rounded-xl bg-white bg-clip-border text-gray-700 shadow-md transition-shadow duration-300 ease-in-out hover:shadow-lg hover:shadow-gray-900/10 border-2">
<div
class="simple-kpi-widget"
[ngStyle]="getAllStyles()"
[ngClass]="['custom-styled', getInteractionClasses()]"
[attr.data-widget-id]="widgetId"
[attr.data-source]="dataSource"
[title]="enableTooltip ? (title + ': ' + value + (unit ? ' ' + unit : '')) : null"
[attr.role]="enableClick ? 'button' : 'img'"
[attr.tabindex]="enableClick ? '0' : null"
[attr.aria-label]="title + ': ' + value + (unit ? ' ' + unit : '')"
[attr.data-security]="getSecurityAttributes()"
(click)="onWidgetClick($event)"
(keydown.enter)="enableClick ? onWidgetClick($event) : null"
(keydown.space)="enableClick ? onWidgetClick($event) : null">
<!-- Custom CSS -->
<style *ngIf="hasCustomCSS()" [innerHTML]="customCSS"></style>
<!-- Header -->
<div [style.background]="backgroundColor" class="relative mx-4 -mt-4 rounded-xl bg-clip-border text-white shadow-lg shadow-blue-500/40 p-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<i *ngIf="icon" [style.color]="iconColor" [class]="'bi ' + icon + ' text-3xl'"></i>
<h4 class="text-lg font-semibold truncate">{{ title }}</h4>
<div class="widget-header" [style.background]="backgroundColor">
<div class="header-content">
<div class="header-left">
<i *ngIf="icon" [style.color]="iconColor" [class]="'bi ' + icon + ' header-icon'"></i>
<h4 class="widget-title" [style.color]="textColor">{{ title }}</h4>
</div>
<div *ngIf="configObj?.trendValue" class="text-sm font-medium">
{{ configObj.trendValue }}
<div *ngIf="showTrend && trendValue" class="trend-indicator" [ngStyle]="getTrendStyles()">
{{ trendValue }}
</div>
</div>
</div>
<!-- Body -->
<div class="flex-1 flex justify-center items-center p-3">
<div class="widget-body">
<!-- Loading State -->
<div *ngIf="isLoading" class="text-center">
<div class="animate-spin rounded-full h-12 w-12 border-t-4 border-b-4 border-blue-500 mx-auto"></div>
<p class="text-gray-500 mt-2 text-base">Loading Data...</p>
<div *ngIf="isLoading" class="loading-state">
<div class="loading-spinner"></div>
<p class="loading-text" [style.color]="textColor">Loading Data...</p>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="text-center text-red-500">
<i class="bi bi-x-octagon-fill text-4xl"></i>
<p class="mt-2 text-lg font-semibold">Error Loading</p>
<p class="text-sm text-red-400">{{ errorMessage }}</p>
<div *ngIf="hasError" class="error-state">
<i class="bi bi-x-octagon-fill error-icon"></i>
<p class="error-title">Error Loading</p>
<p class="error-message">{{ errorMessage }}</p>
</div>
<!-- Content -->
<div *ngIf="!isLoading && !hasError" class="text-center">
<p class="block font-sans text-5xl font-bold leading-snug tracking-normal text-blue-gray-900 antialiased">
<div *ngIf="!isLoading && !hasError && hasRequiredRole()" class="widget-content">
<div class="kpi-value" [style.color]="accentColor">
{{ value }}
</p>
<p *ngIf="unit" class="block font-sans text-xl font-normal leading-relaxed text-blue-gray-600 antialiased">
</div>
<div *ngIf="unit" class="kpi-unit" [style.color]="textColor">
{{ unit }}
</p>
</div>
<!-- Data Source Info (Debug Mode) -->
<div *ngIf="dataSource !== 'static'" class="data-source-info" [style.color]="textColor">
<small>{{ getDataSourceInfo() }}</small>
</div>
</div>
<!-- Security Warning -->
<div *ngIf="!isLoading && !hasError && !hasRequiredRole()" class="security-warning">
<i class="bi bi-shield-exclamation-fill"></i>
<p>Access Denied</p>
<small>Insufficient permissions</small>
</div>
</div>
<!-- Export Button (if enabled) -->
<div *ngIf="enableExport" class="export-button">
<button
class="btn btn-sm btn-outline-light"
(click)="exportData($event)"
title="Export Data">
<i class="bi bi-download"></i>
</button>
</div>
<!-- Refresh Button (if enabled) -->
<div *ngIf="enableRefresh && dataSource !== 'static'" class="refresh-button">
<button
class="btn btn-sm btn-outline-light"
(click)="refreshData($event)"
title="Refresh Data">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
</div>
/* Simple KPI Widget Styles */
.simple-kpi-widget {
display: flex;
flex-direction: column;
height: 100%;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: white;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
overflow: hidden;
position: relative;
&:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transform: translateY(-2px);
}
&.custom-styled {
// Custom styles will be applied via the customCSS property
}
}
/* Header */
.widget-header {
padding: 1rem;
border-radius: 8px 8px 0 0;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
pointer-events: none;
}
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 1;
}
.header-left {
display: flex;
align-items: center;
gap: 0.75rem;
}
.header-icon {
font-size: 1.5rem;
opacity: 0.9;
}
.widget-title {
font-size: 1.125rem;
font-weight: 600;
margin: 0;
opacity: 0.95;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.trend-indicator {
font-size: 0.875rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 4px;
background: rgba(255, 255, 255, 0.2);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* Body */
.widget-body {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
position: relative;
}
/* Loading State */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.loading-spinner {
width: 3rem;
height: 3rem;
border: 3px solid #e5e7eb;
border-top: 3px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
font-size: 0.875rem;
font-weight: 500;
opacity: 0.7;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Error State */
.error-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
text-align: center;
}
.error-icon {
font-size: 2.5rem;
color: #ef4444;
opacity: 0.8;
}
.error-title {
font-size: 1.125rem;
font-weight: 600;
color: #ef4444;
margin: 0;
}
.error-message {
font-size: 0.875rem;
color: #f87171;
margin: 0;
opacity: 0.8;
}
/* Content */
.widget-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
text-align: center;
}
.kpi-value {
font-size: 3rem;
font-weight: 700;
line-height: 1;
margin: 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
background: linear-gradient(135deg, currentColor 0%, rgba(255, 255, 255, 0.8) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.kpi-unit {
font-size: 1.25rem;
font-weight: 500;
margin: 0;
opacity: 0.8;
}
/* Responsive Design */
@media (max-width: 768px) {
.widget-header {
padding: 0.75rem;
}
.header-left {
gap: 0.5rem;
}
.header-icon {
font-size: 1.25rem;
}
.widget-title {
font-size: 1rem;
}
.widget-body {
padding: 1rem;
}
.kpi-value {
font-size: 2.5rem;
}
.kpi-unit {
font-size: 1.125rem;
}
.trend-indicator {
font-size: 0.75rem;
padding: 0.25rem 0.375rem;
}
}
@media (max-width: 480px) {
.widget-header {
padding: 0.5rem;
}
.header-left {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.header-content {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.widget-title {
font-size: 0.875rem;
}
.widget-body {
padding: 0.75rem;
}
.kpi-value {
font-size: 2rem;
}
.kpi-unit {
font-size: 1rem;
}
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
.simple-kpi-widget {
background: #1f2937;
border-color: #374151;
color: #f9fafb;
}
.loading-spinner {
border-color: #374151;
border-top-color: #60a5fa;
}
.loading-text {
color: #9ca3af;
}
.error-icon {
color: #f87171;
}
.error-title {
color: #f87171;
}
.error-message {
color: #fca5a5;
}
}
/* Animation Enhancements */
.simple-kpi-widget.animate-in {
animation: fadeInUp 0.5s ease-out;
}
.simple-kpi-widget.animate-out {
animation: fadeOutDown 0.3s ease-in;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeOutDown {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(20px);
}
}
/* Accessibility */
.simple-kpi-widget {
&:focus-within {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
}
.widget-title {
&:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
border-radius: 4px;
}
}
/* Print Styles */
@media print {
.simple-kpi-widget {
box-shadow: none;
border: 1px solid #000;
break-inside: avoid;
}
.widget-header {
background: #f8f9fa !important;
color: #000 !important;
}
.kpi-value {
-webkit-text-fill-color: initial !important;
color: #000 !important;
}
.kpi-unit {
color: #000 !important;
}
}
/* High Contrast Mode */
@media (prefers-contrast: high) {
.simple-kpi-widget {
border-width: 2px;
}
.widget-header {
border-bottom: 2px solid currentColor;
}
.trend-indicator {
border-width: 2px;
}
}
/* Interaction Styles */
.simple-kpi-widget {
&.hover-enabled {
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
}
&.click-enabled {
cursor: pointer;
&:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
&:active {
transform: translateY(1px);
}
}
&.tooltip-enabled {
position: relative;
}
&.responsive {
@media (max-width: 768px) {
.widget-header {
padding: 12px;
}
.header-content {
flex-direction: column;
gap: 8px;
}
.kpi-value {
font-size: 1.5rem;
}
}
}
}
/* Security Warning */
.security-warning {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 20px;
text-align: center;
color: #dc3545;
background: rgba(220, 53, 69, 0.1);
border-radius: 8px;
i {
font-size: 2rem;
margin-bottom: 10px;
}
p {
margin: 5px 0;
font-weight: 600;
}
small {
font-size: 0.875rem;
opacity: 0.8;
}
}
/* Data Source Info */
.data-source-info {
position: absolute;
bottom: 8px;
right: 8px;
font-size: 0.75rem;
opacity: 0.7;
background: rgba(0, 0, 0, 0.1);
padding: 2px 6px;
border-radius: 4px;
}
/* Action Buttons */
.export-button,
.refresh-button {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
button {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
transition: all 0.2s ease;
&:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
&:focus {
outline: 2px solid #007bff;
outline-offset: 1px;
}
}
}
.export-button {
top: 8px;
}
.refresh-button {
top: 40px;
}
/* Animation Keyframes */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
/* Layout Constraints */
.simple-kpi-widget {
&.aspect-ratio-16-9 {
aspect-ratio: 16/9;
}
&.aspect-ratio-4-3 {
aspect-ratio: 4/3;
}
&.aspect-ratio-1-1 {
aspect-ratio: 1/1;
}
&.aspect-ratio-3-2 {
aspect-ratio: 3/2;
}
}
/* High Contrast Mode */
@media (prefers-contrast: high) {
.simple-kpi-widget {
border: 2px solid;
.widget-header {
border-bottom: 2px solid;
}
.trend-indicator {
border: 1px solid;
background: transparent;
}
}
}
/* Reduced Motion */
@media (prefers-reduced-motion: reduce) {
.simple-kpi-widget {
transition: none;
&:hover {
transform: none;
}
&.hover-enabled:hover {
transform: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
.simple-kpi-widget .loading-spinner {
animation: none;
}
.simple-kpi-widget.animate-in,
.simple-kpi-widget.animate-out {
animation: none;
}
.export-button button,
.refresh-button button {
&:hover {
transform: none;
}
}
}
......@@ -12,35 +12,171 @@ import { BaseWidgetComponent } from '../base-widget.component';
styleUrls: ['./simple-kpi-widget.component.scss']
})
export class SimpleKpiWidgetComponent extends BaseWidgetComponent {
// Widget identification
public widgetId: string = 'simple-kpi-widget';
// Display properties
public value: string = '...';
public unit: string = '';
public icon: string = '';
public backgroundColor: string = 'linear-gradient(to top right, #3366FF, #00CCFF)';
public iconColor: string = '#FFFFFF';
public borderColor: string = '#FFFFFF';
public textColor: string = '#FFFFFF';
public accentColor: string = '#FFFFFF';
public borderRadius: number = 8;
// Trend properties
public showTrend: boolean = false;
public trendValue: string = '';
public trendType: string = 'percentage';
public trendColor: string = '#28a745';
// Style properties
public fontSize: number = 16;
public fontWeight: string = 'normal';
public fontFamily: string = 'system-ui, -apple-system, sans-serif';
public padding: number = 16;
public margin: number = 8;
public borderWidth: number = 1;
public customCSS: string = '';
// Animation properties
public enableAnimations: boolean = true;
public animationType: string = 'fade';
public animationDuration: number = 300;
public animationDelay: number = 0;
public hoverEffects: boolean = true;
// Interaction properties
public enableTooltip: boolean = true;
public enableClick: boolean = true;
public enableHover: boolean = true;
public enableSelection: boolean = false;
public enableExport: boolean = false;
public enableRefresh: boolean = true;
public clickAction: string = 'none';
public customClickHandler: string = '';
// Layout properties
public width: number = 300;
public height: number = 200;
public minWidth: number = 200;
public minHeight: number = 150;
public maxWidth: number = 600;
public maxHeight: number = 400;
public aspectRatio: string = 'auto';
public responsive: boolean = true;
// Data properties
public dataSource: string = 'static';
public apiEndpoint: string = '';
public refreshInterval: number = 0;
public cacheEnabled: boolean = false;
public cacheDuration: number = 300;
public dataTransform: string = '';
// Security properties
public requireAuth: boolean = false;
public allowedRoles: string = '';
public dataEncryption: boolean = false;
public auditLog: boolean = false;
public rateLimit: number = 0;
constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
}
applyInitialConfig(): void {
// Basic configuration
this.title = this.configObj.title || 'KPI';
this.unit = this.configObj.unit || '';
this.icon = this.configObj.icon || 'info';
// Handle color property (fallback to backgroundColor)
const bgColor = this.configObj.backgroundColor || this.configObj.color || 'linear-gradient(to top right, #3366FF, #00CCFF)';
this.backgroundColor = bgColor.startsWith('#') ? `linear-gradient(to top right, ${bgColor}, ${bgColor}dd)` : bgColor;
this.iconColor = this.configObj.iconColor || '#FFFFFF';
// Style configuration
this.backgroundColor = this.configObj.backgroundColor || 'linear-gradient(to top right, #3366FF, #00CCFF)';
this.textColor = this.configObj.textColor || '#FFFFFF';
this.accentColor = this.configObj.accentColor || '#FFFFFF';
this.borderColor = this.configObj.borderColor || '#FFFFFF';
this.borderRadius = this.configObj.borderRadius || 8;
this.iconColor = this.configObj.iconColor || '#FFFFFF';
// Typography configuration
this.fontSize = this.configObj.fontSize || 16;
this.fontWeight = this.configObj.fontWeight || 'normal';
this.fontFamily = this.configObj.fontFamily || 'system-ui, -apple-system, sans-serif';
// Layout configuration
this.padding = this.configObj.padding || 16;
this.margin = this.configObj.margin || 8;
this.borderWidth = this.configObj.borderWidth || 1;
// Custom CSS
this.customCSS = this.configObj.customCSS || '';
// Trend configuration
this.showTrend = this.configObj.showTrend || false;
this.trendType = this.configObj.trendType || 'percentage';
this.trendColor = this.configObj.trendColor || '#28a745';
// Animation configuration
this.enableAnimations = this.configObj.enableAnimations !== undefined ? this.configObj.enableAnimations : true;
this.animationType = this.configObj.animationType || 'fade';
this.animationDuration = this.configObj.animationDuration || 300;
this.animationDelay = this.configObj.animationDelay || 0;
this.hoverEffects = this.configObj.hoverEffects !== undefined ? this.configObj.hoverEffects : true;
// Interaction configuration
this.enableTooltip = this.configObj.enableTooltip !== undefined ? this.configObj.enableTooltip : true;
this.enableClick = this.configObj.enableClick !== undefined ? this.configObj.enableClick : true;
this.enableHover = this.configObj.enableHover !== undefined ? this.configObj.enableHover : true;
this.enableSelection = this.configObj.enableSelection !== undefined ? this.configObj.enableSelection : false;
this.enableExport = this.configObj.enableExport !== undefined ? this.configObj.enableExport : false;
this.enableRefresh = this.configObj.enableRefresh !== undefined ? this.configObj.enableRefresh : true;
this.clickAction = this.configObj.clickAction || 'none';
this.customClickHandler = this.configObj.customClickHandler || '';
// Layout configuration
this.width = this.configObj.width || 300;
this.height = this.configObj.height || 200;
this.minWidth = this.configObj.minWidth || 200;
this.minHeight = this.configObj.minHeight || 150;
this.maxWidth = this.configObj.maxWidth || 600;
this.maxHeight = this.configObj.maxHeight || 400;
this.aspectRatio = this.configObj.aspectRatio || 'auto';
this.responsive = this.configObj.responsive !== undefined ? this.configObj.responsive : true;
// Data configuration
this.dataSource = this.configObj.dataSource || 'static';
this.apiEndpoint = this.configObj.apiEndpoint || '';
this.refreshInterval = this.configObj.refreshInterval || 0;
this.cacheEnabled = this.configObj.cacheEnabled !== undefined ? this.configObj.cacheEnabled : false;
this.cacheDuration = this.configObj.cacheDuration || 300;
this.dataTransform = this.configObj.dataTransform || '';
// Security configuration
this.requireAuth = this.configObj.requireAuth !== undefined ? this.configObj.requireAuth : false;
this.allowedRoles = this.configObj.allowedRoles || '';
this.dataEncryption = this.configObj.dataEncryption !== undefined ? this.configObj.dataEncryption : false;
this.auditLog = this.configObj.auditLog !== undefined ? this.configObj.auditLog : false;
this.rateLimit = this.configObj.rateLimit || 0;
// Handle gradient background for hex colors
if (this.backgroundColor.startsWith('#')) {
this.backgroundColor = `linear-gradient(to top right, ${this.backgroundColor}, ${this.backgroundColor}dd)`;
}
this.value = '-'; // Initial state before data loads
}
onDataUpdate(data: any[]): void {
override onDataUpdate(data: any[]): void {
// Transform data if transform function is provided
const transformedData = this.transformData(data);
// Handle count aggregation separately as it doesn't need a valueField
if (this.configObj.aggregation === 'count') {
this.value = (data?.length || 0).toLocaleString();
this.value = (transformedData?.length || 0).toLocaleString();
this.updateTrendData(transformedData);
return;
}
......@@ -52,24 +188,424 @@ export class SimpleKpiWidgetComponent extends BaseWidgetComponent {
}
// If data is empty, result is 0
if (!data || data.length === 0) {
if (!transformedData || transformedData.length === 0) {
this.value = '0';
this.trendValue = '';
return;
}
let kpiValue = 0;
if (this.configObj.aggregation === 'sum') {
kpiValue = data.reduce((sum, item) => sum + (item[this.configObj.valueField] || 0), 0);
kpiValue = transformedData.reduce((sum, item) => sum + (item[this.configObj.valueField] || 0), 0);
} else if (this.configObj.aggregation === 'average') {
const sum = transformedData.reduce((sum, item) => sum + (item[this.configObj.valueField] || 0), 0);
kpiValue = sum / transformedData.length;
} else if (this.configObj.aggregation === 'max') {
kpiValue = Math.max(...transformedData.map(item => item[this.configObj.valueField] || 0));
} else if (this.configObj.aggregation === 'min') {
kpiValue = Math.min(...transformedData.map(item => item[this.configObj.valueField] || 0));
} else {
// Default to first value if no aggregation is specified
kpiValue = data[0][this.configObj.valueField] || 0;
kpiValue = transformedData[0][this.configObj.valueField] || 0;
}
// Format the value based on configuration
this.value = this.formatValue(kpiValue);
// Update trend data if enabled
this.updateTrendData(transformedData, kpiValue);
}
private formatValue(value: number): string {
if (this.configObj.decimalPlaces !== undefined) {
return value.toFixed(this.configObj.decimalPlaces);
}
return value.toLocaleString();
}
private updateTrendData(data: any[], currentValue?: number): void {
if (!this.showTrend || !this.configObj.trendField) {
this.trendValue = '';
return;
}
// Calculate trend based on trendField
if (data && data.length > 0) {
const trendData = data.map(item => item[this.configObj.trendField] || 0);
if (this.trendType === 'percentage') {
// Calculate percentage change
if (trendData.length >= 2) {
const current = trendData[0];
const previous = trendData[1];
const change = ((current - previous) / previous) * 100;
this.trendValue = `${change >= 0 ? '+' : ''}${change.toFixed(1)}%`;
this.trendColor = change >= 0 ? '#28a745' : '#dc3545';
}
} else if (this.trendType === 'absolute') {
// Calculate absolute change
if (trendData.length >= 2) {
const current = trendData[0];
const previous = trendData[1];
const change = current - previous;
this.trendValue = `${change >= 0 ? '+' : ''}${change.toLocaleString()}`;
this.trendColor = change >= 0 ? '#28a745' : '#dc3545';
}
} else if (this.trendType === 'ratio') {
// Calculate ratio
if (trendData.length >= 2) {
const current = trendData[0];
const previous = trendData[1];
const ratio = previous > 0 ? current / previous : 1;
this.trendValue = `${ratio.toFixed(2)}x`;
this.trendColor = ratio >= 1 ? '#28a745' : '#dc3545';
}
}
}
this.value = kpiValue.toLocaleString();
}
onReset(): void {
// Reset to default values
this.title = 'KPI (Default)';
this.value = '123,456';
this.unit = '#';
this.icon = 'info';
// Style reset
this.backgroundColor = 'linear-gradient(to top right, #3366FF, #00CCFF)';
this.textColor = '#FFFFFF';
this.accentColor = '#FFFFFF';
this.borderColor = '#FFFFFF';
this.iconColor = '#FFFFFF';
this.borderRadius = 8;
this.fontSize = 16;
this.fontWeight = 'normal';
this.fontFamily = 'system-ui, -apple-system, sans-serif';
this.padding = 16;
this.margin = 8;
this.borderWidth = 1;
this.customCSS = '';
// Trend reset
this.showTrend = false;
this.trendValue = '';
this.trendType = 'percentage';
this.trendColor = '#28a745';
// Animation reset
this.enableAnimations = true;
this.animationType = 'fade';
this.animationDuration = 300;
this.animationDelay = 0;
this.hoverEffects = true;
// Interaction reset
this.enableTooltip = true;
this.enableClick = true;
this.enableHover = true;
this.enableSelection = false;
this.enableExport = false;
this.enableRefresh = true;
this.clickAction = 'none';
this.customClickHandler = '';
// Layout reset
this.width = 300;
this.height = 200;
this.minWidth = 200;
this.minHeight = 150;
this.maxWidth = 600;
this.maxHeight = 400;
this.aspectRatio = 'auto';
this.responsive = true;
// Data reset
this.dataSource = 'static';
this.apiEndpoint = '';
this.refreshInterval = 0;
this.cacheEnabled = false;
this.cacheDuration = 300;
this.dataTransform = '';
// Security reset
this.requireAuth = false;
this.allowedRoles = '';
this.dataEncryption = false;
this.auditLog = false;
this.rateLimit = 0;
}
// Helper method to get computed styles for dynamic styling
getWidgetStyles(): { [key: string]: string } {
return {
'background': this.backgroundColor,
'color': this.textColor,
'border-color': this.borderColor,
'border-radius': `${this.borderRadius}px`,
'border-width': `${this.borderWidth}px`,
'padding': `${this.padding}px`,
'margin': `${this.margin}px`,
'font-size': `${this.fontSize}px`,
'font-weight': this.fontWeight,
'font-family': this.fontFamily
};
}
// Helper method to combine all styles
getAllStyles(): { [key: string]: string } {
return {
...this.getWidgetStyles(),
...this.getLayoutStyles(),
...this.getAnimationStyles()
};
}
// Helper method to get trend styles
getTrendStyles(): { [key: string]: string } {
return {
'color': this.trendColor,
'font-weight': 'bold'
};
}
// Helper method to check if custom CSS should be applied
hasCustomCSS(): boolean {
return !!(this.customCSS && this.customCSS.trim().length > 0);
}
// Helper method to get animation styles
getAnimationStyles(): { [key: string]: string } {
if (!this.enableAnimations) {
return { 'animation': 'none' };
}
const animationMap: { [key: string]: string } = {
'fade': 'fadeIn',
'slide': 'slideInUp',
'bounce': 'bounceIn',
'pulse': 'pulse',
'none': 'none'
};
return {
'animation': `${animationMap[this.animationType]} ${this.animationDuration}ms ease-in-out`,
'animation-delay': `${this.animationDelay}ms`,
'animation-fill-mode': 'both'
};
}
// Helper method to get layout styles
getLayoutStyles(): { [key: string]: string } {
const styles: { [key: string]: string } = {
'width': `${this.width}px`,
'height': `${this.height}px`,
'min-width': `${this.minWidth}px`,
'min-height': `${this.minHeight}px`,
'max-width': `${this.maxWidth}px`,
'max-height': `${this.maxHeight}px`
};
// Handle aspect ratio
if (this.aspectRatio !== 'auto') {
const [width, height] = this.aspectRatio.split(':');
const ratio = parseFloat(height) / parseFloat(width);
styles['aspect-ratio'] = this.aspectRatio;
}
return styles;
}
// Helper method to get interaction classes
getInteractionClasses(): string {
const classes: string[] = [];
if (this.enableHover && this.hoverEffects) {
classes.push('hover-enabled');
}
if (this.enableClick) {
classes.push('click-enabled');
}
if (this.enableTooltip) {
classes.push('tooltip-enabled');
}
if (this.responsive) {
classes.push('responsive');
}
return classes.join(' ');
}
// Helper method to handle click events
onWidgetClick(event: Event): void {
if (!this.enableClick) {
return;
}
// Audit logging
if (this.auditLog) {
console.log('Widget clicked:', {
widgetId: this.widgetId,
timestamp: new Date().toISOString(),
clickAction: this.clickAction
});
}
switch (this.clickAction) {
case 'drill_down':
this.handleDrillDown(event);
break;
case 'open_modal':
this.handleOpenModal(event);
break;
case 'navigate':
this.handleNavigate(event);
break;
case 'custom':
this.handleCustomClick(event);
break;
default:
// No action
break;
}
}
// Helper methods for different click actions
private handleDrillDown(event: Event): void {
console.log('Drill down action triggered');
// Implement drill down logic
}
private handleOpenModal(event: Event): void {
console.log('Open modal action triggered');
// Implement modal opening logic
}
private handleNavigate(event: Event): void {
console.log('Navigate action triggered');
// Implement navigation logic
}
private handleCustomClick(event: Event): void {
if (this.customClickHandler) {
try {
// Safely execute custom handler
const handler = new Function('event', 'widget', this.customClickHandler);
handler(event, this);
} catch (error) {
console.error('Error executing custom click handler:', error);
}
}
}
// Helper method to get security attributes
getSecurityAttributes(): { [key: string]: string } {
const attrs: { [key: string]: string } = {};
if (this.requireAuth) {
attrs['data-require-auth'] = 'true';
}
if (this.allowedRoles) {
attrs['data-allowed-roles'] = this.allowedRoles;
}
if (this.dataEncryption) {
attrs['data-encrypted'] = 'true';
}
return attrs;
}
// Helper method to check if user has required role
hasRequiredRole(): boolean {
if (!this.requireAuth || !this.allowedRoles) {
return true;
}
// This would typically check against user's actual roles
// For now, we'll assume the user has the required role
return true;
}
// Helper method to get data source info
getDataSourceInfo(): string {
switch (this.dataSource) {
case 'api':
return `API: ${this.apiEndpoint}`;
case 'websocket':
return 'WebSocket Connection';
case 'file':
return 'File Upload';
default:
return 'Static Data';
}
}
// Export data functionality
exportData(event: Event): void {
event.stopPropagation();
const exportData = {
widgetId: this.widgetId,
title: this.title,
value: this.value,
unit: this.unit,
timestamp: new Date().toISOString(),
data: this.originalData
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `kpi-widget-${this.widgetId}-${new Date().toISOString().split('T')[0]}.json`;
link.click();
window.URL.revokeObjectURL(url);
console.log('Data exported:', exportData);
}
// Refresh data functionality
refreshData(event: Event): void {
event.stopPropagation();
if (this.isLoading) {
return; // Prevent multiple simultaneous refreshes
}
this.isLoading = true;
this.hasError = false;
// Simulate API call or data refresh
setTimeout(() => {
// This would typically make an actual API call
console.log('Refreshing data from:', this.apiEndpoint);
// For demo purposes, we'll just trigger a data update
if (this.originalData) {
this.onDataUpdate(this.originalData);
}
this.isLoading = false;
}, 1000);
}
// Method to transform data if transform function is provided
private transformData(data: any[]): any[] {
if (!this.dataTransform || !data) {
return data;
}
try {
const transformFunction = new Function('data', this.dataTransform);
return transformFunction(data);
} catch (error) {
console.error('Error transforming data:', error);
return data;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment