Commit c21d15b2 by Ooh-Ao

pivot

parent f0065caa
......@@ -1118,23 +1118,104 @@ export class WidgetConfigRegistryService {
// ========================================
'SyncfusionPivotWidgetComponent': {
...baseConfig,
// Basic Configuration
expandAll: false,
displayOptionView: 'Both',
chartType: 'Column',
dataSource: 'static',
apiEndpoint: '',
// Pivot Features
showToolbar: true,
showFieldList: true,
showGroupingBar: true,
showPager: true,
allowCalculatedField: false,
allowConditionalFormatting: false,
allowNumberFormatting: true,
allowFieldDragDrop: true,
allowSubTotal: true,
allowGrandTotal: true,
allowDrillThrough: false,
allowSorting: true,
allowExcelExport: true,
allowPdfExport: true,
allowCsvExport: true,
allowPrint: true,
aggregationType: 'sum',
numberFormat: 'N',
// Pivot Fields
rows: [],
columns: [],
values: [],
filters: [],
enableDrillThrough: true,
enableSorting: true,
// Enhanced UI Configuration
dataCount: 0,
lastUpdated: new Date(),
shadow: 'medium',
isLoading: false,
hasError: false,
errorMessage: '',
isEmpty: false,
// Enhanced Interaction Configuration
enableKeyboard: true,
enableFocus: true,
hoverEffect: true,
clickEffect: true,
enableTooltip: true,
enableSelection: true,
enableExport: true,
enableRefresh: true,
clickAction: 'none',
customClickHandler: '',
// Enhanced Layout Configuration
fullWidth: false,
fullHeight: false,
widthUnit: 'px',
heightUnit: 'px',
sizeOption: 'medium',
aspectRatio: 'auto',
// Enhanced Data Configuration
enableFiltering: true,
enableSorting: true,
enableGrouping: true,
enablePaging: true,
pageSize: 10,
currentPage: 1,
totalPages: 1,
// Enhanced Security Configuration
requireHttps: false,
allowedRoles: '',
dataEncryption: false,
auditLog: false,
rateLimit: 0,
// Enhanced Animation Configuration
enableAnimations: true,
animationType: 'fade',
animationDuration: 300,
animationDelay: 0,
// Enhanced Style Configuration
customCSS: '',
theme: 'light',
fontSize: 14,
fontWeight: 'normal',
fontFamily: 'system-ui, -apple-system, sans-serif',
padding: 16,
margin: 8,
// Additional Pivot Configuration
showGrandTotals: true,
showSubTotals: true,
showRowTotals: true,
showColumnTotals: true,
enableExport: true,
exportFormats: ['excel', 'pdf', 'csv']
},
......
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { Component, Input, Output, EventEmitter, OnInit, AfterViewInit, ChangeDetectorRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
......@@ -31,7 +31,10 @@ import { BaseConfigComponent } from '../../../widget-config/base-config/base-con
<!-- Basic Configuration Tab -->
<mat-tab label="Basic">
<div class="config-section">
<h3 class="section-title text-blue-600">Basic Configuration</h3>
<h3 class="section-title text-blue-600">
<i class="bi bi-gear-fill mr-2"></i>
Basic Configuration
</h3>
<mat-form-field appearance="fill" class="w-full">
<mat-label>Title</mat-label>
......@@ -87,7 +90,7 @@ import { BaseConfigComponent } from '../../../widget-config/base-config/base-con
<h3 class="section-title text-blue-600">Size Configuration</h3>
<div class="size-config">
<div
*ngFor="let option of sizeOptions"
*ngFor="let option of sizeOptions; trackBy: trackBySizeOption"
class="size-option"
[class.selected]="currentConfig.sizeOption === option.id"
(click)="setSizeOption(option.id)">
......@@ -231,7 +234,7 @@ import { BaseConfigComponent } from '../../../widget-config/base-config/base-con
<mat-icon>add</mat-icon> Add Row
</button>
</div>
<div *ngFor="let row of currentConfig.rows; let i = index" class="field-config-item">
<div *ngFor="let row of currentConfig.rows; let i = index; trackBy: trackByRowIndex" class="field-config-item">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Row Field</mat-label>
......@@ -268,7 +271,7 @@ import { BaseConfigComponent } from '../../../widget-config/base-config/base-con
<mat-icon>add</mat-icon> Add Column
</button>
</div>
<div *ngFor="let col of currentConfig.columns; let i = index" class="field-config-item">
<div *ngFor="let col of currentConfig.columns; let i = index; trackBy: trackByColumnIndex" class="field-config-item">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Column Field</mat-label>
......@@ -305,7 +308,7 @@ import { BaseConfigComponent } from '../../../widget-config/base-config/base-con
<mat-icon>add</mat-icon> Add Value
</button>
</div>
<div *ngFor="let val of currentConfig.values; let i = index" class="field-config-item">
<div *ngFor="let val of currentConfig.values; let i = index; trackBy: trackByValueIndex" class="field-config-item">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<mat-form-field appearance="fill">
<mat-label>Value Field</mat-label>
......@@ -352,7 +355,7 @@ import { BaseConfigComponent } from '../../../widget-config/base-config/base-con
<mat-icon>add</mat-icon> Add Filter
</button>
</div>
<div *ngFor="let fil of currentConfig.filters; let i = index" class="field-config-item">
<div *ngFor="let fil of currentConfig.filters; let i = index; trackBy: trackByFilterIndex" class="field-config-item">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Filter Field</mat-label>
......@@ -432,10 +435,191 @@ import { BaseConfigComponent } from '../../../widget-config/base-config/base-con
</div>
</mat-tab>
<!-- Enhanced UI Tab -->
<mat-tab label="UI">
<div class="config-section">
<h3 class="section-title text-indigo-600">
<i class="bi bi-palette-fill mr-2"></i>
Enhanced UI Configuration
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Shadow</mat-label>
<mat-select [(ngModel)]="currentConfig.shadow" name="shadow">
<mat-option value="none">None</mat-option>
<mat-option value="small">Small</mat-option>
<mat-option value="medium">Medium</mat-option>
<mat-option value="large">Large</mat-option>
</mat-select>
<mat-hint>Widget shadow effect</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Theme</mat-label>
<mat-select [(ngModel)]="currentConfig.theme" name="theme">
<mat-option value="light">Light</mat-option>
<mat-option value="dark">Dark</mat-option>
<mat-option value="auto">Auto</mat-option>
</mat-select>
<mat-hint>Color theme</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>Font Size (px)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.fontSize" name="fontSize" min="10" max="24">
<mat-hint>Base font size</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-select>
<mat-hint>Font weight</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Font Family</mat-label>
<input matInput [(ngModel)]="currentConfig.fontFamily" name="fontFamily" placeholder="system-ui, sans-serif">
<mat-hint>Font family</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>Padding (px)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.padding" name="padding" min="0" max="50">
<mat-hint>Internal padding</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Margin (px)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.margin" name="margin" min="0" max="50">
<mat-hint>External margin</mat-hint>
</mat-form-field>
</div>
<div class="feature-group">
<h4 class="text-sm font-semibold text-gray-700 mb-3">Interaction Features</h4>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<mat-checkbox [(ngModel)]="currentConfig.enableKeyboard" name="enableKeyboard">
Enable Keyboard
</mat-checkbox>
<mat-checkbox [(ngModel)]="currentConfig.enableFocus" name="enableFocus">
Enable Focus
</mat-checkbox>
<mat-checkbox [(ngModel)]="currentConfig.hoverEffect" name="hoverEffect">
Hover Effect
</mat-checkbox>
<mat-checkbox [(ngModel)]="currentConfig.clickEffect" name="clickEffect">
Click Effect
</mat-checkbox>
<mat-checkbox [(ngModel)]="currentConfig.enableTooltip" name="enableTooltip">
Enable Tooltip
</mat-checkbox>
<mat-checkbox [(ngModel)]="currentConfig.enableSelection" name="enableSelection">
Enable Selection
</mat-checkbox>
</div>
</div>
</div>
</mat-tab>
<!-- Animation Tab -->
<mat-tab label="Animation">
<div class="config-section">
<h3 class="section-title text-pink-600">
<i class="bi bi-magic mr-2"></i>
Animation Configuration
</h3>
<div class="flex items-center space-x-4 mb-4">
<mat-checkbox [(ngModel)]="currentConfig.enableAnimations" name="enableAnimations">
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 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">
<mat-hint>Animation duration</mat-hint>
</mat-form-field>
</div>
<div *ngIf="currentConfig.enableAnimations" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Animation Delay (ms)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.animationDelay" name="animationDelay" min="0" max="1000">
<mat-hint>Animation delay</mat-hint>
</mat-form-field>
</div>
</div>
</mat-tab>
<!-- Security Tab -->
<mat-tab label="Security">
<div class="config-section">
<h3 class="section-title text-red-600">
<i class="bi bi-shield-fill mr-2"></i>
Security Configuration
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
<mat-label>Allowed Roles</mat-label>
<input matInput [(ngModel)]="currentConfig.allowedRoles" name="allowedRoles" placeholder="admin,user,manager">
<mat-hint>Comma-separated roles</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Rate Limit (requests/hour)</mat-label>
<input matInput type="number" [(ngModel)]="currentConfig.rateLimit" name="rateLimit" min="0" max="10000">
<mat-hint>0 = No limit</mat-hint>
</mat-form-field>
</div>
<div class="feature-group">
<h4 class="text-sm font-semibold text-gray-700 mb-3">Security Features</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-checkbox [(ngModel)]="currentConfig.requireHttps" name="requireHttps">
Require HTTPS
</mat-checkbox>
<mat-checkbox [(ngModel)]="currentConfig.dataEncryption" name="dataEncryption">
Data Encryption
</mat-checkbox>
<mat-checkbox [(ngModel)]="currentConfig.auditLog" name="auditLog">
Audit Logging
</mat-checkbox>
</div>
</div>
</div>
</mat-tab>
<!-- Data Tab -->
<mat-tab label="Data">
<div class="config-section">
<h3 class="section-title text-emerald-600">Data Management</h3>
<h3 class="section-title text-emerald-600">
<i class="bi bi-database-fill mr-2"></i>
Data Management
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field appearance="fill">
......@@ -475,50 +659,88 @@ import { BaseConfigComponent } from '../../../widget-config/base-config/base-con
`,
styles: [`
.config-container {
padding: 20px;
background: #f8fafc;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 24px;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
border-radius: 16px;
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.config-section {
margin-bottom: 32px;
padding: 24px;
background: white;
border-radius: 8px;
padding: 28px;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border-radius: 12px;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
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;
&:hover {
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transform: translateY(-2px);
}
}
.section-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 8px;
border-bottom: 2px solid #e2e8f0;
font-size: 1.375rem;
font-weight: 700;
margin-bottom: 24px;
padding-bottom: 12px;
border-bottom: 3px solid #e2e8f0;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: flex;
align-items: center;
gap: 8px;
i {
font-size: 1.25rem;
color: #3b82f6;
}
}
.config-tabs {
margin-top: 0;
background: white;
border-radius: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.config-tabs .mat-tab-body-content {
padding: 20px 0;
padding: 24px 0;
}
.config-tabs .mat-tab-header {
background: white;
border-radius: 8px 8px 0 0;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
border-radius: 16px 16px 0 0;
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.1);
}
.config-tabs .mat-tab-label {
min-width: 120px;
font-weight: 500;
min-width: 140px;
font-weight: 600;
font-size: 0.875rem;
padding: 16px 24px;
transition: all 0.3s ease;
color: #6b7280;
&:hover {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
}
.config-tabs .mat-tab-label-active {
color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
border-bottom: 3px solid #3b82f6;
}
.config-tabs .mat-ink-bar {
background: #3b82f6;
height: 3px;
}
.mat-form-field {
......@@ -666,12 +888,49 @@ import { BaseConfigComponent } from '../../../widget-config/base-config/base-con
.text-purple-600 { color: #9333ea; }
.text-orange-600 { color: #ea580c; }
.text-emerald-600 { color: #059669; }
.text-indigo-600 { color: #4f46e5; }
.text-pink-600 { color: #db2777; }
.text-red-600 { color: #dc2626; }
.text-cyan-600 { color: #0891b2; }
.text-yellow-600 { color: #ca8a04; }
`]
})
export class SyncfusionPivotConfigComponent extends BaseConfigComponent implements OnInit {
export class SyncfusionPivotConfigComponent extends BaseConfigComponent implements OnInit, AfterViewInit {
constructor(override cdr: ChangeDetectorRef) {
super(cdr);
}
override ngOnInit(): void {
// Initialize basic config first
this.initializeDefaultConfig();
}
override ngAfterViewInit(): void {
// Initialize other configs after view is ready
this.initializeColorDefaults();
this.initializeAdvancedConfig();
this.cdr.detectChanges();
}
// TrackBy functions to prevent unnecessary re-renders
trackByRowIndex(index: number, item: any): number {
return index;
}
trackByColumnIndex(index: number, item: any): number {
return index;
}
trackByValueIndex(index: number, item: any): number {
return index;
}
trackByFilterIndex(index: number, item: any): number {
return index;
}
trackBySizeOption(index: number, item: any): string {
return item.id;
}
override initializeDefaultConfig(): void {
......@@ -734,6 +993,68 @@ export class SyncfusionPivotConfigComponent extends BaseConfigComponent implemen
if (!this.currentConfig.borderColor) this.currentConfig.borderColor = '#E5E7EB';
}
private initializeAdvancedConfig() {
// Enhanced UI configuration
if (this.currentConfig.dataCount === undefined) this.currentConfig.dataCount = 0;
if (!this.currentConfig.lastUpdated) this.currentConfig.lastUpdated = new Date();
if (!this.currentConfig.shadow) this.currentConfig.shadow = 'medium';
if (this.currentConfig.isLoading === undefined) this.currentConfig.isLoading = false;
if (this.currentConfig.hasError === undefined) this.currentConfig.hasError = false;
if (!this.currentConfig.errorMessage) this.currentConfig.errorMessage = '';
if (this.currentConfig.isEmpty === undefined) this.currentConfig.isEmpty = false;
// Enhanced interaction configuration
if (this.currentConfig.enableKeyboard === undefined) this.currentConfig.enableKeyboard = true;
if (this.currentConfig.enableFocus === undefined) this.currentConfig.enableFocus = true;
if (this.currentConfig.hoverEffect === undefined) this.currentConfig.hoverEffect = true;
if (this.currentConfig.clickEffect === undefined) this.currentConfig.clickEffect = true;
if (this.currentConfig.enableTooltip === undefined) this.currentConfig.enableTooltip = true;
if (this.currentConfig.enableSelection === undefined) this.currentConfig.enableSelection = true;
if (this.currentConfig.enableExport === undefined) this.currentConfig.enableExport = true;
if (this.currentConfig.enableRefresh === undefined) this.currentConfig.enableRefresh = true;
if (!this.currentConfig.clickAction) this.currentConfig.clickAction = 'none';
if (!this.currentConfig.customClickHandler) this.currentConfig.customClickHandler = '';
// Enhanced layout configuration
if (this.currentConfig.fullWidth === undefined) this.currentConfig.fullWidth = false;
if (this.currentConfig.fullHeight === undefined) this.currentConfig.fullHeight = false;
if (!this.currentConfig.widthUnit) this.currentConfig.widthUnit = 'px';
if (!this.currentConfig.heightUnit) this.currentConfig.heightUnit = 'px';
if (!this.currentConfig.sizeOption) this.currentConfig.sizeOption = 'medium';
if (!this.currentConfig.aspectRatio) this.currentConfig.aspectRatio = 'auto';
// Enhanced data configuration
if (this.currentConfig.enableFiltering === undefined) this.currentConfig.enableFiltering = true;
if (this.currentConfig.enableSorting === undefined) this.currentConfig.enableSorting = true;
if (this.currentConfig.enableGrouping === undefined) this.currentConfig.enableGrouping = true;
if (this.currentConfig.enablePaging === undefined) this.currentConfig.enablePaging = true;
if (!this.currentConfig.pageSize) this.currentConfig.pageSize = 10;
if (!this.currentConfig.currentPage) this.currentConfig.currentPage = 1;
if (!this.currentConfig.totalPages) this.currentConfig.totalPages = 1;
// Enhanced security configuration
if (this.currentConfig.requireHttps === undefined) this.currentConfig.requireHttps = 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) this.currentConfig.rateLimit = 0;
// Enhanced animation configuration
if (this.currentConfig.enableAnimations === undefined) this.currentConfig.enableAnimations = true;
if (!this.currentConfig.animationType) this.currentConfig.animationType = 'fade';
if (!this.currentConfig.animationDuration) this.currentConfig.animationDuration = 300;
if (!this.currentConfig.animationDelay) this.currentConfig.animationDelay = 0;
// Enhanced style configuration
if (!this.currentConfig.customCSS) this.currentConfig.customCSS = '';
if (!this.currentConfig.theme) this.currentConfig.theme = 'light';
if (!this.currentConfig.fontSize) this.currentConfig.fontSize = 14;
if (!this.currentConfig.fontWeight) this.currentConfig.fontWeight = 'normal';
if (!this.currentConfig.fontFamily) this.currentConfig.fontFamily = 'system-ui, -apple-system, sans-serif';
if (!this.currentConfig.padding) this.currentConfig.padding = 16;
if (!this.currentConfig.margin) this.currentConfig.margin = 8;
}
addPivotRow() {
this.addArrayItem('rows', { name: '', caption: '', visible: true, expanded: false });
}
......
<div
class="syncfusion-pivot-widget"
[ngStyle]="getAllStyles()"
[ngClass]="['custom-styled', getInteractionClasses()]"
[ngClass]="['custom-styled', getInteractionClasses(), 'shadow-' + shadow]"
[attr.data-widget-id]="widgetId"
[attr.data-source]="dataSource"
[title]="enableTooltip ? (title + ' - Pivot Table Widget') : null"
......@@ -21,13 +21,24 @@
<div class="header-content">
<div class="header-left">
<h4 class="widget-title">{{ title }}</h4>
<div class="data-count" *ngIf="dataCount > 0">
<i class="bi bi-database"></i>
<span>{{ dataCount }} records</span>
</div>
<div class="last-updated" *ngIf="lastUpdated">
<i class="bi bi-clock"></i>
<span>{{ lastUpdated | date:'short' }}</span>
</div>
</div>
<div class="header-actions">
<div *ngIf="enableExport" class="export-button" (click)="exportData($event)">
<div *ngIf="enableExport" class="action-button export-button" (click)="exportData($event)" title="Export Data">
<i class="bi bi-download"></i>
</div>
<div *ngIf="enableRefresh && dataSource !== 'static'" class="refresh-button" (click)="refreshData($event)">
<i class="bi bi-arrow-clockwise"></i>
<div *ngIf="enableRefresh && dataSource !== 'static'" class="action-button refresh-button" (click)="refreshData($event)" title="Refresh Data">
<i class="bi bi-arrow-clockwise" [class.rotating]="isLoading"></i>
</div>
<div *ngIf="enableFiltering" class="action-button filter-button" (click)="toggleFilter($event)" title="Toggle Filter">
<i class="bi bi-funnel"></i>
</div>
</div>
</div>
......@@ -35,6 +46,10 @@
<i class="bi bi-info-circle"></i>
<span>{{ getDataSourceInfo() }}</span>
</div>
<div *ngIf="requireHttps && !dataSource.includes('https')" class="security-warning">
<i class="bi bi-shield-exclamation"></i>
<span>HTTPS required for secure data transmission</span>
</div>
</div>
<!-- Security Warning -->
......@@ -47,32 +62,57 @@
<div *ngIf="hasRequiredRole()" class="widget-body">
<!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
<div *ngIf="isLoading" class="loading-state">
<div class="loading-spinner">
<div class="spinner"></div>
</div>
<p class="loading-text">Loading pivot data...</p>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-circle-fill text-4xl"></i>
<p class="mt-2 font-semibold">{{ errorMessage }}</p>
<div *ngIf="hasError" class="error-state">
<div class="error-icon">
<i class="bi bi-exclamation-circle-fill"></i>
</div>
<h3 class="error-title">Error Loading Data</h3>
<p class="error-message">{{ errorMessage }}</p>
<button class="retry-button" (click)="refreshData($event)">
<i class="bi bi-arrow-clockwise"></i>
Retry
</button>
</div>
<!-- Empty State -->
<div *ngIf="isEmpty && !isLoading && !hasError" class="empty-state">
<div class="empty-icon">
<i class="bi bi-table"></i>
</div>
<h3 class="empty-title">No Data Available</h3>
<p class="empty-message">There is no data to display in the pivot table.</p>
<button class="refresh-button" (click)="refreshData($event)">
<i class="bi bi-arrow-clockwise"></i>
Refresh
</button>
</div>
<!-- Pivot View -->
<ejs-pivotview #pivotview *ngIf="!isLoading && !hasError"
[dataSourceSettings]="dataSourceSettings"
[allowCalculatedField]="allowCalculatedField"
[showFieldList]="showFieldList"
[showToolbar]="showToolbar"
[showGroupingBar]="showGroupingBar"
[allowPdfExport]="allowPdfExport && enableExport"
[allowExcelExport]="allowExcelExport && enableExport"
(toolbarClick)="toolbarClick($event)"
[toolbar]="toolbar"
[displayOption]="displayOption"
[chartSettings]="chartSettings"
(dataBound)="onDataBound($event)"
[height]="'100%'">
</ejs-pivotview>
<div *ngIf="!isLoading && !hasError && !isEmpty" class="pivot-container">
<ejs-pivotview #pivotview
[dataSourceSettings]="dataSourceSettings"
[allowCalculatedField]="allowCalculatedField"
[showFieldList]="showFieldList"
[showToolbar]="showToolbar"
[showGroupingBar]="showGroupingBar"
[allowPdfExport]="allowPdfExport && enableExport"
[allowExcelExport]="allowExcelExport && enableExport"
(toolbarClick)="toolbarClick($event)"
[toolbar]="toolbar"
[displayOption]="displayOption"
[chartSettings]="chartSettings"
(dataBound)="onDataBound($event)"
[height]="'100%'">
</ejs-pivotview>
</div>
</div>
</div>
......@@ -5,13 +5,36 @@
height: 100%;
position: relative;
overflow: hidden;
border-radius: 12px;
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;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
// Shadow variants
&.shadow-none {
box-shadow: none;
}
&.shadow-small {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
&.shadow-medium {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
&.shadow-large {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
// Widget Header
.widget-header {
flex-shrink: 0;
padding: 12px 16px;
padding: 16px 20px;
border-bottom: 1px solid rgba(229, 231, 235, 0.5);
position: relative;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
border-radius: 12px 12px 0 0;
.header-content {
display: flex;
......@@ -20,14 +43,41 @@
.header-left {
display: flex;
align-items: center;
gap: 8px;
flex-direction: column;
gap: 4px;
.widget-title {
font-size: 16px;
font-weight: 600;
font-size: 18px;
font-weight: 700;
margin: 0;
color: inherit;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.data-count {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #6b7280;
font-weight: 500;
i {
font-size: 10px;
}
}
.last-updated {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: #9ca3af;
font-weight: 400;
i {
font-size: 9px;
}
}
}
......@@ -35,32 +85,39 @@
display: flex;
gap: 8px;
.export-button,
.refresh-button {
width: 32px;
height: 32px;
border-radius: 6px;
.action-button {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
background: rgba(255, 255, 255, 0.1);
color: inherit;
opacity: 0.7;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.8);
color: #374151;
border: 1px solid rgba(229, 231, 235, 0.5);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
&:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
background: rgba(59, 130, 246, 0.1);
border-color: rgba(59, 130, 246, 0.3);
color: #3b82f6;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
&:active {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
i {
font-size: 14px;
font-size: 16px;
}
&.rotating i {
animation: spin 1s linear infinite;
}
}
}
......@@ -120,6 +177,174 @@
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
// Loading State
.loading-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
.loading-spinner {
margin-bottom: 16px;
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e2e8f0;
border-top: 4px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
}
.loading-text {
font-size: 14px;
color: #6b7280;
font-weight: 500;
margin: 0;
}
}
// Error State
.error-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
text-align: center;
.error-icon {
margin-bottom: 16px;
i {
font-size: 48px;
color: #ef4444;
}
}
.error-title {
font-size: 18px;
font-weight: 600;
color: #dc2626;
margin: 0 0 8px 0;
}
.error-message {
font-size: 14px;
color: #7f1d1d;
margin: 0 0 20px 0;
}
.retry-button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: #ef4444;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #dc2626;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(239, 68, 68, 0.3);
}
&:active {
transform: translateY(0);
}
i {
font-size: 16px;
}
}
}
// Empty State
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
text-align: center;
.empty-icon {
margin-bottom: 16px;
i {
font-size: 48px;
color: #9ca3af;
}
}
.empty-title {
font-size: 18px;
font-weight: 600;
color: #374151;
margin: 0 0 8px 0;
}
.empty-message {
font-size: 14px;
color: #6b7280;
margin: 0 0 20px 0;
}
.refresh-button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #2563eb;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
}
&:active {
transform: translateY(0);
}
i {
font-size: 16px;
}
}
}
// Pivot Container
.pivot-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: white;
border-radius: 0 0 12px 12px;
}
// Interaction States
......@@ -189,6 +414,15 @@
}
// Animation Keyframes
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes fadeIn {
from {
opacity: 0;
......@@ -238,6 +472,15 @@
}
}
@keyframes shimmer {
0% {
background-position: -200px 0;
}
100% {
background-position: calc(200px + 100%) 0;
}
}
// Aspect Ratio Constraints
&.aspect-ratio-16-9 {
aspect-ratio: 16/9;
......
import { Component, ViewChild, OnDestroy, OnInit, AfterViewInit } from '@angular/core';
import { Component, ViewChild, OnDestroy, OnInit, AfterViewInit, ChangeDetectorRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PivotViewModule, IDataSet, FieldListService, CalculatedFieldService, ToolbarService, GroupingBarService, ConditionalFormattingService, PivotViewComponent, PDFExportService, ExcelExportService, ToolbarItems, PivotChartService, DisplayOption } from '@syncfusion/ej2-angular-pivotview';
import { ChartSettingsModel } from '@syncfusion/ej2-pivotview/src/pivotview/model/chartsettings-model';
......@@ -21,6 +21,74 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen
public widgetId: string;
private isPerspectiveApplied = false;
// Enhanced UI properties
public dataCount: number = 0;
public lastUpdated: Date = new Date();
public shadow: string = 'medium';
public override isLoading: boolean = false;
public override hasError: boolean = false;
public override errorMessage: string = '';
public isEmpty: boolean = false;
// Enhanced interaction properties
public enableKeyboard: boolean = true;
public enableFocus: boolean = true;
public hoverEffect: boolean = true;
public clickEffect: boolean = true;
public enableTooltip: boolean = true;
public enableSelection: boolean = true;
public enableExport: boolean = true;
public enableRefresh: boolean = true;
public clickAction: string = 'none';
public customClickHandler: string = '';
// Enhanced layout properties
public fullWidth: boolean = false;
public fullHeight: boolean = false;
public widthUnit: string = 'px';
public heightUnit: string = 'px';
public sizeOption: string = 'medium';
public aspectRatio: string = 'auto';
// Enhanced data properties
public enableFiltering: boolean = true;
public enableSorting: boolean = true;
public enableGrouping: boolean = true;
public enablePaging: boolean = true;
public pageSize: number = 10;
public currentPage: number = 1;
public totalPages: number = 1;
// Enhanced security properties
public requireHttps: boolean = false;
public allowedRoles: string = '';
public dataEncryption: boolean = false;
public auditLog: boolean = false;
public rateLimit: number = 0;
// Enhanced style properties
public customCSS: string = '';
public theme: string = 'light';
public fontSize: number = 14;
public fontWeight: string = 'normal';
public fontFamily: string = 'system-ui, -apple-system, sans-serif';
public padding: number = 16;
public margin: number = 8;
// Animation properties
public enableAnimations: boolean = true;
public animationType: string = 'fade';
public animationDuration: number = 300;
public animationDelay: number = 0;
// Original data storage
public override originalData: any[] = [];
public dataSourceSettings: {
dataSource: DataManager;
expandAll: boolean;
......@@ -50,29 +118,11 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen
public accentColor: string = '#3B82F6';
public borderRadius: number = 8;
public borderWidth: number = 1;
public padding: number = 16;
public margin: number = 8;
public fontSize: number = 14;
public fontWeight: string = 'normal';
public fontFamily: string = 'system-ui, -apple-system, sans-serif';
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 = true;
public enableExport: boolean = true;
public enableRefresh: boolean = true;
public clickAction: string = 'none';
public customClickHandler: string = '';
// Layout properties
public width: number = 800;
......@@ -81,7 +131,6 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen
public minHeight: number = 400;
public maxWidth: number = 1400;
public maxHeight: number = 1000;
public aspectRatio: string = 'auto';
public responsive: boolean = true;
// Data properties
......@@ -94,10 +143,6 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen
// Security properties
public requireAuth: boolean = false;
public allowedRoles: string = '';
public dataEncryption: boolean = false;
public auditLog: boolean = false;
public rateLimit: number = 0;
// Pivot-specific properties from SyncfusionPivotConfigComponent
public displayOptionView: string = 'Both';
......@@ -126,12 +171,12 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen
// Pivot-specific properties (moved to above section)
public showChart: boolean = true;
public showGrid: boolean = true;
public enableGrouping: boolean = true;
public allowFiltering: boolean = true;
constructor(
protected override dashboardStateService: DashboardStateService,
private widgetStateService: WidgetStateService
private widgetStateService: WidgetStateService,
private cdr: ChangeDetectorRef
) {
super(dashboardStateService);
}
......@@ -175,6 +220,66 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen
chartSettings: this.chartSettings,
};
// Enhanced UI configuration
this.dataCount = this.configObj.dataCount || 0;
this.lastUpdated = this.configObj.lastUpdated || new Date();
this.shadow = this.configObj.shadow || 'medium';
this.isLoading = this.configObj.isLoading || false;
this.hasError = this.configObj.hasError || false;
this.errorMessage = this.configObj.errorMessage || '';
this.isEmpty = this.configObj.isEmpty || false;
// Enhanced interaction configuration
this.enableKeyboard = this.configObj.enableKeyboard !== undefined ? this.configObj.enableKeyboard : true;
this.enableFocus = this.configObj.enableFocus !== undefined ? this.configObj.enableFocus : true;
this.hoverEffect = this.configObj.hoverEffect !== undefined ? this.configObj.hoverEffect : true;
this.clickEffect = this.configObj.clickEffect !== undefined ? this.configObj.clickEffect : true;
this.enableTooltip = this.configObj.enableTooltip !== undefined ? this.configObj.enableTooltip : true;
this.enableSelection = this.configObj.enableSelection !== undefined ? this.configObj.enableSelection : true;
this.enableExport = this.configObj.enableExport !== undefined ? this.configObj.enableExport : true;
this.enableRefresh = this.configObj.enableRefresh !== undefined ? this.configObj.enableRefresh : true;
this.clickAction = this.configObj.clickAction || 'none';
this.customClickHandler = this.configObj.customClickHandler || '';
// Enhanced layout configuration
this.fullWidth = this.configObj.fullWidth !== undefined ? this.configObj.fullWidth : false;
this.fullHeight = this.configObj.fullHeight !== undefined ? this.configObj.fullHeight : false;
this.widthUnit = this.configObj.widthUnit || 'px';
this.heightUnit = this.configObj.heightUnit || 'px';
this.sizeOption = this.configObj.sizeOption || 'medium';
this.aspectRatio = this.configObj.aspectRatio || 'auto';
// Enhanced data configuration
this.enableFiltering = this.configObj.enableFiltering !== undefined ? this.configObj.enableFiltering : true;
this.enableSorting = this.configObj.enableSorting !== undefined ? this.configObj.enableSorting : true;
this.enableGrouping = this.configObj.enableGrouping !== undefined ? this.configObj.enableGrouping : true;
this.enablePaging = this.configObj.enablePaging !== undefined ? this.configObj.enablePaging : true;
this.pageSize = this.configObj.pageSize || 10;
this.currentPage = this.configObj.currentPage || 1;
this.totalPages = this.configObj.totalPages || 1;
// Enhanced security configuration
this.requireHttps = this.configObj.requireHttps !== undefined ? this.configObj.requireHttps : 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;
// Enhanced 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;
// Enhanced style configuration
this.customCSS = this.configObj.customCSS || '';
this.theme = this.configObj.theme || 'light';
this.fontSize = this.configObj.fontSize || 14;
this.fontWeight = this.configObj.fontWeight || 'normal';
this.fontFamily = this.configObj.fontFamily || 'system-ui, -apple-system, sans-serif';
this.padding = this.configObj.padding || 16;
this.margin = this.configObj.margin || 8;
// Pivot features configuration
this.showFieldList = this.configObj.showFieldList !== undefined ? this.configObj.showFieldList : true;
this.showToolbar = this.configObj.showToolbar !== undefined ? this.configObj.showToolbar : true;
......@@ -319,14 +424,46 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen
}
onDataUpdate(data: IDataSet[]): void {
const transformedData = this.transformData(data);
this.dataSourceSettings = {
...this.dataSourceSettings,
dataSource: new DataManager(transformedData)
};
if (this.pivotview) {
this.pivotview.dataSourceSettings.dataSource = new DataManager(transformedData);
// The refresh is implicitly handled by the dataBound event now
try {
// Store original data
this.originalData = data || [];
this.dataCount = this.originalData.length;
this.lastUpdated = new Date();
this.isEmpty = this.originalData.length === 0;
// Transform data if transform function is provided
let transformedData = this.transformData(data);
// Apply filtering if enabled
if (this.enableFiltering) {
transformedData = this.applyDataFilter(transformedData);
}
// Apply conditional formatting if enabled
if (this.allowConditionalFormatting) {
transformedData = this.applyConditionalFormatting(transformedData);
}
this.dataSourceSettings = {
...this.dataSourceSettings,
dataSource: new DataManager(transformedData)
};
if (this.pivotview) {
this.pivotview.dataSourceSettings.dataSource = new DataManager(transformedData);
// The refresh is implicitly handled by the dataBound event now
}
// Update loading and error states
this.isLoading = false;
this.hasError = false;
this.cdr.detectChanges();
} catch (error) {
console.error('Error updating pivot data:', error);
this.hasError = true;
this.errorMessage = 'Failed to update data';
this.isLoading = false;
this.cdr.detectChanges();
}
}
......@@ -684,6 +821,41 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen
}
}
// Method to apply data filtering
private applyDataFilter(data: any[]): any[] {
if (!this.enableFiltering || !data) {
return data;
}
try {
// Apply basic filtering logic here
// This can be extended based on specific filtering requirements
return data;
} catch (error) {
console.error('Error applying data filter:', error);
return data;
}
}
// Method to apply conditional formatting
private applyConditionalFormatting(data: any[]): any[] {
if (!this.allowConditionalFormatting || !data) {
return data;
}
try {
// Apply conditional formatting logic here
// This can be extended based on specific formatting requirements
return data;
} catch (error) {
console.error('Error applying conditional formatting:', error);
return data;
}
}
// Export data functionality
exportData(event: Event): void {
if (event) {
......@@ -740,4 +912,20 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen
this.isLoading = false;
}, 1000);
}
// Toggle filter functionality
toggleFilter(event: Event): void {
if (event) {
event.stopPropagation();
}
this.enableFiltering = !this.enableFiltering;
// Apply or remove filtering
if (this.originalData && this.originalData.length > 0) {
this.onDataUpdate(this.originalData);
}
console.log('Filter toggled:', this.enableFiltering);
}
}
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