Commit 6b8b4df6 by Ooh-Ao

widget

parent a25087d6
...@@ -732,6 +732,7 @@ export class DashboardManagementComponent implements OnInit { ...@@ -732,6 +732,7 @@ export class DashboardManagementComponent implements OnInit {
} }
onDatasetSelected(datasetId: string): void { onDatasetSelected(datasetId: string): void {
console.log('Dataset selected:', datasetId); // Added for debugging/clarity
if (this.dashboardData) { if (this.dashboardData) {
this.dashboardData.datasetId = datasetId; this.dashboardData.datasetId = datasetId;
this.dashboardStateService.selectDataset(datasetId); this.dashboardStateService.selectDataset(datasetId);
......
...@@ -26,10 +26,13 @@ export class DashboardStateService { ...@@ -26,10 +26,13 @@ export class DashboardStateService {
if (dataset && dataset.url) { if (dataset && dataset.url) {
return this.http.get<any[]>(dataset.url).pipe( return this.http.get<any[]>(dataset.url).pipe(
map(data => { map(data => {
console.log('Fetched data for dataset:', data); // Log fetched data
if (data && data.length > 0) { if (data && data.length > 0) {
const columns = Object.keys(data[0]); const columns = Object.keys(data[0]);
console.log('Derived columns:', columns); // Log derived columns
return { data, columns }; return { data, columns };
} else { } else {
console.log('Fetched data is empty or invalid.'); // Log empty data
return { data: [], columns: [] }; return { data: [], columns: [] };
} }
}) })
......
<ejs-chart [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis"> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Chart -->
<ejs-chart *ngIf="!isLoading && !hasError" [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis">
<e-series-collection> <e-series-collection>
<e-series [dataSource]="chartData" type="Area" xName="x" yName="y" name="Sales"></e-series> <e-series [dataSource]="chartData" type="Area" xName="x" yName="y" name="Sales"></e-series>
</e-series-collection> </e-series-collection>
......
import { Component, Input, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChartModule, AreaSeriesService, CategoryService, LegendService, TooltipService, DataLabelService } from '@syncfusion/ej2-angular-charts'; import { ChartModule, AreaSeriesService, CategoryService, LegendService, TooltipService, DataLabelService } from '@syncfusion/ej2-angular-charts';
import { DashboardStateService, SelectedDataset } from '../../services/dashboard-state.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({ @Component({
selector: 'app-area-chart-widget', selector: 'app-area-chart-widget',
...@@ -10,47 +11,29 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard ...@@ -10,47 +11,29 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard
providers: [AreaSeriesService, CategoryService, LegendService, TooltipService, DataLabelService], providers: [AreaSeriesService, CategoryService, LegendService, TooltipService, DataLabelService],
templateUrl: './area-chart-widget.component.html', templateUrl: './area-chart-widget.component.html',
}) })
export class AreaChartWidgetComponent implements OnInit { export class AreaChartWidgetComponent extends BaseWidgetComponent {
@Input() config: any;
public chartData: Object[]; public chartData: Object[];
public title: string = 'Area Chart';
public primaryXAxis: Object; public primaryXAxis: Object;
public primaryYAxis: Object; public primaryYAxis: Object;
constructor(private dashboardStateService: DashboardStateService) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
this.primaryXAxis = { valueType: 'Category', title: 'Month' };
this.primaryYAxis = { title: 'Sales' };
this.dashboardStateService.selectedDataset$.subscribe((selectedDataset: SelectedDataset | null) => {
if (selectedDataset && selectedDataset.data && this.config && this.config.xField && this.config.yField) {
this.updateChart(selectedDataset.data);
} else {
this.resetChart();
}
});
} }
updateChart(data: any[]): void { onDataUpdate(data: any[]): void {
this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] })); this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] }));
if (this.config.title) { this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' };
this.title = this.config.title; this.primaryYAxis = { title: this.config.yAxisTitle || '' };
}
if (this.config.xAxisTitle) {
this.primaryXAxis = { ...this.primaryXAxis, title: this.config.xAxisTitle, valueType: 'Category' };
}
if (this.config.yAxisTitle) {
this.primaryYAxis = { ...this.primaryYAxis, title: this.config.yAxisTitle };
}
} }
resetChart(): void { onReset(): void {
this.title = 'Area Chart (Default)';
this.chartData = [ this.chartData = [
{ month: 'Jan', sales: 35 }, { month: 'Feb', sales: 28 }, { x: 'Jan', y: 35 }, { x: 'Feb', y: 28 },
{ month: 'Mar', sales: 34 }, { month: 'Apr', sales: 32 }, { x: 'Mar', y: 34 }, { x: 'Apr', y: 32 },
{ month: 'May', sales: 40 }, { month: 'Jun', sales: 30 }, { x: 'May', y: 40 }, { x: 'Jun', y: 30 },
]; ];
this.primaryXAxis = { valueType: 'Category', title: 'Month' };
this.primaryYAxis = { title: 'Sales' };
} }
} }
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DatasetService } from '../services/dataset.service'; import { DashboardStateService } from '../services/dashboard-state.service';
import { HttpClient, HttpClientModule } from '@angular/common/http'; import { BaseWidgetComponent } from './base-widget.component';
import { DatasetModel } from '../models/widgets.model';
@Component({ @Component({
selector: 'app-attendance-overview-widget', selector: 'app-attendance-overview-widget',
standalone: true, standalone: true,
imports: [CommonModule, HttpClientModule], imports: [CommonModule],
providers: [DatasetService, HttpClient],
template: ` template: `
<div class="p-4 h-full bg-yellow-50"> <div class="p-4 h-full bg-yellow-50">
<h3 class="font-bold text-yellow-800">{{ title }}</h3> <h3 class="font-bold text-yellow-800">{{ title }}</h3>
<p class="text-sm text-yellow-600">Present: {{ present }}</p>
<p class="text-sm text-yellow-600">On Leave: {{ onLeave }}</p> <!-- Loading State -->
<p class="text-sm text-yellow-600">Absent: {{ absent }}</p> <div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-yellow-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-2xl"></i>
<p class="mt-1 text-sm">{{ errorMessage }}</p>
</div>
<!-- Content -->
<div *ngIf="!isLoading && !hasError">
<p class="text-sm text-yellow-600">Present: {{ present }}</p>
<p class="text-sm text-yellow-600">On Leave: {{ onLeave }}</p>
<p class="text-sm text-yellow-600">Absent: {{ absent }}</p>
</div>
</div> </div>
` `
}) })
export class AttendanceOverviewWidgetComponent implements OnInit, OnChanges { export class AttendanceOverviewWidgetComponent extends BaseWidgetComponent {
@Input() config: any; public present: number = 0;
title: string = 'Today\'s Attendance'; public onLeave: number = 0;
present: number = 0; public absent: number = 0;
onLeave: number = 0;
absent: number = 0;
constructor(private datasetService: DatasetService, private http: HttpClient) { }
ngOnInit(): void { constructor(protected override dashboardStateService: DashboardStateService) {
if (this.config && this.config.datasetId) { super(dashboardStateService);
this.loadData();
} else {
this.resetData();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['config'] && this.config && this.config.datasetId) {
this.loadData();
} else if (changes['config'] && (!this.config || !this.config.datasetId)) {
this.resetData();
}
} }
loadData(): void { onDataUpdate(data: any[]): void {
if (this.config.datasetId) { if (data.length > 0) {
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { const firstItem = data[0];
if (dataset && dataset.url) { this.present = firstItem[this.config.presentField] || 0;
this.http.get<any[]>(dataset.url).subscribe(data => { this.onLeave = firstItem[this.config.onLeaveField] || 0;
if (data.length > 0) { this.absent = firstItem[this.config.absentField] || 0;
const firstItem = data[0];
this.present = firstItem[this.config.presentField] || 0;
this.onLeave = firstItem[this.config.onLeaveField] || 0;
this.absent = firstItem[this.config.absentField] || 0;
}
if (this.config.title) {
this.title = this.config.title;
}
});
}
});
} }
} }
resetData(): void { onReset(): void {
this.title = 'Attendance (Default)';
this.present = 150; this.present = 150;
this.onLeave = 10; this.onLeave = 10;
this.absent = 5; this.absent = 5;
......
<ejs-chart [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis"> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Chart -->
<ejs-chart *ngIf="!isLoading && !hasError" [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis">
<e-series-collection> <e-series-collection>
<e-series [dataSource]="chartData" [type]="type" xName="x" yName="y" name="Data"></e-series> <e-series [dataSource]="chartData" [type]="type" xName="x" yName="y" name="Data"></e-series>
</e-series-collection> </e-series-collection>
......
import { Component, Input, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChartModule, BarSeriesService, ColumnSeriesService, CategoryService, LegendService, TooltipService, DataLabelService } from '@syncfusion/ej2-angular-charts'; import { ChartModule, BarSeriesService, ColumnSeriesService, CategoryService, LegendService, TooltipService, DataLabelService } from '@syncfusion/ej2-angular-charts';
import { DashboardStateService, SelectedDataset } from '../../services/dashboard-state.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({ @Component({
selector: 'app-bar-chart-widget', selector: 'app-bar-chart-widget',
...@@ -10,51 +11,33 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard ...@@ -10,51 +11,33 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard
providers: [BarSeriesService, ColumnSeriesService, CategoryService, LegendService, TooltipService, DataLabelService], providers: [BarSeriesService, ColumnSeriesService, CategoryService, LegendService, TooltipService, DataLabelService],
templateUrl: './bar-chart-widget.component.html', templateUrl: './bar-chart-widget.component.html',
}) })
export class BarChartWidgetComponent implements OnInit { export class BarChartWidgetComponent extends BaseWidgetComponent {
@Input() config: any; public chartData: Object[];
@Input() chartData: Object[]; public type: 'Bar' | 'Column' = 'Column';
@Input() title: string = 'Bar Chart';
@Input() type: 'Bar' | 'Column' = 'Column';
public primaryXAxis: Object; public primaryXAxis: Object;
public primaryYAxis: Object; public primaryYAxis: Object;
constructor(private dashboardStateService: DashboardStateService) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
this.primaryXAxis = { valueType: 'Category', title: 'Country' };
this.primaryYAxis = { title: 'Sales' };
this.dashboardStateService.selectedDataset$.subscribe((selectedDataset: SelectedDataset | null) => {
if (selectedDataset && selectedDataset.data && this.config && this.config.xField && this.config.yField) {
this.updateChart(selectedDataset.data);
} else {
this.resetChart();
}
});
} }
updateChart(data: any[]): void { onDataUpdate(data: any[]): void {
this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] })); this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] }));
if (this.config.title) { this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' };
this.title = this.config.title; this.primaryYAxis = { title: this.config.yAxisTitle || '' };
} this.type = this.config.type || 'Column';
if (this.config.xAxisTitle) {
this.primaryXAxis = { ...this.primaryXAxis, title: this.config.xAxisTitle };
}
if (this.config.yAxisTitle) {
this.primaryYAxis = { ...this.primaryYAxis, title: this.config.yAxisTitle };
}
if (this.config.type) {
this.type = this.config.type;
}
} }
resetChart(): void { onReset(): void {
this.title = 'Bar Chart (Default)';
this.chartData = [ this.chartData = [
{ country: 'USA', sales: 20 }, { x: 'USA', y: 20 },
{ country: 'China', sales: 25 }, { x: 'China', y: 25 },
{ country: 'Japan', sales: 18 }, { x: 'Japan', y: 18 },
{ country: 'Germany', sales: 15 }, { x: 'Germany', y: 15 },
]; ];
this.primaryXAxis = { valueType: 'Category', title: 'Country' };
this.primaryYAxis = { title: 'Sales' };
} }
} }
import { Input, OnInit, OnDestroy, Directive } from '@angular/core';
import { Subscription } from 'rxjs';
import { DashboardStateService, SelectedDataset } from '../services/dashboard-state.service';
@Directive() // Use @Directive() for base classes without their own template
export abstract class BaseWidgetComponent implements OnInit, OnDestroy {
@Input() config: any;
public title: string;
public isLoading = true;
public hasError = false;
public errorMessage: string | null = null;
protected subscription: Subscription = new Subscription();
constructor(protected dashboardStateService: DashboardStateService) {}
ngOnInit(): void {
this.title = this.config?.title || 'Widget';
const datasetSub = this.dashboardStateService.selectedDataset$.subscribe({
next: (selectedDataset: SelectedDataset | null) => {
this.isLoading = true;
this.hasError = false;
if (selectedDataset && selectedDataset.data) {
try {
this.onDataUpdate(selectedDataset.data);
this.isLoading = false;
} catch (error) {
this.handleError(error);
}
} else {
this.onReset();
this.isLoading = false;
}
},
error: (err) => {
this.handleError(err);
}
});
this.subscription.add(datasetSub);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
private handleError(error: any): void {
this.isLoading = false;
this.hasError = true;
this.errorMessage = 'An error occurred while loading widget data.';
console.error('Widget Error:', error);
this.onReset();
}
/**
* Abstract method to be implemented by child components.
* This method is called when new data is available from the selected dataset.
* @param data The data from the selected dataset.
*/
abstract onDataUpdate(data: any[]): void;
/**
* Abstract method to be implemented by child components.
* This method is called when there is no dataset selected or data is not available.
*/
abstract onReset(): void;
}
<div class="bg-white p-6 rounded-lg shadow-md h-full flex flex-col"> <div class="bg-white p-6 rounded-lg shadow-md h-full flex flex-col">
<h3 class="text-lg font-semibold text-gray-500 mb-4">Monthly Activity</h3>
<div #chart class="h-full w-full"></div> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Chart -->
<div *ngIf="!isLoading && !hasError" #chart class="h-full w-full"></div>
</div> </div>
import { Component, OnInit, ViewChild, ElementRef, AfterViewInit, Input, SimpleChanges, OnChanges } from '@angular/core'; import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MockDataService } from '../../common/mock-data.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { DatasetModel } from '../../models/widgets.model'; import { BaseWidgetComponent } from '../base-widget.component';
import { DatasetService } from '../../services/dataset.service';
import { HttpClient } from '@angular/common/http';
// Declare ApexCharts globally if it's loaded via a script tag // Declare ApexCharts globally if it's loaded via a script tag
declare var ApexCharts: any; declare var ApexCharts: any;
...@@ -14,21 +12,56 @@ declare var ApexCharts: any; ...@@ -14,21 +12,56 @@ declare var ApexCharts: any;
imports: [CommonModule], imports: [CommonModule],
templateUrl: './chart-widget.component.html', templateUrl: './chart-widget.component.html',
}) })
export class ChartWidgetComponent implements OnInit, AfterViewInit, OnChanges { export class ChartWidgetComponent extends BaseWidgetComponent implements AfterViewInit {
@ViewChild('chart') chartElement!: ElementRef; @ViewChild('chart') chartElement!: ElementRef;
@Input() config: any;
chartOptions: any;
private chartInstance: any; // To store the ApexCharts instance private chartInstance: any; // To store the ApexCharts instance
constructor(private datasetService: DatasetService, private http: HttpClient) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
}
ngAfterViewInit(): void {
if (typeof ApexCharts !== 'undefined' && this.chartElement) {
// Initial chart options are set in onReset
this.chartInstance = new ApexCharts(this.chartElement.nativeElement, this.getChartOptions([], []));
this.chartInstance.render();
// The subscription in BaseWidget will trigger onDataUpdate or onReset, which will update the chart.
} else {
console.error('ApexCharts is not loaded or chart element is not available.');
}
}
onDataUpdate(data: any[]): void {
const categories = data.map(item => item[this.config.xField]);
const series = this.config.yFields.map((yField: any) => ({
name: yField.name || yField.field,
data: data.map(item => item[yField.field])
}));
if (this.chartInstance) {
this.chartInstance.updateOptions(this.getChartOptions(series, categories));
}
}
ngOnInit(): void { onReset(): void {
// Initial chart options, will be updated with fetched data this.title = 'Chart (Default)';
this.chartOptions = { const defaultSeries = [{
series: [], name: 'Sample Series',
data: [10, 41, 35, 51, 49, 62, 69, 91, 148]
}];
const defaultCategories = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep'];
if (this.chartInstance) {
this.chartInstance.updateOptions(this.getChartOptions(defaultSeries, defaultCategories));
}
}
private getChartOptions(series: any[], categories: any[]): any {
return {
series: series,
chart: { chart: {
type: 'bar', type: this.config?.type || 'bar',
height: 250, height: 250,
toolbar: { toolbar: {
show: false show: false
...@@ -50,11 +83,11 @@ export class ChartWidgetComponent implements OnInit, AfterViewInit, OnChanges { ...@@ -50,11 +83,11 @@ export class ChartWidgetComponent implements OnInit, AfterViewInit, OnChanges {
colors: ['transparent'] colors: ['transparent']
}, },
xaxis: { xaxis: {
categories: [], categories: categories,
}, },
yaxis: { yaxis: {
title: { title: {
text: 'Number' text: this.config?.yAxisTitle || 'Number'
} }
}, },
fill: { fill: {
...@@ -66,69 +99,10 @@ export class ChartWidgetComponent implements OnInit, AfterViewInit, OnChanges { ...@@ -66,69 +99,10 @@ export class ChartWidgetComponent implements OnInit, AfterViewInit, OnChanges {
return val + " units" return val + " units"
} }
} }
},
title: {
text: this.title
} }
}; };
} }
ngOnChanges(changes: SimpleChanges): void {
if (changes['config'] && this.config && this.config.datasetId) {
this.loadData();
} else if (changes['config'] && (!this.config || !this.config.datasetId)) {
// Reset chart if config or datasetId is removed
this.chartOptions.series = [];
this.chartOptions.xaxis.categories = [];
if (this.chartInstance) {
this.chartInstance.updateOptions(this.chartOptions);
}
}
}
ngAfterViewInit(): void {
if (typeof ApexCharts !== 'undefined' && this.chartElement) {
this.chartInstance = new ApexCharts(this.chartElement.nativeElement, this.chartOptions);
this.chartInstance.render();
} else {
console.error('ApexCharts is not loaded or chart element is not available.');
}
}
loadData(): void {
if (this.config.datasetId) {
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => {
if (dataset && dataset.url) {
this.http.get<any[]>(dataset.url).subscribe(data => {
// Transform data for ApexCharts
// Assuming config.xField for categories and config.yFields for series
const categories = data.map(item => item[this.config.xField]);
const series = this.config.yFields.map((yField: any) => ({
name: yField.name || yField.field,
data: data.map(item => item[yField.field])
}));
const newOptions = {
series: series,
xaxis: {
categories: categories,
},
title: {
text: this.config.title || 'Chart'
},
yaxis: {
title: {
text: this.config.yAxisTitle || 'Number'
}
}
};
if (this.chartInstance) {
this.chartInstance.updateOptions(newOptions);
} else {
// If chartInstance is not yet created (e.g., first load), update chartOptions directly
this.chartOptions = { ...this.chartOptions, ...newOptions };
}
});
}
});
}
}
} }
<ejs-chart [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis"> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Chart -->
<ejs-chart *ngIf="!isLoading && !hasError && config?.series" [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis">
<e-series-collection> <e-series-collection>
<e-series *ngFor="let yField of config.yFields" [dataSource]="chartData" [type]="yField.type || 'Column'" xName="x" [yName]="yField.field" [name]="yField.name"></e-series> <e-series *ngFor="let series of config.series"
[dataSource]="chartData"
[type]="series.type || 'Column'"
[xName]="series.xName"
[yName]="series.yName"
[name]="series.name">
</e-series>
</e-series-collection> </e-series-collection>
</ejs-chart> </ejs-chart>
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChartModule, ColumnSeriesService, LineSeriesService, CategoryService, LegendService, TooltipService, DataLabelService } from '@syncfusion/ej2-angular-charts'; import { ChartModule, ColumnSeriesService, LineSeriesService, CategoryService, LegendService, TooltipService, DataLabelService } from '@syncfusion/ej2-angular-charts';
import { DatasetService } from '../../services/dataset.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { HttpClient, HttpClientModule } from '@angular/common/http'; import { BaseWidgetComponent } from '../base-widget.component';
import { DatasetModel } from '../../models/widgets.model';
@Component({ @Component({
selector: 'app-combo-chart-widget', selector: 'app-combo-chart-widget',
standalone: true, standalone: true,
imports: [CommonModule, ChartModule, HttpClientModule], imports: [CommonModule, ChartModule],
providers: [ColumnSeriesService, LineSeriesService, CategoryService, LegendService, TooltipService, DataLabelService, DatasetService, HttpClient], providers: [ColumnSeriesService, LineSeriesService, CategoryService, LegendService, TooltipService, DataLabelService],
templateUrl: './combo-chart-widget.component.html', templateUrl: './combo-chart-widget.component.html',
}) })
export class ComboChartWidgetComponent implements OnInit, OnChanges { export class ComboChartWidgetComponent extends BaseWidgetComponent {
@Input() config: any; public chartData: Object[];
@Input() chartData: Object[];
@Input() title: string = 'Combo Chart';
public primaryXAxis: Object; public primaryXAxis: Object;
public primaryYAxis: Object; public primaryYAxis: Object;
constructor(private datasetService: DatasetService, private http: HttpClient) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
this.primaryXAxis = { valueType: 'Category', title: 'Month' };
this.primaryYAxis = { title: 'Value' };
if (!this.config || !this.config.datasetId) {
this.chartData = [
{ month: 'Jan', sales: 35, profit: 10 }, { month: 'Feb', sales: 28, profit: 12 },
{ month: 'Mar', sales: 34, profit: 8 }, { month: 'Apr', sales: 32, profit: 15 },
{ month: 'May', sales: 40, profit: 11 }, { month: 'Jun', sales: 30, profit: 13 },
];
}
} }
ngOnChanges(changes: SimpleChanges): void { onDataUpdate(data: any[]): void {
if (changes['config'] && this.config && this.config.datasetId) { this.chartData = data;
this.loadData(); this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' };
} else if (changes['config'] && (!this.config || !this.config.datasetId)) { this.primaryYAxis = { title: this.config.yAxisTitle || '' };
this.chartData = [
{ month: 'Jan', sales: 35, profit: 10 }, { month: 'Feb', sales: 28, profit: 12 },
{ month: 'Mar', sales: 34, profit: 8 }, { month: 'Apr', sales: 32, profit: 15 },
{ month: 'May', sales: 40, profit: 11 }, { month: 'Jun', sales: 30, profit: 13 },
];
}
} }
loadData(): void { onReset(): void {
if (this.config.datasetId) { this.title = 'Combo Chart (Default)';
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { this.chartData = [
if (dataset && dataset.url) { { x: 'Jan', y1: 35, y2: 10 }, { x: 'Feb', y1: 28, y2: 12 },
this.http.get<any[]>(dataset.url).subscribe(data => { { x: 'Mar', y1: 34, y2: 8 }, { x: 'Apr', y1: 32, y2: 15 },
// Transform data for combo chart { x: 'May', y1: 40, y2: 11 }, { x: 'Jun', y1: 30, y2: 13 },
// Assuming config.xField and config.yFields (array of y-fields) ];
this.chartData = data.map(item => { this.primaryXAxis = { valueType: 'Category', title: 'Month' };
const transformedItem: any = { x: item[this.config.xField] }; this.primaryYAxis = { title: 'Value' };
this.config.yFields.forEach((field: string) => { // Provide a default series config for the reset state
transformedItem[field] = item[field]; if (!this.config) {
}); this.config = {};
return transformedItem;
});
if (this.config.title) {
this.title = this.config.title;
}
if (this.config.xAxisTitle) {
this.primaryXAxis = { ...this.primaryXAxis, title: this.config.xAxisTitle };
}
if (this.config.yAxisTitle) {
this.primaryYAxis = { ...this.primaryYAxis, title: this.config.yAxisTitle };
}
});
}
});
} }
this.config.series = [
{ type: 'Column', xName: 'x', yName: 'y1', name: 'Sales' },
{ type: 'Line', xName: 'x', yName: 'y2', name: 'Profit' }
];
} }
} }
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DatasetService } from '../services/dataset.service'; import { DashboardStateService } from '../services/dashboard-state.service';
import { HttpClient, HttpClientModule } from '@angular/common/http'; import { BaseWidgetComponent } from './base-widget.component';
import { DatasetModel } from '../models/widgets.model';
@Component({ @Component({
selector: 'app-company-info-widget', selector: 'app-company-info-widget',
standalone: true, standalone: true,
imports: [CommonModule, HttpClientModule], imports: [CommonModule],
providers: [DatasetService, HttpClient],
template: ` template: `
<div class="p-4 h-full bg-blue-50"> <div class="p-4 h-full bg-blue-50">
<h3 class="font-bold text-blue-800">{{ title }}</h3> <h3 class="font-bold text-blue-800">{{ title }}</h3>
<p class="text-sm text-blue-600">Company Name: {{ companyName }}</p> <!-- Loading State -->
<p class="text-sm text-blue-600">Address: {{ address }}</p> <div *ngIf="isLoading" class="flex justify-center items-center h-full">
<p class="text-sm text-blue-600">Contact: {{ contact }}</p> <div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-2xl"></i>
<p class="mt-1 text-sm">{{ errorMessage }}</p>
</div>
<!-- Content -->
<div *ngIf="!isLoading && !hasError">
<p class="text-sm text-blue-600">Company Name: {{ companyName }}</p>
<p class="text-sm text-blue-600">Address: {{ address }}</p>
<p class="text-sm text-blue-600">Contact: {{ contact }}</p>
</div>
</div> </div>
` `
}) })
export class CompanyInfoWidgetComponent implements OnInit, OnChanges { export class CompanyInfoWidgetComponent extends BaseWidgetComponent {
@Input() config: any; public companyName: string = '';
title: string = 'Company Information'; public address: string = '';
companyName: string = ''; public contact: string = '';
address: string = '';
contact: string = '';
constructor(private datasetService: DatasetService, private http: HttpClient) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
if (this.config && this.config.datasetId) {
this.loadData();
} else {
this.resetData();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['config'] && this.config && this.config.datasetId) {
this.loadData();
} else if (changes['config'] && (!this.config || !this.config.datasetId)) {
this.resetData();
}
} }
loadData(): void { onDataUpdate(data: any[]): void {
if (this.config.datasetId) { if (data.length > 0) {
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { const firstItem = data[0];
if (dataset && dataset.url) { this.companyName = firstItem[this.config.companyNameField] || '';
this.http.get<any[]>(dataset.url).subscribe(data => { this.address = firstItem[this.config.addressField] || '';
if (data.length > 0) { this.contact = firstItem[this.config.contactField] || '';
const firstItem = data[0];
this.companyName = firstItem[this.config.companyNameField] || '';
this.address = firstItem[this.config.addressField] || '';
this.contact = firstItem[this.config.contactField] || '';
}
if (this.config.title) {
this.title = this.config.title;
}
});
}
});
} }
} }
resetData(): void { onReset(): void {
this.title = 'Company Info (Default)';
this.companyName = 'My Company Inc.'; this.companyName = 'My Company Inc.';
this.address = '123 Main St, Anytown USA'; this.address = '123 Main St, Anytown USA';
this.contact = 'info@mycompany.com'; this.contact = 'info@mycompany.com';
......
...@@ -4,8 +4,24 @@ ...@@ -4,8 +4,24 @@
<h5 class="card-title">{{ title }}</h5> <h5 class="card-title">{{ title }}</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<p>Company Name: {{ companyName }}</p>
<p>Address: {{ address }}</p> <!-- Loading State -->
<p>Contact: {{ contact }}</p> <div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Content -->
<div *ngIf="!isLoading && !hasError">
<p>Company Name: {{ companyName }}</p>
<p>Address: {{ address }}</p>
<p>Contact: {{ contact }}</p>
</div>
</div> </div>
</div> </div>
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DatasetService } from '../../services/dataset.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { HttpClient, HttpClientModule } from '@angular/common/http'; import { BaseWidgetComponent } from '../base-widget.component';
import { DatasetModel } from '../../models/widgets.model';
@Component({ @Component({
selector: 'app-company-info-subfolder-widget', selector: 'app-company-info-subfolder-widget',
standalone: true, standalone: true,
imports: [CommonModule, HttpClientModule], imports: [CommonModule],
providers: [DatasetService, HttpClient],
templateUrl: './company-info-widget.component.html', templateUrl: './company-info-widget.component.html',
styleUrls: ['./company-info-widget.component.scss'] styleUrls: ['./company-info-widget.component.scss']
}) })
export class CompanyInfoSubfolderWidgetComponent implements OnInit, OnChanges { export class CompanyInfoSubfolderWidgetComponent extends BaseWidgetComponent {
@Input() config: any; public companyName: string = '';
title: string = 'Company Information'; public address: string = '';
companyName: string = ''; public contact: string = '';
address: string = '';
contact: string = '';
constructor(private datasetService: DatasetService, private http: HttpClient) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
if (this.config && this.config.datasetId) {
this.loadData();
} else {
this.resetData();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['config'] && this.config && this.config.datasetId) {
this.loadData();
} else if (changes['config'] && (!this.config || !this.config.datasetId)) {
this.resetData();
}
} }
loadData(): void { onDataUpdate(data: any[]): void {
if (this.config.datasetId) { if (data.length > 0) {
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { const firstItem = data[0];
if (dataset && dataset.url) { this.companyName = firstItem[this.config.companyNameField] || '';
this.http.get<any[]>(dataset.url).subscribe(data => { this.address = firstItem[this.config.addressField] || '';
if (data.length > 0) { this.contact = firstItem[this.config.contactField] || '';
const firstItem = data[0];
this.companyName = firstItem[this.config.companyNameField] || '';
this.address = firstItem[this.config.addressField] || '';
this.contact = firstItem[this.config.contactField] || '';
}
if (this.config.title) {
this.title = this.config.title;
}
});
}
});
} }
} }
resetData(): void { onReset(): void {
this.title = 'Company Info (Default)';
this.companyName = 'My Company Inc. (Subfolder)'; this.companyName = 'My Company Inc. (Subfolder)';
this.address = '456 Subfolder Ave, Anytown USA'; this.address = '456 Subfolder Ave, Anytown USA';
this.contact = 'info@subfolder.com'; this.contact = 'info@subfolder.com';
......
<ejs-accumulationchart [title]="title" [legendSettings]="legendSettings" [enableSmartLabels]="true"> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Chart -->
<ejs-accumulationchart *ngIf="!isLoading && !hasError" [title]="title" [legendSettings]="legendSettings" [enableSmartLabels]="true">
<e-accumulation-series-collection> <e-accumulation-series-collection>
<e-accumulation-series [dataSource]="chartData" type="Doughnut" xName="x" yName="y" [dataLabel]="{ visible: true, name: 'text', position: 'Inside' }" innerRadius="40%"></e-accumulation-series> <e-accumulation-series [dataSource]="chartData" type="Doughnut" xName="x" yName="y" [dataLabel]="{ visible: true, name: 'text', position: 'Inside' }" innerRadius="40%"></e-accumulation-series>
</e-accumulation-series-collection> </e-accumulation-series-collection>
......
import { Component, Input, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { AccumulationChartModule, AccumulationDataLabelService, AccumulationLegendService, AccumulationTooltipService } from '@syncfusion/ej2-angular-charts'; import { AccumulationChartModule, AccumulationDataLabelService, AccumulationLegendService, AccumulationTooltipService } from '@syncfusion/ej2-angular-charts';
import { DashboardStateService, SelectedDataset } from '../../services/dashboard-state.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({ @Component({
selector: 'app-doughnut-chart-widget', selector: 'app-doughnut-chart-widget',
...@@ -10,27 +11,18 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard ...@@ -10,27 +11,18 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard
providers: [AccumulationDataLabelService, AccumulationLegendService, AccumulationTooltipService], providers: [AccumulationDataLabelService, AccumulationLegendService, AccumulationTooltipService],
templateUrl: './doughnut-chart-widget.component.html', templateUrl: './doughnut-chart-widget.component.html',
}) })
export class DoughnutChartWidgetComponent implements OnInit { export class DoughnutChartWidgetComponent extends BaseWidgetComponent {
@Input() chartData: Object[]; public chartData: Object[];
@Input() title: string = 'Doughnut Chart'; public legendSettings: Object;
@Input() legendSettings: Object;
@Input() config: any;
constructor(private dashboardStateService: DashboardStateService) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
this.legendSettings = { visible: true };
this.dashboardStateService.selectedDataset$.subscribe((selectedDataset: SelectedDataset | null) => {
if (selectedDataset && selectedDataset.data && this.config && this.config.xField && this.config.yField) {
this.updateChart(selectedDataset.data);
} else {
this.resetChart();
}
});
} }
updateChart(data: any[]): void { onDataUpdate(data: any[]): void {
this.legendSettings = { visible: true };
let transformedData = data; let transformedData = data;
if (this.config.aggregation === 'count') { if (this.config.aggregation === 'count') {
const counts = transformedData.reduce((acc, item) => { const counts = transformedData.reduce((acc, item) => {
const key = item[this.config.xField]; const key = item[this.config.xField];
...@@ -42,19 +34,18 @@ export class DoughnutChartWidgetComponent implements OnInit { ...@@ -42,19 +34,18 @@ export class DoughnutChartWidgetComponent implements OnInit {
transformedData = transformedData.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] })); transformedData = transformedData.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] }));
} }
this.chartData = transformedData; this.chartData = transformedData;
if (this.config.title) {
this.title = this.config.title;
}
} }
resetChart(): void { onReset(): void {
this.title = 'Doughnut Chart (Default)';
this.legendSettings = { visible: true };
this.chartData = [ this.chartData = [
{ browser: 'Chrome', users: 37 }, { x: 'Chrome', y: 37 },
{ browser: 'Firefox', users: 17 }, { x: 'Firefox', y: 17 },
{ browser: 'Internet Explorer', users: 19 }, { x: 'Internet Explorer', y: 19 },
{ browser: 'Edge', users: 4 }, { x: 'Edge', y: 4 },
{ browser: 'Safari', users: 11 }, { x: 'Safari', y: 11 },
{ browser: 'Other', users: 12 }, { x: 'Other', y: 12 },
]; ];
} }
} }
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DatasetService } from '../services/dataset.service'; import { DashboardStateService } from '../services/dashboard-state.service';
import { HttpClient, HttpClientModule } from '@angular/common/http'; import { BaseWidgetComponent } from './base-widget.component';
import { DatasetModel } from '../models/widgets.model';
@Component({ @Component({
selector: 'app-employee-directory-widget', selector: 'app-employee-directory-widget',
standalone: true, standalone: true,
imports: [CommonModule, HttpClientModule], imports: [CommonModule],
providers: [DatasetService, HttpClient],
template: ` template: `
<div class="p-4 h-full bg-indigo-50"> <div class="p-4 h-full bg-indigo-50">
<h3 class="font-bold text-indigo-800">{{ title }}</h3> <h3 class="font-bold text-indigo-800">{{ title }}</h3>
<div *ngIf="employees.length > 0">
<div *ngFor="let employee of employees" class="mb-2"> <!-- Loading State -->
<p class="text-sm text-indigo-600">{{ employee.name }} - {{ employee.position }} ({{ employee.department }})</p> <div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-indigo-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-2xl"></i>
<p class="mt-1 text-sm">{{ errorMessage }}</p>
</div>
<!-- Content -->
<div *ngIf="!isLoading && !hasError">
<div *ngIf="employees.length > 0">
<div *ngFor="let employee of employees" class="mb-2">
<p class="text-sm text-indigo-600">{{ employee.name }} - {{ employee.position }} ({{ employee.department }})</p>
</div>
</div> </div>
<p *ngIf="employees.length === 0" class="text-sm text-indigo-600">No employees found.</p>
</div> </div>
<p *ngIf="employees.length === 0" class="text-sm text-indigo-600">No employees found.</p>
</div> </div>
` `
}) })
export class EmployeeDirectoryWidgetComponent implements OnInit, OnChanges { export class EmployeeDirectoryWidgetComponent extends BaseWidgetComponent {
@Input() config: any; public employees: any[] = [];
title: string = 'Employee Directory';
employees: any[] = [];
constructor(private datasetService: DatasetService, private http: HttpClient) { }
ngOnInit(): void {
if (this.config && this.config.datasetId) {
this.loadData();
} else {
this.resetData();
}
}
ngOnChanges(changes: SimpleChanges): void { constructor(protected override dashboardStateService: DashboardStateService) {
if (changes['config'] && this.config && this.config.datasetId) { super(dashboardStateService);
this.loadData();
} else if (changes['config'] && (!this.config || !this.config.datasetId)) {
this.resetData();
}
} }
loadData(): void { onDataUpdate(data: any[]): void {
if (this.config.datasetId) { this.employees = data.map(item => ({
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { name: item[this.config.nameField] || '',
if (dataset && dataset.url) { position: item[this.config.positionField] || '',
this.http.get<any[]>(dataset.url).subscribe(data => { department: item[this.config.departmentField] || '',
this.employees = data.map(item => ({ }));
name: item[this.config.nameField] || '',
position: item[this.config.positionField] || '',
department: item[this.config.departmentField] || '',
}));
if (this.config.title) {
this.title = this.config.title;
}
});
}
});
}
} }
resetData(): void { onReset(): void {
this.title = 'Employee Directory (Default)';
this.employees = [ this.employees = [
{ name: 'John Doe', position: 'Software Engineer', department: 'IT' }, { name: 'John Doe', position: 'Software Engineer', department: 'IT' },
{ name: 'Jane Smith', position: 'Project Manager', department: 'Operations' }, { name: 'Jane Smith', position: 'Project Manager', department: 'Operations' },
......
<ejs-maps [titleSettings]="{ text: title }" [zoomSettings]="zoomSettings" [layers]="layers"> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Chart -->
<ejs-maps *ngIf="!isLoading && !hasError" [titleSettings]="{ text: title }" [zoomSettings]="zoomSettings" [layers]="layers">
</ejs-maps> </ejs-maps>
import { Component, Input, OnInit, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MapsModule, MarkerService, ZoomService, DataLabelService, LegendService, MapsTooltipService } from '@syncfusion/ej2-angular-maps'; import { MapsModule, MarkerService, ZoomService, DataLabelService, LegendService, MapsTooltipService } from '@syncfusion/ej2-angular-maps';
import { DatasetModel } from '../../models/widgets.model'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { DatasetService } from '../../services/dataset.service'; import { BaseWidgetComponent } from '../base-widget.component';
import { HttpClient } from '@angular/common/http';
@Component({ @Component({
selector: 'app-filled-map-widget', selector: 'app-filled-map-widget',
...@@ -12,90 +11,46 @@ import { HttpClient } from '@angular/common/http'; ...@@ -12,90 +11,46 @@ import { HttpClient } from '@angular/common/http';
providers: [MarkerService, ZoomService, DataLabelService, LegendService, MapsTooltipService], providers: [MarkerService, ZoomService, DataLabelService, LegendService, MapsTooltipService],
templateUrl: './filled-map-widget.component.html', templateUrl: './filled-map-widget.component.html',
}) })
export class FilledMapWidgetComponent implements OnInit { export class FilledMapWidgetComponent extends BaseWidgetComponent {
@Input() config: any;
@Input() title: string = 'Filled Map';
@Input() mapData: Object[];
public zoomSettings: Object; public zoomSettings: Object;
public layers: Object[]; public layers: Object[];
constructor(private datasetService: DatasetService, private http: HttpClient) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
this.zoomSettings = { enable: true };
if (!this.config || !this.config.datasetId) {
this.mapData = [
{ country: 'United States', value: 80 },
{ country: 'Canada', value: 60 },
{ country: 'Mexico', value: 40 },
];
}
this.layers = [
{
shapeData: new Object({ data: 'https://cdn.syncfusion.com/maps/map-data/world-map.json' }),
shapeDataPath: 'name',
shapePropertyPath: 'name',
dataSource: this.mapData,
tooltipSettings: { visible: true, valuePath: 'value' },
shapeSettings: {
fill: '#E5EEF6',
colorValuePath: 'value',
colorMapping: [
{ value: 0, color: '#C3E6CB' },
{ value: 50, color: '#FFECB5' },
{ value: 100, color: '#F5C6CB' }
]
}
}
];
} }
ngOnChanges(changes: SimpleChanges): void { onDataUpdate(data: any[]): void {
if (changes['config'] && this.config && this.config.datasetId) { this.zoomSettings = { enable: true };
this.loadData(); const mapData = data.map(item => ({
} else if (changes['config'] && (!this.config || !this.config.datasetId)) { country: item[this.config.countryField],
this.mapData = [ value: item[this.config.valueField]
{ country: 'United States', value: 80 }, }));
{ country: 'Canada', value: 60 }, this.updateLayers(mapData);
{ country: 'Mexico', value: 40 },
];
this.updateLayers();
}
} }
loadData(): void { onReset(): void {
if (this.config.datasetId) { this.title = 'Filled Map (Default)';
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { this.zoomSettings = { enable: true };
if (dataset && dataset.url) { const defaultMapData = [
this.http.get<any[]>(dataset.url).subscribe(data => { { country: 'United States', value: 80 },
this.mapData = data.map(item => ({ { country: 'Canada', value: 60 },
country: item[this.config.countryField], { country: 'Mexico', value: 40 },
value: item[this.config.valueField] ];
})); this.updateLayers(defaultMapData);
if (this.config.title) {
this.title = this.config.title;
}
this.updateLayers();
});
}
});
}
} }
updateLayers(): void { private updateLayers(dataSource: any[]): void {
this.layers = [ this.layers = [
{ {
shapeData: new Object({ data: 'https://cdn.syncfusion.com/maps/map-data/world-map.json' }), shapeData: new Object({ data: 'https://cdn.syncfusion.com/maps/map-data/world-map.json' }),
shapeDataPath: 'name', shapeDataPath: 'name',
shapePropertyPath: 'name', shapePropertyPath: 'name',
dataSource: this.mapData, dataSource: dataSource,
tooltipSettings: { visible: true, valuePath: 'value' }, tooltipSettings: { visible: true, valuePath: 'value' },
shapeSettings: { shapeSettings: {
fill: '#E5EEF6', fill: '#E5EEF6',
colorValuePath: 'value', colorValuePath: 'value',
colorMapping: [ colorMapping: this.config?.colorMapping || [
{ value: 0, color: '#C3E6CB' }, { value: 0, color: '#C3E6CB' },
{ value: 50, color: '#FFECB5' }, { value: 50, color: '#FFECB5' },
{ value: 100, color: '#F5C6CB' } { value: 100, color: '#F5C6CB' }
......
<ejs-accumulationchart [title]="title" [legendSettings]="legendSettings" [enableSmartLabels]="true"> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Chart -->
<ejs-accumulationchart *ngIf="!isLoading && !hasError" [title]="title" [legendSettings]="legendSettings" [enableSmartLabels]="true">
<e-accumulation-series-collection> <e-accumulation-series-collection>
<e-accumulation-series [dataSource]="chartData" type="Funnel" xName="x" yName="y" [dataLabel]="{ visible: true, name: 'text', position: 'Inside' }"></e-accumulation-series> <e-accumulation-series [dataSource]="chartData" type="Funnel" xName="x" yName="y" [dataLabel]="{ visible: true, name: 'text', position: 'Inside' }"></e-accumulation-series>
</e-accumulation-series-collection> </e-accumulation-series-collection>
......
import { Component, Input, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { AccumulationChartModule, FunnelSeriesService, AccumulationDataLabelService, AccumulationLegendService, AccumulationTooltipService } from '@syncfusion/ej2-angular-charts'; import { AccumulationChartModule, FunnelSeriesService, AccumulationDataLabelService, AccumulationLegendService, AccumulationTooltipService } from '@syncfusion/ej2-angular-charts';
import { DashboardStateService, SelectedDataset } from '../../services/dashboard-state.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({ @Component({
selector: 'app-funnel-chart-widget', selector: 'app-funnel-chart-widget',
...@@ -10,31 +11,22 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard ...@@ -10,31 +11,22 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard
providers: [FunnelSeriesService, AccumulationDataLabelService, AccumulationLegendService, AccumulationTooltipService], providers: [FunnelSeriesService, AccumulationDataLabelService, AccumulationLegendService, AccumulationTooltipService],
templateUrl: './funnel-chart-widget.component.html', templateUrl: './funnel-chart-widget.component.html',
}) })
export class FunnelChartWidgetComponent implements OnInit { export class FunnelChartWidgetComponent extends BaseWidgetComponent {
@Input() config: any; public chartData: Object[];
@Input() chartData: Object[]; public legendSettings: Object;
@Input() title: string = 'Funnel Chart';
@Input() legendSettings: Object; constructor(protected override dashboardStateService: DashboardStateService) {
constructor(private dashboardStateService: DashboardStateService) { } super(dashboardStateService);
ngOnInit(): void {
this.legendSettings = { visible: true };
this.dashboardStateService.selectedDataset$.subscribe((selectedDataset: SelectedDataset | null) => {
if (selectedDataset && selectedDataset.data && this.config && this.config.xField && this.config.yField) {
this.updateChart(selectedDataset.data);
} else {
this.resetChart();
}
});
} }
updateChart(data: any[]): void { onDataUpdate(data: any[]): void {
this.legendSettings = { visible: true };
this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] })); this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] }));
if (this.config.title) {
this.title = this.config.title;
}
} }
resetChart(): void { onReset(): void {
this.title = 'Funnel Chart (Default)';
this.legendSettings = { visible: true };
this.chartData = [ this.chartData = [
{ x: 'Website Visitors', y: 10000 }, { x: 'Website Visitors', y: 10000 },
{ x: 'Leads', y: 8000 }, { x: 'Leads', y: 8000 },
......
<ejs-circulargauge [title]="title" [axes]="axes"> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Chart -->
<ejs-circulargauge *ngIf="!isLoading && !hasError" [title]="title" [axes]="axes">
</ejs-circulargauge> </ejs-circulargauge>
import { Component, Input, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CircularGaugeModule, GaugeTooltipService, AnnotationsService } from '@syncfusion/ej2-angular-circulargauge'; import { CircularGaugeModule, GaugeTooltipService, AnnotationsService } from '@syncfusion/ej2-angular-circulargauge';
import { DashboardStateService, SelectedDataset } from '../../services/dashboard-state.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({ @Component({
selector: 'app-gauge-chart-widget', selector: 'app-gauge-chart-widget',
...@@ -10,55 +11,48 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard ...@@ -10,55 +11,48 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard
providers: [GaugeTooltipService, AnnotationsService], providers: [GaugeTooltipService, AnnotationsService],
templateUrl: './gauge-chart-widget.component.html', templateUrl: './gauge-chart-widget.component.html',
}) })
export class GaugeChartWidgetComponent implements OnInit { export class GaugeChartWidgetComponent extends BaseWidgetComponent {
@Input() config: any;
@Input() value: number = 70;
@Input() title: string = 'Gauge Chart';
public axes: Object[]; public axes: Object[];
constructor(private dashboardStateService: DashboardStateService) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
this.resetChart(); // Set initial state
this.dashboardStateService.selectedDataset$.subscribe((selectedDataset: SelectedDataset | null) => {
if (selectedDataset && selectedDataset.data && this.config && this.config.valueField) {
this.updateChart(selectedDataset.data);
} else {
this.resetChart();
}
});
} }
updateChart(data: any[]): void { onDataUpdate(data: any[]): void {
let value = 0;
if (data.length > 0) { if (data.length > 0) {
this.value = data[0][this.config.valueField]; // Assuming single value for gauge // Assuming gauge shows a single value, either aggregated or the first item's value
} if (this.config.aggregation === 'sum') {
if (this.config.title) { value = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0);
this.title = this.config.title; } else if (this.config.aggregation === 'avg') {
const sum = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0);
value = sum / data.length;
} else {
value = data[0][this.config.valueField];
}
} }
this.setAxes(); this.setAxes(value);
} }
resetChart(): void { onReset(): void {
this.value = 70; this.title = 'Gauge (Default)';
this.setAxes(); this.setAxes(70);
} }
setAxes(): void { private setAxes(value: number): void {
this.axes = [{ this.axes = [{
line: { width: 0 }, line: { width: 0 },
labelStyle: { font: { size: '0px' } }, labelStyle: { font: { size: '0px' } },
majorTicks: { height: 0 }, majorTicks: { height: 0 },
minorTicks: { height: 0 }, minorTicks: { height: 0 },
pointers: [{ pointers: [{
value: this.value, value: value,
radius: '80%', radius: '80%',
pointerWidth: 8, pointerWidth: 8,
cap: { radius: 7 }, cap: { radius: 7 },
needleTail: { length: '18%' } needleTail: { length: '18%' }
}], }],
ranges: [ ranges: this.config?.ranges || [
{ start: 0, end: 50, color: '#E0B9B9' }, { start: 0, end: 50, color: '#E0B9B9' },
{ start: 50, end: 75, color: '#B9D7EA' }, { start: 50, end: 75, color: '#B9D7EA' },
{ start: 75, end: 100, color: '#B9EAB9' } { start: 75, end: 100, color: '#B9EAB9' }
......
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { HttpClient, HttpClientModule } from '@angular/common/http'; import { DashboardStateService } from '../services/dashboard-state.service';
import { DatasetService } from '../services/dataset.service'; import { BaseWidgetComponent } from './base-widget.component';
import { DatasetModel } from '../models/widgets.model';
@Component({ @Component({
selector: 'app-headcount-widget', selector: 'app-headcount-widget',
standalone: true, standalone: true,
imports: [CommonModule, HttpClientModule], imports: [CommonModule],
providers: [DatasetService, HttpClient],
template: ` template: `
<div class="p-4 h-full bg-green-50"> <div class="p-4 h-full bg-green-50">
<h3 class="font-bold text-green-800">{{ title }}</h3> <h3 class="font-bold text-green-800">{{ title }}</h3>
<p class="text-sm text-green-600">Total Headcount: {{ totalHeadcount }}</p>
<div *ngIf="breakdown.length > 0"> <!-- Loading State -->
<p class="text-sm text-green-600">By {{ config?.categoryField || 'Category' }}:</p> <div *ngIf="isLoading" class="flex justify-center items-center h-full">
<ul> <div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-green-500"></div>
<li *ngFor="let item of breakdown" class="text-xs text-green-600"> </div>
{{ item.category }}: {{ item.count }}
</li> <!-- Error State -->
</ul> <div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-2xl"></i>
<p class="mt-1 text-sm">{{ errorMessage }}</p>
</div> </div>
<!-- Content -->
<div *ngIf="!isLoading && !hasError">
<p class="text-sm text-green-600">Total Headcount: {{ totalHeadcount }}</p>
<div *ngIf="breakdown.length > 0">
<p class="text-sm text-green-600">By {{ config?.categoryField || 'Category' }}:</p>
<ul>
<li *ngFor="let item of breakdown" class="text-xs text-green-600">
{{ item.category }}: {{ item.count }}
</li>
</ul>
</div>
</div>
</div> </div>
` `
}) })
export class HeadcountWidgetComponent implements OnInit, OnChanges { export class HeadcountWidgetComponent extends BaseWidgetComponent {
@Input() config: any; public totalHeadcount: number = 0;
title: string = 'Employee Headcount'; public breakdown: { category: string, count: number }[] = [];
totalHeadcount: number = 0;
breakdown: { category: string, count: number }[] = [];
constructor(private datasetService: DatasetService, private http: HttpClient) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
if (this.config && this.config.datasetId) {
this.loadData();
} else {
this.resetData();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['config'] && this.config && this.config.datasetId) {
this.loadData();
} else if (changes['config'] && (!this.config || !this.config.datasetId)) {
this.resetData();
}
} }
loadData(): void { onDataUpdate(data: any[]): void {
if (this.config.datasetId) { this.totalHeadcount = data.length;
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { if (this.config.categoryField) {
if (dataset && dataset.url) { const counts = data.reduce((acc, item) => {
this.http.get<any[]>(dataset.url).subscribe(data => { const category = item[this.config.categoryField];
this.totalHeadcount = data.length; acc[category] = (acc[category] || 0) + 1;
if (this.config.categoryField) { return acc;
const counts = data.reduce((acc, item) => { }, {});
const category = item[this.config.categoryField]; this.breakdown = Object.keys(counts).map(key => ({ category: key, count: counts[key] }));
acc[category] = (acc[category] || 0) + 1;
return acc;
}, {});
this.breakdown = Object.keys(counts).map(key => ({ category: key, count: counts[key] }));
}
if (this.config.title) {
this.title = this.config.title;
}
});
}
});
} }
} }
resetData(): void { onReset(): void {
this.title = 'Headcount (Default)';
this.totalHeadcount = 200; this.totalHeadcount = 200;
this.breakdown = [ this.breakdown = [
{ category: 'IT', count: 50 }, { category: 'IT', count: 50 },
......
...@@ -3,21 +3,42 @@ ...@@ -3,21 +3,42 @@
<h5 class="card-title">{{ title }}</h5> <h5 class="card-title">{{ title }}</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<table class="w-full text-sm text-left text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50"> <!-- Loading State -->
<tr> <div *ngIf="isLoading" class="flex justify-center items-center h-full">
<th *ngFor="let header of headers" scope="col" class="px-6 py-3"> <div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
{{ header }} </div>
</th>
</tr> <!-- Error State -->
</thead> <div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<tbody> <i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<tr *ngFor="let row of data" class="bg-white border-b"> <p class="mt-2">{{ errorMessage }}</p>
<td *ngFor="let cell of row" class="px-6 py-4"> </div>
{{ cell }}
</td> <!-- Data Table -->
</tr> <div *ngIf="!isLoading && !hasError">
</tbody> <table class="w-full text-sm text-left text-gray-500">
</table> <thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th *ngFor="let header of headers" scope="col" class="px-6 py-3">
{{ header }}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of data" class="bg-white border-b">
<td *ngFor="let cell of row" class="px-6 py-4">
{{ cell }}
</td>
</tr>
<tr *ngIf="data.length === 0">
<td [attr.colspan]="headers.length" class="px-6 py-4 text-center text-gray-500">
No data available.
</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DatasetService } from '../../services/dataset.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { HttpClient, HttpClientModule } from '@angular/common/http'; import { BaseWidgetComponent } from '../base-widget.component';
import { DatasetModel } from '../../models/widgets.model';
@Component({ @Component({
selector: 'app-matrix-widget', selector: 'app-matrix-widget',
standalone: true, standalone: true,
imports: [CommonModule, HttpClientModule], imports: [CommonModule],
providers: [DatasetService, HttpClient],
templateUrl: './matrix-widget.component.html', templateUrl: './matrix-widget.component.html',
styleUrls: ['./matrix-widget.component.scss'] styleUrls: ['./matrix-widget.component.scss']
}) })
export class MatrixWidgetComponent implements OnInit, OnChanges { export class MatrixWidgetComponent extends BaseWidgetComponent {
@Input() title: string = 'Matrix'; public headers: string[] = [];
@Input() headers: string[] = []; public data: any[][] = [];
@Input() data: any[][] = [];
@Input() config: any;
constructor(private datasetService: DatasetService, private http: HttpClient) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
if (!this.config || !this.config.datasetId) {
this.headers = ['Category', 'Q1', 'Q2', 'Q3', 'Q4'];
this.data = [
['Product A', 100, 120, 150, 130],
['Product B', 80, 90, 110, 100],
['Product C', 150, 130, 160, 140],
];
}
} }
ngOnChanges(changes: SimpleChanges): void { onDataUpdate(data: any[]): void {
if (changes['config'] && this.config && this.config.datasetId) { if (this.config?.columns && data?.length > 0) {
this.loadData(); this.headers = this.config.columns.map((col: any) => col.headerText);
} else if (changes['config'] && (!this.config || !this.config.datasetId)) { this.data = data.map(row => this.config.columns.map((col: any) => row[col.field]));
this.headers = ['Category', 'Q1', 'Q2', 'Q3', 'Q4']; } else {
this.data = [ this.onReset();
['Product A', 100, 120, 150, 130],
['Product B', 80, 90, 110, 100],
['Product C', 150, 130, 160, 140],
];
} }
} }
loadData(): void { onReset(): void {
if (this.config.datasetId) { this.title = 'Matrix (Default)';
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { this.headers = ['Category', 'Q1', 'Q2', 'Q3', 'Q4'];
if (dataset && dataset.url) { this.data = [
this.http.get<any[]>(dataset.url).subscribe(data => { ['Product A', 100, 120, 150, 130],
if (this.config.columns && data.length > 0) { ['Product B', 80, 90, 110, 100],
this.headers = this.config.columns.map((col: any) => col.headerText); ['Product C', 150, 130, 160, 140],
this.data = data.map(row => this.config.columns.map((col: any) => row[col.field])); ];
}
if (this.config.title) {
this.title = this.config.title;
}
});
}
});
}
} }
} }
...@@ -3,9 +3,28 @@ ...@@ -3,9 +3,28 @@
<h5 class="card-title">{{ title }}</h5> <h5 class="card-title">{{ title }}</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div *ngFor="let item of cardData" class="flex justify-between items-center py-2 border-b last:border-b-0">
<span class="text-gray-600">{{ item.label }}</span> <!-- Loading State -->
<span class="font-bold text-lg">{{ item.value }} <span class="text-sm text-gray-500">{{ item.unit }}</span></span> <div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div> </div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Content -->
<div *ngIf="!isLoading && !hasError">
<div *ngFor="let item of cardData" class="flex justify-between items-center py-2 border-b last:border-b-0">
<span class="text-gray-600">{{ item.label }}</span>
<span class="font-bold text-lg">{{ item.value }} <span class="text-sm text-gray-500">{{ item.unit }}</span></span>
</div>
<div *ngIf="!cardData || cardData.length === 0" class="text-center text-gray-500 py-4">
No data available.
</div>
</div>
</div> </div>
</div> </div>
import { Component, Input, OnInit, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DatasetModel } from '../../models/widgets.model'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { DatasetService } from '../../services/dataset.service'; import { BaseWidgetComponent } from '../base-widget.component';
import { HttpClient } from '@angular/common/http';
@Component({ @Component({
selector: 'app-multi-row-card-widget', selector: 'app-multi-row-card-widget',
...@@ -11,49 +10,27 @@ import { HttpClient } from '@angular/common/http'; ...@@ -11,49 +10,27 @@ import { HttpClient } from '@angular/common/http';
templateUrl: './multi-row-card-widget.component.html', templateUrl: './multi-row-card-widget.component.html',
styleUrls: ['./multi-row-card-widget.component.scss'] styleUrls: ['./multi-row-card-widget.component.scss']
}) })
export class MultiRowCardWidgetComponent implements OnInit { export class MultiRowCardWidgetComponent extends BaseWidgetComponent {
@Input() config: any; public cardData: any[];
@Input() cardData: any[];
@Input() title: string = 'Multi-Row Card'; constructor(protected override dashboardStateService: DashboardStateService) {
constructor(private datasetService: DatasetService, private http: HttpClient) { } super(dashboardStateService);
ngOnInit(): void {
if (!this.config || !this.config.datasetId) {
this.cardData = [
{ label: 'Total Sales', value: '1,234,567', unit: 'USD' },
{ label: 'New Customers', value: '5,432', unit: '' },
{ label: 'Conversion Rate', value: '12.34', unit: '%' },
];
}
} }
ngOnChanges(changes: SimpleChanges): void { onDataUpdate(data: any[]): void {
if (changes['config'] && this.config && this.config.datasetId) { this.cardData = data.map(item => ({
this.loadData(); label: item[this.config.labelField],
} else if (changes['config'] && (!this.config || !this.config.datasetId)) { value: item[this.config.valueField],
this.cardData = [ unit: item[this.config.unitField] || ''
{ label: 'Total Sales', value: '1,234,567', unit: 'USD' }, }));
{ label: 'New Customers', value: '5,432', unit: '' },
{ label: 'Conversion Rate', value: '12.34', unit: '%' },
];
}
} }
loadData(): void { onReset(): void {
if (this.config.datasetId) { this.title = 'Multi-Row Card (Default)';
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { this.cardData = [
if (dataset && dataset.url) { { label: 'Total Sales', value: '1,234,567', unit: 'USD' },
this.http.get<any[]>(dataset.url).subscribe(data => { { label: 'New Customers', value: '5,432', unit: '' },
this.cardData = data.map(item => ({ { label: 'Conversion Rate', value: '12.34', unit: '%' },
label: item[this.config.labelField], ];
value: item[this.config.valueField],
unit: item[this.config.unitField] || ''
}));
if (this.config.title) {
this.title = this.config.title;
}
});
}
});
}
} }
} }
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DashboardStateService } from '../services/dashboard-state.service';
import { HttpClient, HttpClientModule } from '@angular/common/http'; import { BaseWidgetComponent } from './base-widget.component';
import { DatasetService } from '../services/dataset.service';
import { DatasetModel } from '../models/widgets.model';
@Component({ @Component({
selector: 'app-payroll-summary-widget', selector: 'app-payroll-summary-widget',
standalone: true, standalone: true,
imports: [CommonModule, HttpClientModule], imports: [CommonModule],
providers: [DatasetService, HttpClient],
template: ` template: `
<div class="p-4 h-full bg-red-50"> <div class="p-4 h-full bg-red-50">
<h3 class="font-bold text-red-800">{{ title }}</h3> <h3 class="font-bold text-red-800">{{ title }}</h3>
<p class="text-sm text-red-600">Total Payroll: {{ totalPayroll | currency }}</p>
<p class="text-sm text-red-600">Employees Paid: {{ employeesPaid }}</p> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-red-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-2xl"></i>
<p class="mt-1 text-sm">{{ errorMessage }}</p>
</div>
<!-- Content -->
<div *ngIf="!isLoading && !hasError">
<p class="text-sm text-red-600">Total Payroll: {{ totalPayroll | currency }}</p>
<p class="text-sm text-red-600">Employees Paid: {{ employeesPaid }}</p>
</div>
</div> </div>
` `
}) })
export class PayrollSummaryWidgetComponent implements OnInit, OnChanges { export class PayrollSummaryWidgetComponent extends BaseWidgetComponent {
@Input() config: any; public totalPayroll: number = 0;
title: string = 'Payroll Summary'; public employeesPaid: number = 0;
totalPayroll: number = 0;
employeesPaid: number = 0;
constructor(private datasetService: DatasetService, private http: HttpClient) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
if (this.config && this.config.datasetId) {
this.loadData();
} else {
this.resetData();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['config'] && this.config && this.config.datasetId) {
this.loadData();
} else if (changes['config'] && (!this.config || !this.config.datasetId)) {
this.resetData();
}
} }
loadData(): void { onDataUpdate(data: any[]): void {
if (this.config.datasetId) { if (data.length > 0) {
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { const firstItem = data[0];
if (dataset && dataset.url) { this.totalPayroll = firstItem[this.config.totalPayrollField] || 0;
this.http.get<any[]>(dataset.url).subscribe(data => { this.employeesPaid = firstItem[this.config.employeesPaidField] || 0;
if (data.length > 0) {
const firstItem = data[0];
this.totalPayroll = firstItem[this.config.totalPayrollField] || 0;
this.employeesPaid = firstItem[this.config.employeesPaidField] || 0;
}
if (this.config.title) {
this.title = this.config.title;
}
});
}
});
} }
} }
resetData(): void { onReset(): void {
this.title = 'Payroll Summary (Default)';
this.totalPayroll = 1500000; this.totalPayroll = 1500000;
this.employeesPaid = 180; this.employeesPaid = 180;
} }
......
...@@ -4,8 +4,24 @@ ...@@ -4,8 +4,24 @@
<h5 class="card-title">{{ title }}</h5> <h5 class="card-title">{{ title }}</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<p>Employee: {{ employeeName }}</p>
<p>Pay Period: {{ payPeriod }}</p> <!-- Loading State -->
<p>Net Pay: {{ netPay | currency }}</p> <div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Content -->
<div *ngIf="!isLoading && !hasError">
<p>Employee: {{ employeeName }}</p>
<p>Pay Period: {{ payPeriod }}</p>
<p>Net Pay: {{ netPay | currency }}</p>
</div>
</div> </div>
</div> </div>
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DatasetService } from '../../services/dataset.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { HttpClient, HttpClientModule } from '@angular/common/http'; import { BaseWidgetComponent } from '../base-widget.component';
import { DatasetModel } from '../../models/widgets.model';
@Component({ @Component({
selector: 'app-payroll-widget', selector: 'app-payroll-widget',
standalone: true, standalone: true,
imports: [CommonModule, HttpClientModule], imports: [CommonModule],
providers: [DatasetService, HttpClient],
templateUrl: './payroll-widget.component.html', templateUrl: './payroll-widget.component.html',
styleUrls: ['./payroll-widget.component.scss'] styleUrls: ['./payroll-widget.component.scss']
}) })
export class PayrollWidgetComponent implements OnInit, OnChanges { export class PayrollWidgetComponent extends BaseWidgetComponent {
@Input() config: any; public employeeName: string = '';
title: string = 'Payroll Details'; public payPeriod: string = '';
employeeName: string = ''; public netPay: number = 0;
payPeriod: string = '';
netPay: number = 0;
constructor(private datasetService: DatasetService, private http: HttpClient) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
if (this.config && this.config.datasetId) {
this.loadData();
} else {
this.resetData();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['config'] && this.config && this.config.datasetId) {
this.loadData();
} else if (changes['config'] && (!this.config || !this.config.datasetId)) {
this.resetData();
}
} }
loadData(): void { onDataUpdate(data: any[]): void {
if (this.config.datasetId) { if (data.length > 0) {
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { const firstItem = data[0];
if (dataset && dataset.url) { this.employeeName = firstItem[this.config.employeeNameField] || '';
this.http.get<any[]>(dataset.url).subscribe(data => { this.payPeriod = firstItem[this.config.payPeriodField] || '';
if (data.length > 0) { this.netPay = firstItem[this.config.netPayField] || 0;
const firstItem = data[0];
this.employeeName = firstItem[this.config.employeeNameField] || '';
this.payPeriod = firstItem[this.config.payPeriodField] || '';
this.netPay = firstItem[this.config.netPayField] || 0;
}
if (this.config.title) {
this.title = this.config.title;
}
});
}
});
} }
} }
resetData(): void { onReset(): void {
this.title = 'Payroll (Default)';
this.employeeName = 'N/A'; this.employeeName = 'N/A';
this.payPeriod = 'N/A'; this.payPeriod = 'N/A';
this.netPay = 0; this.netPay = 0;
......
<ejs-accumulationchart [title]="title" [legendSettings]="legendSettings" [enableSmartLabels]="true"> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Chart -->
<ejs-accumulationchart *ngIf="!isLoading && !hasError" [title]="title" [legendSettings]="legendSettings" [enableSmartLabels]="true">
<e-accumulation-series-collection> <e-accumulation-series-collection>
<e-accumulation-series [dataSource]="chartData" type="Pie" xName="x" yName="y" [dataLabel]="{ visible: true, name: 'text', position: 'Inside' }"></e-accumulation-series> <e-accumulation-series [dataSource]="chartData" type="Pie" xName="x" yName="y" [dataLabel]="{ visible: true, name: 'text', position: 'Inside' }"></e-accumulation-series>
</e-accumulation-series-collection> </e-accumulation-series-collection>
......
import { Component, Input, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { AccumulationChartModule, PieSeriesService, AccumulationDataLabelService, AccumulationLegendService, AccumulationTooltipService } from '@syncfusion/ej2-angular-charts'; import { AccumulationChartModule, PieSeriesService, AccumulationDataLabelService, AccumulationLegendService, AccumulationTooltipService } from '@syncfusion/ej2-angular-charts';
import { DashboardStateService, SelectedDataset } from '../../services/dashboard-state.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({ @Component({
selector: 'app-pie-chart-widget', selector: 'app-pie-chart-widget',
...@@ -10,27 +11,18 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard ...@@ -10,27 +11,18 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard
providers: [PieSeriesService, AccumulationDataLabelService, AccumulationLegendService, AccumulationTooltipService], providers: [PieSeriesService, AccumulationDataLabelService, AccumulationLegendService, AccumulationTooltipService],
templateUrl: './pie-chart-widget.component.html', templateUrl: './pie-chart-widget.component.html',
}) })
export class PieChartWidgetComponent implements OnInit { export class PieChartWidgetComponent extends BaseWidgetComponent {
@Input() chartData: Object[]; public chartData: Object[];
@Input() title: string = 'Pie Chart'; public legendSettings: Object;
@Input() legendSettings: Object;
@Input() config: any;
constructor(private dashboardStateService: DashboardStateService) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
this.legendSettings = { visible: true };
this.dashboardStateService.selectedDataset$.subscribe((selectedDataset: SelectedDataset | null) => {
if (selectedDataset && selectedDataset.data && this.config && this.config.xField && this.config.yField) {
this.updateChart(selectedDataset.data);
} else {
this.resetChart();
}
});
} }
updateChart(data: any[]): void { onDataUpdate(data: any[]): void {
this.legendSettings = { visible: true };
let transformedData = data; let transformedData = data;
if (this.config.aggregation === 'count') { if (this.config.aggregation === 'count') {
const counts = transformedData.reduce((acc, item) => { const counts = transformedData.reduce((acc, item) => {
const key = item[this.config.xField]; const key = item[this.config.xField];
...@@ -42,19 +34,18 @@ export class PieChartWidgetComponent implements OnInit { ...@@ -42,19 +34,18 @@ export class PieChartWidgetComponent implements OnInit {
transformedData = transformedData.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] })); transformedData = transformedData.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] }));
} }
this.chartData = transformedData; this.chartData = transformedData;
if (this.config.title) {
this.title = this.config.title;
}
} }
resetChart(): void { onReset(): void {
this.title = 'Pie Chart (Default)';
this.legendSettings = { visible: true };
this.chartData = [ this.chartData = [
{ browser: 'Chrome', users: 37 }, { x: 'Chrome', y: 37 },
{ browser: 'Firefox', users: 17 }, { x: 'Firefox', y: 17 },
{ browser: 'Internet Explorer', users: 19 }, { x: 'Internet Explorer', y: 19 },
{ browser: 'Edge', users: 4 }, { x: 'Edge', y: 4 },
{ browser: 'Safari', users: 11 }, { x: 'Safari', y: 11 },
{ browser: 'Other', users: 12 }, { x: 'Other', y: 12 },
]; ];
} }
} }
<ejs-chart [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis"> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Chart -->
<ejs-chart *ngIf="!isLoading && !hasError" [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis">
<e-series-collection> <e-series-collection>
<e-series [dataSource]="chartData" [type]="type" xName="x" yName="y" [size]="config?.sizeField" name="Data"></e-series> <e-series [dataSource]="chartData" [type]="type" xName="x" yName="y" [size]="config?.sizeField" name="Data"></e-series>
</e-series-collection> </e-series-collection>
......
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChartModule, ScatterSeriesService, BubbleSeriesService, CategoryService, LegendService, TooltipService, DataLabelService } from '@syncfusion/ej2-angular-charts'; import { ChartModule, ScatterSeriesService, BubbleSeriesService, CategoryService, LegendService, TooltipService, DataLabelService } from '@syncfusion/ej2-angular-charts';
import { DatasetService } from '../../services/dataset.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { HttpClient, HttpClientModule } from '@angular/common/http'; import { BaseWidgetComponent } from '../base-widget.component';
import { DatasetModel } from '../../models/widgets.model';
@Component({ @Component({
selector: 'app-scatter-bubble-chart-widget', selector: 'app-scatter-bubble-chart-widget',
standalone: true, standalone: true,
imports: [CommonModule, ChartModule, HttpClientModule], imports: [CommonModule, ChartModule],
providers: [ScatterSeriesService, BubbleSeriesService, CategoryService, LegendService, TooltipService, DataLabelService, DatasetService, HttpClient], providers: [ScatterSeriesService, BubbleSeriesService, CategoryService, LegendService, TooltipService, DataLabelService],
templateUrl: './scatter-bubble-chart-widget.component.html', templateUrl: './scatter-bubble-chart-widget.component.html',
}) })
export class ScatterBubbleChartWidgetComponent implements OnInit, OnChanges { export class ScatterBubbleChartWidgetComponent extends BaseWidgetComponent {
@Input() chartData: Object[]; public chartData: Object[];
@Input() title: string = 'Scatter & Bubble Chart'; public type: 'Scatter' | 'Bubble' = 'Scatter';
@Input() type: 'Scatter' | 'Bubble' = 'Scatter';
@Input() config: any;
public primaryXAxis: Object; public primaryXAxis: Object;
public primaryYAxis: Object; public primaryYAxis: Object;
constructor(private datasetService: DatasetService, private http: HttpClient) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
}
ngOnInit(): void { onDataUpdate(data: any[]): void {
this.primaryXAxis = { title: 'X-Value' }; this.type = this.config.type || 'Scatter';
this.primaryYAxis = { title: 'Y-Value' }; if (this.type === 'Bubble') {
if (!this.config || !this.config.datasetId) { this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField], size: item[this.config.sizeField] }));
if (this.type === 'Scatter') { } else {
this.chartData = [ this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] }));
{ x: 10, y: 35 }, { x: 15, y: 28 }, }
{ x: 20, y: 34 }, { x: 25, y: 32 }, this.primaryXAxis = { title: this.config.xAxisTitle || '' };
{ x: 30, y: 40 }, { x: 35, y: 30 }, this.primaryYAxis = { title: this.config.yAxisTitle || '' };
]; }
} else { // Bubble
onReset(): void {
this.title = 'Scatter Chart (Default)';
this.type = this.config?.type || 'Scatter';
if (this.type === 'Bubble') {
this.chartData = [ this.chartData = [
{ x: 10, y: 35, size: 5 }, { x: 15, y: 28, size: 8 }, { x: 10, y: 35, size: 5 }, { x: 15, y: 28, size: 8 },
{ x: 20, y: 34, size: 6 }, { x: 25, y: 32, size: 10 }, { x: 20, y: 34, size: 6 }, { x: 25, y: 32, size: 10 },
{ x: 30, y: 40, size: 7 }, { x: 35, y: 30, size: 9 }, { x: 30, y: 40, size: 7 }, { x: 35, y: 30, size: 9 },
]; ];
} } else {
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['config'] && this.config && this.config.datasetId) {
this.loadData();
} else if (changes['config'] && (!this.config || !this.config.datasetId)) {
if (this.type === 'Scatter') {
this.chartData = [ this.chartData = [
{ x: 10, y: 35 }, { x: 15, y: 28 }, { x: 10, y: 35 }, { x: 15, y: 28 },
{ x: 20, y: 34 }, { x: 25, y: 32 }, { x: 20, y: 34 }, { x: 25, y: 32 },
{ x: 30, y: 40 }, { x: 35, y: 30 }, { x: 30, y: 40 }, { x: 35, y: 30 },
]; ];
} else { // Bubble
this.chartData = [
{ x: 10, y: 35, size: 5 }, { x: 15, y: 28, size: 8 },
{ x: 20, y: 34, size: 6 }, { x: 25, y: 32, size: 10 },
{ x: 30, y: 40, size: 7 }, { x: 35, y: 30, size: 9 },
];
}
}
}
loadData(): void {
if (this.config.datasetId) {
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => {
if (dataset && dataset.url) {
this.http.get<any[]>(dataset.url).subscribe(data => {
if (this.config.type === 'Bubble') {
this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField], size: item[this.config.sizeField] }));
} else {
this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] }));
}
if (this.config.title) {
this.title = this.config.title;
}
if (this.config.xAxisTitle) {
this.primaryXAxis = { ...this.primaryXAxis, title: this.config.xAxisTitle };
}
if (this.config.yAxisTitle) {
this.primaryYAxis = { ...this.primaryYAxis, title: this.config.yAxisTitle };
}
if (this.config.type) {
this.type = this.config.type;
}
});
}
});
} }
this.primaryXAxis = { title: 'X-Value' };
this.primaryYAxis = { title: 'Y-Value' };
} }
} }
<div class="card h-100"> <div class="card h-100">
<div class="card-body flex flex-col justify-center items-center"> <div class="card-body">
<h5 class="text-lg font-semibold text-gray-600 mb-2">{{ title }}</h5>
<div class="text-5xl font-bold text-gray-800 mb-2"> <!-- Loading State -->
{{ value }}<span *ngIf="unit" class="text-2xl ml-1">{{ unit }}</span> <div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div> </div>
<div *ngIf="trendValue" class="flex items-center text-sm">
<span *ngIf="trend === 'up'" class="text-green-500 flex items-center"> <!-- Error State -->
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"></path></svg> <div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
{{ trendValue }} <i class="bi bi-exclamation-triangle-fill text-4xl"></i>
</span> <p class="mt-2">{{ errorMessage }}</p>
<span *ngIf="trend === 'down'" class="text-red-500 flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path></svg>
{{ trendValue }}
</span>
<span *ngIf="trend === 'neutral' && trendValue" class="text-gray-500">
{{ trendValue }}
</span>
</div> </div>
<!-- Content -->
<div *ngIf="!isLoading && !hasError" class="flex flex-col justify-center items-center h-full">
<h5 class="text-lg font-semibold text-gray-600 mb-2">{{ title }}</h5>
<div class="text-5xl font-bold text-gray-800 mb-2">
{{ value }}<span *ngIf="unit" class="text-2xl ml-1">{{ unit }}</span>
</div>
<div *ngIf="trendValue" class="flex items-center text-sm">
<span *ngIf="trend === 'up'" class="text-green-500 flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"></path></svg>
{{ trendValue }}
</span>
<span *ngIf="trend === 'down'" class="text-red-500 flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path></svg>
{{ trendValue }}
</span>
<span *ngIf="trend === 'neutral' && trendValue" class="text-gray-500">
{{ trendValue }}
</span>
</div>
</div>
</div> </div>
</div> </div>
import { Component, Input, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DashboardStateService, SelectedDataset } from '../../services/dashboard-state.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({ @Component({
selector: 'app-simple-kpi-widget', selector: 'app-simple-kpi-widget',
...@@ -10,47 +11,38 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard ...@@ -10,47 +11,38 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard
templateUrl: './simple-kpi-widget.component.html', templateUrl: './simple-kpi-widget.component.html',
styleUrls: ['./simple-kpi-widget.component.scss'] styleUrls: ['./simple-kpi-widget.component.scss']
}) })
export class SimpleKpiWidgetComponent implements OnInit { export class SimpleKpiWidgetComponent extends BaseWidgetComponent {
@Input() title: string = 'Key Performance Indicator'; public value: string = '0';
@Input() value: string = '123,456'; public unit: string = '';
@Input() unit: string = ''; public trend: 'up' | 'down' | 'neutral' = 'neutral';
@Input() trend: 'up' | 'down' | 'neutral' = 'neutral'; public trendValue: string = '';
@Input() trendValue: string = '';
@Input() config: any;
constructor(private dashboardStateService: DashboardStateService) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
this.dashboardStateService.selectedDataset$.subscribe((selectedDataset: SelectedDataset | null) => {
if (selectedDataset && selectedDataset.data && this.config && this.config.valueField) {
this.updateWidget(selectedDataset.data);
} else {
this.resetWidget();
}
});
} }
updateWidget(data: any[]): void { onDataUpdate(data: any[]): void {
if (data.length > 0) { if (data.length > 0) {
let kpiValue = data[0][this.config.valueField]; let kpiValue = 0;
if (this.config.aggregation === 'count') { if (this.config.aggregation === 'count') {
kpiValue = data.length; kpiValue = data.length;
} else if (this.config.aggregation === 'sum') { } else if (this.config.aggregation === 'sum') {
kpiValue = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0); kpiValue = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0);
} else {
// Default to first value if no aggregation
kpiValue = data[0][this.config.valueField];
} }
this.value = kpiValue.toLocaleString(); this.value = kpiValue.toLocaleString();
this.unit = this.config.unit || ''; this.unit = this.config.unit || '';
this.trend = this.config.trend || 'neutral'; this.trend = this.config.trend || 'neutral';
this.trendValue = this.config.trendValue || ''; this.trendValue = this.config.trendValue || '';
} }
if (this.config.title) {
this.title = this.config.title;
}
} }
resetWidget(): void { onReset(): void {
this.title = 'KPI (Default)';
this.value = '123,456'; this.value = '123,456';
this.unit = ''; this.unit = '#';
this.trend = 'neutral'; this.trend = 'neutral';
this.trendValue = ''; this.trendValue = '';
} }
......
...@@ -3,21 +3,42 @@ ...@@ -3,21 +3,42 @@
<h5 class="card-title">{{ title }}</h5> <h5 class="card-title">{{ title }}</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<table class="w-full text-sm text-left text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50"> <!-- Loading State -->
<tr> <div *ngIf="isLoading" class="flex justify-center items-center h-full">
<th *ngFor="let header of headers" scope="col" class="px-6 py-3"> <div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
{{ header }} </div>
</th>
</tr> <!-- Error State -->
</thead> <div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<tbody> <i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<tr *ngFor="let row of data" class="bg-white border-b"> <p class="mt-2">{{ errorMessage }}</p>
<td *ngFor="let cell of row" class="px-6 py-4"> </div>
{{ cell }}
</td> <!-- Data Table -->
</tr> <div *ngIf="!isLoading && !hasError">
</tbody> <table class="w-full text-sm text-left text-gray-500">
</table> <thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th *ngFor="let header of headers" scope="col" class="px-6 py-3">
{{ header }}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of data" class="bg-white border-b">
<td *ngFor="let cell of row" class="px-6 py-4">
{{ cell }}
</td>
</tr>
<tr *ngIf="data.length === 0">
<td [attr.colspan]="headers.length" class="px-6 py-4 text-center text-gray-500">
No data available.
</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
import { Component, Input, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DashboardStateService, SelectedDataset } from '../../services/dashboard-state.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({ @Component({
selector: 'app-simple-table-widget', selector: 'app-simple-table-widget',
...@@ -10,36 +11,25 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard ...@@ -10,36 +11,25 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard
templateUrl: './simple-table-widget.component.html', templateUrl: './simple-table-widget.component.html',
styleUrls: ['./simple-table-widget.component.scss'] styleUrls: ['./simple-table-widget.component.scss']
}) })
export class SimpleTableWidgetComponent implements OnInit { export class SimpleTableWidgetComponent extends BaseWidgetComponent {
@Input() config: any; public headers: string[] = [];
@Input() title: string = 'Table'; public data: any[][] = [];
@Input() headers: string[] = [];
@Input() data: any[][] = [];
constructor(private dashboardStateService: DashboardStateService) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
this.resetTable();
this.dashboardStateService.selectedDataset$.subscribe((selectedDataset: SelectedDataset | null) => {
if (selectedDataset && selectedDataset.data && this.config && this.config.columns) {
this.updateTable(selectedDataset.data);
} else {
this.resetTable();
}
});
} }
updateTable(data: any[]): void { onDataUpdate(data: any[]): void {
if (data.length > 0) { if (data && data.length > 0 && this.config?.columns) {
this.headers = this.config.columns.map((col: any) => col.headerText); this.headers = this.config.columns.map((col: any) => col.headerText);
this.data = data.map(row => this.config.columns.map((col: any) => row[col.field])); this.data = data.map(row => this.config.columns.map((col: any) => row[col.field]));
} } else {
if (this.config.title) { this.onReset();
this.title = this.config.title;
} }
} }
resetTable(): void { onReset(): void {
// Display placeholder data or an empty state
this.headers = ['ID', 'Name', 'Status']; this.headers = ['ID', 'Name', 'Status'];
this.data = [ this.data = [
[1, 'Item A', 'Active'], [1, 'Item A', 'Active'],
......
...@@ -3,7 +3,20 @@ ...@@ -3,7 +3,20 @@
<h5 class="card-title">{{ title }}</h5> <h5 class="card-title">{{ title }}</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<select
<!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-2xl"></i>
<p class="mt-1">{{ errorMessage }}</p>
</div>
<!-- Content -->
<select *ngIf="!isLoading && !hasError"
[(ngModel)]="selectedValue" [(ngModel)]="selectedValue"
(change)="onSelectionChange()" (change)="onSelectionChange()"
class="w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white" class="w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white"
...@@ -12,5 +25,6 @@ ...@@ -12,5 +25,6 @@
{{ option }} {{ option }}
</option> </option>
</select> </select>
</div> </div>
</div> </div>
import { Component, Input, OnInit, Output, EventEmitter, SimpleChanges, OnChanges } from '@angular/core'; import { Component, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { DatasetModel } from '../../models/widgets.model'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { DatasetService } from '../../services/dataset.service'; import { BaseWidgetComponent } from '../base-widget.component';
import { HttpClient } from '@angular/common/http';
@Component({ @Component({
selector: 'app-slicer-widget', selector: 'app-slicer-widget',
standalone: true, standalone: true,
...@@ -11,52 +11,32 @@ import { HttpClient } from '@angular/common/http'; ...@@ -11,52 +11,32 @@ import { HttpClient } from '@angular/common/http';
templateUrl: './slicer-widget.component.html', templateUrl: './slicer-widget.component.html',
styleUrls: ['./slicer-widget.component.scss'] styleUrls: ['./slicer-widget.component.scss']
}) })
export class SlicerWidgetComponent implements OnInit, OnChanges { export class SlicerWidgetComponent extends BaseWidgetComponent {
@Input() config: any;
public title: string = 'Slicer';
public options: string[] = []; public options: string[] = [];
public selectedValue: string = ''; public selectedValue: string = '';
@Output() selectionChange = new EventEmitter<string>(); @Output() selectionChange = new EventEmitter<string>();
constructor(private datasetService: DatasetService, private http: HttpClient) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
if (!this.config || !this.config.datasetId) {
this.options = ['All', 'Option 1', 'Option 2', 'Option 3'];
this.selectedValue = this.options[0];
}
} }
ngOnChanges(changes: SimpleChanges): void { onDataUpdate(data: any[]): void {
if (changes['config'] && this.config && this.config.datasetId) { if (this.config.optionsField) {
this.loadData(); const uniqueOptions = [...new Set(data.map((item: any) => item[this.config.optionsField]))];
} else if (changes['config'] && (!this.config || !this.config.datasetId)) { this.options = ['All', ...uniqueOptions.map(String)];
this.options = ['All', 'Option 1', 'Option 2', 'Option 3'];
this.selectedValue = this.options[0]; this.selectedValue = this.options[0];
} }
} }
loadData(): void { onReset(): void {
if (this.config.datasetId) { this.title = 'Slicer (Default)';
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { this.options = ['All', 'Option 1', 'Option 2', 'Option 3'];
if (dataset && dataset.url) { this.selectedValue = this.options[0];
this.http.get<any[]>(dataset.url).subscribe((data: any[]) => {
if (this.config.optionsField) {
const uniqueOptions = [...new Set(data.map((item: any) => item[this.config.optionsField]))];
this.options = ['All', ...uniqueOptions.map(String)]; // Ensure options are strings
this.selectedValue = this.options[0];
}
if (this.config.title) {
this.title = this.config.title;
}
});
}
});
}
} }
onSelectionChange(): void { onSelectionChange(): void {
// Note: In a real scenario, this should probably update the global state
// via DashboardStateService so other widgets can react to the filter.
this.selectionChange.emit(this.selectedValue); this.selectionChange.emit(this.selectedValue);
} }
} }
...@@ -3,8 +3,24 @@ ...@@ -3,8 +3,24 @@
<div class="card-header"> <div class="card-header">
<h5 class="card-title">{{ title }}</h5> <h5 class="card-title">{{ title }}</h5>
</div> </div>
<div class="card-body flex flex-col justify-center items-center"> <div class="card-body">
<p class="text-xl font-semibold">Status: {{ status }}</p>
<p class="text-lg">Hours Today: {{ hours }}</p> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Content -->
<div *ngIf="!isLoading && !hasError" class="flex flex-col justify-center items-center h-full">
<p class="text-xl font-semibold">Status: {{ status }}</p>
<p class="text-lg">Hours Today: {{ hours }}</p>
</div>
</div> </div>
</div> </div>
import { Component, Input, OnInit, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DatasetService } from '../../services/dataset.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { HttpClient } from '@angular/common/http'; import { BaseWidgetComponent } from '../base-widget.component';
import { DatasetModel } from '../../models/widgets.model';
@Component({ @Component({
selector: 'app-time-tracking-widget', selector: 'app-time-tracking-widget',
...@@ -12,47 +11,28 @@ import { DatasetModel } from '../../models/widgets.model'; ...@@ -12,47 +11,28 @@ import { DatasetModel } from '../../models/widgets.model';
templateUrl: './time-tracking-widget.component.html', templateUrl: './time-tracking-widget.component.html',
styleUrls: ['./time-tracking-widget.component.scss'] styleUrls: ['./time-tracking-widget.component.scss']
}) })
export class TimeTrackingWidgetComponent implements OnInit { export class TimeTrackingWidgetComponent extends BaseWidgetComponent {
@Input() data: any; public status: string = '';
@Input() config: any; public hours: string = '';
@Input() title: string = 'Time Tracking';
status: string = '';
hours: string = '';
constructor(protected override dashboardStateService: DashboardStateService) {
constructor(private datasetService: DatasetService, private http: HttpClient) { } super(dashboardStateService);
ngOnInit(): void {
if (!this.config || !this.config.datasetId) {
this.status = 'Clocked In';
this.hours = '8.0';
}
} }
ngOnChanges(changes: SimpleChanges): void { onDataUpdate(data: any[]): void {
if (changes['config'] && this.config && this.config.datasetId) { if (data.length > 0) {
this.loadData(); // Assuming time tracking data is for a single user, so take the first item
} else if (changes['config'] && (!this.config || !this.config.datasetId)) { const timeEntry = data[0];
this.status = 'Clocked In'; this.status = this.config.statusField ? timeEntry[this.config.statusField] : 'N/A';
this.hours = '8.0'; this.hours = this.config.hoursField ? timeEntry[this.config.hoursField] : 'N/A';
} else {
this.onReset();
} }
} }
loadData(): void { onReset(): void {
if (this.config.datasetId) { this.title = 'Time Tracking (Default)';
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { this.status = 'Clocked In';
if (dataset && dataset.url) { this.hours = '8.0';
this.http.get<any[]>(dataset.url).subscribe(data => {
if (data.length > 0) {
this.status = this.config.statusField ? data[0][this.config.statusField] : 'N/A';
this.hours = this.config.hoursField ? data[0][this.config.hoursField] : 'N/A';
}
if (this.config.title) {
this.title = this.config.title;
}
});
}
});
}
} }
} }
<ejs-treemap [titleSettings]="{ text: title }" [dataSource]="dataSource" [weightValuePath]="weightValuePath" [leafItemSettings]="leafItemSettings"> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Chart -->
<ejs-treemap *ngIf="!isLoading && !hasError" [titleSettings]="{ text: title }" [dataSource]="dataSource" [weightValuePath]="weightValuePath" [leafItemSettings]="leafItemSettings">
</ejs-treemap> </ejs-treemap>
import { Component, Input, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TreeMapModule, TreeMapTooltipService, TreeMapLegendService, TreeMapHighlightService, TreeMapSelectionService } from '@syncfusion/ej2-angular-treemap'; import { TreeMapModule, TreeMapTooltipService, TreeMapLegendService, TreeMapHighlightService, TreeMapSelectionService } from '@syncfusion/ej2-angular-treemap';
import { DashboardStateService, SelectedDataset } from '../../services/dashboard-state.service'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({ @Component({
selector: 'app-treemap-widget', selector: 'app-treemap-widget',
...@@ -10,50 +11,38 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard ...@@ -10,50 +11,38 @@ import { DashboardStateService, SelectedDataset } from '../../services/dashboard
providers: [TreeMapTooltipService, TreeMapLegendService, TreeMapHighlightService, TreeMapSelectionService], providers: [TreeMapTooltipService, TreeMapLegendService, TreeMapHighlightService, TreeMapSelectionService],
templateUrl: './treemap-widget.component.html', templateUrl: './treemap-widget.component.html',
}) })
export class TreemapWidgetComponent implements OnInit { export class TreemapWidgetComponent extends BaseWidgetComponent {
@Input() config: any;
@Input() title: string = 'Treemap';
public dataSource: Object[]; public dataSource: Object[];
public weightValuePath: string; public weightValuePath: string;
public leafItemSettings: Object; public leafItemSettings: Object;
constructor(private dashboardStateService: DashboardStateService) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
this.resetChart();
this.dashboardStateService.selectedDataset$.subscribe((selectedDataset: SelectedDataset | null) => {
if (selectedDataset && selectedDataset.data && this.config && this.config.valueField) {
this.updateChart(selectedDataset.data);
} else {
this.resetChart();
}
});
} }
updateChart(data: any[]): void { onDataUpdate(data: any[]): void {
this.dataSource = data; this.dataSource = data;
if (this.config.title) { this.weightValuePath = this.config.valueField;
this.title = this.config.title; this.leafItemSettings = {
} labelPath: this.config.groupField,
if (this.config.groupField) { showLabels: true
this.leafItemSettings = { labelPath: this.config.groupField }; };
}
if (this.config.valueField) {
this.weightValuePath = this.config.valueField;
}
} }
resetChart(): void { onReset(): void {
this.title = 'Treemap (Default)';
this.dataSource = [ this.dataSource = [
{ Continent: 'Asia', Countries: 'China', population: 1400 }, { group: 'Asia', country: 'China', value: 1400 },
{ Continent: 'Asia', Countries: 'India', population: 1350 }, { group: 'Asia', country: 'India', value: 1350 },
{ Continent: 'Europe', Countries: 'Germany', population: 83 }, { group: 'Europe', country: 'Germany', value: 83 },
{ Continent: 'Europe', Countries: 'France', population: 67 }, { group: 'Europe', country: 'France', value: 67 },
{ Continent: 'North America', Countries: 'USA', population: 330 }, { group: 'North America', country: 'USA', value: 330 },
{ Continent: 'North America', Countries: 'Canada', population: 38 }, { group: 'North America', country: 'Canada', value: 38 },
]; ];
this.weightValuePath = 'population'; this.weightValuePath = 'value';
this.leafItemSettings = { labelPath: 'Countries' }; this.leafItemSettings = {
labelPath: 'country',
showLabels: true
};
} }
} }
<ejs-chart [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis"> <!-- Loading State -->
<div *ngIf="isLoading" class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div *ngIf="hasError" class="flex flex-col justify-center items-center h-full text-red-500">
<i class="bi bi-exclamation-triangle-fill text-4xl"></i>
<p class="mt-2">{{ errorMessage }}</p>
</div>
<!-- Chart -->
<ejs-chart *ngIf="!isLoading && !hasError" [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis">
<e-series-collection> <e-series-collection>
<e-series [dataSource]="chartData" type="Waterfall" xName="x" yName="y" name="Amount"></e-series> <e-series [dataSource]="chartData" type="Waterfall" xName="x" yName="y" name="Amount"></e-series>
</e-series-collection> </e-series-collection>
......
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChartModule, WaterfallSeriesService, CategoryService, LegendService, TooltipService, DataLabelService } from '@syncfusion/ej2-angular-charts'; import { ChartModule, WaterfallSeriesService, CategoryService, LegendService, TooltipService, DataLabelService } from '@syncfusion/ej2-angular-charts';
import { DatasetModel } from '../../models/widgets.model'; import { DashboardStateService } from '../../services/dashboard-state.service';
import { DatasetService } from '../../services/dataset.service'; import { BaseWidgetComponent } from '../base-widget.component';
import { HttpClient, HttpClientModule } from '@angular/common/http';
@Component({ @Component({
selector: 'app-waterfall-chart-widget', selector: 'app-waterfall-chart-widget',
standalone: true, standalone: true,
imports: [CommonModule, ChartModule, HttpClientModule], imports: [CommonModule, ChartModule],
providers: [WaterfallSeriesService, CategoryService, LegendService, TooltipService, DataLabelService, DatasetService, HttpClient], providers: [WaterfallSeriesService, CategoryService, LegendService, TooltipService, DataLabelService],
templateUrl: './waterfall-chart-widget.component.html', templateUrl: './waterfall-chart-widget.component.html',
}) })
export class WaterfallChartWidgetComponent implements OnInit, OnChanges { export class WaterfallChartWidgetComponent extends BaseWidgetComponent {
@Input() config: any;
public chartData: Object[]; public chartData: Object[];
public title: string = 'Waterfall Chart';
public primaryXAxis: Object; public primaryXAxis: Object;
public primaryYAxis: Object; public primaryYAxis: Object;
constructor(private datasetService: DatasetService, private http: HttpClient) { } constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
ngOnInit(): void {
this.primaryXAxis = { valueType: 'Category', title: 'Category' };
this.primaryYAxis = { title: 'Amount' };
if (!this.config || !this.config.datasetId) {
this.chartData = [
{ category: 'Starting Balance', amount: 10000 },
{ category: 'Revenue', amount: 5000 },
{ category: 'Expenses', amount: -2000 },
{ category: 'Investments', amount: 3000 },
{ category: 'Ending Balance', amount: 16000 },
];
}
} }
ngOnChanges(changes: SimpleChanges): void { onDataUpdate(data: any[]): void {
if (changes['config'] && this.config && this.config.datasetId) { this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] }));
this.loadData(); this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' };
} else if (changes['config'] && (!this.config || !this.config.datasetId)) { this.primaryYAxis = { title: this.config.yAxisTitle || '' };
this.chartData = [
{ category: 'Starting Balance', amount: 10000 },
{ category: 'Revenue', amount: 5000 },
{ category: 'Expenses', amount: -2000 },
{ category: 'Investments', amount: 3000 },
{ category: 'Ending Balance', amount: 16000 },
];
}
} }
loadData(): void { onReset(): void {
if (this.config.datasetId) { this.title = 'Waterfall Chart (Default)';
this.datasetService.getDatasetById(this.config.datasetId).subscribe((dataset: DatasetModel | undefined) => { this.chartData = [
if (dataset && dataset.url) { { x: 'Initial', y: 100 },
this.http.get<any[]>(dataset.url).subscribe(data => { { x: 'Sales', y: 50 },
this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] })); { x: 'Expenses', y: -20 },
if (this.config.title) { { x: 'Refunds', y: -15 },
this.title = this.config.title; { x: 'Final', isSum: true }
} ];
if (this.config.xAxisTitle) { this.primaryXAxis = { valueType: 'Category', title: 'Category' };
this.primaryXAxis = { ...this.primaryXAxis, title: this.config.xAxisTitle }; this.primaryYAxis = { title: 'Amount' };
}
if (this.config.yAxisTitle) {
this.primaryYAxis = { ...this.primaryYAxis, title: this.config.yAxisTitle };
}
});
}
});
}
} }
} }
ข้อเสนอแนะเพื่อการปรับปรุง
ข้อเสนอแนะเพื่อการปรับปรุง
โค้ดโดยรวมเขียนได้ดีครับ แต่มีบางจุดที่สามารถปรับปรุงเพื่อให้ดูแลรักษาง่ายและขยายความสามารถในอนาคตได้ดียิ่งขึ้นครับ
2. ปรับปรุงหน้าจอตั้งค่า Widget (`widget-config.component`):
* ปัญหา: ไฟล์ widget-config.component.html มี *ngIf จำนวนมากเพื่อแสดงฟอร์มสำหรับวิดเจ็ตแต่ละประเภท ซึ่งจะทำให้ไฟล์นี้ใหญ่และซับซ้อนขึ้นเรื่อยๆ เมื่อมีวิดเจ็ตใหม่ๆ เพิ่มเข้ามา
* ข้อเสนอแนะ: อาจจะแยกฟอร์มการตั้งค่าของแต่ละวิดเจ็ตออกเป็นคอมโพเนนต์ของตัวเอง แล้วให้ widget-config.component ทำหน้าที่แค่โหลดคอมโพเนนต์ฟอร์มนั้นๆ แบบไดนามิก (คล้ายกับวิธีที่แดชบอร์ดโหลดวิดเจ็ต)
3. แยกการลงทะเบียน Widget (`widgetComponentMap`):
* ปัญหา: ปัจจุบันการเพิ่มวิดเจ็ตใหม่จะต้องมีการแก้ไข widgetComponentMap ใน dashboard-management.component.ts ทุกครั้ง
* ข้อเสนอแนะ: สร้าง WidgetRegistryService เพื่อทำหน้าที่ลงทะเบียนวิดเจ็ตโดยเฉพาะ จะช่วยให้ dashboard-management.component สะอาดขึ้น และการเพิ่มวิดเจ็ตใหม่จะไม่ต้องไปยุ่งกับโค้ดส่วนกลาง
โดยรวมแล้วฟังก์ชันการทำงานของแดชบอร์ดสมบูรณ์ดีครับ ข้อเสนอแนะเหล่านี้เป็นแนวทางในการปรับปรุงโครงสร้างเพื่อรองรับการพัฒนาในระยะยาวครับ
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