Commit c200249b by Ooh-Ao

config

parent 162ff658
......@@ -36,7 +36,7 @@
"@syncfusion/ej2-angular-base": "^31.1.17",
"@syncfusion/ej2-angular-buttons": "^31.1.17",
"@syncfusion/ej2-angular-calendars": "^31.1.19",
"@syncfusion/ej2-angular-charts": "^31.1.17",
"@syncfusion/ej2-angular-charts": "^31.1.19",
"@syncfusion/ej2-angular-circulargauge": "^31.1.17",
"@syncfusion/ej2-angular-dropdowns": "^31.1.17",
"@syncfusion/ej2-angular-grids": "^31.1.17",
......
......@@ -138,12 +138,16 @@ export class DashboardManagementComponent implements OnInit {
if (dashboard) {
this.dashboardData = dashboard;
// Ensure all widgets have a unique instanceId for this session
// Ensure all widgets have a unique instanceId for this session and parse JSON strings
if (dashboard.widgets) {
dashboard.widgets.forEach((widget: any) => {
if (!widget.instanceId) {
widget.instanceId = `inst_${Date.now()}_${Math.random()}`;
}
// Parse JSON strings to objects
widget.config = this.parseJsonString(widget.config);
widget.perspective = this.parseJsonString(widget.perspective);
widget.data = this.parseJsonString(widget.data);
});
}
......@@ -198,7 +202,9 @@ export class DashboardManagementComponent implements OnInit {
saveDashboardName(): void {
if (this.dashboardData) {
this.dashboardDataService.saveDashboard(this.dashboardData).pipe(
const dashboardToSave = this.convertWidgetsToStrings(this.dashboardData);
this.dashboardDataService.saveDashboard(dashboardToSave).pipe(
catchError(error => {
this.notificationService.error('Error', 'Failed to save dashboard name.');
return throwError(() => error);
......@@ -252,27 +258,18 @@ export class DashboardManagementComponent implements OnInit {
});
console.log('Current panels from layout:', currentPanels);
console.log('Updated widget positions:', this.dashboardData.widgets);
const dashboardToSave = JSON.parse(JSON.stringify(this.dashboardData));
if (dashboardToSave.widgets) {
const allWidgetStates = this.widgetStateService.getAllWidgetStates(); // Get all current widget states
dashboardToSave.widgets.forEach((widget: any) => {
// Remove the internal-only instanceId before saving
delete widget.instanceId;
// Convert objects back to strings for saving
const dashboardToSave = this.convertWidgetsToStrings(this.dashboardData);
if (widget.config && typeof widget.config === 'object') {
widget.config = JSON.stringify(widget.config);
}
// Update perspective from WidgetStateService if available
// Update perspectives from WidgetStateService
if (dashboardToSave.widgets) {
const allWidgetStates = this.widgetStateService.getAllWidgetStates();
dashboardToSave.widgets.forEach((widget: any) => {
const currentPerspective = allWidgetStates.get(widget.widgetId);
if (currentPerspective !== undefined) { // Check for undefined to allow nulls
if (currentPerspective !== undefined) {
widget.perspective = currentPerspective;
}
// Ensure perspective is stringified if it's an object (though it should be string from getWidgetState)
if (widget.perspective && typeof widget.perspective === 'object') {
widget.perspective = JSON.stringify(widget.perspective);
}
});
}
......@@ -363,38 +360,43 @@ export class DashboardManagementComponent implements OnInit {
});
}
mapWidgetsToPanels(widgets: WidgetModel[]): DashboardPanel[] {
return widgets.map(widget => {
// Ensure config is an object before passing to component
let configObject: any = {}; // Use any to easily add properties
if (typeof widget.config === 'string') {
try {
configObject = JSON.parse(widget.config);
} catch (e) {
console.error('Error parsing widget config string:', widget.config, e);
}
} else if (typeof widget.config === 'object' && widget.config !== null) {
configObject = { ...widget.config }; // Create a shallow copy
mapWidgetsToPanels(widgets: any[]): DashboardPanel[] {
return widgets
.map(widget => {
// Use parsed config object (already parsed in loadDashboard)
const configObject = {
...(typeof widget.config === 'object' ? widget.config : {}),
widgetId: widget.widgetId
};
const perspectiveObject = typeof widget.perspective === 'object' ? widget.perspective : {};
const dataObject = typeof widget.data === 'object' ? widget.data : {};
const componentType = this.widgetComponentRegistryService.getComponent(widget.component);
if (!componentType) {
console.warn(`Component not found for widget: ${widget.component}`);
return null;
}
// Inject widgetId into the config for the component
configObject.widgetId = widget.widgetId;
console.log(`Mapping widget ${widget.widgetId} to panel with config:`, configObject);
return {
const panel = {
id: `${(widget as any).instanceId}-${widget.y}-${widget.x}`,
header: widget.thName,
sizeX: widget.cols,
sizeY: widget.rows,
row: widget.y,
col: widget.x,
componentType: this.widgetComponentRegistryService.getComponent(widget.component),
componentType: componentType,
componentInputs: {
config: configObject,
perspective: widget.perspective
config: JSON.stringify(configObject),
perspective: JSON.stringify(perspectiveObject),
data: JSON.stringify(dataObject)
},
originalWidget: widget
};
});
return panel;
})
.filter(panel => panel !== null) as DashboardPanel[];
}
openWidgetDialog(): void {
......@@ -404,5 +406,42 @@ export class DashboardManagementComponent implements OnInit {
closeWidgetDialog(): void {
this.isWidgetDialogVisible = false;
}
/**
* Parse JSON string to object with error handling
*/
private parseJsonString(jsonString: any): any {
if (typeof jsonString === 'string') {
try {
return JSON.parse(jsonString);
} catch (error) {
console.warn('Failed to parse JSON string:', jsonString, error);
return {};
}
}
return jsonString || {};
}
/**
* Convert widget objects back to strings for saving
*/
private convertWidgetsToStrings(dashboard: any): any {
const dashboardToSave = JSON.parse(JSON.stringify(dashboard));
if (dashboardToSave.widgets) {
dashboardToSave.widgets.forEach((widget: any) => {
delete widget.instanceId;
if (widget.config && typeof widget.config === 'object') {
widget.config = JSON.stringify(widget.config);
}
if (widget.data && typeof widget.data === 'object') {
widget.data = JSON.stringify(widget.data);
}
if (widget.perspective && typeof widget.perspective === 'object') {
widget.perspective = JSON.stringify(widget.perspective);
}
});
}
return dashboardToSave;
}
}
......@@ -4,7 +4,7 @@
<div *ngIf="dashboardData" class="dashboard-viewer-container p-4">
<h1 class="text-2xl font-bold mb-4 text-gray-800">{{ dashboardData.thName }}</h1>
<div class="control-section">
<ejs-dashboardlayout id='dashboard_viewer' #viewerLayout [cellSpacing]="cellSpacing" [columns]="6" [allowResizing]="false" [allowDragging]="false">
<ejs-dashboardlayout id='dashboard_viewer' #viewerLayout [cellSpacing]="cellSpacing" [columns]="columns" [allowResizing]="false" [allowDragging]="false">
<e-panels>
<e-panel *ngFor="let panel of panels" [row]="panel.row" [col]="panel.col" [sizeX]="panel.sizeX" [sizeY]="panel.sizeY" [id]="panel.id">
<ng-template #header>
......
......@@ -40,6 +40,10 @@ import { SlicerWidgetComponent } from '../widgets/slicer-widget/slicer-widget.co
import { SimpleTableWidgetComponent } from '../widgets/simple-table-widget/simple-table-widget.component';
import { WaterfallChartWidgetComponent } from '../widgets/waterfall-chart-widget/waterfall-chart-widget.component';
import { TreemapWidgetComponent } from '../widgets/treemap-widget/treemap-widget.component';
import { CalendarWidgetComponent } from '../widgets/calendar-widget/calendar-widget.component';
import { NotificationWidgetComponent } from '../widgets/notification-widget/notification-widget.component';
import { WeatherWidgetComponent } from '../widgets/weather-widget/weather-widget.component';
import { ClockWidgetComponent } from '../widgets/clock-widget/clock-widget.component';
export interface DashboardPanel extends PanelModel {
componentType: Type<any>;
......@@ -52,7 +56,7 @@ export interface DashboardPanel extends PanelModel {
imports: [
CommonModule, RouterModule, DashboardLayoutModule, NgComponentOutlet,
// Add all widget components here to make them available for NgComponentOutlet
CompanyInfoWidgetComponent, HeadcountWidgetComponent, AttendanceOverviewWidgetComponent, PayrollSummaryWidgetComponent, EmployeeDirectoryWidgetComponent, KpiWidgetComponent, WelcomeWidgetComponent, ChartWidgetComponent, QuickLinksWidgetComponent, SyncfusionDatagridWidgetComponent, SyncfusionPivotWidgetComponent, SyncfusionChartWidgetComponent, DataTableWidgetComponent, AreaChartWidgetComponent, BarChartWidgetComponent, PieChartWidgetComponent, ScatterBubbleChartWidgetComponent, MultiRowCardWidgetComponent, ComboChartWidgetComponent, DoughnutChartWidgetComponent, FunnelChartWidgetComponent, GaugeChartWidgetComponent, SimpleKpiWidgetComponent, FilledMapWidgetComponent, MatrixWidgetComponent, SlicerWidgetComponent, SimpleTableWidgetComponent, WaterfallChartWidgetComponent, TreemapWidgetComponent
CompanyInfoWidgetComponent, HeadcountWidgetComponent, AttendanceOverviewWidgetComponent, PayrollSummaryWidgetComponent, EmployeeDirectoryWidgetComponent, KpiWidgetComponent, WelcomeWidgetComponent, ChartWidgetComponent, QuickLinksWidgetComponent, SyncfusionDatagridWidgetComponent, SyncfusionPivotWidgetComponent, SyncfusionChartWidgetComponent, DataTableWidgetComponent, AreaChartWidgetComponent, BarChartWidgetComponent, PieChartWidgetComponent, ScatterBubbleChartWidgetComponent, MultiRowCardWidgetComponent, ComboChartWidgetComponent, DoughnutChartWidgetComponent, FunnelChartWidgetComponent, GaugeChartWidgetComponent, SimpleKpiWidgetComponent, FilledMapWidgetComponent, MatrixWidgetComponent, SlicerWidgetComponent, SimpleTableWidgetComponent, WaterfallChartWidgetComponent, TreemapWidgetComponent, CalendarWidgetComponent, NotificationWidgetComponent, WeatherWidgetComponent, ClockWidgetComponent
],
templateUrl: './dashboard-viewer.component.html',
styleUrls: ['./dashboard-viewer.component.scss'],
......@@ -61,6 +65,7 @@ export class DashboardViewerComponent implements OnInit {
public panels: DashboardPanel[] = [];
public cellSpacing: number[] = [10, 10];
public columns: number = 6;
public dashboardData: DashboardModel | null = null;
public errorMessage: string | null = null;
......@@ -91,7 +96,7 @@ export class DashboardViewerComponent implements OnInit {
widget.instanceId = `inst_${Date.now()}_${Math.random()}`;
}
const keysToProcess: Array<keyof WidgetModel> = ['config', 'data'];
const keysToProcess: Array<keyof WidgetModel> = ['config', 'data', 'perspective'];
keysToProcess.forEach(key => {
if ((widget as any)[key] && typeof (widget as any)[key] === 'string') {
try {
......@@ -118,8 +123,23 @@ export class DashboardViewerComponent implements OnInit {
});
}
mapWidgetsToPanels(widgets: WidgetModel[]): DashboardPanel[] {
return widgets.map(widget => {
mapWidgetsToPanels(widgets: any[]): DashboardPanel[] {
return widgets
.map(widget => {
// Use parsed config object (already parsed in ngOnInit)
const configObject = {
...(typeof widget.config === 'object' ? widget.config : {}),
widgetId: widget.widgetId
};
const perspectiveObject = typeof widget.perspective === 'object' ? widget.perspective : {};
const dataObject = typeof widget.data === 'object' ? widget.data : {};
const componentType = this.widgetComponentRegistryService.getComponent(widget.component);
if (!componentType) {
console.warn(`Component not found for widget: ${widget.component}`);
return null;
}
return {
id: `${(widget as any).instanceId}-${widget.y}-${widget.x}`,
header: widget.thName,
......@@ -127,9 +147,29 @@ export class DashboardViewerComponent implements OnInit {
sizeY: widget.rows,
row: widget.y,
col: widget.x,
componentType: this.widgetComponentRegistryService.getComponent(widget.component),
componentInputs: { config: widget.config || {}, perspective: widget.perspective },
componentType: componentType,
componentInputs: {
config: JSON.stringify(configObject),
perspective: JSON.stringify(perspectiveObject),
data: JSON.stringify(dataObject)
},
};
});
})
.filter(panel => panel !== null) as DashboardPanel[];
}
/**
* Parse JSON string to object with error handling
*/
private parseJsonString(jsonString: any): any {
if (typeof jsonString === 'string') {
try {
return JSON.parse(jsonString);
} catch (error) {
console.warn('Failed to parse JSON string:', jsonString, error);
return {};
}
}
return jsonString || {};
}
}
export interface WidgetConfigField {
key: string;
label: string;
type: 'text' | 'number' | 'boolean' | 'select' | 'multiselect' | 'color' | 'range' | 'json';
required?: boolean;
defaultValue?: any;
options?: Array<{ value: any; label: string }>;
min?: number;
max?: number;
step?: number;
placeholder?: string;
description?: string;
group?: string;
order?: number;
}
export interface WidgetConfigGroup {
name: string;
label: string;
description?: string;
order: number;
fields: WidgetConfigField[];
}
export interface WidgetConfigSchema {
widgetType: string;
groups: WidgetConfigGroup[];
filters?: WidgetFilterConfig[];
dataMapping?: DataMappingConfig[];
}
export interface WidgetFilterConfig {
key: string;
label: string;
type: 'date' | 'number' | 'select' | 'multiselect' | 'text' | 'boolean';
field: string;
operator: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'in' | 'between';
defaultValue?: any;
options?: Array<{ value: any; label: string }>;
required?: boolean;
}
export interface DataMappingConfig {
key: string;
label: string;
type: 'field' | 'calculated' | 'constant';
required?: boolean;
defaultValue?: any;
calculation?: string;
validation?: {
min?: number;
max?: number;
pattern?: string;
message?: string;
};
}
export interface WidgetConfigInstance {
widgetId: string;
config: { [key: string]: any };
filters: { [key: string]: any };
dataMapping: { [key: string]: any };
lastModified: Date;
version: number;
}
......@@ -35,9 +35,9 @@ export interface IWidget {
rows: number;
x: number;
y: number;
data: any;
config: any;
perspective: any;
data: string;
config: string;
perspective: string;
}
export class WidgetModel implements IWidget {
......@@ -49,9 +49,9 @@ export class WidgetModel implements IWidget {
rows: number;
x: number;
y: number;
data: any;
config: any;
perspective: any;
data: string;
config: string;
perspective: string;
constructor(data: Partial<IWidget>) {
this.widgetId = data.widgetId ?? '';
......@@ -62,10 +62,61 @@ export class WidgetModel implements IWidget {
this.rows = data.rows ?? 1;
this.x = data.x ?? 0;
this.y = data.y ?? 0;
this.data = data.data ?? {};
this.config = data.config ?? {};
this.perspective = data.perspective ?? {};
this.data = this.convertToString(data.data);
this.config = this.convertToString(data.config);
this.perspective = this.convertToString(data.perspective);
}
// Helper method to convert object to string or keep string as is
private convertToString(value: any): string {
if (typeof value === 'string') {
return value;
}
if (value === null || value === undefined) {
return '{}';
}
return JSON.stringify(value);
}
// Helper methods to get parsed JSON objects
getDataObject(): any {
try {
return JSON.parse(this.data);
} catch (error) {
console.warn('Failed to parse data JSON:', error);
return {};
}
}
getConfigObject(): any {
try {
return JSON.parse(this.config);
} catch (error) {
console.warn('Failed to parse config JSON:', error);
return {};
}
}
getPerspectiveObject(): any {
try {
return JSON.parse(this.perspective);
} catch (error) {
console.warn('Failed to parse perspective JSON:', error);
return {};
}
}
// Helper methods to set objects as JSON strings
setDataObject(dataObj: any): void {
this.data = JSON.stringify(dataObj);
}
setConfigObject(configObj: any): void {
this.config = JSON.stringify(configObj);
}
setPerspectiveObject(perspectiveObj: any): void {
this.perspective = JSON.stringify(perspectiveObj);
}
}
......
......@@ -30,6 +30,11 @@ import { SlicerWidgetComponent } from '../widgets/slicer-widget/slicer-widget.co
import { SimpleTableWidgetComponent } from '../widgets/simple-table-widget/simple-table-widget.component';
import { WaterfallChartWidgetComponent } from '../widgets/waterfall-chart-widget/waterfall-chart-widget.component';
import { TreemapWidgetComponent } from '../widgets/treemap-widget/treemap-widget.component';
// New Syncfusion-based widgets
import { CalendarWidgetComponent } from '../widgets/calendar-widget/calendar-widget.component';
import { NotificationWidgetComponent } from '../widgets/notification-widget/notification-widget.component';
import { WeatherWidgetComponent } from '../widgets/weather-widget/weather-widget.component';
import { ClockWidgetComponent } from '../widgets/clock-widget/clock-widget.component';
@Injectable({
providedIn: 'root',
......@@ -65,15 +70,21 @@ export class WidgetComponentRegistryService {
SimpleTableWidgetComponent: SimpleTableWidgetComponent,
WaterfallChartWidgetComponent: WaterfallChartWidgetComponent,
TreemapWidgetComponent: TreemapWidgetComponent,
// New Syncfusion-based widgets
CalendarWidgetComponent: CalendarWidgetComponent,
NotificationWidgetComponent: NotificationWidgetComponent,
WeatherWidgetComponent: WeatherWidgetComponent,
ClockWidgetComponent: ClockWidgetComponent,
};
getComponent(componentName: string): Type<any> {
getComponent(componentName: string): Type<any> | null {
const component = this.widgetComponentMap[componentName];
if (!component) {
console.warn(
`Warning: Widget component "${componentName}" not found in registry.`
);
// Consider returning a default/error component here
console.log('Available components:', Object.keys(this.widgetComponentMap));
return null;
}
return component;
}
......
.dynamic-widget-config-dialog {
max-width: 800px;
width: 100%;
.config-content {
max-height: 70vh;
overflow-y: auto;
padding: 16px 0;
.no-schema-warning {
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
background-color: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 4px;
margin-bottom: 16px;
color: #856404;
mat-icon {
color: #f39c12;
}
}
.tab-content {
padding: 16px 0;
.group-fields {
display: grid;
gap: 16px;
padding: 16px 0;
.field-container {
&.required {
mat-form-field::before {
content: '*';
color: #f44336;
margin-right: 4px;
}
}
.boolean-field {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #fafafa;
mat-slide-toggle {
margin: 0;
}
}
.range-field {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #fafafa;
label {
font-weight: 500;
color: rgba(0, 0, 0, 0.87);
}
.range-value {
text-align: center;
font-weight: 500;
color: #2196f3;
}
}
.color-field {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #fafafa;
label {
font-weight: 500;
color: rgba(0, 0, 0, 0.87);
min-width: 120px;
}
.color-input {
width: 50px;
height: 40px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.color-value {
font-family: monospace;
background-color: #f5f5f5;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
}
.field-description {
margin-top: 4px;
color: rgba(0, 0, 0, 0.6);
font-style: italic;
}
}
}
.filters-section {
.filter-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
margin-top: 16px;
.filter-card {
mat-card-header {
padding-bottom: 8px;
}
mat-card-content {
padding-top: 0;
}
}
}
}
.mapping-section {
.mapping-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
margin-top: 16px;
.mapping-card {
mat-card-header {
padding-bottom: 8px;
}
mat-card-content {
padding-top: 0;
.calculation-info {
margin-top: 8px;
padding: 8px;
background-color: #e3f2fd;
border-radius: 4px;
border-left: 4px solid #2196f3;
small {
color: #1976d2;
font-family: monospace;
}
}
}
}
}
}
}
}
mat-dialog-actions {
padding: 16px 24px;
border-top: 1px solid #e0e0e0;
background-color: #fafafa;
button {
margin-left: 8px;
mat-icon {
margin-right: 4px;
}
}
}
}
// Responsive design
@media (max-width: 768px) {
.dynamic-widget-config-dialog {
max-width: 95vw;
.config-content {
.tab-content {
.filters-section .filter-grid,
.mapping-section .mapping-grid {
grid-template-columns: 1fr;
}
}
}
}
}
// Dark theme support
@media (prefers-color-scheme: dark) {
.dynamic-widget-config-dialog {
.config-content {
.no-schema-warning {
background-color: #3e2723;
border-color: #5d4037;
color: #ffcc02;
}
.tab-content {
.group-fields .field-container {
.boolean-field,
.range-field,
.color-field {
background-color: #424242;
border-color: #616161;
}
}
.mapping-section .mapping-grid .mapping-card mat-card-content .calculation-info {
background-color: #1a237e;
border-left-color: #3f51b5;
}
}
}
mat-dialog-actions {
background-color: #424242;
border-top-color: #616161;
}
}
}
import { Component, OnInit, Inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatTabsModule } from '@angular/material/tabs';
import { MatCardModule } from '@angular/material/card';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSliderModule } from '@angular/material/slider';
import { ColorPickerModule } from 'ngx-color-picker';
import { WidgetConfigSchemaService } from '../services/widget-config-schema.service';
import {
WidgetConfigSchema,
WidgetConfigGroup,
WidgetConfigField,
WidgetFilterConfig,
DataMappingConfig
} from '../models/widget-config.model';
import { WidgetModel } from '../models/widgets.model';
export interface DynamicWidgetConfigDialogData {
widget: WidgetModel;
availableColumns: string[];
currentConfig?: any;
currentFilters?: any;
currentDataMapping?: any;
}
@Component({
selector: 'app-dynamic-widget-config',
standalone: true,
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatDialogModule,
MatFormFieldModule,
MatSelectModule,
MatInputModule,
MatButtonModule,
MatIconModule,
MatCheckboxModule,
MatExpansionModule,
MatTabsModule,
MatCardModule,
MatSlideToggleModule,
MatSliderModule,
ColorPickerModule
],
templateUrl: './dynamic-widget-config.component.html',
styleUrls: ['./dynamic-widget-config.component.scss']
})
export class DynamicWidgetConfigComponent implements OnInit {
configForm!: FormGroup;
schema: WidgetConfigSchema | null = null;
availableColumns: string[] = [];
widgetType: string = '';
constructor(
private fb: FormBuilder,
public dialogRef: MatDialogRef<DynamicWidgetConfigComponent>,
@Inject(MAT_DIALOG_DATA) public data: DynamicWidgetConfigDialogData,
private widgetConfigSchemaService: WidgetConfigSchemaService
) {}
ngOnInit(): void {
this.availableColumns = this.data.availableColumns;
this.widgetType = this.data.widget.component;
this.schema = this.widgetConfigSchemaService.getConfigSchema(this.widgetType);
if (this.schema) {
this.initializeForm();
this.populateForm();
} else {
console.warn(`No schema found for widget type: ${this.widgetType}`);
this.initializeBasicForm();
}
}
private initializeForm(): void {
const formControls: { [key: string]: any } = {};
// Initialize config fields
this.schema!.groups.forEach(group => {
group.fields.forEach(field => {
const validators = [];
if (field.required) {
validators.push(Validators.required);
}
if (field.type === 'number' && field.min !== undefined) {
validators.push(Validators.min(field.min));
}
if (field.type === 'number' && field.max !== undefined) {
validators.push(Validators.max(field.max));
}
formControls[field.key] = [field.defaultValue || '', validators];
});
});
// Initialize filters
if (this.schema!.filters) {
this.schema!.filters.forEach(filter => {
formControls[`filter_${filter.key}`] = [filter.defaultValue || ''];
});
}
// Initialize data mapping
if (this.schema!.dataMapping) {
this.schema!.dataMapping.forEach(mapping => {
formControls[`mapping_${mapping.key}`] = [mapping.defaultValue || ''];
});
}
this.configForm = this.fb.group(formControls);
}
private initializeBasicForm(): void {
// Fallback form for widgets without schema
this.configForm = this.fb.group({
title: ['', Validators.required],
description: ['']
});
}
private populateForm(): void {
// Populate config values
if (this.data.currentConfig) {
Object.keys(this.data.currentConfig).forEach(key => {
if (this.configForm.get(key)) {
this.configForm.get(key)?.setValue(this.data.currentConfig[key]);
}
});
}
// Populate filter values
if (this.data.currentFilters) {
Object.keys(this.data.currentFilters).forEach(key => {
const filterControl = this.configForm.get(`filter_${key}`);
if (filterControl) {
filterControl.setValue(this.data.currentFilters[key]);
}
});
}
// Populate data mapping values
if (this.data.currentDataMapping) {
Object.keys(this.data.currentDataMapping).forEach(key => {
const mappingControl = this.configForm.get(`mapping_${key}`);
if (mappingControl) {
mappingControl.setValue(this.data.currentDataMapping[key]);
}
});
}
}
getFieldValue(field: WidgetConfigField): any {
return this.configForm.get(field.key)?.value;
}
getFilterValue(filter: WidgetFilterConfig): any {
return this.configForm.get(`filter_${filter.key}`)?.value;
}
getMappingValue(mapping: DataMappingConfig): any {
return this.configForm.get(`mapping_${mapping.key}`)?.value;
}
getFieldOptions(field: WidgetConfigField): Array<{ value: any; label: string }> {
if (field.type === 'select' || field.type === 'multiselect') {
if (field.options) {
return field.options;
}
}
return [];
}
getFilterOptions(filter: WidgetFilterConfig): Array<{ value: any; label: string }> {
if (filter.options) {
return filter.options;
}
return [];
}
isFieldRequired(field: WidgetConfigField): boolean {
return field.required || false;
}
getFieldErrorMessage(field: WidgetConfigField): string {
const control = this.configForm.get(field.key);
if (control?.errors) {
if (control.errors['required']) {
return `${field.label} is required`;
}
if (control.errors['min']) {
return `${field.label} must be at least ${field.min}`;
}
if (control.errors['max']) {
return `${field.label} must be at most ${field.max}`;
}
}
return '';
}
onSave(): void {
if (this.configForm.valid) {
const formValue = this.configForm.value;
// Separate config, filters, and data mapping
const config: { [key: string]: any } = {};
const filters: { [key: string]: any } = {};
const dataMapping: { [key: string]: any } = {};
// Extract config values
this.schema?.groups.forEach(group => {
group.fields.forEach(field => {
if (formValue[field.key] !== undefined) {
config[field.key] = formValue[field.key];
}
});
});
// Extract filter values
this.schema?.filters?.forEach(filter => {
const value = formValue[`filter_${filter.key}`];
if (value !== undefined && value !== null && value !== '') {
filters[filter.key] = value;
}
});
// Extract data mapping values
this.schema?.dataMapping?.forEach(mapping => {
const value = formValue[`mapping_${mapping.key}`];
if (value !== undefined && value !== null && value !== '') {
dataMapping[mapping.key] = value;
}
});
this.dialogRef.close({
config,
filters,
dataMapping
});
}
}
onCancel(): void {
this.dialogRef.close();
}
onReset(): void {
if (this.schema) {
// Reset to default values
this.schema.groups.forEach(group => {
group.fields.forEach(field => {
if (field.defaultValue !== undefined) {
this.configForm.get(field.key)?.setValue(field.defaultValue);
}
});
});
// Reset filters
this.schema.filters?.forEach(filter => {
if (filter.defaultValue !== undefined) {
this.configForm.get(`filter_${filter.key}`)?.setValue(filter.defaultValue);
} else {
this.configForm.get(`filter_${filter.key}`)?.setValue('');
}
});
}
}
getGroups(): WidgetConfigGroup[] {
return this.schema?.groups || [];
}
getFilters(): WidgetFilterConfig[] {
return this.schema?.filters || [];
}
getDataMappings(): DataMappingConfig[] {
return this.schema?.dataMapping || [];
}
hasFilters(): boolean {
return this.getFilters().length > 0;
}
hasDataMappings(): boolean {
return this.getDataMappings().length > 0;
}
}
......@@ -30,6 +30,11 @@ import { QuickLinksWidgetComponent } from '../widgets/quick-links-widget/quick-l
import { SyncfusionDatagridWidgetComponent } from '../widgets/syncfusion-datagrid-widget/syncfusion-datagrid-widget.component';
import { SyncfusionPivotWidgetComponent } from '../widgets/syncfusion-pivot-widget/syncfusion-pivot-widget.component';
import { SyncfusionChartWidgetComponent } from '../widgets/syncfusion-chart-widget/syncfusion-chart-widget.component';
// New Syncfusion-based widgets
import { CalendarWidgetComponent } from '../widgets/calendar-widget/calendar-widget.component';
import { NotificationWidgetComponent } from '../widgets/notification-widget/notification-widget.component';
import { WeatherWidgetComponent } from '../widgets/weather-widget/weather-widget.component';
import { ClockWidgetComponent } from '../widgets/clock-widget/clock-widget.component';
@Component({
selector: 'app-widget-form',
......@@ -53,6 +58,11 @@ import { SyncfusionChartWidgetComponent } from '../widgets/syncfusion-chart-widg
SyncfusionDatagridWidgetComponent,
SyncfusionPivotWidgetComponent,
SyncfusionChartWidgetComponent,
// New Syncfusion-based widgets
CalendarWidgetComponent,
NotificationWidgetComponent,
WeatherWidgetComponent,
ClockWidgetComponent,
],
templateUrl: './widget-form.component.html',
})
......@@ -106,15 +116,40 @@ export class WidgetFormComponent implements OnInit {
}
updatePreview(componentName: string): void {
this.previewComponentType =
this.widgetComponentRegistryService.getComponent(componentName);
if (!componentName) {
this.previewComponentType = null;
return;
}
const component = this.widgetComponentRegistryService.getComponent(componentName);
if (component) {
this.previewComponentType = component;
} else {
console.error(`Preview not available for widget type: ${componentName}`);
this.previewComponentType = null;
}
}
onSubmit(): void {
if (this.widgetForm.invalid) {
return;
}
this.dialogRef.close(this.widgetForm.value);
const formValue = this.widgetForm.value;
// ถ้าเป็น widget ใหม่ ให้สร้าง widgetId
if (this.isNew) {
formValue.widgetId = this.generateWidgetId();
}
this.dialogRef.close(formValue);
}
private generateWidgetId(): string {
// สร้าง widgetId โดยใช้ timestamp และ random number
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
return `widget_${timestamp}_${random}`;
}
cancel(): void {
......
......@@ -33,6 +33,11 @@ import { NotificationService } from '../../../shared/services/notification.servi
import { WidgetModel } from '../models/widgets.model';
import { WidgetService } from '../services/widgets.service';
import { SimpleKpiWidgetComponent } from '../widgets/simple-kpi-widget/simple-kpi-widget.component';
// New Syncfusion-based widgets
import { CalendarWidgetComponent } from '../widgets/calendar-widget/calendar-widget.component';
import { NotificationWidgetComponent } from '../widgets/notification-widget/notification-widget.component';
import { WeatherWidgetComponent } from '../widgets/weather-widget/weather-widget.component';
import { ClockWidgetComponent } from '../widgets/clock-widget/clock-widget.component';
import { WidgetFormComponent } from './widget-form.component';
import { ClickEventArgs } from '@syncfusion/ej2-angular-navigations';
import { RouterModule } from '@angular/router';
......@@ -59,6 +64,11 @@ import { RouterModule } from '@angular/router';
SyncfusionPivotWidgetComponent,
SyncfusionChartWidgetComponent,
SimpleKpiWidgetComponent,
// New Syncfusion-based widgets
CalendarWidgetComponent,
NotificationWidgetComponent,
WeatherWidgetComponent,
ClockWidgetComponent,
],
providers: [
ToolbarService,
......
......@@ -21,14 +21,14 @@ export class AreaChartWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Area Chart';
this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' };
this.primaryYAxis = { title: this.config.yAxisTitle || '' };
this.title = this.configObj.title || 'Area Chart';
this.primaryXAxis = { valueType: 'Category', title: this.configObj.xAxisTitle || '' };
this.primaryYAxis = { title: this.configObj.yAxisTitle || '' };
this.chartData = []; // Start with no data
}
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.configObj.xField], y: item[this.configObj.yField] }));
}
onReset(): void {
......
......@@ -20,7 +20,7 @@ export class AttendanceOverviewWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Attendance';
this.title = this.configObj.title || 'Attendance';
this.present = 0;
this.onLeave = 0;
this.absent = 0;
......@@ -29,9 +29,9 @@ export class AttendanceOverviewWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void {
if (data.length > 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;
this.present = firstItem[this.configObj.presentField] || 0;
this.onLeave = firstItem[this.configObj.onLeaveField] || 0;
this.absent = firstItem[this.configObj.absentField] || 0;
}
}
......
......@@ -23,15 +23,15 @@ export class BarChartWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Bar Chart';
this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' };
this.primaryYAxis = { title: this.config.yAxisTitle || '' };
this.type = this.config.type || 'Column';
this.title = this.configObj.title || 'Bar Chart';
this.primaryXAxis = { valueType: 'Category', title: this.configObj.xAxisTitle || '' };
this.primaryYAxis = { title: this.configObj.yAxisTitle || '' };
this.type = this.configObj.type || 'Column';
this.chartData = [];
}
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.configObj.xField], y: item[this.configObj.yField] }));
}
onReset(): void {
......
import { Input, OnInit, OnDestroy, Directive } from '@angular/core';
import { Subscription } from 'rxjs';
import { DashboardStateService, SelectedDataset } from '../services/dashboard-state.service';
import { WidgetConfigInstance, WidgetFilterConfig } from '../models/widget-config.model';
@Directive() // Use @Directive() for base classes without their own template
export abstract class BaseWidgetComponent implements OnInit, OnDestroy {
@Input() config: any;
@Input() config: string = '{}';
@Input() data: string = '[]';
@Input() filters: { [key: string]: any } = {};
@Input() dataMapping: { [key: string]: any } = {};
@Input() perspective: string | null = null;
public title: string;
public isLoading = true;
public hasError = false;
public errorMessage: string | null = null;
public filteredData: any[] = [];
public appliedFilters: { [key: string]: any } = {};
// Parsed config object for easier access
protected configObj: any = {};
protected subscription: Subscription = new Subscription();
protected originalData: any[] = [];
constructor(protected dashboardStateService: DashboardStateService) {}
ngOnInit(): void {
this.applyInitialConfig(); // Apply config first
// Initialize config first - ensure config is not undefined
this.initializeConfig();
this.initializeFilters(); // Initialize filters
const datasetSub = this.dashboardStateService.selectedDataset$.subscribe({
next: (selectedDataset: SelectedDataset | null) => {
......@@ -25,7 +37,9 @@ export abstract class BaseWidgetComponent implements OnInit, OnDestroy {
this.hasError = false;
if (selectedDataset && selectedDataset.data) {
try {
this.onDataUpdate(selectedDataset.data);
this.originalData = selectedDataset.data;
this.applyFilters();
this.onDataUpdate(this.filteredData);
this.isLoading = false;
} catch (error) {
this.handleError(error);
......@@ -71,4 +85,212 @@ export abstract class BaseWidgetComponent implements OnInit, OnDestroy {
* Abstract method for child components to fall back to a default state on error.
*/
abstract onReset(): void;
/**
* Initialize config with default values if undefined
*/
protected initializeConfig(): void {
// Ensure config string is not empty
if (!this.config) {
this.config = '{}';
}
// Parse config string to object
try {
this.configObj = JSON.parse(this.config);
} catch (error) {
console.warn('Failed to parse config JSON:', error);
this.configObj = {};
}
// Parse data string to array
try {
this.originalData = JSON.parse(this.data);
} catch (error) {
console.warn('Failed to parse data JSON:', error);
this.originalData = [];
}
// Ensure filters is not undefined
if (!this.filters) {
this.filters = {};
}
// Ensure dataMapping is not undefined
if (!this.dataMapping) {
this.dataMapping = {};
}
// Now call the child component's applyInitialConfig
this.applyInitialConfig();
}
/**
* Initialize filters with default values
*/
protected initializeFilters(): void {
this.appliedFilters = { ...this.filters };
}
/**
* Apply filters to the original data
*/
protected applyFilters(): void {
if (!this.originalData || this.originalData.length === 0) {
this.filteredData = [];
return;
}
this.filteredData = this.originalData.filter(item => {
return this.evaluateFilters(item);
});
}
/**
* Evaluate if an item passes all active filters
*/
private evaluateFilters(item: any): boolean {
for (const [filterKey, filterValue] of Object.entries(this.appliedFilters)) {
if (filterValue === null || filterValue === undefined || filterValue === '') {
continue; // Skip empty filters
}
const fieldValue = this.getFieldValue(item, filterKey);
if (!this.evaluateFilterCondition(fieldValue, filterValue, filterKey)) {
return false;
}
}
return true;
}
/**
* Get field value from item using data mapping
*/
private getFieldValue(item: any, filterKey: string): any {
// First check if there's a specific mapping for this filter
if (this.dataMapping[filterKey]) {
return this.getMappedValue(item, this.dataMapping[filterKey]);
}
// Fallback to direct field access
return item[filterKey];
}
/**
* Get mapped value using data mapping configuration
*/
private getMappedValue(item: any, mapping: any): any {
if (mapping.type === 'field') {
return item[mapping.field];
} else if (mapping.type === 'calculated') {
return this.calculateValue(item, mapping.calculation);
} else if (mapping.type === 'constant') {
return mapping.value;
}
return item[mapping.field || mapping];
}
/**
* Calculate value using a calculation expression
*/
private calculateValue(item: any, calculation: string): any {
try {
// Replace field names with actual values
let expression = calculation;
const fieldMatches = calculation.match(/\{(\w+)\}/g);
if (fieldMatches) {
fieldMatches.forEach(match => {
const fieldName = match.replace(/[{}]/g, '');
const value = item[fieldName] || 0;
expression = expression.replace(match, value.toString());
});
}
// Safe calculation using Function constructor instead of eval
// This is safer than eval as it doesn't have access to local scope
const safeCalculation = new Function('return ' + expression);
return safeCalculation();
} catch (error) {
console.warn('Calculation error:', error);
return 0;
}
}
/**
* Evaluate filter condition based on operator
*/
private evaluateFilterCondition(fieldValue: any, filterValue: any, filterKey: string): boolean {
// For now, implement basic string/number comparisons
// Can be extended to support more complex operators
if (Array.isArray(filterValue)) {
// Multi-select filter
return filterValue.includes(fieldValue);
}
if (typeof filterValue === 'string' && typeof fieldValue === 'string') {
return fieldValue.toLowerCase().includes(filterValue.toLowerCase());
}
return fieldValue === filterValue;
}
/**
* Update filters and reapply
*/
public updateFilters(newFilters: { [key: string]: any }): void {
this.appliedFilters = { ...newFilters };
this.applyFilters();
this.onDataUpdate(this.filteredData);
}
/**
* Update a specific filter
*/
public updateFilter(filterKey: string, value: any): void {
this.appliedFilters[filterKey] = value;
this.applyFilters();
this.onDataUpdate(this.filteredData);
}
/**
* Clear all filters
*/
public clearFilters(): void {
this.appliedFilters = {};
this.applyFilters();
this.onDataUpdate(this.filteredData);
}
/**
* Get current filter values
*/
public getCurrentFilters(): { [key: string]: any } {
return { ...this.appliedFilters };
}
/**
* Check if filters are active
*/
public hasActiveFilters(): boolean {
return Object.values(this.appliedFilters).some(value =>
value !== null && value !== undefined && value !== ''
);
}
/**
* Get filtered data count
*/
public getFilteredDataCount(): number {
return this.filteredData.length;
}
/**
* Get original data count
*/
public getOriginalDataCount(): number {
return this.originalData.length;
}
}
......@@ -20,7 +20,6 @@
[enableRtl]="calendarSettings.enableRtl"
[start]="calendarSettings.start"
[depth]="calendarSettings.depth"
[cssClass]="calendarSettings.cssClass"
(change)="onDateChange($event)">
</ejs-calendar>
......
......@@ -21,12 +21,12 @@ export class CalendarWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Calendar';
this.title = this.configObj?.title || 'Calendar';
this.calendarSettings = {
enableRtl: this.config.enableRtl || false,
start: this.config.start || 'Year',
depth: this.config.depth || 'Year',
cssClass: this.config.cssClass || ''
enableRtl: this.configObj?.enableRtl || false,
start: this.configObj?.start || 'Year',
depth: this.configObj?.depth || 'Year',
cssClass: this.configObj?.cssClass || undefined
};
this.events = [];
}
......@@ -35,10 +35,10 @@ export class CalendarWidgetComponent extends BaseWidgetComponent {
if (data && data.length > 0) {
// Map data to events format
this.events = data.map(item => ({
date: new Date(item[this.config.dateField || 'date']),
title: item[this.config.titleField || 'title'],
description: item[this.config.descriptionField || 'description'],
type: item[this.config.typeField || 'type'] || 'default'
date: new Date(item[this.configObj?.dateField || 'date']),
title: item[this.configObj?.titleField || 'title'],
description: item[this.configObj?.descriptionField || 'description'],
type: item[this.configObj?.typeField || 'type'] || 'default'
}));
}
}
......
......@@ -30,15 +30,15 @@ export class ChartWidgetComponent extends BaseWidgetComponent implements AfterVi
}
applyInitialConfig(): void {
this.title = this.config?.title || 'Chart';
this.title = this.configObj?.title || 'Chart';
if (this.chartInstance) {
this.chartInstance.updateOptions(this.getChartOptions([], []));
}
}
onDataUpdate(data: any[]): void {
const categories = data.map(item => item[this.config.xField]);
const series = this.config.yFields.map((yField: any) => ({
const categories = data.map(item => item[this.configObj.xField]);
const series = this.configObj.yFields.map((yField: any) => ({
name: yField.name || yField.field,
data: data.map(item => item[yField.field])
}));
......@@ -65,7 +65,7 @@ export class ChartWidgetComponent extends BaseWidgetComponent implements AfterVi
return {
series: series,
chart: {
type: this.config?.type || 'bar',
type: this.configObj?.type || 'bar',
height: 250,
toolbar: {
show: false
......@@ -91,7 +91,7 @@ export class ChartWidgetComponent extends BaseWidgetComponent implements AfterVi
},
yaxis: {
title: {
text: this.config?.yAxisTitle || 'Number'
text: this.configObj?.yAxisTitle || 'Number'
}
},
fill: {
......
......@@ -53,14 +53,14 @@ export class ClockWidgetComponent extends BaseWidgetComponent implements OnInit,
}
applyInitialConfig(): void {
this.title = this.config.title || 'Clock';
this.timezone = this.config.timezone || 'Asia/Bangkok';
this.clockType = this.config.clockType || 'analog';
this.showSeconds = this.config.showSeconds !== false;
this.showDate = this.config.showDate !== false;
this.showTimezone = this.config.showTimezone !== false;
this.format24Hour = this.config.format24Hour !== false;
this.updateInterval = this.config.updateInterval || 1000;
this.title = this.configObj?.title || 'Clock';
this.timezone = this.configObj?.timezone || 'Asia/Bangkok';
this.clockType = this.configObj?.clockType || 'analog';
this.showSeconds = this.configObj?.showSeconds !== false;
this.showDate = this.configObj?.showDate !== false;
this.showTimezone = this.configObj?.showTimezone !== false;
this.format24Hour = this.configObj?.format24Hour !== false;
this.updateInterval = this.configObj?.updateInterval || 1000;
this.updateClock();
}
......@@ -68,9 +68,9 @@ export class ClockWidgetComponent extends BaseWidgetComponent implements OnInit,
onDataUpdate(data: any[]): void {
if (data && data.length > 0) {
const timeData = data[0];
this.timezone = timeData[this.config.timezoneField || 'timezone'] || this.timezone;
this.clockType = timeData[this.config.clockTypeField || 'clockType'] || this.clockType;
this.format24Hour = timeData[this.config.format24HourField || 'format24Hour'] !== false;
this.timezone = timeData[this.configObj.timezoneField || 'timezone'] || this.timezone;
this.clockType = timeData[this.configObj.clockTypeField || 'clockType'] || this.clockType;
this.format24Hour = timeData[this.configObj.format24HourField || 'format24Hour'] !== false;
this.updateClock();
}
}
......
......@@ -10,9 +10,9 @@
</div>
<!-- Chart -->
<ejs-chart *ngIf="!isLoading && !hasError && config?.series" [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis">
<ejs-chart *ngIf="!isLoading && !hasError && configObj?.series" [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis">
<e-series-collection>
<e-series *ngFor="let series of config.series"
<e-series *ngFor="let series of configObj.series"
[dataSource]="chartData"
[type]="series.type || 'Column'"
[xName]="series.xName"
......
......@@ -21,9 +21,9 @@ export class ComboChartWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Combo Chart';
this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' };
this.primaryYAxis = { title: this.config.yAxisTitle || '' };
this.title = this.configObj.title || 'Combo Chart';
this.primaryXAxis = { valueType: 'Category', title: this.configObj.xAxisTitle || '' };
this.primaryYAxis = { title: this.configObj.yAxisTitle || '' };
this.chartData = [];
}
......@@ -41,9 +41,9 @@ export class ComboChartWidgetComponent extends BaseWidgetComponent {
this.primaryXAxis = { valueType: 'Category', title: 'Month' };
this.primaryYAxis = { title: 'Value' };
if (!this.config) {
this.config = {};
this.config = '{}';
}
this.config.series = [
this.configObj.series = [
{ type: 'Column', xName: 'x', yName: 'y1', name: 'Sales' },
{ type: 'Line', xName: 'x', yName: 'y2', name: 'Profit' }
];
......
......@@ -41,7 +41,7 @@ export class CompanyInfoWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Company Info';
this.title = this.configObj.title || 'Company Info';
this.companyName = '...';
this.address = '...';
this.contact = '...';
......@@ -50,9 +50,9 @@ export class CompanyInfoWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void {
if (data.length > 0) {
const firstItem = data[0];
this.companyName = firstItem[this.config.companyNameField] || '';
this.address = firstItem[this.config.addressField] || '';
this.contact = firstItem[this.config.contactField] || '';
this.companyName = firstItem[this.configObj.companyNameField] || '';
this.address = firstItem[this.configObj.addressField] || '';
this.contact = firstItem[this.configObj.contactField] || '';
}
}
......
......@@ -21,7 +21,7 @@ export class CompanyInfoSubfolderWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Company Info';
this.title = this.configObj.title || 'Company Info';
this.companyName = '...';
this.address = '...';
this.contact = '...';
......@@ -30,9 +30,9 @@ export class CompanyInfoSubfolderWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void {
if (data.length > 0) {
const firstItem = data[0];
this.companyName = firstItem[this.config.companyNameField] || '';
this.address = firstItem[this.config.addressField] || '';
this.contact = firstItem[this.config.contactField] || '';
this.companyName = firstItem[this.configObj.companyNameField] || '';
this.address = firstItem[this.configObj.addressField] || '';
this.contact = firstItem[this.configObj.contactField] || '';
}
}
......
......@@ -20,7 +20,7 @@ export class DoughnutChartWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Doughnut Chart';
this.title = this.configObj.title || 'Doughnut Chart';
this.legendSettings = { visible: true };
this.chartData = [];
}
......@@ -28,15 +28,15 @@ export class DoughnutChartWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void {
let transformedData = data;
if (this.config.aggregation === 'count') {
if (this.configObj.aggregation === 'count') {
const counts = transformedData.reduce((acc, item) => {
const key = item[this.config.xField];
const key = item[this.configObj.xField];
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
transformedData = Object.keys(counts).map(key => ({ x: key, y: counts[key] }));
} else {
transformedData = transformedData.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] }));
transformedData = transformedData.map(item => ({ x: item[this.configObj.xField], y: item[this.configObj.yField] }));
}
this.chartData = transformedData;
}
......
......@@ -13,7 +13,10 @@ import { GridModule, PageService, SortService, FilterService, GroupService } fro
})
export class DataTableWidgetComponent implements OnInit, OnChanges {
// This component is "Dumb". It receives all its configuration and data via this input.
@Input() config: any;
@Input() config: string = '{}';
// Parsed config object for easier access
protected configObj: any = {};
public data: object[] = [];
public columns: any[] = [];
......@@ -24,13 +27,28 @@ export class DataTableWidgetComponent implements OnInit, OnChanges {
ngOnInit(): void {
console.log('DataTableWidgetComponent: ngOnInit - config', this.config);
this.parseConfig();
this.updateWidgetFromConfig();
}
private parseConfig(): void {
if (!this.config) {
this.config = '{}';
}
try {
this.configObj = JSON.parse(this.config);
} catch (error) {
console.warn('Failed to parse config JSON:', error);
this.configObj = {};
}
}
ngOnChanges(changes: SimpleChanges): void {
console.log('DataTableWidgetComponent: ngOnChanges - changes', changes);
// If the config object changes, re-render the widget
if (changes['config']) {
this.parseConfig();
this.updateWidgetFromConfig();
}
}
......@@ -38,12 +56,12 @@ export class DataTableWidgetComponent implements OnInit, OnChanges {
private updateWidgetFromConfig(): void {
console.log('DataTableWidgetComponent: updateWidgetFromConfig - config', this.config);
if (this.config) {
this.title = this.config.title || 'Data Table';
this.data = this.config.data || [];
this.title = this.configObj.title || 'Data Table';
this.data = this.configObj.data || [];
console.log('DataTableWidgetComponent: updateWidgetFromConfig - data', this.data);
// If columns are defined in config, use them. Otherwise, generate from data.
if (this.config.columns && this.config.columns.length > 0) {
this.columns = this.config.columns;
if (this.configObj.columns && this.configObj.columns.length > 0) {
this.columns = this.configObj.columns;
} else if (this.data.length > 0) {
// Auto-generate columns from the first data item's keys
this.columns = Object.keys(this.data[0]).map(key => ({
......
......@@ -25,16 +25,16 @@ export class EmployeeDirectoryWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Employee Directory';
this.title = this.configObj.title || 'Employee Directory';
this.employees = [];
}
onDataUpdate(data: any[]): void {
this.employees = data.map(item => ({
name: item[this.config.nameField] || '',
position: item[this.config.positionField] || '',
department: item[this.config.departmentField] || '',
photoUrl: this.config.photoField ? item[this.config.photoField] : undefined
name: item[this.configObj.nameField] || '',
position: item[this.configObj.positionField] || '',
department: item[this.configObj.departmentField] || '',
photoUrl: this.configObj.photoField ? item[this.configObj.photoField] : undefined
}));
}
......
......@@ -20,15 +20,15 @@ export class FilledMapWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Map';
this.title = this.configObj.title || 'Map';
this.zoomSettings = { enable: true };
this.updateLayers([]);
}
onDataUpdate(data: any[]): void {
const mapData = data.map(item => ({
country: item[this.config.countryField],
value: item[this.config.valueField]
country: item[this.configObj.countryField],
value: item[this.configObj.valueField]
}));
this.updateLayers(mapData);
}
......@@ -55,7 +55,7 @@ export class FilledMapWidgetComponent extends BaseWidgetComponent {
shapeSettings: {
fill: '#E5EEF6',
colorValuePath: 'value',
colorMapping: this.config?.colorMapping || [
colorMapping: this.configObj?.colorMapping || [
{ value: 0, color: '#C3E6CB' },
{ value: 50, color: '#FFECB5' },
{ value: 100, color: '#F5C6CB' }
......
......@@ -20,13 +20,13 @@ export class FunnelChartWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Funnel Chart';
this.title = this.configObj.title || 'Funnel Chart';
this.legendSettings = { visible: true };
this.chartData = [];
}
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.configObj.xField], y: item[this.configObj.yField] }));
}
onReset(): void {
......
......@@ -19,20 +19,20 @@ export class GaugeChartWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Gauge';
this.title = this.configObj.title || 'Gauge';
this.setAxes(0);
}
onDataUpdate(data: any[]): void {
let value = 0;
if (data.length > 0) {
if (this.config.aggregation === 'sum') {
value = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0);
} else if (this.config.aggregation === 'avg') {
const sum = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0);
if (this.configObj.aggregation === 'sum') {
value = data.reduce((sum, item) => sum + (item[this.configObj.valueField] || 0), 0);
} else if (this.configObj.aggregation === 'avg') {
const sum = data.reduce((sum, item) => sum + (item[this.configObj.valueField] || 0), 0);
value = sum / data.length;
} else {
value = data[0][this.config.valueField];
value = data[0][this.configObj.valueField];
}
}
this.setAxes(value);
......@@ -56,7 +56,7 @@ export class GaugeChartWidgetComponent extends BaseWidgetComponent {
cap: { radius: 7 },
needleTail: { length: '18%' }
}],
ranges: this.config?.ranges || [
ranges: this.configObj?.ranges || [
{ start: 0, end: 50, color: '#E0B9B9' },
{ start: 50, end: 75, color: '#B9D7EA' },
{ start: 75, end: 100, color: '#B9EAB9' }
......
......@@ -23,14 +23,14 @@
<!-- Content -->
<div *ngIf="!isLoading && !hasError" class="h-full">
<!-- Bar Chart -->
<ejs-chart *ngIf="config.chartType === 'bar'" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis" [tooltip]="tooltip" height="100%">
<ejs-chart *ngIf="configObj.chartType === 'bar'" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis" [tooltip]="tooltip" height="100%">
<e-series-collection>
<e-series [dataSource]="breakdown" type="Bar" xName="category" yName="count" name="Headcount"></e-series>
</e-series-collection>
</ejs-chart>
<!-- Doughnut Chart -->
<ejs-accumulationchart *ngIf="config.chartType === 'doughnut'" [legendSettings]="legendSettings" [tooltip]="tooltip" height="100%">
<ejs-accumulationchart *ngIf="configObj.chartType === 'doughnut'" [legendSettings]="legendSettings" [tooltip]="tooltip" height="100%">
<e-accumulation-series-collection>
<e-accumulation-series [dataSource]="breakdown" xName="category" yName="count" innerRadius="40%" [dataLabel]="dataLabel"></e-accumulation-series>
</e-accumulation-series-collection>
......
......@@ -27,7 +27,7 @@ export class HeadcountWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Headcount';
this.title = this.configObj.title || 'Headcount';
this.totalHeadcount = 0;
this.breakdown = [];
......@@ -51,9 +51,9 @@ export class HeadcountWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void {
this.totalHeadcount = data.length;
if (this.config.categoryField) {
if (this.configObj.categoryField) {
const counts = data.reduce((acc, item) => {
const category = item[this.config.categoryField];
const category = item[this.configObj.categoryField];
acc[category] = (acc[category] || 0) + 1;
return acc;
}, {});
......
......@@ -17,24 +17,24 @@ export class KpiWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'KPI';
this.title = this.configObj.title || 'KPI';
this.kpiData = {
value: '...',
unit: this.config.unit || '',
trend: this.config.trend || 'neutral',
trendValue: this.config.trendValue || ''
unit: this.configObj.unit || '',
trend: this.configObj.trend || 'neutral',
trendValue: this.configObj.trendValue || ''
};
}
onDataUpdate(data: any[]): void {
if (data.length > 0) {
let kpiValue = 0;
if (this.config.aggregation === 'count') {
if (this.configObj.aggregation === 'count') {
kpiValue = data.length;
} else if (this.config.aggregation === 'sum') {
kpiValue = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0);
} else if (this.configObj.aggregation === 'sum') {
kpiValue = data.reduce((sum, item) => sum + (item[this.configObj.valueField] || 0), 0);
} else {
kpiValue = data[0][this.config.valueField];
kpiValue = data[0][this.configObj.valueField];
}
this.kpiData.value = kpiValue.toLocaleString();
}
......
......@@ -30,12 +30,12 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let row of data" class="bg-white border-b hover:bg-gray-50">
<tr *ngFor="let row of matrixData" class="bg-white border-b hover:bg-gray-50">
<td *ngFor="let cell of row" class="px-6 py-4">
{{ cell }}
</td>
</tr>
<tr *ngIf="data.length === 0">
<tr *ngIf="matrixData.length === 0">
<td [attr.colspan]="headers.length" class="px-6 py-4 text-center text-gray-400">
No data available.
</td>
......
......@@ -12,28 +12,28 @@ import { BaseWidgetComponent } from '../base-widget.component';
})
export class MatrixWidgetComponent extends BaseWidgetComponent {
public headers: string[] = [];
public data: any[][] = [];
public matrixData: any[][] = [];
constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
}
applyInitialConfig(): void {
this.title = this.config.title || 'Matrix';
this.headers = this.config.columns ? this.config.columns.map((col: any) => col.headerText) : [];
this.data = [];
this.title = this.configObj.title || 'Matrix';
this.headers = this.configObj.columns ? this.configObj.columns.map((col: any) => col.headerText) : [];
this.matrixData = [];
}
onDataUpdate(data: any[]): void {
if (this.config?.columns && data?.length > 0) {
this.data = data.map(row => this.config.columns.map((col: any) => row[col.field]));
if (this.configObj?.columns && data?.length > 0) {
this.matrixData = data.map(row => this.configObj.columns.map((col: any) => row[col.field]));
}
}
onReset(): void {
this.title = 'Matrix (Default)';
this.headers = ['Category', 'Q1', 'Q2', 'Q3', 'Q4'];
this.data = [
this.matrixData = [
['Product A', 100, 120, 150, 130],
['Product B', 80, 90, 110, 100],
['Product C', 150, 130, 160, 140],
......
......@@ -18,15 +18,15 @@ export class MultiRowCardWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Multi-Row Card';
this.title = this.configObj.title || 'Multi-Row Card';
this.cardData = [];
}
onDataUpdate(data: any[]): void {
this.cardData = data.map(item => ({
label: item[this.config.labelField],
value: item[this.config.valueField],
unit: item[this.config.unitField] || ''
label: item[this.configObj.labelField],
value: item[this.configObj.valueField],
unit: item[this.configObj.unitField] || ''
}));
}
......
......@@ -38,7 +38,7 @@
<div class="notification-header">
<h4 class="notification-title">{{ notification.title }}</h4>
<div class="notification-time">
{{ notification.timestamp | date:'short' }}
{{ isValidDate(notification.timestamp) ? (notification.timestamp | date:'short') : 'Invalid Date' }}
</div>
</div>
<p class="notification-message">{{ notification.message }}</p>
......
......@@ -26,20 +26,20 @@ export class NotificationWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Notifications';
this.title = this.configObj?.title || 'Notifications';
this.toastSettings = {
position: this.config.toastPosition || { X: 'Right', Y: 'Top' },
showCloseButton: this.config.showCloseButton !== false,
showProgressBar: this.config.showProgressBar !== false,
timeOut: this.config.timeOut || 4000,
newestOnTop: this.config.newestOnTop !== false,
cssClass: this.config.cssClass || ''
position: this.configObj?.toastPosition || { X: 'Right', Y: 'Top' },
showCloseButton: this.configObj?.showCloseButton !== false,
showProgressBar: this.configObj?.showProgressBar !== false,
timeOut: this.configObj?.timeOut || 4000,
newestOnTop: this.configObj?.newestOnTop !== false,
cssClass: this.configObj?.cssClass || undefined
};
this.messageSettings = {
severity: this.config.severity || 'Normal',
variant: this.config.variant || 'Filled',
showIcon: this.config.showIcon !== false,
showCloseIcon: this.config.showCloseIcon !== false
severity: this.configObj?.severity || 'Normal',
variant: this.configObj?.variant || 'Filled',
showIcon: this.configObj?.showIcon !== false,
showCloseIcon: this.configObj?.showCloseIcon !== false
};
this.notifications = [];
this.unreadCount = 0;
......@@ -47,15 +47,28 @@ export class NotificationWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void {
if (data && data.length > 0) {
this.notifications = data.map(item => ({
id: item[this.config.idField || 'id'],
title: item[this.config.titleField || 'title'],
message: item[this.config.messageField || 'message'],
type: item[this.config.typeField || 'type'] || 'info',
timestamp: new Date(item[this.config.timestampField || 'timestamp']),
isRead: item[this.config.isReadField || 'isRead'] || false,
priority: item[this.config.priorityField || 'priority'] || 'normal'
})).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
this.notifications = data.map(item => {
const timestampValue = item[this.configObj.timestampField || 'timestamp'];
let timestamp: Date;
if (timestampValue instanceof Date) {
timestamp = timestampValue;
} else if (timestampValue && !isNaN(new Date(timestampValue).getTime())) {
timestamp = new Date(timestampValue);
} else {
timestamp = new Date(); // fallback to current date
}
return {
id: item[this.configObj.idField || 'id'],
title: item[this.configObj.titleField || 'title'],
message: item[this.configObj.messageField || 'message'],
type: item[this.configObj.typeField || 'type'] || 'info',
timestamp: timestamp,
isRead: item[this.configObj.isReadField || 'isRead'] || false,
priority: item[this.configObj.priorityField || 'priority'] || 'normal'
};
}).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
this.updateUnreadCount();
}
......@@ -145,4 +158,8 @@ export class NotificationWidgetComponent extends BaseWidgetComponent {
getTypeClass(type: string): string {
return `notification-${type}`;
}
isValidDate(date: any): boolean {
return date && date.getTime && !isNaN(date.getTime());
}
}
......@@ -20,7 +20,7 @@ export class PayrollSummaryWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Payroll Summary';
this.title = this.configObj.title || 'Payroll Summary';
this.totalPayroll = 0;
this.employeesPaid = 0;
}
......@@ -28,8 +28,8 @@ export class PayrollSummaryWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void {
if (data.length > 0) {
const firstItem = data[0];
this.totalPayroll = firstItem[this.config.totalPayrollField] || 0;
this.employeesPaid = firstItem[this.config.employeesPaidField] || 0;
this.totalPayroll = firstItem[this.configObj.totalPayrollField] || 0;
this.employeesPaid = firstItem[this.configObj.employeesPaidField] || 0;
}
}
......
......@@ -20,7 +20,7 @@ export class PieChartWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Pie Chart';
this.title = this.configObj.title || 'Pie Chart';
this.legendSettings = { visible: true };
this.chartData = [];
}
......@@ -28,15 +28,15 @@ export class PieChartWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void {
let transformedData = data;
if (this.config.aggregation === 'count') {
if (this.configObj.aggregation === 'count') {
const counts = transformedData.reduce((acc, item) => {
const key = item[this.config.xField];
const key = item[this.configObj.xField];
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
transformedData = Object.keys(counts).map(key => ({ x: key, y: counts[key] }));
} else {
transformedData = transformedData.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] }));
transformedData = transformedData.map(item => ({ x: item[this.configObj.xField], y: item[this.configObj.yField] }));
}
this.chartData = transformedData;
}
......
......@@ -17,16 +17,16 @@ export class QuickLinksWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Quick Links';
this.quickLinks = this.config.links || []; // Use links from config if available
this.title = this.configObj.title || 'Quick Links';
this.quickLinks = this.configObj.links || []; // Use links from config if available
}
onDataUpdate(data: any[]): void {
if (this.config.nameField && this.config.urlField) {
if (this.configObj.nameField && this.configObj.urlField) {
this.quickLinks = data.map(item => ({
name: item[this.config.nameField],
url: item[this.config.urlField],
icon: this.config.iconField ? item[this.config.iconField] : 'link-45deg'
name: item[this.configObj.nameField],
url: item[this.configObj.urlField],
icon: this.configObj.iconField ? item[this.configObj.iconField] : 'link-45deg'
}));
}
}
......
......@@ -12,6 +12,6 @@
<!-- Chart -->
<ejs-chart *ngIf="!isLoading && !hasError" [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis">
<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]="configObj?.sizeField" name="Data"></e-series>
</e-series-collection>
</ejs-chart>
......@@ -23,24 +23,24 @@ export class ScatterBubbleChartWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Scatter Chart';
this.type = this.config.type || 'Scatter';
this.primaryXAxis = { title: this.config.xAxisTitle || '' };
this.primaryYAxis = { title: this.config.yAxisTitle || '' };
this.title = this.configObj.title || 'Scatter Chart';
this.type = this.configObj.type || 'Scatter';
this.primaryXAxis = { title: this.configObj.xAxisTitle || '' };
this.primaryYAxis = { title: this.configObj.yAxisTitle || '' };
this.chartData = [];
}
onDataUpdate(data: any[]): void {
if (this.type === 'Bubble') {
this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField], size: item[this.config.sizeField] }));
this.chartData = data.map(item => ({ x: item[this.configObj.xField], y: item[this.configObj.yField], size: item[this.configObj.sizeField] }));
} else {
this.chartData = data.map(item => ({ x: item[this.config.xField], y: item[this.config.yField] }));
this.chartData = data.map(item => ({ x: item[this.configObj.xField], y: item[this.configObj.yField] }));
}
}
onReset(): void {
this.title = 'Scatter Chart (Default)';
this.type = this.config?.type || 'Scatter';
this.type = this.configObj?.type || 'Scatter';
if (this.type === 'Bubble') {
this.chartData = [
{ x: 10, y: 35, size: 5 }, { x: 15, y: 28, size: 8 },
......
......@@ -8,7 +8,9 @@
<i *ngIf="icon" [style.color]="iconColor" [class]="'bi ' + icon + ' text-3xl'"></i>
<h4 class="text-lg font-semibold truncate">{{ title }}</h4>
</div>
<!-- Removed trendValue display -->
<div *ngIf="configObj?.trendValue" class="text-sm font-medium">
{{ configObj.trendValue }}
</div>
</div>
</div>
......
......@@ -24,27 +24,30 @@ export class SimpleKpiWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'KPI';
this.unit = this.config.unit || '';
this.icon = this.config.icon || 'info';
this.backgroundColor = this.config.backgroundColor || 'linear-gradient(to top right, #3366FF, #00CCFF)';
this.iconColor = this.config.iconColor || '#FFFFFF';
this.borderColor = this.config.borderColor || '#FFFFFF';
this.title = this.configObj.title || 'KPI';
this.unit = this.configObj.unit || '';
this.icon = this.configObj.icon || 'info';
// Handle color property (fallback to backgroundColor)
const bgColor = this.configObj.backgroundColor || this.configObj.color || 'linear-gradient(to top right, #3366FF, #00CCFF)';
this.backgroundColor = bgColor.startsWith('#') ? `linear-gradient(to top right, ${bgColor}, ${bgColor}dd)` : bgColor;
this.iconColor = this.configObj.iconColor || '#FFFFFF';
this.borderColor = this.configObj.borderColor || '#FFFFFF';
this.value = '-'; // Initial state before data loads
}
onDataUpdate(data: any[]): void {
console.log('SimpleKpiWidget onDataUpdate config:', this.config);
// Handle count aggregation separately as it doesn't need a valueField
if (this.config.aggregation === 'count') {
if (this.configObj.aggregation === 'count') {
this.value = (data?.length || 0).toLocaleString();
return;
}
// For other aggregations, valueField is required
if (!this.config.valueField) {
if (!this.configObj.valueField) {
this.value = 'N/A'; // Indicate a configuration error
console.error('SimpleKpiWidget Error: valueField is not configured for this widget.', this.config);
console.error('SimpleKpiWidget Error: valueField is not configured for this widget.', this.configObj);
return;
}
......@@ -55,11 +58,11 @@ export class SimpleKpiWidgetComponent extends BaseWidgetComponent {
}
let kpiValue = 0;
if (this.config.aggregation === 'sum') {
kpiValue = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0);
if (this.configObj.aggregation === 'sum') {
kpiValue = data.reduce((sum, item) => sum + (item[this.configObj.valueField] || 0), 0);
} else {
// Default to first value if no aggregation is specified
kpiValue = data[0][this.config.valueField] || 0;
kpiValue = data[0][this.configObj.valueField] || 0;
}
this.value = kpiValue.toLocaleString();
}
......
......@@ -30,12 +30,12 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let row of data" class="bg-white border-b hover:bg-gray-50">
<tr *ngFor="let row of tableData" class="bg-white border-b hover:bg-gray-50">
<td *ngFor="let cell of row" class="px-6 py-4">
{{ cell }}
</td>
</tr>
<tr *ngIf="data.length === 0">
<tr *ngIf="tableData.length === 0">
<td [attr.colspan]="headers.length" class="px-6 py-4 text-center text-gray-400">
No data available.
</td>
......
......@@ -13,28 +13,28 @@ import { BaseWidgetComponent } from '../base-widget.component';
})
export class SimpleTableWidgetComponent extends BaseWidgetComponent {
public headers: string[] = [];
public data: any[][] = [];
public tableData: any[][] = [];
constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
}
applyInitialConfig(): void {
this.title = this.config.title || 'Table';
this.headers = this.config.columns ? this.config.columns.map((col: any) => col.headerText) : [];
this.data = [];
this.title = this.configObj.title || 'Table';
this.headers = this.configObj.columns ? this.configObj.columns.map((col: any) => col.headerText) : [];
this.tableData = [];
}
onDataUpdate(data: any[]): void {
if (data && data.length > 0 && this.config?.columns) {
this.data = data.map(row => this.config.columns.map((col: any) => row[col.field]));
if (data && data.length > 0 && this.configObj?.columns) {
this.tableData = data.map(row => this.configObj.columns.map((col: any) => row[col.field]));
}
}
onReset(): void {
this.title = 'Table (Default)';
this.headers = ['ID', 'Name', 'Status'];
this.data = [
this.tableData = [
[1, 'Item A', 'Active'],
[2, 'Item B', 'Inactive'],
[3, 'Item C', 'Active'],
......
......@@ -21,14 +21,14 @@ export class SlicerWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Slicer';
this.title = this.configObj.title || 'Slicer';
this.options = ['All'];
this.selectedValue = 'All';
}
onDataUpdate(data: any[]): void {
if (this.config.optionsField) {
const uniqueOptions = [...new Set(data.map((item: any) => item[this.config.optionsField]))];
if (this.configObj.optionsField) {
const uniqueOptions = [...new Set(data.map((item: any) => item[this.configObj.optionsField]))];
this.options = ['All', ...uniqueOptions.map(String)];
this.selectedValue = this.options[0];
}
......
......@@ -23,15 +23,15 @@ export class SyncfusionChartWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Syncfusion Chart';
this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' };
this.primaryYAxis = { title: this.config.yAxisTitle || '' };
this.title = this.configObj.title || 'Syncfusion Chart';
this.primaryXAxis = { valueType: 'Category', title: this.configObj.xAxisTitle || '' };
this.primaryYAxis = { title: this.configObj.yAxisTitle || '' };
this.chartData = new DataManager([]);
}
onDataUpdate(data: any[]): void {
const dm = new DataManager(data);
this.chartData = new DataManager(dm.executeLocal(new Query()).map((item: any) => ({ x: item[this.config.xField], y: item[this.config.yField] })));
this.chartData = new DataManager(dm.executeLocal(new Query()).map((item: any) => ({ x: item[this.configObj.xField], y: item[this.configObj.yField] })));
}
onReset(): void {
......
......@@ -21,7 +21,7 @@
<!-- Grid -->
<ejs-grid #grid *ngIf="!isLoading && !hasError"
[dataSource]="data"
[dataSource]="gridData"
[allowPaging]="true"
[pageSettings]="pageSettings"
[allowSorting]="true"
......
import { Component, ViewChild, OnInit, OnDestroy, AfterViewInit } from '@angular/core';
import { Component, ViewChild, OnInit, OnDestroy, AfterViewInit, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { GridModule, PageService, SortService, FilterService, GroupService, ToolbarService, ExcelExportService, PdfExportService, GridComponent, ToolbarItems, SearchSettingsModel, GroupSettingsModel, FilterSettingsModel, SelectionSettingsModel, AggregateService, ColumnMenuService, DetailRowService, ReorderService, EditService, PdfExportProperties, ExcelExportProperties, LoadingIndicatorModel, Column,SearchService } from '@syncfusion/ej2-angular-grids';
import { MenuEventArgs } from '@syncfusion/ej2-navigations';
......@@ -62,7 +62,7 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple
public widgetId: string; // Added widgetId property
private isPerspectiveApplied = false;
public data: DataManager = new DataManager([]);
@Input() public gridData: DataManager = new DataManager([]);
public columns: any[] = [];
public pageSettings: Object = { pageSize: 10 };
public toolbar: ToolbarItems[]; // Make it configurable
......@@ -92,16 +92,16 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple
override ngOnInit(): void { // Added override
super.ngOnInit(); // Call parent's ngOnInit
if (this.config && this.config.widgetId) {
this.widgetId = this.config.widgetId; // Initialize widgetId
if (this.config && this.configObj.widgetId) {
this.widgetId = this.configObj.widgetId; // Initialize widgetId
this.widgetStateService.registerWidget(this.widgetId, this);
}
}
override ngOnDestroy(): void { // Added override
super.ngOnDestroy(); // Call parent's ngOnDestroy
if (this.config && this.config.widgetId) {
this.widgetStateService.unregisterWidget(this.config.widgetId);
if (this.config && this.configObj.widgetId) {
this.widgetStateService.unregisterWidget(this.configObj.widgetId);
}
}
......@@ -110,19 +110,19 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple
}
applyInitialConfig(): void {
this.title = this.config.title || 'Data Grid';
this.columns = this.config.columns || [];
this.data = new DataManager([]);
this.pageSettings = this.config.pageSettings || { pageSize: 10 };
this.toolbar = this.config.toolbar || ['Search', 'ExcelExport', 'PdfExport', 'CsvExport'];
this.searchSettings = this.config.searchSettings || { fields: [], operator: 'contains', ignoreCase: true };
this.groupSettings = this.config.groupSettings || { allowReordering: true, showGroupedColumn: true, showDropArea: false };
this.filterSettings = this.config.filterSettings || { type: 'Excel' };
this.editSettings = this.config.editSettings || { allowEditing: true, mode: 'Batch' };
this.selectionOptions = this.config.selectionOptions || { checkboxOnly: true };
this.loadingIndicator = this.config.loadingIndicator || { indicatorType: 'Shimmer' };
this.query = this.config.query || new Query().addParams('dataCount', '1000');
this.columnMenuItems = this.config.columnMenuItems || [
this.title = this.configObj.title || 'Data Grid';
this.columns = this.configObj.columns || [];
this.gridData = new DataManager([]);
this.pageSettings = this.configObj.pageSettings || { pageSize: 10 };
this.toolbar = this.configObj.toolbar || ['Search', 'ExcelExport', 'PdfExport', 'CsvExport'];
this.searchSettings = this.configObj.searchSettings || { fields: [], operator: 'contains', ignoreCase: true };
this.groupSettings = this.configObj.groupSettings || { allowReordering: true, showGroupedColumn: true, showDropArea: false };
this.filterSettings = this.configObj.filterSettings || { type: 'Excel' };
this.editSettings = this.configObj.editSettings || { allowEditing: true, mode: 'Batch' };
this.selectionOptions = this.configObj.selectionOptions || { checkboxOnly: true };
this.loadingIndicator = this.configObj.loadingIndicator || { indicatorType: 'Shimmer' };
this.query = this.configObj.query || new Query().addParams('dataCount', '1000');
this.columnMenuItems = this.configObj.columnMenuItems || [
'AutoFit', 'AutoFitAll', 'SortAscending', 'SortDescending',
'Group', 'Ungroup', 'ColumnChooser', 'Filter',
{ text: 'Sum', id: 'aggregate_sum' },
......@@ -131,14 +131,14 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple
{ text: 'Min', id: 'aggregate_min' },
{ text: 'Max', id: 'aggregate_max' }
];
this.aggregatesSum = this.config.aggregatesSum || [];
this.aggregatesCount = this.config.aggregatesCount || [];
this.aggregatesAvg = this.config.aggregatesAvg || [];
this.aggregatesMin = this.config.aggregatesMin || [];
this.aggregatesMax = this.config.aggregatesMax || [];
this.allowReordering = this.config.allowReordering !== undefined ? this.config.allowReordering : true;
this.showColumnMenu = this.config.showColumnMenu !== undefined ? this.config.showColumnMenu : true;
this.allowMultiSorting = this.config.allowMultiSorting !== undefined ? this.config.allowMultiSorting : true;
this.aggregatesSum = this.configObj.aggregatesSum || [];
this.aggregatesCount = this.configObj.aggregatesCount || [];
this.aggregatesAvg = this.configObj.aggregatesAvg || [];
this.aggregatesMin = this.configObj.aggregatesMin || [];
this.aggregatesMax = this.configObj.aggregatesMax || [];
this.allowReordering = this.configObj.allowReordering !== undefined ? this.configObj.allowReordering : true;
this.showColumnMenu = this.configObj.showColumnMenu !== undefined ? this.configObj.showColumnMenu : true;
this.allowMultiSorting = this.configObj.allowMultiSorting !== undefined ? this.configObj.allowMultiSorting : true;
}
/**
......@@ -170,9 +170,9 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple
}
onDataUpdate(data: any[]): void {
this.data = new DataManager(data);
if (this.config.columns && this.config.columns.length > 0) {
this.columns = this.config.columns;
this.gridData = new DataManager(data);
if (this.configObj.columns && this.configObj.columns.length > 0) {
this.columns = this.configObj.columns;
} else if (data.length > 0) {
this.columns = Object.keys(data[0]).map(key => ({
field: key,
......@@ -193,7 +193,7 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple
onReset(): void {
this.title = 'Data Grid (Default)';
this.data = new DataManager([]);
this.gridData = new DataManager([]);
this.columns = [{ field: 'Message', headerText: 'Please select a dataset' }];
this.pageSettings = { pageSize: 10 };
this.toolbar = ['Search', 'ExcelExport', 'PdfExport', 'CsvExport'];
......
......@@ -50,12 +50,12 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen
override ngOnInit(): void {
super.ngOnInit();
if (this.config && this.config.widgetId) {
this.widgetId = this.config.widgetId;
if (this.config && this.configObj.widgetId) {
this.widgetId = this.configObj.widgetId;
this.widgetStateService.registerWidget(this.widgetId, this);
}
this.toolbar = this.config.showToolbar !== false ? ['Grid', 'Chart', 'Export', 'SubTotal', 'GrandTotal', 'ConditionalFormatting', 'NumberFormatting', 'FieldList'] : [];
this.displayOption = { view: this.config.displayOptionView || 'Both' } as DisplayOption;
this.toolbar = this.configObj.showToolbar !== false ? ['Grid', 'Chart', 'Export', 'SubTotal', 'GrandTotal', 'ConditionalFormatting', 'NumberFormatting', 'FieldList'] : [];
this.displayOption = { view: this.configObj.displayOptionView || 'Both' } as DisplayOption;
}
override ngOnDestroy(): void {
......@@ -70,17 +70,17 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen
}
applyInitialConfig(): void {
this.title = this.config.title || 'Pivot Table';
this.chartSettings = this.config.chartSettings || {
this.title = this.configObj.title || 'Pivot Table';
this.chartSettings = this.configObj.chartSettings || {
chartSeries: { type: 'Column' }
};
this.dataSourceSettings = {
dataSource: new DataManager([]),
expandAll: this.config.expandAll || false,
rows: this.config.rows || [],
columns: this.config.columns || [],
values: this.config.values || [],
filters: this.config.filters || [],
expandAll: this.configObj.expandAll || false,
rows: this.configObj.rows || [],
columns: this.configObj.columns || [],
values: this.configObj.values || [],
filters: this.configObj.filters || [],
chartSettings: this.chartSettings,
};
}
......
......@@ -21,10 +21,10 @@ export class TreemapWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Treemap';
this.weightValuePath = this.config.valueField;
this.title = this.configObj.title || 'Treemap';
this.weightValuePath = this.configObj.valueField;
this.leafItemSettings = {
labelPath: this.config.groupField,
labelPath: this.configObj.groupField,
showLabels: true
};
this.dataSource = [];
......
......@@ -21,14 +21,14 @@ export class WaterfallChartWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Waterfall Chart';
this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' };
this.primaryYAxis = { title: this.config.yAxisTitle || '' };
this.title = this.configObj.title || 'Waterfall Chart';
this.primaryXAxis = { valueType: 'Category', title: this.configObj.xAxisTitle || '' };
this.primaryYAxis = { title: this.configObj.yAxisTitle || '' };
this.chartData = [];
}
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.configObj.xField], y: item[this.configObj.yField] }));
}
onReset(): void {
......
......@@ -23,9 +23,9 @@ export class WeatherWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Weather';
this.location = this.config.location || 'Bangkok, Thailand';
this.refreshInterval = this.config.refreshInterval || 300000;
this.title = this.configObj?.title || 'Weather';
this.location = this.configObj?.location || 'Bangkok, Thailand';
this.refreshInterval = this.configObj?.refreshInterval || 300000;
this.weatherData = {};
this.currentWeather = {};
this.forecast = [];
......@@ -37,23 +37,23 @@ export class WeatherWidgetComponent extends BaseWidgetComponent {
const weatherItem = data[0];
this.currentWeather = {
temperature: weatherItem[this.config.temperatureField || 'temperature'],
humidity: weatherItem[this.config.humidityField || 'humidity'],
windSpeed: weatherItem[this.config.windSpeedField || 'windSpeed'],
pressure: weatherItem[this.config.pressureField || 'pressure'],
description: weatherItem[this.config.descriptionField || 'description'],
icon: weatherItem[this.config.iconField || 'icon'],
feelsLike: weatherItem[this.config.feelsLikeField || 'feelsLike']
temperature: weatherItem[this.configObj.temperatureField || 'temperature'],
humidity: weatherItem[this.configObj.humidityField || 'humidity'],
windSpeed: weatherItem[this.configObj.windSpeedField || 'windSpeed'],
pressure: weatherItem[this.configObj.pressureField || 'pressure'],
description: weatherItem[this.configObj.descriptionField || 'description'],
icon: weatherItem[this.configObj.iconField || 'icon'],
feelsLike: weatherItem[this.configObj.feelsLikeField || 'feelsLike']
};
// Process forecast data if available
if (data.length > 1) {
this.forecast = data.slice(1).map(item => ({
day: item[this.config.dayField || 'day'],
high: item[this.config.highField || 'high'],
low: item[this.config.lowField || 'low'],
description: item[this.config.forecastDescriptionField || 'description'],
icon: item[this.config.forecastIconField || 'icon']
day: item[this.configObj.dayField || 'day'],
high: item[this.configObj.highField || 'high'],
low: item[this.configObj.lowField || 'low'],
description: item[this.configObj.forecastDescriptionField || 'description'],
icon: item[this.configObj.forecastIconField || 'icon']
}));
}
......
......@@ -17,24 +17,24 @@ export class WelcomeWidgetComponent extends BaseWidgetComponent {
}
applyInitialConfig(): void {
this.title = this.config.title || 'Welcome';
if (this.config.messageType === 'static') {
this.welcomeMessage = this.config.staticMessage || 'Welcome!';
this.title = this.configObj.title || 'Welcome';
if (this.configObj.messageType === 'static') {
this.welcomeMessage = this.configObj.staticMessage || 'Welcome!';
} else {
this.welcomeMessage = '...'; // Placeholder while waiting for data
}
}
onDataUpdate(data: any[]): void {
if (this.config.messageType === 'dynamic') {
if (data.length > 0 && this.config.messageField) {
this.welcomeMessage = data[0][this.config.messageField];
if (this.configObj.messageType === 'dynamic') {
if (data.length > 0 && this.configObj.messageField) {
this.welcomeMessage = data[0][this.configObj.messageField];
} else {
this.welcomeMessage = 'No message data';
}
} else {
// For static messages, the message is already set in applyInitialConfig
this.welcomeMessage = this.config.staticMessage || 'Welcome!';
this.welcomeMessage = this.configObj.staticMessage || 'Welcome!';
}
}
......
import { Component } from '@angular/core';
@Component({
selector: 'app-dashboard-viewer',
template: '<p>dashboard-viewer works!</p>',
standalone: true
})
export class DashboardViewerComponent {
}
......@@ -109,11 +109,6 @@ export const portalManageRoutes: Routes = [
component: HomeComponent, // Assuming HomeComponent is a generic dashboard or a placeholder
canActivate: [moduleAccessGuard]
},
{
path: 'dashboard-viewer',
loadComponent: () => import('./dashboard-viewer/dashboard-viewer.component').then(m => m.DashboardViewerComponent),
canActivate: [moduleAccessGuard]
},
// === Generic App Routes ===
// These routes are for simple apps that don't need special module-level services.
......
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