Commit c200249b by Ooh-Ao

config

parent 162ff658
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
"@syncfusion/ej2-angular-base": "^31.1.17", "@syncfusion/ej2-angular-base": "^31.1.17",
"@syncfusion/ej2-angular-buttons": "^31.1.17", "@syncfusion/ej2-angular-buttons": "^31.1.17",
"@syncfusion/ej2-angular-calendars": "^31.1.19", "@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-circulargauge": "^31.1.17",
"@syncfusion/ej2-angular-dropdowns": "^31.1.17", "@syncfusion/ej2-angular-dropdowns": "^31.1.17",
"@syncfusion/ej2-angular-grids": "^31.1.17", "@syncfusion/ej2-angular-grids": "^31.1.17",
......
...@@ -138,12 +138,16 @@ export class DashboardManagementComponent implements OnInit { ...@@ -138,12 +138,16 @@ export class DashboardManagementComponent implements OnInit {
if (dashboard) { if (dashboard) {
this.dashboardData = 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) { if (dashboard.widgets) {
dashboard.widgets.forEach((widget: any) => { dashboard.widgets.forEach((widget: any) => {
if (!widget.instanceId) { if (!widget.instanceId) {
widget.instanceId = `inst_${Date.now()}_${Math.random()}`; 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 { ...@@ -198,7 +202,9 @@ export class DashboardManagementComponent implements OnInit {
saveDashboardName(): void { saveDashboardName(): void {
if (this.dashboardData) { if (this.dashboardData) {
this.dashboardDataService.saveDashboard(this.dashboardData).pipe( const dashboardToSave = this.convertWidgetsToStrings(this.dashboardData);
this.dashboardDataService.saveDashboard(dashboardToSave).pipe(
catchError(error => { catchError(error => {
this.notificationService.error('Error', 'Failed to save dashboard name.'); this.notificationService.error('Error', 'Failed to save dashboard name.');
return throwError(() => error); return throwError(() => error);
...@@ -252,27 +258,18 @@ export class DashboardManagementComponent implements OnInit { ...@@ -252,27 +258,18 @@ export class DashboardManagementComponent implements OnInit {
}); });
console.log('Current panels from layout:', currentPanels); console.log('Current panels from layout:', currentPanels);
console.log('Updated widget positions:', this.dashboardData.widgets); 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) => { // Convert objects back to strings for saving
// Remove the internal-only instanceId before saving const dashboardToSave = this.convertWidgetsToStrings(this.dashboardData);
delete widget.instanceId;
if (widget.config && typeof widget.config === 'object') { // Update perspectives from WidgetStateService
widget.config = JSON.stringify(widget.config); if (dashboardToSave.widgets) {
} const allWidgetStates = this.widgetStateService.getAllWidgetStates();
dashboardToSave.widgets.forEach((widget: any) => {
// Update perspective from WidgetStateService if available
const currentPerspective = allWidgetStates.get(widget.widgetId); const currentPerspective = allWidgetStates.get(widget.widgetId);
if (currentPerspective !== undefined) { // Check for undefined to allow nulls if (currentPerspective !== undefined) {
widget.perspective = currentPerspective; 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 { ...@@ -363,38 +360,43 @@ export class DashboardManagementComponent implements OnInit {
}); });
} }
mapWidgetsToPanels(widgets: WidgetModel[]): DashboardPanel[] { mapWidgetsToPanels(widgets: any[]): DashboardPanel[] {
return widgets.map(widget => { return widgets
// Ensure config is an object before passing to component .map(widget => {
let configObject: any = {}; // Use any to easily add properties // Use parsed config object (already parsed in loadDashboard)
if (typeof widget.config === 'string') { const configObject = {
try { ...(typeof widget.config === 'object' ? widget.config : {}),
configObject = JSON.parse(widget.config); widgetId: widget.widgetId
} catch (e) { };
console.error('Error parsing widget config string:', widget.config, e); 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;
} }
} else if (typeof widget.config === 'object' && widget.config !== null) {
configObject = { ...widget.config }; // Create a shallow copy
}
// Inject widgetId into the config for the component const panel = {
configObject.widgetId = widget.widgetId; id: `${(widget as any).instanceId}-${widget.y}-${widget.x}`,
console.log(`Mapping widget ${widget.widgetId} to panel with config:`, configObject); header: widget.thName,
return { sizeX: widget.cols,
id: `${(widget as any).instanceId}-${widget.y}-${widget.x}`, sizeY: widget.rows,
header: widget.thName, row: widget.y,
sizeX: widget.cols, col: widget.x,
sizeY: widget.rows, componentType: componentType,
row: widget.y, componentInputs: {
col: widget.x, config: JSON.stringify(configObject),
componentType: this.widgetComponentRegistryService.getComponent(widget.component), perspective: JSON.stringify(perspectiveObject),
componentInputs: { data: JSON.stringify(dataObject)
config: configObject, },
perspective: widget.perspective originalWidget: widget
}, };
originalWidget: widget
};
}); return panel;
})
.filter(panel => panel !== null) as DashboardPanel[];
} }
openWidgetDialog(): void { openWidgetDialog(): void {
...@@ -404,5 +406,42 @@ export class DashboardManagementComponent implements OnInit { ...@@ -404,5 +406,42 @@ export class DashboardManagementComponent implements OnInit {
closeWidgetDialog(): void { closeWidgetDialog(): void {
this.isWidgetDialogVisible = false; 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 @@ ...@@ -4,7 +4,7 @@
<div *ngIf="dashboardData" class="dashboard-viewer-container p-4"> <div *ngIf="dashboardData" class="dashboard-viewer-container p-4">
<h1 class="text-2xl font-bold mb-4 text-gray-800">{{ dashboardData.thName }}</h1> <h1 class="text-2xl font-bold mb-4 text-gray-800">{{ dashboardData.thName }}</h1>
<div class="control-section"> <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-panels>
<e-panel *ngFor="let panel of panels" [row]="panel.row" [col]="panel.col" [sizeX]="panel.sizeX" [sizeY]="panel.sizeY" [id]="panel.id"> <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> <ng-template #header>
......
...@@ -40,6 +40,10 @@ import { SlicerWidgetComponent } from '../widgets/slicer-widget/slicer-widget.co ...@@ -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 { SimpleTableWidgetComponent } from '../widgets/simple-table-widget/simple-table-widget.component';
import { WaterfallChartWidgetComponent } from '../widgets/waterfall-chart-widget/waterfall-chart-widget.component'; import { WaterfallChartWidgetComponent } from '../widgets/waterfall-chart-widget/waterfall-chart-widget.component';
import { TreemapWidgetComponent } from '../widgets/treemap-widget/treemap-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 { export interface DashboardPanel extends PanelModel {
componentType: Type<any>; componentType: Type<any>;
...@@ -52,7 +56,7 @@ export interface DashboardPanel extends PanelModel { ...@@ -52,7 +56,7 @@ export interface DashboardPanel extends PanelModel {
imports: [ imports: [
CommonModule, RouterModule, DashboardLayoutModule, NgComponentOutlet, CommonModule, RouterModule, DashboardLayoutModule, NgComponentOutlet,
// Add all widget components here to make them available for 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', templateUrl: './dashboard-viewer.component.html',
styleUrls: ['./dashboard-viewer.component.scss'], styleUrls: ['./dashboard-viewer.component.scss'],
...@@ -61,6 +65,7 @@ export class DashboardViewerComponent implements OnInit { ...@@ -61,6 +65,7 @@ export class DashboardViewerComponent implements OnInit {
public panels: DashboardPanel[] = []; public panels: DashboardPanel[] = [];
public cellSpacing: number[] = [10, 10]; public cellSpacing: number[] = [10, 10];
public columns: number = 6;
public dashboardData: DashboardModel | null = null; public dashboardData: DashboardModel | null = null;
public errorMessage: string | null = null; public errorMessage: string | null = null;
...@@ -91,7 +96,7 @@ export class DashboardViewerComponent implements OnInit { ...@@ -91,7 +96,7 @@ export class DashboardViewerComponent implements OnInit {
widget.instanceId = `inst_${Date.now()}_${Math.random()}`; 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 => { keysToProcess.forEach(key => {
if ((widget as any)[key] && typeof (widget as any)[key] === 'string') { if ((widget as any)[key] && typeof (widget as any)[key] === 'string') {
try { try {
...@@ -118,18 +123,53 @@ export class DashboardViewerComponent implements OnInit { ...@@ -118,18 +123,53 @@ export class DashboardViewerComponent implements OnInit {
}); });
} }
mapWidgetsToPanels(widgets: WidgetModel[]): DashboardPanel[] { mapWidgetsToPanels(widgets: any[]): DashboardPanel[] {
return widgets.map(widget => { return widgets
return { .map(widget => {
id: `${(widget as any).instanceId}-${widget.y}-${widget.x}`, // Use parsed config object (already parsed in ngOnInit)
header: widget.thName, const configObject = {
sizeX: widget.cols, ...(typeof widget.config === 'object' ? widget.config : {}),
sizeY: widget.rows, widgetId: widget.widgetId
row: widget.y, };
col: widget.x, const perspectiveObject = typeof widget.perspective === 'object' ? widget.perspective : {};
componentType: this.widgetComponentRegistryService.getComponent(widget.component), const dataObject = typeof widget.data === 'object' ? widget.data : {};
componentInputs: { config: widget.config || {}, perspective: widget.perspective },
}; 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,
sizeX: widget.cols,
sizeY: widget.rows,
row: widget.y,
col: widget.x,
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 { ...@@ -35,9 +35,9 @@ export interface IWidget {
rows: number; rows: number;
x: number; x: number;
y: number; y: number;
data: any; data: string;
config: any; config: string;
perspective: any; perspective: string;
} }
export class WidgetModel implements IWidget { export class WidgetModel implements IWidget {
...@@ -49,9 +49,9 @@ export class WidgetModel implements IWidget { ...@@ -49,9 +49,9 @@ export class WidgetModel implements IWidget {
rows: number; rows: number;
x: number; x: number;
y: number; y: number;
data: any; data: string;
config: any; config: string;
perspective: any; perspective: string;
constructor(data: Partial<IWidget>) { constructor(data: Partial<IWidget>) {
this.widgetId = data.widgetId ?? ''; this.widgetId = data.widgetId ?? '';
...@@ -62,10 +62,61 @@ export class WidgetModel implements IWidget { ...@@ -62,10 +62,61 @@ export class WidgetModel implements IWidget {
this.rows = data.rows ?? 1; this.rows = data.rows ?? 1;
this.x = data.x ?? 0; this.x = data.x ?? 0;
this.y = data.y ?? 0; this.y = data.y ?? 0;
this.data = data.data ?? {}; this.data = this.convertToString(data.data);
this.config = data.config ?? {}; this.config = this.convertToString(data.config);
this.perspective = data.perspective ?? {}; 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 ...@@ -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 { SimpleTableWidgetComponent } from '../widgets/simple-table-widget/simple-table-widget.component';
import { WaterfallChartWidgetComponent } from '../widgets/waterfall-chart-widget/waterfall-chart-widget.component'; import { WaterfallChartWidgetComponent } from '../widgets/waterfall-chart-widget/waterfall-chart-widget.component';
import { TreemapWidgetComponent } from '../widgets/treemap-widget/treemap-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({ @Injectable({
providedIn: 'root', providedIn: 'root',
...@@ -65,15 +70,21 @@ export class WidgetComponentRegistryService { ...@@ -65,15 +70,21 @@ export class WidgetComponentRegistryService {
SimpleTableWidgetComponent: SimpleTableWidgetComponent, SimpleTableWidgetComponent: SimpleTableWidgetComponent,
WaterfallChartWidgetComponent: WaterfallChartWidgetComponent, WaterfallChartWidgetComponent: WaterfallChartWidgetComponent,
TreemapWidgetComponent: TreemapWidgetComponent, 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]; const component = this.widgetComponentMap[componentName];
if (!component) { if (!component) {
console.warn( console.warn(
`Warning: Widget component "${componentName}" not found in registry.` `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; 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 ...@@ -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 { SyncfusionDatagridWidgetComponent } from '../widgets/syncfusion-datagrid-widget/syncfusion-datagrid-widget.component';
import { SyncfusionPivotWidgetComponent } from '../widgets/syncfusion-pivot-widget/syncfusion-pivot-widget.component'; import { SyncfusionPivotWidgetComponent } from '../widgets/syncfusion-pivot-widget/syncfusion-pivot-widget.component';
import { SyncfusionChartWidgetComponent } from '../widgets/syncfusion-chart-widget/syncfusion-chart-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({ @Component({
selector: 'app-widget-form', selector: 'app-widget-form',
...@@ -53,6 +58,11 @@ import { SyncfusionChartWidgetComponent } from '../widgets/syncfusion-chart-widg ...@@ -53,6 +58,11 @@ import { SyncfusionChartWidgetComponent } from '../widgets/syncfusion-chart-widg
SyncfusionDatagridWidgetComponent, SyncfusionDatagridWidgetComponent,
SyncfusionPivotWidgetComponent, SyncfusionPivotWidgetComponent,
SyncfusionChartWidgetComponent, SyncfusionChartWidgetComponent,
// New Syncfusion-based widgets
CalendarWidgetComponent,
NotificationWidgetComponent,
WeatherWidgetComponent,
ClockWidgetComponent,
], ],
templateUrl: './widget-form.component.html', templateUrl: './widget-form.component.html',
}) })
...@@ -106,15 +116,40 @@ export class WidgetFormComponent implements OnInit { ...@@ -106,15 +116,40 @@ export class WidgetFormComponent implements OnInit {
} }
updatePreview(componentName: string): void { updatePreview(componentName: string): void {
this.previewComponentType = if (!componentName) {
this.widgetComponentRegistryService.getComponent(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 { onSubmit(): void {
if (this.widgetForm.invalid) { if (this.widgetForm.invalid) {
return; 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 { cancel(): void {
......
...@@ -33,6 +33,11 @@ import { NotificationService } from '../../../shared/services/notification.servi ...@@ -33,6 +33,11 @@ import { NotificationService } from '../../../shared/services/notification.servi
import { WidgetModel } from '../models/widgets.model'; import { WidgetModel } from '../models/widgets.model';
import { WidgetService } from '../services/widgets.service'; import { WidgetService } from '../services/widgets.service';
import { SimpleKpiWidgetComponent } from '../widgets/simple-kpi-widget/simple-kpi-widget.component'; 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 { WidgetFormComponent } from './widget-form.component';
import { ClickEventArgs } from '@syncfusion/ej2-angular-navigations'; import { ClickEventArgs } from '@syncfusion/ej2-angular-navigations';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
...@@ -59,6 +64,11 @@ import { RouterModule } from '@angular/router'; ...@@ -59,6 +64,11 @@ import { RouterModule } from '@angular/router';
SyncfusionPivotWidgetComponent, SyncfusionPivotWidgetComponent,
SyncfusionChartWidgetComponent, SyncfusionChartWidgetComponent,
SimpleKpiWidgetComponent, SimpleKpiWidgetComponent,
// New Syncfusion-based widgets
CalendarWidgetComponent,
NotificationWidgetComponent,
WeatherWidgetComponent,
ClockWidgetComponent,
], ],
providers: [ providers: [
ToolbarService, ToolbarService,
......
...@@ -21,14 +21,14 @@ export class AreaChartWidgetComponent extends BaseWidgetComponent { ...@@ -21,14 +21,14 @@ export class AreaChartWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Area Chart'; this.title = this.configObj.title || 'Area Chart';
this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' }; this.primaryXAxis = { valueType: 'Category', title: this.configObj.xAxisTitle || '' };
this.primaryYAxis = { title: this.config.yAxisTitle || '' }; this.primaryYAxis = { title: this.configObj.yAxisTitle || '' };
this.chartData = []; // Start with no data this.chartData = []; // Start with no data
} }
onDataUpdate(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.configObj.xField], y: item[this.configObj.yField] }));
} }
onReset(): void { onReset(): void {
......
...@@ -20,7 +20,7 @@ export class AttendanceOverviewWidgetComponent extends BaseWidgetComponent { ...@@ -20,7 +20,7 @@ export class AttendanceOverviewWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Attendance'; this.title = this.configObj.title || 'Attendance';
this.present = 0; this.present = 0;
this.onLeave = 0; this.onLeave = 0;
this.absent = 0; this.absent = 0;
...@@ -29,9 +29,9 @@ export class AttendanceOverviewWidgetComponent extends BaseWidgetComponent { ...@@ -29,9 +29,9 @@ export class AttendanceOverviewWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (data.length > 0) { if (data.length > 0) {
const firstItem = data[0]; const firstItem = data[0];
this.present = firstItem[this.config.presentField] || 0; this.present = firstItem[this.configObj.presentField] || 0;
this.onLeave = firstItem[this.config.onLeaveField] || 0; this.onLeave = firstItem[this.configObj.onLeaveField] || 0;
this.absent = firstItem[this.config.absentField] || 0; this.absent = firstItem[this.configObj.absentField] || 0;
} }
} }
......
...@@ -23,15 +23,15 @@ export class BarChartWidgetComponent extends BaseWidgetComponent { ...@@ -23,15 +23,15 @@ export class BarChartWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Bar Chart'; this.title = this.configObj.title || 'Bar Chart';
this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' }; this.primaryXAxis = { valueType: 'Category', title: this.configObj.xAxisTitle || '' };
this.primaryYAxis = { title: this.config.yAxisTitle || '' }; this.primaryYAxis = { title: this.configObj.yAxisTitle || '' };
this.type = this.config.type || 'Column'; this.type = this.configObj.type || 'Column';
this.chartData = []; this.chartData = [];
} }
onDataUpdate(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.configObj.xField], y: item[this.configObj.yField] }));
} }
onReset(): void { onReset(): void {
......
import { Input, OnInit, OnDestroy, Directive } from '@angular/core'; import { Input, OnInit, OnDestroy, Directive } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { DashboardStateService, SelectedDataset } from '../services/dashboard-state.service'; 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 @Directive() // Use @Directive() for base classes without their own template
export abstract class BaseWidgetComponent implements OnInit, OnDestroy { 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; @Input() perspective: string | null = null;
public title: string; public title: string;
public isLoading = true; public isLoading = true;
public hasError = false; public hasError = false;
public errorMessage: string | null = null; 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 subscription: Subscription = new Subscription();
protected originalData: any[] = [];
constructor(protected dashboardStateService: DashboardStateService) {} constructor(protected dashboardStateService: DashboardStateService) {}
ngOnInit(): void { 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({ const datasetSub = this.dashboardStateService.selectedDataset$.subscribe({
next: (selectedDataset: SelectedDataset | null) => { next: (selectedDataset: SelectedDataset | null) => {
...@@ -25,7 +37,9 @@ export abstract class BaseWidgetComponent implements OnInit, OnDestroy { ...@@ -25,7 +37,9 @@ export abstract class BaseWidgetComponent implements OnInit, OnDestroy {
this.hasError = false; this.hasError = false;
if (selectedDataset && selectedDataset.data) { if (selectedDataset && selectedDataset.data) {
try { try {
this.onDataUpdate(selectedDataset.data); this.originalData = selectedDataset.data;
this.applyFilters();
this.onDataUpdate(this.filteredData);
this.isLoading = false; this.isLoading = false;
} catch (error) { } catch (error) {
this.handleError(error); this.handleError(error);
...@@ -71,4 +85,212 @@ export abstract class BaseWidgetComponent implements OnInit, OnDestroy { ...@@ -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 method for child components to fall back to a default state on error.
*/ */
abstract onReset(): void; 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 @@ ...@@ -20,7 +20,6 @@
[enableRtl]="calendarSettings.enableRtl" [enableRtl]="calendarSettings.enableRtl"
[start]="calendarSettings.start" [start]="calendarSettings.start"
[depth]="calendarSettings.depth" [depth]="calendarSettings.depth"
[cssClass]="calendarSettings.cssClass"
(change)="onDateChange($event)"> (change)="onDateChange($event)">
</ejs-calendar> </ejs-calendar>
......
...@@ -21,12 +21,12 @@ export class CalendarWidgetComponent extends BaseWidgetComponent { ...@@ -21,12 +21,12 @@ export class CalendarWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Calendar'; this.title = this.configObj?.title || 'Calendar';
this.calendarSettings = { this.calendarSettings = {
enableRtl: this.config.enableRtl || false, enableRtl: this.configObj?.enableRtl || false,
start: this.config.start || 'Year', start: this.configObj?.start || 'Year',
depth: this.config.depth || 'Year', depth: this.configObj?.depth || 'Year',
cssClass: this.config.cssClass || '' cssClass: this.configObj?.cssClass || undefined
}; };
this.events = []; this.events = [];
} }
...@@ -35,10 +35,10 @@ export class CalendarWidgetComponent extends BaseWidgetComponent { ...@@ -35,10 +35,10 @@ export class CalendarWidgetComponent extends BaseWidgetComponent {
if (data && data.length > 0) { if (data && data.length > 0) {
// Map data to events format // Map data to events format
this.events = data.map(item => ({ this.events = data.map(item => ({
date: new Date(item[this.config.dateField || 'date']), date: new Date(item[this.configObj?.dateField || 'date']),
title: item[this.config.titleField || 'title'], title: item[this.configObj?.titleField || 'title'],
description: item[this.config.descriptionField || 'description'], description: item[this.configObj?.descriptionField || 'description'],
type: item[this.config.typeField || 'type'] || 'default' type: item[this.configObj?.typeField || 'type'] || 'default'
})); }));
} }
} }
......
...@@ -30,15 +30,15 @@ export class ChartWidgetComponent extends BaseWidgetComponent implements AfterVi ...@@ -30,15 +30,15 @@ export class ChartWidgetComponent extends BaseWidgetComponent implements AfterVi
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config?.title || 'Chart'; this.title = this.configObj?.title || 'Chart';
if (this.chartInstance) { if (this.chartInstance) {
this.chartInstance.updateOptions(this.getChartOptions([], [])); this.chartInstance.updateOptions(this.getChartOptions([], []));
} }
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
const categories = data.map(item => item[this.config.xField]); const categories = data.map(item => item[this.configObj.xField]);
const series = this.config.yFields.map((yField: any) => ({ const series = this.configObj.yFields.map((yField: any) => ({
name: yField.name || yField.field, name: yField.name || yField.field,
data: data.map(item => item[yField.field]) data: data.map(item => item[yField.field])
})); }));
...@@ -65,7 +65,7 @@ export class ChartWidgetComponent extends BaseWidgetComponent implements AfterVi ...@@ -65,7 +65,7 @@ export class ChartWidgetComponent extends BaseWidgetComponent implements AfterVi
return { return {
series: series, series: series,
chart: { chart: {
type: this.config?.type || 'bar', type: this.configObj?.type || 'bar',
height: 250, height: 250,
toolbar: { toolbar: {
show: false show: false
...@@ -91,7 +91,7 @@ export class ChartWidgetComponent extends BaseWidgetComponent implements AfterVi ...@@ -91,7 +91,7 @@ export class ChartWidgetComponent extends BaseWidgetComponent implements AfterVi
}, },
yaxis: { yaxis: {
title: { title: {
text: this.config?.yAxisTitle || 'Number' text: this.configObj?.yAxisTitle || 'Number'
} }
}, },
fill: { fill: {
......
...@@ -53,14 +53,14 @@ export class ClockWidgetComponent extends BaseWidgetComponent implements OnInit, ...@@ -53,14 +53,14 @@ export class ClockWidgetComponent extends BaseWidgetComponent implements OnInit,
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Clock'; this.title = this.configObj?.title || 'Clock';
this.timezone = this.config.timezone || 'Asia/Bangkok'; this.timezone = this.configObj?.timezone || 'Asia/Bangkok';
this.clockType = this.config.clockType || 'analog'; this.clockType = this.configObj?.clockType || 'analog';
this.showSeconds = this.config.showSeconds !== false; this.showSeconds = this.configObj?.showSeconds !== false;
this.showDate = this.config.showDate !== false; this.showDate = this.configObj?.showDate !== false;
this.showTimezone = this.config.showTimezone !== false; this.showTimezone = this.configObj?.showTimezone !== false;
this.format24Hour = this.config.format24Hour !== false; this.format24Hour = this.configObj?.format24Hour !== false;
this.updateInterval = this.config.updateInterval || 1000; this.updateInterval = this.configObj?.updateInterval || 1000;
this.updateClock(); this.updateClock();
} }
...@@ -68,9 +68,9 @@ export class ClockWidgetComponent extends BaseWidgetComponent implements OnInit, ...@@ -68,9 +68,9 @@ export class ClockWidgetComponent extends BaseWidgetComponent implements OnInit,
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (data && data.length > 0) { if (data && data.length > 0) {
const timeData = data[0]; const timeData = data[0];
this.timezone = timeData[this.config.timezoneField || 'timezone'] || this.timezone; this.timezone = timeData[this.configObj.timezoneField || 'timezone'] || this.timezone;
this.clockType = timeData[this.config.clockTypeField || 'clockType'] || this.clockType; this.clockType = timeData[this.configObj.clockTypeField || 'clockType'] || this.clockType;
this.format24Hour = timeData[this.config.format24HourField || 'format24Hour'] !== false; this.format24Hour = timeData[this.configObj.format24HourField || 'format24Hour'] !== false;
this.updateClock(); this.updateClock();
} }
} }
......
...@@ -10,13 +10,13 @@ ...@@ -10,13 +10,13 @@
</div> </div>
<!-- Chart --> <!-- 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-collection>
<e-series *ngFor="let series of config.series" <e-series *ngFor="let series of configObj.series"
[dataSource]="chartData" [dataSource]="chartData"
[type]="series.type || 'Column'" [type]="series.type || 'Column'"
[xName]="series.xName" [xName]="series.xName"
[yName]="series.yName" [yName]="series.yName"
[name]="series.name"> [name]="series.name">
</e-series> </e-series>
</e-series-collection> </e-series-collection>
......
...@@ -21,9 +21,9 @@ export class ComboChartWidgetComponent extends BaseWidgetComponent { ...@@ -21,9 +21,9 @@ export class ComboChartWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Combo Chart'; this.title = this.configObj.title || 'Combo Chart';
this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' }; this.primaryXAxis = { valueType: 'Category', title: this.configObj.xAxisTitle || '' };
this.primaryYAxis = { title: this.config.yAxisTitle || '' }; this.primaryYAxis = { title: this.configObj.yAxisTitle || '' };
this.chartData = []; this.chartData = [];
} }
...@@ -41,9 +41,9 @@ export class ComboChartWidgetComponent extends BaseWidgetComponent { ...@@ -41,9 +41,9 @@ export class ComboChartWidgetComponent extends BaseWidgetComponent {
this.primaryXAxis = { valueType: 'Category', title: 'Month' }; this.primaryXAxis = { valueType: 'Category', title: 'Month' };
this.primaryYAxis = { title: 'Value' }; this.primaryYAxis = { title: 'Value' };
if (!this.config) { if (!this.config) {
this.config = {}; this.config = '{}';
} }
this.config.series = [ this.configObj.series = [
{ type: 'Column', xName: 'x', yName: 'y1', name: 'Sales' }, { type: 'Column', xName: 'x', yName: 'y1', name: 'Sales' },
{ type: 'Line', xName: 'x', yName: 'y2', name: 'Profit' } { type: 'Line', xName: 'x', yName: 'y2', name: 'Profit' }
]; ];
......
...@@ -41,7 +41,7 @@ export class CompanyInfoWidgetComponent extends BaseWidgetComponent { ...@@ -41,7 +41,7 @@ export class CompanyInfoWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Company Info'; this.title = this.configObj.title || 'Company Info';
this.companyName = '...'; this.companyName = '...';
this.address = '...'; this.address = '...';
this.contact = '...'; this.contact = '...';
...@@ -50,9 +50,9 @@ export class CompanyInfoWidgetComponent extends BaseWidgetComponent { ...@@ -50,9 +50,9 @@ export class CompanyInfoWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (data.length > 0) { if (data.length > 0) {
const firstItem = data[0]; const firstItem = data[0];
this.companyName = firstItem[this.config.companyNameField] || ''; this.companyName = firstItem[this.configObj.companyNameField] || '';
this.address = firstItem[this.config.addressField] || ''; this.address = firstItem[this.configObj.addressField] || '';
this.contact = firstItem[this.config.contactField] || ''; this.contact = firstItem[this.configObj.contactField] || '';
} }
} }
......
...@@ -21,7 +21,7 @@ export class CompanyInfoSubfolderWidgetComponent extends BaseWidgetComponent { ...@@ -21,7 +21,7 @@ export class CompanyInfoSubfolderWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Company Info'; this.title = this.configObj.title || 'Company Info';
this.companyName = '...'; this.companyName = '...';
this.address = '...'; this.address = '...';
this.contact = '...'; this.contact = '...';
...@@ -30,9 +30,9 @@ export class CompanyInfoSubfolderWidgetComponent extends BaseWidgetComponent { ...@@ -30,9 +30,9 @@ export class CompanyInfoSubfolderWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (data.length > 0) { if (data.length > 0) {
const firstItem = data[0]; const firstItem = data[0];
this.companyName = firstItem[this.config.companyNameField] || ''; this.companyName = firstItem[this.configObj.companyNameField] || '';
this.address = firstItem[this.config.addressField] || ''; this.address = firstItem[this.configObj.addressField] || '';
this.contact = firstItem[this.config.contactField] || ''; this.contact = firstItem[this.configObj.contactField] || '';
} }
} }
......
...@@ -20,7 +20,7 @@ export class DoughnutChartWidgetComponent extends BaseWidgetComponent { ...@@ -20,7 +20,7 @@ export class DoughnutChartWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Doughnut Chart'; this.title = this.configObj.title || 'Doughnut Chart';
this.legendSettings = { visible: true }; this.legendSettings = { visible: true };
this.chartData = []; this.chartData = [];
} }
...@@ -28,15 +28,15 @@ export class DoughnutChartWidgetComponent extends BaseWidgetComponent { ...@@ -28,15 +28,15 @@ export class DoughnutChartWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
let transformedData = data; let transformedData = data;
if (this.config.aggregation === 'count') { if (this.configObj.aggregation === 'count') {
const counts = transformedData.reduce((acc, item) => { const counts = transformedData.reduce((acc, item) => {
const key = item[this.config.xField]; const key = item[this.configObj.xField];
acc[key] = (acc[key] || 0) + 1; acc[key] = (acc[key] || 0) + 1;
return acc; return acc;
}, {}); }, {});
transformedData = Object.keys(counts).map(key => ({ x: key, y: counts[key] })); transformedData = Object.keys(counts).map(key => ({ x: key, y: counts[key] }));
} else { } 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; this.chartData = transformedData;
} }
......
...@@ -13,7 +13,10 @@ import { GridModule, PageService, SortService, FilterService, GroupService } fro ...@@ -13,7 +13,10 @@ import { GridModule, PageService, SortService, FilterService, GroupService } fro
}) })
export class DataTableWidgetComponent implements OnInit, OnChanges { export class DataTableWidgetComponent implements OnInit, OnChanges {
// This component is "Dumb". It receives all its configuration and data via this input. // 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 data: object[] = [];
public columns: any[] = []; public columns: any[] = [];
...@@ -24,13 +27,28 @@ export class DataTableWidgetComponent implements OnInit, OnChanges { ...@@ -24,13 +27,28 @@ export class DataTableWidgetComponent implements OnInit, OnChanges {
ngOnInit(): void { ngOnInit(): void {
console.log('DataTableWidgetComponent: ngOnInit - config', this.config); console.log('DataTableWidgetComponent: ngOnInit - config', this.config);
this.parseConfig();
this.updateWidgetFromConfig(); 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 { ngOnChanges(changes: SimpleChanges): void {
console.log('DataTableWidgetComponent: ngOnChanges - changes', changes); console.log('DataTableWidgetComponent: ngOnChanges - changes', changes);
// If the config object changes, re-render the widget // If the config object changes, re-render the widget
if (changes['config']) { if (changes['config']) {
this.parseConfig();
this.updateWidgetFromConfig(); this.updateWidgetFromConfig();
} }
} }
...@@ -38,12 +56,12 @@ export class DataTableWidgetComponent implements OnInit, OnChanges { ...@@ -38,12 +56,12 @@ export class DataTableWidgetComponent implements OnInit, OnChanges {
private updateWidgetFromConfig(): void { private updateWidgetFromConfig(): void {
console.log('DataTableWidgetComponent: updateWidgetFromConfig - config', this.config); console.log('DataTableWidgetComponent: updateWidgetFromConfig - config', this.config);
if (this.config) { if (this.config) {
this.title = this.config.title || 'Data Table'; this.title = this.configObj.title || 'Data Table';
this.data = this.config.data || []; this.data = this.configObj.data || [];
console.log('DataTableWidgetComponent: updateWidgetFromConfig - data', this.data); console.log('DataTableWidgetComponent: updateWidgetFromConfig - data', this.data);
// If columns are defined in config, use them. Otherwise, generate from data. // If columns are defined in config, use them. Otherwise, generate from data.
if (this.config.columns && this.config.columns.length > 0) { if (this.configObj.columns && this.configObj.columns.length > 0) {
this.columns = this.config.columns; this.columns = this.configObj.columns;
} else if (this.data.length > 0) { } else if (this.data.length > 0) {
// Auto-generate columns from the first data item's keys // Auto-generate columns from the first data item's keys
this.columns = Object.keys(this.data[0]).map(key => ({ this.columns = Object.keys(this.data[0]).map(key => ({
......
...@@ -25,16 +25,16 @@ export class EmployeeDirectoryWidgetComponent extends BaseWidgetComponent { ...@@ -25,16 +25,16 @@ export class EmployeeDirectoryWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Employee Directory'; this.title = this.configObj.title || 'Employee Directory';
this.employees = []; this.employees = [];
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
this.employees = data.map(item => ({ this.employees = data.map(item => ({
name: item[this.config.nameField] || '', name: item[this.configObj.nameField] || '',
position: item[this.config.positionField] || '', position: item[this.configObj.positionField] || '',
department: item[this.config.departmentField] || '', department: item[this.configObj.departmentField] || '',
photoUrl: this.config.photoField ? item[this.config.photoField] : undefined photoUrl: this.configObj.photoField ? item[this.configObj.photoField] : undefined
})); }));
} }
......
...@@ -20,15 +20,15 @@ export class FilledMapWidgetComponent extends BaseWidgetComponent { ...@@ -20,15 +20,15 @@ export class FilledMapWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Map'; this.title = this.configObj.title || 'Map';
this.zoomSettings = { enable: true }; this.zoomSettings = { enable: true };
this.updateLayers([]); this.updateLayers([]);
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
const mapData = data.map(item => ({ const mapData = data.map(item => ({
country: item[this.config.countryField], country: item[this.configObj.countryField],
value: item[this.config.valueField] value: item[this.configObj.valueField]
})); }));
this.updateLayers(mapData); this.updateLayers(mapData);
} }
...@@ -55,7 +55,7 @@ export class FilledMapWidgetComponent extends BaseWidgetComponent { ...@@ -55,7 +55,7 @@ export class FilledMapWidgetComponent extends BaseWidgetComponent {
shapeSettings: { shapeSettings: {
fill: '#E5EEF6', fill: '#E5EEF6',
colorValuePath: 'value', colorValuePath: 'value',
colorMapping: this.config?.colorMapping || [ colorMapping: this.configObj?.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' }
......
...@@ -20,13 +20,13 @@ export class FunnelChartWidgetComponent extends BaseWidgetComponent { ...@@ -20,13 +20,13 @@ export class FunnelChartWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Funnel Chart'; this.title = this.configObj.title || 'Funnel Chart';
this.legendSettings = { visible: true }; this.legendSettings = { visible: true };
this.chartData = []; this.chartData = [];
} }
onDataUpdate(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.configObj.xField], y: item[this.configObj.yField] }));
} }
onReset(): void { onReset(): void {
......
...@@ -19,20 +19,20 @@ export class GaugeChartWidgetComponent extends BaseWidgetComponent { ...@@ -19,20 +19,20 @@ export class GaugeChartWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Gauge'; this.title = this.configObj.title || 'Gauge';
this.setAxes(0); this.setAxes(0);
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
let value = 0; let value = 0;
if (data.length > 0) { if (data.length > 0) {
if (this.config.aggregation === 'sum') { if (this.configObj.aggregation === 'sum') {
value = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0); value = data.reduce((sum, item) => sum + (item[this.configObj.valueField] || 0), 0);
} else if (this.config.aggregation === 'avg') { } else if (this.configObj.aggregation === 'avg') {
const sum = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0); const sum = data.reduce((sum, item) => sum + (item[this.configObj.valueField] || 0), 0);
value = sum / data.length; value = sum / data.length;
} else { } else {
value = data[0][this.config.valueField]; value = data[0][this.configObj.valueField];
} }
} }
this.setAxes(value); this.setAxes(value);
...@@ -56,7 +56,7 @@ export class GaugeChartWidgetComponent extends BaseWidgetComponent { ...@@ -56,7 +56,7 @@ export class GaugeChartWidgetComponent extends BaseWidgetComponent {
cap: { radius: 7 }, cap: { radius: 7 },
needleTail: { length: '18%' } needleTail: { length: '18%' }
}], }],
ranges: this.config?.ranges || [ ranges: this.configObj?.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' }
......
...@@ -23,14 +23,14 @@ ...@@ -23,14 +23,14 @@
<!-- Content --> <!-- Content -->
<div *ngIf="!isLoading && !hasError" class="h-full"> <div *ngIf="!isLoading && !hasError" class="h-full">
<!-- Bar Chart --> <!-- 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-collection>
<e-series [dataSource]="breakdown" type="Bar" xName="category" yName="count" name="Headcount"></e-series> <e-series [dataSource]="breakdown" type="Bar" xName="category" yName="count" name="Headcount"></e-series>
</e-series-collection> </e-series-collection>
</ejs-chart> </ejs-chart>
<!-- Doughnut 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-collection>
<e-accumulation-series [dataSource]="breakdown" xName="category" yName="count" innerRadius="40%" [dataLabel]="dataLabel"></e-accumulation-series> <e-accumulation-series [dataSource]="breakdown" xName="category" yName="count" innerRadius="40%" [dataLabel]="dataLabel"></e-accumulation-series>
</e-accumulation-series-collection> </e-accumulation-series-collection>
......
...@@ -27,7 +27,7 @@ export class HeadcountWidgetComponent extends BaseWidgetComponent { ...@@ -27,7 +27,7 @@ export class HeadcountWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Headcount'; this.title = this.configObj.title || 'Headcount';
this.totalHeadcount = 0; this.totalHeadcount = 0;
this.breakdown = []; this.breakdown = [];
...@@ -51,9 +51,9 @@ export class HeadcountWidgetComponent extends BaseWidgetComponent { ...@@ -51,9 +51,9 @@ export class HeadcountWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
this.totalHeadcount = data.length; this.totalHeadcount = data.length;
if (this.config.categoryField) { if (this.configObj.categoryField) {
const counts = data.reduce((acc, item) => { const counts = data.reduce((acc, item) => {
const category = item[this.config.categoryField]; const category = item[this.configObj.categoryField];
acc[category] = (acc[category] || 0) + 1; acc[category] = (acc[category] || 0) + 1;
return acc; return acc;
}, {}); }, {});
......
...@@ -17,24 +17,24 @@ export class KpiWidgetComponent extends BaseWidgetComponent { ...@@ -17,24 +17,24 @@ export class KpiWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'KPI'; this.title = this.configObj.title || 'KPI';
this.kpiData = { this.kpiData = {
value: '...', value: '...',
unit: this.config.unit || '', unit: this.configObj.unit || '',
trend: this.config.trend || 'neutral', trend: this.configObj.trend || 'neutral',
trendValue: this.config.trendValue || '' trendValue: this.configObj.trendValue || ''
}; };
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (data.length > 0) { if (data.length > 0) {
let kpiValue = 0; let kpiValue = 0;
if (this.config.aggregation === 'count') { if (this.configObj.aggregation === 'count') {
kpiValue = data.length; kpiValue = data.length;
} else if (this.config.aggregation === 'sum') { } else if (this.configObj.aggregation === 'sum') {
kpiValue = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0); kpiValue = data.reduce((sum, item) => sum + (item[this.configObj.valueField] || 0), 0);
} else { } else {
kpiValue = data[0][this.config.valueField]; kpiValue = data[0][this.configObj.valueField];
} }
this.kpiData.value = kpiValue.toLocaleString(); this.kpiData.value = kpiValue.toLocaleString();
} }
......
...@@ -30,12 +30,12 @@ ...@@ -30,12 +30,12 @@
</tr> </tr>
</thead> </thead>
<tbody> <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"> <td *ngFor="let cell of row" class="px-6 py-4">
{{ cell }} {{ cell }}
</td> </td>
</tr> </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"> <td [attr.colspan]="headers.length" class="px-6 py-4 text-center text-gray-400">
No data available. No data available.
</td> </td>
......
...@@ -12,28 +12,28 @@ import { BaseWidgetComponent } from '../base-widget.component'; ...@@ -12,28 +12,28 @@ import { BaseWidgetComponent } from '../base-widget.component';
}) })
export class MatrixWidgetComponent extends BaseWidgetComponent { export class MatrixWidgetComponent extends BaseWidgetComponent {
public headers: string[] = []; public headers: string[] = [];
public data: any[][] = []; public matrixData: any[][] = [];
constructor(protected override dashboardStateService: DashboardStateService) { constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService); super(dashboardStateService);
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Matrix'; this.title = this.configObj.title || 'Matrix';
this.headers = this.config.columns ? this.config.columns.map((col: any) => col.headerText) : []; this.headers = this.configObj.columns ? this.configObj.columns.map((col: any) => col.headerText) : [];
this.data = []; this.matrixData = [];
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (this.config?.columns && data?.length > 0) { if (this.configObj?.columns && data?.length > 0) {
this.data = data.map(row => this.config.columns.map((col: any) => row[col.field])); this.matrixData = data.map(row => this.configObj.columns.map((col: any) => row[col.field]));
} }
} }
onReset(): void { onReset(): void {
this.title = 'Matrix (Default)'; this.title = 'Matrix (Default)';
this.headers = ['Category', 'Q1', 'Q2', 'Q3', 'Q4']; this.headers = ['Category', 'Q1', 'Q2', 'Q3', 'Q4'];
this.data = [ this.matrixData = [
['Product A', 100, 120, 150, 130], ['Product A', 100, 120, 150, 130],
['Product B', 80, 90, 110, 100], ['Product B', 80, 90, 110, 100],
['Product C', 150, 130, 160, 140], ['Product C', 150, 130, 160, 140],
......
...@@ -18,15 +18,15 @@ export class MultiRowCardWidgetComponent extends BaseWidgetComponent { ...@@ -18,15 +18,15 @@ export class MultiRowCardWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Multi-Row Card'; this.title = this.configObj.title || 'Multi-Row Card';
this.cardData = []; this.cardData = [];
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
this.cardData = data.map(item => ({ this.cardData = data.map(item => ({
label: item[this.config.labelField], label: item[this.configObj.labelField],
value: item[this.config.valueField], value: item[this.configObj.valueField],
unit: item[this.config.unitField] || '' unit: item[this.configObj.unitField] || ''
})); }));
} }
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
<div class="notification-header"> <div class="notification-header">
<h4 class="notification-title">{{ notification.title }}</h4> <h4 class="notification-title">{{ notification.title }}</h4>
<div class="notification-time"> <div class="notification-time">
{{ notification.timestamp | date:'short' }} {{ isValidDate(notification.timestamp) ? (notification.timestamp | date:'short') : 'Invalid Date' }}
</div> </div>
</div> </div>
<p class="notification-message">{{ notification.message }}</p> <p class="notification-message">{{ notification.message }}</p>
......
...@@ -26,20 +26,20 @@ export class NotificationWidgetComponent extends BaseWidgetComponent { ...@@ -26,20 +26,20 @@ export class NotificationWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Notifications'; this.title = this.configObj?.title || 'Notifications';
this.toastSettings = { this.toastSettings = {
position: this.config.toastPosition || { X: 'Right', Y: 'Top' }, position: this.configObj?.toastPosition || { X: 'Right', Y: 'Top' },
showCloseButton: this.config.showCloseButton !== false, showCloseButton: this.configObj?.showCloseButton !== false,
showProgressBar: this.config.showProgressBar !== false, showProgressBar: this.configObj?.showProgressBar !== false,
timeOut: this.config.timeOut || 4000, timeOut: this.configObj?.timeOut || 4000,
newestOnTop: this.config.newestOnTop !== false, newestOnTop: this.configObj?.newestOnTop !== false,
cssClass: this.config.cssClass || '' cssClass: this.configObj?.cssClass || undefined
}; };
this.messageSettings = { this.messageSettings = {
severity: this.config.severity || 'Normal', severity: this.configObj?.severity || 'Normal',
variant: this.config.variant || 'Filled', variant: this.configObj?.variant || 'Filled',
showIcon: this.config.showIcon !== false, showIcon: this.configObj?.showIcon !== false,
showCloseIcon: this.config.showCloseIcon !== false showCloseIcon: this.configObj?.showCloseIcon !== false
}; };
this.notifications = []; this.notifications = [];
this.unreadCount = 0; this.unreadCount = 0;
...@@ -47,15 +47,28 @@ export class NotificationWidgetComponent extends BaseWidgetComponent { ...@@ -47,15 +47,28 @@ export class NotificationWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (data && data.length > 0) { if (data && data.length > 0) {
this.notifications = data.map(item => ({ this.notifications = data.map(item => {
id: item[this.config.idField || 'id'], const timestampValue = item[this.configObj.timestampField || 'timestamp'];
title: item[this.config.titleField || 'title'], let timestamp: Date;
message: item[this.config.messageField || 'message'],
type: item[this.config.typeField || 'type'] || 'info', if (timestampValue instanceof Date) {
timestamp: new Date(item[this.config.timestampField || 'timestamp']), timestamp = timestampValue;
isRead: item[this.config.isReadField || 'isRead'] || false, } else if (timestampValue && !isNaN(new Date(timestampValue).getTime())) {
priority: item[this.config.priorityField || 'priority'] || 'normal' timestamp = new Date(timestampValue);
})).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); } 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(); this.updateUnreadCount();
} }
...@@ -145,4 +158,8 @@ export class NotificationWidgetComponent extends BaseWidgetComponent { ...@@ -145,4 +158,8 @@ export class NotificationWidgetComponent extends BaseWidgetComponent {
getTypeClass(type: string): string { getTypeClass(type: string): string {
return `notification-${type}`; return `notification-${type}`;
} }
isValidDate(date: any): boolean {
return date && date.getTime && !isNaN(date.getTime());
}
} }
...@@ -20,7 +20,7 @@ export class PayrollSummaryWidgetComponent extends BaseWidgetComponent { ...@@ -20,7 +20,7 @@ export class PayrollSummaryWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Payroll Summary'; this.title = this.configObj.title || 'Payroll Summary';
this.totalPayroll = 0; this.totalPayroll = 0;
this.employeesPaid = 0; this.employeesPaid = 0;
} }
...@@ -28,8 +28,8 @@ export class PayrollSummaryWidgetComponent extends BaseWidgetComponent { ...@@ -28,8 +28,8 @@ export class PayrollSummaryWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (data.length > 0) { if (data.length > 0) {
const firstItem = data[0]; const firstItem = data[0];
this.totalPayroll = firstItem[this.config.totalPayrollField] || 0; this.totalPayroll = firstItem[this.configObj.totalPayrollField] || 0;
this.employeesPaid = firstItem[this.config.employeesPaidField] || 0; this.employeesPaid = firstItem[this.configObj.employeesPaidField] || 0;
} }
} }
......
...@@ -20,7 +20,7 @@ export class PieChartWidgetComponent extends BaseWidgetComponent { ...@@ -20,7 +20,7 @@ export class PieChartWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Pie Chart'; this.title = this.configObj.title || 'Pie Chart';
this.legendSettings = { visible: true }; this.legendSettings = { visible: true };
this.chartData = []; this.chartData = [];
} }
...@@ -28,15 +28,15 @@ export class PieChartWidgetComponent extends BaseWidgetComponent { ...@@ -28,15 +28,15 @@ export class PieChartWidgetComponent extends BaseWidgetComponent {
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
let transformedData = data; let transformedData = data;
if (this.config.aggregation === 'count') { if (this.configObj.aggregation === 'count') {
const counts = transformedData.reduce((acc, item) => { const counts = transformedData.reduce((acc, item) => {
const key = item[this.config.xField]; const key = item[this.configObj.xField];
acc[key] = (acc[key] || 0) + 1; acc[key] = (acc[key] || 0) + 1;
return acc; return acc;
}, {}); }, {});
transformedData = Object.keys(counts).map(key => ({ x: key, y: counts[key] })); transformedData = Object.keys(counts).map(key => ({ x: key, y: counts[key] }));
} else { } 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; this.chartData = transformedData;
} }
......
...@@ -17,16 +17,16 @@ export class QuickLinksWidgetComponent extends BaseWidgetComponent { ...@@ -17,16 +17,16 @@ export class QuickLinksWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Quick Links'; this.title = this.configObj.title || 'Quick Links';
this.quickLinks = this.config.links || []; // Use links from config if available this.quickLinks = this.configObj.links || []; // Use links from config if available
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (this.config.nameField && this.config.urlField) { if (this.configObj.nameField && this.configObj.urlField) {
this.quickLinks = data.map(item => ({ this.quickLinks = data.map(item => ({
name: item[this.config.nameField], name: item[this.configObj.nameField],
url: item[this.config.urlField], url: item[this.configObj.urlField],
icon: this.config.iconField ? item[this.config.iconField] : 'link-45deg' icon: this.configObj.iconField ? item[this.configObj.iconField] : 'link-45deg'
})); }));
} }
} }
......
...@@ -12,6 +12,6 @@ ...@@ -12,6 +12,6 @@
<!-- Chart --> <!-- Chart -->
<ejs-chart *ngIf="!isLoading && !hasError" [title]="title" [primaryXAxis]="primaryXAxis" [primaryYAxis]="primaryYAxis"> <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]="configObj?.sizeField" name="Data"></e-series>
</e-series-collection> </e-series-collection>
</ejs-chart> </ejs-chart>
...@@ -23,24 +23,24 @@ export class ScatterBubbleChartWidgetComponent extends BaseWidgetComponent { ...@@ -23,24 +23,24 @@ export class ScatterBubbleChartWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Scatter Chart'; this.title = this.configObj.title || 'Scatter Chart';
this.type = this.config.type || 'Scatter'; this.type = this.configObj.type || 'Scatter';
this.primaryXAxis = { title: this.config.xAxisTitle || '' }; this.primaryXAxis = { title: this.configObj.xAxisTitle || '' };
this.primaryYAxis = { title: this.config.yAxisTitle || '' }; this.primaryYAxis = { title: this.configObj.yAxisTitle || '' };
this.chartData = []; this.chartData = [];
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (this.type === 'Bubble') { 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 { } 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 { onReset(): void {
this.title = 'Scatter Chart (Default)'; this.title = 'Scatter Chart (Default)';
this.type = this.config?.type || 'Scatter'; this.type = this.configObj?.type || 'Scatter';
if (this.type === 'Bubble') { 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 },
......
...@@ -8,7 +8,9 @@ ...@@ -8,7 +8,9 @@
<i *ngIf="icon" [style.color]="iconColor" [class]="'bi ' + icon + ' text-3xl'"></i> <i *ngIf="icon" [style.color]="iconColor" [class]="'bi ' + icon + ' text-3xl'"></i>
<h4 class="text-lg font-semibold truncate">{{ title }}</h4> <h4 class="text-lg font-semibold truncate">{{ title }}</h4>
</div> </div>
<!-- Removed trendValue display --> <div *ngIf="configObj?.trendValue" class="text-sm font-medium">
{{ configObj.trendValue }}
</div>
</div> </div>
</div> </div>
......
...@@ -24,27 +24,30 @@ export class SimpleKpiWidgetComponent extends BaseWidgetComponent { ...@@ -24,27 +24,30 @@ export class SimpleKpiWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'KPI'; this.title = this.configObj.title || 'KPI';
this.unit = this.config.unit || ''; this.unit = this.configObj.unit || '';
this.icon = this.config.icon || 'info'; this.icon = this.configObj.icon || 'info';
this.backgroundColor = this.config.backgroundColor || 'linear-gradient(to top right, #3366FF, #00CCFF)';
this.iconColor = this.config.iconColor || '#FFFFFF'; // Handle color property (fallback to backgroundColor)
this.borderColor = this.config.borderColor || '#FFFFFF'; 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 this.value = '-'; // Initial state before data loads
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
console.log('SimpleKpiWidget onDataUpdate config:', this.config);
// Handle count aggregation separately as it doesn't need a valueField // 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(); this.value = (data?.length || 0).toLocaleString();
return; return;
} }
// For other aggregations, valueField is required // For other aggregations, valueField is required
if (!this.config.valueField) { if (!this.configObj.valueField) {
this.value = 'N/A'; // Indicate a configuration error 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; return;
} }
...@@ -55,11 +58,11 @@ export class SimpleKpiWidgetComponent extends BaseWidgetComponent { ...@@ -55,11 +58,11 @@ export class SimpleKpiWidgetComponent extends BaseWidgetComponent {
} }
let kpiValue = 0; let kpiValue = 0;
if (this.config.aggregation === 'sum') { if (this.configObj.aggregation === 'sum') {
kpiValue = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0); kpiValue = data.reduce((sum, item) => sum + (item[this.configObj.valueField] || 0), 0);
} else { } else {
// Default to first value if no aggregation is specified // 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(); this.value = kpiValue.toLocaleString();
} }
......
...@@ -30,12 +30,12 @@ ...@@ -30,12 +30,12 @@
</tr> </tr>
</thead> </thead>
<tbody> <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"> <td *ngFor="let cell of row" class="px-6 py-4">
{{ cell }} {{ cell }}
</td> </td>
</tr> </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"> <td [attr.colspan]="headers.length" class="px-6 py-4 text-center text-gray-400">
No data available. No data available.
</td> </td>
......
...@@ -13,28 +13,28 @@ import { BaseWidgetComponent } from '../base-widget.component'; ...@@ -13,28 +13,28 @@ import { BaseWidgetComponent } from '../base-widget.component';
}) })
export class SimpleTableWidgetComponent extends BaseWidgetComponent { export class SimpleTableWidgetComponent extends BaseWidgetComponent {
public headers: string[] = []; public headers: string[] = [];
public data: any[][] = []; public tableData: any[][] = [];
constructor(protected override dashboardStateService: DashboardStateService) { constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService); super(dashboardStateService);
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Table'; this.title = this.configObj.title || 'Table';
this.headers = this.config.columns ? this.config.columns.map((col: any) => col.headerText) : []; this.headers = this.configObj.columns ? this.configObj.columns.map((col: any) => col.headerText) : [];
this.data = []; this.tableData = [];
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (data && data.length > 0 && this.config?.columns) { if (data && data.length > 0 && this.configObj?.columns) {
this.data = data.map(row => this.config.columns.map((col: any) => row[col.field])); this.tableData = data.map(row => this.configObj.columns.map((col: any) => row[col.field]));
} }
} }
onReset(): void { onReset(): void {
this.title = 'Table (Default)'; this.title = 'Table (Default)';
this.headers = ['ID', 'Name', 'Status']; this.headers = ['ID', 'Name', 'Status'];
this.data = [ this.tableData = [
[1, 'Item A', 'Active'], [1, 'Item A', 'Active'],
[2, 'Item B', 'Inactive'], [2, 'Item B', 'Inactive'],
[3, 'Item C', 'Active'], [3, 'Item C', 'Active'],
......
...@@ -21,14 +21,14 @@ export class SlicerWidgetComponent extends BaseWidgetComponent { ...@@ -21,14 +21,14 @@ export class SlicerWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Slicer'; this.title = this.configObj.title || 'Slicer';
this.options = ['All']; this.options = ['All'];
this.selectedValue = 'All'; this.selectedValue = 'All';
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (this.config.optionsField) { if (this.configObj.optionsField) {
const uniqueOptions = [...new Set(data.map((item: any) => item[this.config.optionsField]))]; const uniqueOptions = [...new Set(data.map((item: any) => item[this.configObj.optionsField]))];
this.options = ['All', ...uniqueOptions.map(String)]; this.options = ['All', ...uniqueOptions.map(String)];
this.selectedValue = this.options[0]; this.selectedValue = this.options[0];
} }
......
...@@ -23,15 +23,15 @@ export class SyncfusionChartWidgetComponent extends BaseWidgetComponent { ...@@ -23,15 +23,15 @@ export class SyncfusionChartWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Syncfusion Chart'; this.title = this.configObj.title || 'Syncfusion Chart';
this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' }; this.primaryXAxis = { valueType: 'Category', title: this.configObj.xAxisTitle || '' };
this.primaryYAxis = { title: this.config.yAxisTitle || '' }; this.primaryYAxis = { title: this.configObj.yAxisTitle || '' };
this.chartData = new DataManager([]); this.chartData = new DataManager([]);
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
const dm = new DataManager(data); 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 { onReset(): void {
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<!-- Grid --> <!-- Grid -->
<ejs-grid #grid *ngIf="!isLoading && !hasError" <ejs-grid #grid *ngIf="!isLoading && !hasError"
[dataSource]="data" [dataSource]="gridData"
[allowPaging]="true" [allowPaging]="true"
[pageSettings]="pageSettings" [pageSettings]="pageSettings"
[allowSorting]="true" [allowSorting]="true"
...@@ -98,4 +98,4 @@ ...@@ -98,4 +98,4 @@
</ejs-grid> </ejs-grid>
</div> </div>
</div> </div>
\ No newline at end of file
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 { 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 { 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'; import { MenuEventArgs } from '@syncfusion/ej2-navigations';
...@@ -62,7 +62,7 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple ...@@ -62,7 +62,7 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple
public widgetId: string; // Added widgetId property public widgetId: string; // Added widgetId property
private isPerspectiveApplied = false; private isPerspectiveApplied = false;
public data: DataManager = new DataManager([]); @Input() public gridData: DataManager = new DataManager([]);
public columns: any[] = []; public columns: any[] = [];
public pageSettings: Object = { pageSize: 10 }; public pageSettings: Object = { pageSize: 10 };
public toolbar: ToolbarItems[]; // Make it configurable public toolbar: ToolbarItems[]; // Make it configurable
...@@ -92,16 +92,16 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple ...@@ -92,16 +92,16 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple
override ngOnInit(): void { // Added override override ngOnInit(): void { // Added override
super.ngOnInit(); // Call parent's ngOnInit super.ngOnInit(); // Call parent's ngOnInit
if (this.config && this.config.widgetId) { if (this.config && this.configObj.widgetId) {
this.widgetId = this.config.widgetId; // Initialize widgetId this.widgetId = this.configObj.widgetId; // Initialize widgetId
this.widgetStateService.registerWidget(this.widgetId, this); this.widgetStateService.registerWidget(this.widgetId, this);
} }
} }
override ngOnDestroy(): void { // Added override override ngOnDestroy(): void { // Added override
super.ngOnDestroy(); // Call parent's ngOnDestroy super.ngOnDestroy(); // Call parent's ngOnDestroy
if (this.config && this.config.widgetId) { if (this.config && this.configObj.widgetId) {
this.widgetStateService.unregisterWidget(this.config.widgetId); this.widgetStateService.unregisterWidget(this.configObj.widgetId);
} }
} }
...@@ -110,19 +110,19 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple ...@@ -110,19 +110,19 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Data Grid'; this.title = this.configObj.title || 'Data Grid';
this.columns = this.config.columns || []; this.columns = this.configObj.columns || [];
this.data = new DataManager([]); this.gridData = new DataManager([]);
this.pageSettings = this.config.pageSettings || { pageSize: 10 }; this.pageSettings = this.configObj.pageSettings || { pageSize: 10 };
this.toolbar = this.config.toolbar || ['Search', 'ExcelExport', 'PdfExport', 'CsvExport']; this.toolbar = this.configObj.toolbar || ['Search', 'ExcelExport', 'PdfExport', 'CsvExport'];
this.searchSettings = this.config.searchSettings || { fields: [], operator: 'contains', ignoreCase: true }; this.searchSettings = this.configObj.searchSettings || { fields: [], operator: 'contains', ignoreCase: true };
this.groupSettings = this.config.groupSettings || { allowReordering: true, showGroupedColumn: true, showDropArea: false }; this.groupSettings = this.configObj.groupSettings || { allowReordering: true, showGroupedColumn: true, showDropArea: false };
this.filterSettings = this.config.filterSettings || { type: 'Excel' }; this.filterSettings = this.configObj.filterSettings || { type: 'Excel' };
this.editSettings = this.config.editSettings || { allowEditing: true, mode: 'Batch' }; this.editSettings = this.configObj.editSettings || { allowEditing: true, mode: 'Batch' };
this.selectionOptions = this.config.selectionOptions || { checkboxOnly: true }; this.selectionOptions = this.configObj.selectionOptions || { checkboxOnly: true };
this.loadingIndicator = this.config.loadingIndicator || { indicatorType: 'Shimmer' }; this.loadingIndicator = this.configObj.loadingIndicator || { indicatorType: 'Shimmer' };
this.query = this.config.query || new Query().addParams('dataCount', '1000'); this.query = this.configObj.query || new Query().addParams('dataCount', '1000');
this.columnMenuItems = this.config.columnMenuItems || [ this.columnMenuItems = this.configObj.columnMenuItems || [
'AutoFit', 'AutoFitAll', 'SortAscending', 'SortDescending', 'AutoFit', 'AutoFitAll', 'SortAscending', 'SortDescending',
'Group', 'Ungroup', 'ColumnChooser', 'Filter', 'Group', 'Ungroup', 'ColumnChooser', 'Filter',
{ text: 'Sum', id: 'aggregate_sum' }, { text: 'Sum', id: 'aggregate_sum' },
...@@ -131,14 +131,14 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple ...@@ -131,14 +131,14 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple
{ text: 'Min', id: 'aggregate_min' }, { text: 'Min', id: 'aggregate_min' },
{ text: 'Max', id: 'aggregate_max' } { text: 'Max', id: 'aggregate_max' }
]; ];
this.aggregatesSum = this.config.aggregatesSum || []; this.aggregatesSum = this.configObj.aggregatesSum || [];
this.aggregatesCount = this.config.aggregatesCount || []; this.aggregatesCount = this.configObj.aggregatesCount || [];
this.aggregatesAvg = this.config.aggregatesAvg || []; this.aggregatesAvg = this.configObj.aggregatesAvg || [];
this.aggregatesMin = this.config.aggregatesMin || []; this.aggregatesMin = this.configObj.aggregatesMin || [];
this.aggregatesMax = this.config.aggregatesMax || []; this.aggregatesMax = this.configObj.aggregatesMax || [];
this.allowReordering = this.config.allowReordering !== undefined ? this.config.allowReordering : true; this.allowReordering = this.configObj.allowReordering !== undefined ? this.configObj.allowReordering : true;
this.showColumnMenu = this.config.showColumnMenu !== undefined ? this.config.showColumnMenu : true; this.showColumnMenu = this.configObj.showColumnMenu !== undefined ? this.configObj.showColumnMenu : true;
this.allowMultiSorting = this.config.allowMultiSorting !== undefined ? this.config.allowMultiSorting : true; this.allowMultiSorting = this.configObj.allowMultiSorting !== undefined ? this.configObj.allowMultiSorting : true;
} }
/** /**
...@@ -170,9 +170,9 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple ...@@ -170,9 +170,9 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
this.data = new DataManager(data); this.gridData = new DataManager(data);
if (this.config.columns && this.config.columns.length > 0) { if (this.configObj.columns && this.configObj.columns.length > 0) {
this.columns = this.config.columns; this.columns = this.configObj.columns;
} else if (data.length > 0) { } else if (data.length > 0) {
this.columns = Object.keys(data[0]).map(key => ({ this.columns = Object.keys(data[0]).map(key => ({
field: key, field: key,
...@@ -193,7 +193,7 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple ...@@ -193,7 +193,7 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple
onReset(): void { onReset(): void {
this.title = 'Data Grid (Default)'; this.title = 'Data Grid (Default)';
this.data = new DataManager([]); this.gridData = new DataManager([]);
this.columns = [{ field: 'Message', headerText: 'Please select a dataset' }]; this.columns = [{ field: 'Message', headerText: 'Please select a dataset' }];
this.pageSettings = { pageSize: 10 }; this.pageSettings = { pageSize: 10 };
this.toolbar = ['Search', 'ExcelExport', 'PdfExport', 'CsvExport']; this.toolbar = ['Search', 'ExcelExport', 'PdfExport', 'CsvExport'];
......
...@@ -50,12 +50,12 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen ...@@ -50,12 +50,12 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen
override ngOnInit(): void { override ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
if (this.config && this.config.widgetId) { if (this.config && this.configObj.widgetId) {
this.widgetId = this.config.widgetId; this.widgetId = this.configObj.widgetId;
this.widgetStateService.registerWidget(this.widgetId, this); this.widgetStateService.registerWidget(this.widgetId, this);
} }
this.toolbar = this.config.showToolbar !== false ? ['Grid', 'Chart', 'Export', 'SubTotal', 'GrandTotal', 'ConditionalFormatting', 'NumberFormatting', 'FieldList'] : []; this.toolbar = this.configObj.showToolbar !== false ? ['Grid', 'Chart', 'Export', 'SubTotal', 'GrandTotal', 'ConditionalFormatting', 'NumberFormatting', 'FieldList'] : [];
this.displayOption = { view: this.config.displayOptionView || 'Both' } as DisplayOption; this.displayOption = { view: this.configObj.displayOptionView || 'Both' } as DisplayOption;
} }
override ngOnDestroy(): void { override ngOnDestroy(): void {
...@@ -70,17 +70,17 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen ...@@ -70,17 +70,17 @@ export class SyncfusionPivotWidgetComponent extends BaseWidgetComponent implemen
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Pivot Table'; this.title = this.configObj.title || 'Pivot Table';
this.chartSettings = this.config.chartSettings || { this.chartSettings = this.configObj.chartSettings || {
chartSeries: { type: 'Column' } chartSeries: { type: 'Column' }
}; };
this.dataSourceSettings = { this.dataSourceSettings = {
dataSource: new DataManager([]), dataSource: new DataManager([]),
expandAll: this.config.expandAll || false, expandAll: this.configObj.expandAll || false,
rows: this.config.rows || [], rows: this.configObj.rows || [],
columns: this.config.columns || [], columns: this.configObj.columns || [],
values: this.config.values || [], values: this.configObj.values || [],
filters: this.config.filters || [], filters: this.configObj.filters || [],
chartSettings: this.chartSettings, chartSettings: this.chartSettings,
}; };
} }
......
...@@ -21,10 +21,10 @@ export class TreemapWidgetComponent extends BaseWidgetComponent { ...@@ -21,10 +21,10 @@ export class TreemapWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Treemap'; this.title = this.configObj.title || 'Treemap';
this.weightValuePath = this.config.valueField; this.weightValuePath = this.configObj.valueField;
this.leafItemSettings = { this.leafItemSettings = {
labelPath: this.config.groupField, labelPath: this.configObj.groupField,
showLabels: true showLabels: true
}; };
this.dataSource = []; this.dataSource = [];
......
...@@ -21,14 +21,14 @@ export class WaterfallChartWidgetComponent extends BaseWidgetComponent { ...@@ -21,14 +21,14 @@ export class WaterfallChartWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Waterfall Chart'; this.title = this.configObj.title || 'Waterfall Chart';
this.primaryXAxis = { valueType: 'Category', title: this.config.xAxisTitle || '' }; this.primaryXAxis = { valueType: 'Category', title: this.configObj.xAxisTitle || '' };
this.primaryYAxis = { title: this.config.yAxisTitle || '' }; this.primaryYAxis = { title: this.configObj.yAxisTitle || '' };
this.chartData = []; this.chartData = [];
} }
onDataUpdate(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.configObj.xField], y: item[this.configObj.yField] }));
} }
onReset(): void { onReset(): void {
......
...@@ -23,9 +23,9 @@ export class WeatherWidgetComponent extends BaseWidgetComponent { ...@@ -23,9 +23,9 @@ export class WeatherWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Weather'; this.title = this.configObj?.title || 'Weather';
this.location = this.config.location || 'Bangkok, Thailand'; this.location = this.configObj?.location || 'Bangkok, Thailand';
this.refreshInterval = this.config.refreshInterval || 300000; this.refreshInterval = this.configObj?.refreshInterval || 300000;
this.weatherData = {}; this.weatherData = {};
this.currentWeather = {}; this.currentWeather = {};
this.forecast = []; this.forecast = [];
...@@ -37,23 +37,23 @@ export class WeatherWidgetComponent extends BaseWidgetComponent { ...@@ -37,23 +37,23 @@ export class WeatherWidgetComponent extends BaseWidgetComponent {
const weatherItem = data[0]; const weatherItem = data[0];
this.currentWeather = { this.currentWeather = {
temperature: weatherItem[this.config.temperatureField || 'temperature'], temperature: weatherItem[this.configObj.temperatureField || 'temperature'],
humidity: weatherItem[this.config.humidityField || 'humidity'], humidity: weatherItem[this.configObj.humidityField || 'humidity'],
windSpeed: weatherItem[this.config.windSpeedField || 'windSpeed'], windSpeed: weatherItem[this.configObj.windSpeedField || 'windSpeed'],
pressure: weatherItem[this.config.pressureField || 'pressure'], pressure: weatherItem[this.configObj.pressureField || 'pressure'],
description: weatherItem[this.config.descriptionField || 'description'], description: weatherItem[this.configObj.descriptionField || 'description'],
icon: weatherItem[this.config.iconField || 'icon'], icon: weatherItem[this.configObj.iconField || 'icon'],
feelsLike: weatherItem[this.config.feelsLikeField || 'feelsLike'] feelsLike: weatherItem[this.configObj.feelsLikeField || 'feelsLike']
}; };
// Process forecast data if available // Process forecast data if available
if (data.length > 1) { if (data.length > 1) {
this.forecast = data.slice(1).map(item => ({ this.forecast = data.slice(1).map(item => ({
day: item[this.config.dayField || 'day'], day: item[this.configObj.dayField || 'day'],
high: item[this.config.highField || 'high'], high: item[this.configObj.highField || 'high'],
low: item[this.config.lowField || 'low'], low: item[this.configObj.lowField || 'low'],
description: item[this.config.forecastDescriptionField || 'description'], description: item[this.configObj.forecastDescriptionField || 'description'],
icon: item[this.config.forecastIconField || 'icon'] icon: item[this.configObj.forecastIconField || 'icon']
})); }));
} }
......
...@@ -17,24 +17,24 @@ export class WelcomeWidgetComponent extends BaseWidgetComponent { ...@@ -17,24 +17,24 @@ export class WelcomeWidgetComponent extends BaseWidgetComponent {
} }
applyInitialConfig(): void { applyInitialConfig(): void {
this.title = this.config.title || 'Welcome'; this.title = this.configObj.title || 'Welcome';
if (this.config.messageType === 'static') { if (this.configObj.messageType === 'static') {
this.welcomeMessage = this.config.staticMessage || 'Welcome!'; this.welcomeMessage = this.configObj.staticMessage || 'Welcome!';
} else { } else {
this.welcomeMessage = '...'; // Placeholder while waiting for data this.welcomeMessage = '...'; // Placeholder while waiting for data
} }
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (this.config.messageType === 'dynamic') { if (this.configObj.messageType === 'dynamic') {
if (data.length > 0 && this.config.messageField) { if (data.length > 0 && this.configObj.messageField) {
this.welcomeMessage = data[0][this.config.messageField]; this.welcomeMessage = data[0][this.configObj.messageField];
} else { } else {
this.welcomeMessage = 'No message data'; this.welcomeMessage = 'No message data';
} }
} else { } else {
// For static messages, the message is already set in applyInitialConfig // 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 = [ ...@@ -109,11 +109,6 @@ export const portalManageRoutes: Routes = [
component: HomeComponent, // Assuming HomeComponent is a generic dashboard or a placeholder component: HomeComponent, // Assuming HomeComponent is a generic dashboard or a placeholder
canActivate: [moduleAccessGuard] canActivate: [moduleAccessGuard]
}, },
{
path: 'dashboard-viewer',
loadComponent: () => import('./dashboard-viewer/dashboard-viewer.component').then(m => m.DashboardViewerComponent),
canActivate: [moduleAccessGuard]
},
// === Generic App Routes === // === Generic App Routes ===
// These routes are for simple apps that don't need special module-level services. // 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