Commit 9e7c3447 by Ooh-Ao

คลังวิดเจ็ท

parent 71f98397
......@@ -51,20 +51,20 @@
<!-- Widget List -->
<div class="widget-list space-y-2">
<div
*ngFor="let widget of filteredAvailableWidgets"
(click)="addWidgetToDashboard(widget)"
*ngFor="let menuItem of filteredAvailableWidgets"
(click)="addWidgetToDashboard(menuItem)"
class="widget-item p-3 rounded-lg hover:bg-gray-100 cursor-pointer transition-colors duration-200"
>
<p class="font-semibold text-gray-700">{{ widget.thName }}</p>
<p class="font-semibold text-gray-700">{{ menuItem.widget.thName }}</p>
<p class="text-xs text-gray-500">
Size: {{ widget.cols }}x{{ widget.rows }}
Size: {{ menuItem.widget.cols }}x{{ menuItem.widget.rows }}
</p>
</div>
<p
*ngIf="filteredAvailableWidgets.length === 0"
class="text-gray-500 text-center mt-4"
>
No widgets found.
No widgets found for this dataset.
</p>
</div>
</div>
......
import { Component, OnInit, ViewChild, Type, ChangeDetectionStrategy } from '@angular/core';
import { Component, OnInit, ViewChild, Type } from '@angular/core';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { CommonModule, TitleCasePipe } from '@angular/common';
import { NgComponentOutlet } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Observable, of, forkJoin, throwError } from 'rxjs';
import { map, switchMap, tap, catchError, take } from 'rxjs/operators';
import { DashboardLayoutComponent, DashboardLayoutModule, PanelModel } from '@syncfusion/ej2-angular-layouts'; // Import Syncfusion modules
import { MatDialog, MatDialogModule } from '@angular/material/dialog'; // Import MatDialog
import { throwError } from 'rxjs';
import { map, switchMap, catchError, take } from 'rxjs/operators';
import { DashboardLayoutComponent, DashboardLayoutModule, PanelModel } from '@syncfusion/ej2-angular-layouts';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { NotificationService } from '../../shared/services/notification.service';
import { DashboardModel, WidgetModel, DatasetModel } from '../models/widgets.model';
import { MenuItemsWidget } from '../models/m-menuitems-widget.model';
import { DashboardDataService } from '../services/dashboard-data.service';
import { WidgetDataService } from '../services/widget-data.service'; // Import new service
import { WidgetService } from '../services/widgets.service'; // Import WidgetService
import { DashboardStateService, SelectedDataset } from '../services/dashboard-state.service'; // Import SelectedDataset
import { DatasetService } from '../services/dataset.service';
import { WidgetConfigGeneratorService } from '../services/widget-config-generator.service';
import { MMenuitemsWidgetService } from '../services/m-menuitems-widget.service';
import { DashboardStateService, SelectedDataset } from '../services/dashboard-state.service';
import { WidgetConfigComponent } from './widget-config/widget-config.component'; // Import WidgetConfigComponent
import { WidgetConfigComponent } from './widget-config/widget-config.component';
// Import all the widget components
import { CompanyInfoWidgetComponent } from '../widgets/company-info-widget.component';
import { CompanyInfoSubfolderWidgetComponent } from '../widgets/company-info-widget/company-info-widget.component'; // New import
import { HeadcountWidgetComponent } from '../widgets/headcount-widget.component';
import { AttendanceOverviewWidgetComponent } from '../widgets/attendance-overview-widget.component';
import { PayrollSummaryWidgetComponent } from '../widgets/payroll-summary-widget.component';
import { EmployeeDirectoryWidgetComponent } from '../widgets/employee-directory-widget.component';
import { KpiWidgetComponent } from '../widgets/kpi-widget/kpi-widget.component';
import { WelcomeWidgetComponent } from '../widgets/welcome-widget/welcome-widget.component';
import { ChartWidgetComponent } from '../widgets/chart-widget/chart-widget.component';
import { QuickLinksWidgetComponent } from '../widgets/quick-links-widget/quick-links-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 { SyncfusionChartWidgetComponent } from '../widgets/syncfusion-chart-widget/syncfusion-chart-widget.component';
import { DatasetPickerComponent } from './dataset-picker.component';
import { DataTableWidgetComponent } from '../widgets/dynamic-widgets/data-table-widget.component'; // Import new widget
import { AccumulationChartAllModule, ChartAllModule } from '@syncfusion/ej2-angular-charts';
import { HttpClientModule } from '@angular/common/http';
import { AreaChartWidgetComponent } from '../widgets/area-chart-widget/area-chart-widget.component';
import { BarChartWidgetComponent } from '../widgets/bar-chart-widget/bar-chart-widget.component';
import { PieChartWidgetComponent } from '../widgets/pie-chart-widget/pie-chart-widget.component';
import { ScatterBubbleChartWidgetComponent } from '../widgets/scatter-bubble-chart-widget/scatter-bubble-chart-widget.component';
import { MultiRowCardWidgetComponent } from '../widgets/multi-row-card-widget/multi-row-card-widget.component';
import { ComboChartWidgetComponent } from '../widgets/combo-chart-widget/combo-chart-widget.component';
import { DoughnutChartWidgetComponent } from '../widgets/doughnut-chart-widget/doughnut-chart-widget.component';
import { FunnelChartWidgetComponent } from '../widgets/funnel-chart-widget/funnel-chart-widget.component';
import { GaugeChartWidgetComponent } from '../widgets/gauge-chart-widget/gauge-chart-widget.component';
import { SimpleKpiWidgetComponent } from '../widgets/simple-kpi-widget/simple-kpi-widget.component';
import { FilledMapWidgetComponent } from '../widgets/filled-map-widget/filled-map-widget.component';
import { MatrixWidgetComponent } from '../widgets/matrix-widget/matrix-widget.component';
import { SlicerWidgetComponent } from '../widgets/slicer-widget/slicer-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 { TreemapWidgetComponent } from '../widgets/treemap-widget/treemap-widget.component';
import { ChartAllModule, AccumulationChartAllModule } from '@syncfusion/ej2-angular-charts';
import { HttpClientModule } from '@angular/common/http';
export interface DashboardPanel extends PanelModel {
componentType: Type<any>;
......@@ -75,27 +53,17 @@ export interface DashboardPanel extends PanelModel {
SyncfusionPivotWidgetComponent,
SyncfusionChartWidgetComponent,
DatasetPickerComponent,
DataTableWidgetComponent, // Add new widget to imports
AreaChartWidgetComponent,
BarChartWidgetComponent,
PieChartWidgetComponent,
ScatterBubbleChartWidgetComponent,
MultiRowCardWidgetComponent,
ComboChartWidgetComponent,
DoughnutChartWidgetComponent,
FunnelChartWidgetComponent,
GaugeChartWidgetComponent,
WelcomeWidgetComponent,
QuickLinksWidgetComponent,
HeadcountWidgetComponent,
AttendanceOverviewWidgetComponent,
PayrollSummaryWidgetComponent,
EmployeeDirectoryWidgetComponent,
SimpleKpiWidgetComponent,
FilledMapWidgetComponent,
MatrixWidgetComponent,
SlicerWidgetComponent,
SimpleTableWidgetComponent,
WaterfallChartWidgetComponent,
TreemapWidgetComponent,
DashboardLayoutModule,
ChartAllModule,
AccumulationChartAllModule, // Add Syncfusion DashboardLayoutModule
MatDialogModule // Add MatDialogModule
AccumulationChartAllModule,
MatDialogModule
],
templateUrl: './dashboard-management.component.html',
styleUrls: ['./dashboard-management.component.scss'],
......@@ -104,59 +72,35 @@ export class DashboardManagementComponent implements OnInit {
@ViewChild('editLayout') public layout!: DashboardLayoutComponent;
public panels: DashboardPanel[] = [];
public cellSpacing: number[] = [10, 10];
public mediaQuery: string = 'max-width: 700px';
public availableWidgets: WidgetModel[] = [];
public filteredAvailableWidgets: WidgetModel[] = [];
public availableWidgets: MenuItemsWidget[] = [];
public filteredAvailableWidgets: MenuItemsWidget[] = [];
public widgetSearchTerm: string = '';
public dashboardData: DashboardModel | null = null;
public userDashboards: DashboardModel[] = [];
public selectedDashboardId: DashboardModel
public selectedDashboardId: DashboardModel | null = null;
private widgetComponentMap: { [key: string]: Type<any> } = {
'CompanyInfoWidgetComponent': CompanyInfoWidgetComponent,
'HeadcountWidgetComponent': HeadcountWidgetComponent,
'AttendanceOverviewWidgetComponent': AttendanceOverviewWidgetComponent,
'PayrollSummaryWidgetComponent': PayrollSummaryWidgetComponent,
'EmployeeDirectoryWidgetComponent': EmployeeDirectoryWidgetComponent,
'KpiWidgetComponent': KpiWidgetComponent,
'WelcomeWidgetComponent': WelcomeWidgetComponent,
'ChartWidgetComponent': ChartWidgetComponent,
'QuickLinksWidgetComponent': QuickLinksWidgetComponent,
'SyncfusionDatagridWidgetComponent': SyncfusionDatagridWidgetComponent,
'SyncfusionPivotWidgetComponent': SyncfusionPivotWidgetComponent,
'SyncfusionChartWidgetComponent': SyncfusionChartWidgetComponent,
'AreaChartWidgetComponent': AreaChartWidgetComponent,
'BarChartWidgetComponent': BarChartWidgetComponent,
'PieChartWidgetComponent': PieChartWidgetComponent,
'ScatterBubbleChartWidgetComponent': ScatterBubbleChartWidgetComponent,
'MultiRowCardWidgetComponent': MultiRowCardWidgetComponent,
'ComboChartWidgetComponent': ComboChartWidgetComponent,
'DoughnutChartWidgetComponent': DoughnutChartWidgetComponent,
'FunnelChartWidgetComponent': FunnelChartWidgetComponent,
'GaugeChartWidgetComponent': GaugeChartWidgetComponent,
'SimpleKpiWidgetComponent': SimpleKpiWidgetComponent,
'FilledMapWidgetComponent': FilledMapWidgetComponent,
'MatrixWidgetComponent': MatrixWidgetComponent,
'SlicerWidgetComponent': SlicerWidgetComponent,
'SimpleTableWidgetComponent': SimpleTableWidgetComponent,
'WaterfallChartWidgetComponent': WaterfallChartWidgetComponent,
'TreemapWidgetComponent': TreemapWidgetComponent,
'NewDataTableWidget': DataTableWidgetComponent, // Add new widget to map
'CompanyInfoSubfolderWidgetComponent': CompanyInfoSubfolderWidgetComponent // Add new widget to map
CompanyInfoWidgetComponent,
HeadcountWidgetComponent,
AttendanceOverviewWidgetComponent,
PayrollSummaryWidgetComponent,
EmployeeDirectoryWidgetComponent,
WelcomeWidgetComponent,
QuickLinksWidgetComponent,
SyncfusionDatagridWidgetComponent,
SyncfusionPivotWidgetComponent,
SyncfusionChartWidgetComponent,
SimpleKpiWidgetComponent,
};
constructor(
private route: ActivatedRoute,
private dashboardDataService: DashboardDataService,
private widgetDataService: WidgetDataService, // Inject new service
private widgetService: WidgetService, // Inject WidgetService
private mMenuitemsWidgetService: MMenuitemsWidgetService,
private dashboardStateService: DashboardStateService,
private datasetService: DatasetService, // Inject DatasetService
private dialog: MatDialog,
private notificationService: NotificationService,
private widgetConfigGenerator: WidgetConfigGeneratorService
private notificationService: NotificationService
) { }
ngOnInit(): void {
......@@ -168,27 +112,14 @@ export class DashboardManagementComponent implements OnInit {
).subscribe(dashboards => {
this.userDashboards = dashboards;
});
this.widgetService.getListWidgets().pipe(
catchError(error => {
this.notificationService.error('Error', 'Failed to load available widgets.');
return throwError(() => error);
})
).subscribe(widgets => {
this.availableWidgets = [...widgets].map(widget => ({
...widget,
config: widget.config || {}
}));
this.filterWidgets();
});
}
filterWidgets(): void {
if (!this.widgetSearchTerm) {
this.filteredAvailableWidgets = [...this.availableWidgets];
} else {
this.filteredAvailableWidgets = this.availableWidgets.filter(widget =>
widget.thName.toLowerCase().includes(this.widgetSearchTerm.toLowerCase())
this.filteredAvailableWidgets = this.availableWidgets.filter(menuItem =>
menuItem.widget.thName.toLowerCase().includes(this.widgetSearchTerm.toLowerCase())
);
}
}
......@@ -208,29 +139,28 @@ export class DashboardManagementComponent implements OnInit {
})
).subscribe(dashboard => {
if (dashboard) {
if (dashboard.widgets) {
dashboard.widgets.forEach(widget => {
if (widget.config && typeof widget.config === 'string') {
try {
widget.config = JSON.parse(widget.config);
} catch (e) {
console.error('Error parsing widget config string:', widget.config, e);
widget.config = {};
}
}
});
}
this.dashboardData = dashboard;
this.panels = this.mapWidgetsToPanels(dashboard.widgets || []);
if (dashboard.datasetId) {
this.dashboardStateService.selectDataset(dashboard.datasetId);
this.loadWidgetsForDataset(dashboard.datasetId);
} else {
// Clear widget list if no dataset is selected
this.availableWidgets = [];
this.filterWidgets();
}
// this.notificationService.success('Success', 'Dashboard loaded successfully!');
}
});
}
}
loadWidgetsForDataset(datasetId: string): void {
this.mMenuitemsWidgetService.getWidgetsForDataset(datasetId).subscribe(widgets => {
this.availableWidgets = widgets;
this.filterWidgets();
});
}
createNewDashboard(): void {
const newDashboardName = prompt('Enter a name for the new dashboard:');
if (newDashboardName) {
......@@ -242,12 +172,8 @@ export class DashboardManagementComponent implements OnInit {
});
this.dashboardDataService.saveDashboard(newDashboard).pipe(
switchMap(addedDashboard => {
// After saving, refetch the list of all dashboards
return this.dashboardDataService.getDashboards().pipe(
map(dashboards => ({
addedDashboard,
dashboards
}))
map(dashboards => ({ addedDashboard, dashboards }))
);
}),
catchError(error => {
......@@ -255,9 +181,9 @@ export class DashboardManagementComponent implements OnInit {
return throwError(() => error);
})
).subscribe(({ addedDashboard, dashboards }) => {
this.userDashboards = dashboards; // Update the local list with the fresh list
this.selectedDashboardId = addedDashboard; // Select the new dashboard
this.loadSelectedDashboard(); // Load the new dashboard
this.userDashboards = dashboards;
this.selectedDashboardId = addedDashboard;
this.loadSelectedDashboard();
this.notificationService.success('Success', 'New dashboard created successfully!');
});
}
......@@ -271,34 +197,57 @@ export class DashboardManagementComponent implements OnInit {
return throwError(() => error);
})
).subscribe(() => {
// this.notificationService.success('Success', 'Dashboard name saved successfully!');
this.notificationService.success('Success', 'Dashboard name saved successfully!');
});
}
}
mapWidgetsToPanels(widgets: WidgetModel[]): DashboardPanel[] {
return widgets.map(widget => {
return {
id: widget.widgetId,
header: widget.thName,
sizeX: widget.cols,
sizeY: widget.rows,
row: widget.y,
col: widget.x,
componentType: this.widgetComponentMap[widget.component],
componentInputs: { config: widget.config || {} },
originalWidget: widget
};
});
deleteDashboard(): void {
if (!this.selectedDashboardId) return;
if (confirm('Are you sure you want to delete this dashboard?')) {
this.dashboardDataService.deleteDashboard(this.selectedDashboardId).pipe(
switchMap(() => {
// Refresh the list of dashboards after deletion
return this.dashboardDataService.getDashboards();
}),
catchError(error => {
this.notificationService.error('Error', 'Failed to delete dashboard.');
return throwError(() => error);
})
).subscribe(dashboards => {
this.notificationService.success('Success', 'Dashboard deleted successfully!');
this.userDashboards = dashboards;
// Reset selection
this.selectedDashboardId = null;
this.dashboardData = null;
this.panels = [];
this.availableWidgets = [];
this.filterWidgets();
});
}
}
onPanelChange(args: any): void {
if (this.dashboardData && args.changedPanels) {
args.changedPanels.forEach((changedPanel: PanelModel) => {
const widgetIndex = this.dashboardData!.widgets.findIndex(w => w.widgetId === changedPanel.id);
if (widgetIndex > -1) {
const updatedWidget = { ...this.dashboardData!.widgets[widgetIndex] };
updatedWidget.cols = changedPanel.sizeX!;
updatedWidget.rows = changedPanel.sizeY!;
updatedWidget.x = changedPanel.col!;
updatedWidget.y = changedPanel.row!;
this.dashboardData!.widgets[widgetIndex] = updatedWidget;
}
});
}
}
saveLayout(): void {
if (!this.dashboardData || !this.layout) return;
// Get the current layout state directly from the Syncfusion component
const currentPanels = this.layout.serialize();
// Update the widgets array with the latest positions and sizes
currentPanels.forEach((panel: PanelModel) => {
const widgetIndex = this.dashboardData!.widgets.findIndex(w => w.widgetId === panel.id);
if (widgetIndex > -1) {
......@@ -309,16 +258,12 @@ export class DashboardManagementComponent implements OnInit {
}
});
// Now, prepare the data for saving (deep copy and stringify)
const dashboardToSave = JSON.parse(JSON.stringify(this.dashboardData));
if (dashboardToSave.widgets) {
dashboardToSave.widgets.forEach((widget: WidgetModel) => {
const keysToProcess: Array<keyof WidgetModel> = ['config', 'perspective', 'data'];
keysToProcess.forEach(key => {
if ((widget as any)[key] && typeof (widget as any)[key] === 'object') {
(widget as any)[key] = JSON.stringify((widget as any)[key]);
}
});
if (widget.config && typeof widget.config === 'object') {
widget.config = JSON.stringify(widget.config);
}
});
}
......@@ -328,59 +273,40 @@ export class DashboardManagementComponent implements OnInit {
return throwError(() => error);
})
).subscribe(() => {
// this.notificationService.success('Success', 'Dashboard layout saved successfully!');
this.notificationService.success('Success', 'Dashboard layout saved successfully!');
this.loadSelectedDashboard();
});
}
addWidgetToDashboard(widget: WidgetModel): void {
addWidgetToDashboard(menuItem: MenuItemsWidget): void {
if (!this.dashboardData) {
this.notificationService.warning('Warning', 'Please select or create a dashboard first.');
return;
}
if (!this.dashboardData.datasetId) {
this.notificationService.warning('Warning', 'Please select a dataset for the dashboard first.');
return;
}
this.dashboardStateService.selectedDataset$.pipe(
take(1)
).subscribe(selectedDataset => {
if (!selectedDataset || !selectedDataset.columns || selectedDataset.columns.length === 0) {
this.notificationService.error('Error', 'The selected dataset has no columns available.');
return;
}
// Create a new widget instance from the menu item's widget template
const newWidgetInstance = new WidgetModel(menuItem.widget);
const newWidgetInstance = new WidgetModel(widget);
newWidgetInstance.config = this.widgetConfigGenerator.generateConfig(widget, selectedDataset.columns);
this.dashboardData!.widgets.push(newWidgetInstance);
this.panels = this.mapWidgetsToPanels(this.dashboardData!.widgets);
// this.notificationService.info('Info', `Added widget: ${widget.thName}`);
});
}
deleteDashboard(): void {
if (this.selectedDashboardId && confirm('Are you sure you want to delete this dashboard?')) {
this.dashboardDataService.deleteDashboard(this.dashboardData!).pipe(
catchError(error => {
this.notificationService.error('Error', 'Failed to delete dashboard.');
return throwError(() => error);
})
).subscribe(result => {
console.log(result)
this.notificationService.success('Success', 'Dashboard deleted successfully!');
});
// Ensure the config is an object
if (typeof newWidgetInstance.config === 'string') {
try {
newWidgetInstance.config = JSON.parse(newWidgetInstance.config);
} catch (e) {
console.error('Error parsing widget config string:', newWidgetInstance.config, e);
newWidgetInstance.config = {}; // Reset to empty on error
}
}
this.dashboardData.widgets.push(newWidgetInstance);
this.panels = this.mapWidgetsToPanels(this.dashboardData.widgets);
this.notificationService.info('Info', `Added widget: ${newWidgetInstance.thName}`);
}
removeWidgetFromDashboard(panelId: string): void {
if (!this.dashboardData) return;
if (confirm('Are you sure you want to remove this widget?')) {
const updatedDashboard = { ...this.dashboardData, widgets: this.dashboardData.widgets.filter(w => w.widgetId !== panelId) };
this.dashboardData = updatedDashboard;
this.panels = this.mapWidgetsToPanels(updatedDashboard.widgets);
// this.notificationService.info('Info', 'Widget removed from dashboard.');
this.dashboardData.widgets = this.dashboardData.widgets.filter(w => w.widgetId !== panelId);
this.panels = this.mapWidgetsToPanels(this.dashboardData.widgets);
}
}
......@@ -390,50 +316,58 @@ export class DashboardManagementComponent implements OnInit {
this.dashboardData.templateId = dataset.templateId;
this.dashboardData.fileName = dataset.fileName;
this.dashboardStateService.selectDataset(dataset.itemId);
this.loadWidgetsForDataset(dataset.itemId);
}
}
openWidgetConfigDialog(panel: PanelModel & { componentType: Type<any>, componentInputs?: { [key: string]: any }, originalWidget: WidgetModel }): void {
openWidgetConfigDialog(panel: DashboardPanel): void {
const widget = panel.originalWidget;
this.dashboardStateService.selectedDataset$.pipe(take(1)).subscribe((selectedDataset: SelectedDataset | null) => {
const availableColumns = selectedDataset ? selectedDataset.columns : [];
const dialogRef = this.dialog.open(WidgetConfigComponent, {
width: '600px',
data: {
widget: widget, // Pass the whole widget
availableColumns: availableColumns
}
data: { widget, availableColumns }
});
dialogRef.afterClosed().subscribe((result: any) => {
if (result) {
const updatedWidget = { ...widget, config: result };
if (this.dashboardData) {
const widgetIndex = this.dashboardData.widgets.findIndex(w => w.widgetId === updatedWidget.widgetId);
if (widgetIndex > -1) {
this.dashboardData.widgets[widgetIndex] = updatedWidget;
this.panels = this.mapWidgetsToPanels(this.dashboardData.widgets);
}
dialogRef.afterClosed().subscribe(result => {
if (result && this.dashboardData) {
const widgetIndex = this.dashboardData.widgets.findIndex(w => w.widgetId === widget.widgetId);
if (widgetIndex > -1) {
this.dashboardData.widgets[widgetIndex].config = result;
this.panels = this.mapWidgetsToPanels(this.dashboardData.widgets);
}
}
});
});
}
onPanelChange(args: any): void {
if (this.dashboardData && args.changedPanels) {
args.changedPanels.forEach((changedPanel: PanelModel) => {
const widgetIndex = this.dashboardData!.widgets.findIndex(w => w.widgetId === changedPanel.id);
if (widgetIndex > -1) {
const updatedWidget = { ...this.dashboardData!.widgets[widgetIndex] };
updatedWidget.cols = changedPanel.sizeX!;
updatedWidget.rows = changedPanel.sizeY!;
updatedWidget.x = changedPanel.col!;
updatedWidget.y = changedPanel.row!;
this.dashboardData!.widgets[widgetIndex] = updatedWidget;
mapWidgetsToPanels(widgets: WidgetModel[]): DashboardPanel[] {
return widgets.map(widget => {
// Ensure config is an object before passing to component
let configObject = {};
if (typeof widget.config === 'string') {
try {
configObject = JSON.parse(widget.config);
} catch (e) {
console.error('Error parsing widget config string:', widget.config, e);
}
});
}
} else if (typeof widget.config === 'object') {
configObject = widget.config;
}
return {
id: widget.widgetId,
header: widget.thName,
sizeX: widget.cols,
sizeY: widget.rows,
row: widget.y,
col: widget.x,
componentType: this.widgetComponentMap[widget.component],
componentInputs: { config: configObject },
originalWidget: widget
};
});
}
}
import { WidgetModel } from "./widgets.model";
export class MenuItemsWidget {
companyId: string;
itemId: string;
widget: WidgetModel;
data?: string;
config?: string;
perspective?: string;
constructor(initialValues: Partial<MenuItemsWidget> = {}) {
if (initialValues) {
Object.assign(this, initialValues);
}
}
}
import { DatasetWidgetLinkerComponent } from './widget-management/dataset-widget-linker.component';
import { Routes } from '@angular/router';
import { DashboardManagementComponent } from './dashboard-management/dashboard-management.component';
......@@ -25,6 +26,12 @@ export const portalManageRoutes: Routes = [
path: 'dashboard-viewer/:dashboardId',
component: DashboardViewerComponent
},
{
path: 'widget-management',
children: [
{ path: 'linker', component: DatasetWidgetLinkerComponent, title: 'Dataset Widget Linker' }
]
}
// Optional: A redirect for the base portal-manage path
// {
// path: '',
......
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, catchError, tap } from 'rxjs/operators';
import { MenuItemsWidget } from '../models/m-menuitems-widget.model';
@Injectable({
providedIn: 'root'
})
export class MMenuitemsWidgetService {
private dataUrl = 'assets/data/d-menuitems-widget.json';
private linkedWidgetsCache: MenuItemsWidget[] = []; // In-memory cache for simulation
constructor(private http: HttpClient) { }
/**
* Gets all widget menu items and filters them by the provided datasetId.
* In a real application, this filtering would ideally be done on the backend.
* @param datasetId The ID of the dataset to filter widgets for.
* @returns An Observable of MenuItemsWidget[] that are linked to the given datasetId.
*/
getWidgetsForDataset(datasetId: string): Observable<MenuItemsWidget[]> {
// Simulate fetching from backend or use cache if available
if (this.linkedWidgetsCache.length > 0) {
return of(this.linkedWidgetsCache.filter(item => (item as any).datasetId === datasetId));
} else {
return this.http.get<any[]>(this.dataUrl).pipe(
tap(items => {
// Populate cache on first load
this.linkedWidgetsCache = items.map(item => new MenuItemsWidget(item));
}),
map(items => {
const filteredItems = items.filter(item => item.datasetId === datasetId);
return filteredItems.map(item => new MenuItemsWidget(item));
}),
catchError(error => {
console.error('Error loading widget menu items:', error);
return of([]); // Return an empty array on error
})
);
}
}
/**
* SIMULATED: Saves a linked widget. In a real app, this would be a POST/PUT request.
* Updates the in-memory cache and logs the action.
* @param menuItem The MenuItemsWidget to save.
* @returns An Observable of the saved MenuItemsWidget.
*/
saveLinkedWidget(menuItem: MenuItemsWidget): Observable<MenuItemsWidget> {
// Check if it already exists (for update scenario)
const index = this.linkedWidgetsCache.findIndex(item => item.itemId === menuItem.itemId);
if (index > -1) {
this.linkedWidgetsCache[index] = menuItem;
console.log('Simulating update linked widget:', menuItem);
} else {
this.linkedWidgetsCache.push(menuItem);
console.log('Simulating save new linked widget:', menuItem);
}
// In a real app, the backend would return the saved item.
return of(menuItem);
}
/**
* SIMULATED: Deletes a linked widget by its itemId. In a real app, this would be a DELETE request.
* Updates the in-memory cache and logs the action.
* @param itemId The itemId of the MenuItemsWidget to delete.
* @returns An Observable indicating success.
*/
deleteLinkedWidget(itemId: string): Observable<any> {
this.linkedWidgetsCache = this.linkedWidgetsCache.filter(item => item.itemId !== itemId);
console.log('Simulating delete linked widget with ID:', itemId);
// In a real app, this would return a status or confirmation.
return of({ success: true });
}
}
<div class="p-4 sm:p-6 lg:p-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">Dataset Widget Linker</h1>
<p class="mt-2 text-sm text-gray-700">Link available widgets to specific datasets and configure their default settings.</p>
</div>
</div>
<div class="mt-6">
<label for="dataset-select" class="block text-sm font-medium text-gray-700">Select Dataset</label>
<select id="dataset-select" name="dataset-select" [(ngModel)]="selectedDataset" (change)="onDatasetSelected()" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md">
<option [ngValue]="null">-- Select a Dataset --</option>
<option *ngFor="let dataset of datasets" [ngValue]="dataset">{{ dataset.tdesc }} ({{ dataset.itemId }})</option>
</select>
</div>
<div class="mt-8 grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Available Master Widgets -->
<div>
<h2 class="text-lg font-semibold text-gray-900">Available Widgets</h2>
<p class="mt-2 text-sm text-gray-700">Widgets registered in the system.</p>
<div class="mt-4 border border-gray-200 rounded-lg overflow-hidden shadow-sm">
<ul role="list" class="divide-y divide-gray-200">
<li *ngFor="let widget of masterWidgets" class="p-4 flex items-center justify-between hover:bg-gray-50">
<div>
<p class="text-sm font-medium text-gray-900">{{ widget.thName }}</p>
<p class="text-sm text-gray-500">{{ widget.component }} ({{ widget.cols }}x{{ widget.rows }})</p>
</div>
<button (click)="linkWidget(widget)" type="button" class="ml-3 inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Link
</button>
</li>
<li *ngIf="masterWidgets.length === 0" class="p-4 text-center text-sm text-gray-500">
No master widgets found.
</li>
</ul>
</div>
</div>
<!-- Linked Widgets for Selected Dataset -->
<div>
<h2 class="text-lg font-semibold text-gray-900">Linked Widgets ({{ selectedDataset?.tdesc || 'No Dataset Selected' }})</h2>
<p class="mt-2 text-sm text-gray-700">Widgets configured for the selected dataset.</p>
<div class="mt-4 border border-gray-200 rounded-lg overflow-hidden shadow-sm">
<ul role="list" class="divide-y divide-gray-200">
<li *ngFor="let linkedWidget of linkedWidgets" class="p-4 flex items-center justify-between hover:bg-gray-50">
<div>
<p class="text-sm font-medium text-gray-900">{{ linkedWidget.widget.thName }}</p>
<p class="text-sm text-gray-500">{{ linkedWidget.widget.component }}</p>
<p *ngIf="linkedWidget.config" class="text-xs text-gray-400">Config: {{ linkedWidget.config | slice:0:50 }}...</p>
</div>
<button (click)="unlinkWidget(linkedWidget.itemId)" type="button" class="ml-3 inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Unlink
</button>
</li>
<li *ngIf="linkedWidgets.length === 0" class="p-4 text-center text-sm text-gray-500">
No widgets linked to this dataset.
</li>
</ul>
</div>
</div>
</div>
</div>
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { NotificationService } from '../../shared/services/notification.service';
import { WidgetService } from '../services/widgets.service';
import { MMenuitemsWidgetService } from '../services/m-menuitems-widget.service';
import { DatasetService } from '../services/dataset.service';
import { WidgetModel, DatasetModel } from '../models/widgets.model';
import { MenuItemsWidget } from '../models/m-menuitems-widget.model';
import { WidgetConfigComponent } from '../dashboard-management/widget-config/widget-config.component';
import { take } from 'rxjs/operators';
import { DashboardStateService } from '../services/dashboard-state.service';
@Component({
selector: 'app-dataset-widget-linker',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './dataset-widget-linker.component.html',
styleUrls: ['./dataset-widget-linker.component.scss']
})
export class DatasetWidgetLinkerComponent implements OnInit {
public datasets: DatasetModel[] = [];
public selectedDataset: DatasetModel | null = null;
public masterWidgets: WidgetModel[] = [];
public linkedWidgets: MenuItemsWidget[] = [];
constructor(
private widgetService: WidgetService,
private mMenuitemsWidgetService: MMenuitemsWidgetService,
private datasetService: DatasetService,
private notificationService: NotificationService,
private dialog: MatDialog,
private dashboardStateService: DashboardStateService // To get columns for config
) { }
ngOnInit(): void {
this.datasetService.getDatasets().subscribe(datasets => {
this.datasets = datasets;
});
this.widgetService.getListWidgets().subscribe(widgets => {
this.masterWidgets = widgets;
});
}
onDatasetSelected(): void {
if (this.selectedDataset) {
this.loadLinkedWidgetsForDataset(this.selectedDataset.itemId);
// Also select the dataset in the global state to get its columns
this.dashboardStateService.selectDataset(this.selectedDataset.itemId);
} else {
this.linkedWidgets = [];
this.dashboardStateService.selectDataset(null);
}
}
loadLinkedWidgetsForDataset(datasetId: string): void {
this.mMenuitemsWidgetService.getWidgetsForDataset(datasetId).subscribe(linked => {
this.linkedWidgets = linked;
});
}
linkWidget(widget: WidgetModel): void {
if (!this.selectedDataset) {
this.notificationService.warning('Warning', 'Please select a dataset first.');
return;
}
// Check if widget is already linked
if (this.linkedWidgets.some(lw => lw.widget.widgetId === widget.widgetId)) {
this.notificationService.info('Info', 'This widget is already linked to the selected dataset.');
return;
}
// Get available columns for the selected dataset to pass to config dialog
this.dashboardStateService.selectedDataset$.pipe(take(1)).subscribe(selectedDataset => {
const availableColumns = selectedDataset ? selectedDataset.columns : [];
const dialogRef = this.dialog.open(WidgetConfigComponent, {
width: '600px',
data: {
widget: widget, // Pass the master widget template
availableColumns: availableColumns
}
});
dialogRef.afterClosed().subscribe(resultConfig => {
if (resultConfig) {
const newLinkedWidget: MenuItemsWidget = {
companyId: '1', // Placeholder, ideally from user context
itemId: `link-${this.selectedDataset!.itemId}-${widget.widgetId}`,
widget: widget, // The base widget definition
config: JSON.stringify(resultConfig), // Save configured object as string
data: '', // Not used for linking, but part of model
perspective: '' // Not used for linking, but part of model
};
this.mMenuitemsWidgetService.saveLinkedWidget(newLinkedWidget).subscribe(() => {
this.notificationService.success('Success', 'Widget linked successfully!');
this.loadLinkedWidgetsForDataset(this.selectedDataset!.itemId);
});
}
});
});
}
unlinkWidget(itemId: string): void {
if (confirm('Are you sure you want to unlink this widget from the dataset?')) {
this.mMenuitemsWidgetService.deleteLinkedWidget(itemId).subscribe(() => {
this.notificationService.success('Success', 'Widget unlinked successfully!');
this.loadLinkedWidgetsForDataset(this.selectedDataset!.itemId);
});
}
}
}
<div class="container mx-auto p-6">
<div class="flex justify-between items-center mb-4">
<h1 class="text-2xl font-bold">Widget Warehouse for {{ appName | titlecase }}</h1>
<button (click)="addNewWidget()" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Add New Widget
</button>
<div class="p-4 sm:p-6 lg:p-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">Widget Registry</h1>
<p class="mt-2 text-sm text-gray-700">A list of all base widgets registered in the system, fetched from the central API.</p>
</div>
<!-- Add/Delete functionality removed as this now points to a real API -->
</div>
<div class="bg-white shadow-md rounded my-6">
<table class="min-w-full table-auto">
<thead class="bg-gray-200">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Widget Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Component</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Default Size (Cols x Rows)</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Preview</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr *ngFor="let widget of widgets$ | async">
<td class="px-6 py-4 whitespace-nowrap">{{ widget.thName }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ widget.component }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ widget.cols }} x {{ widget.rows }}</td>
<td class="px-6 py-4">
<div class="w-48 h-32 border border-gray-300 rounded-lg overflow-hidden shadow-sm flex items-center justify-center bg-gray-50">
<ng-container *ngComponentOutlet="getComponentType(widget.component)"></ng-container>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<button (click)="editWidget(widget.widgetId)" class="text-indigo-600 hover:text-indigo-900">Edit</button>
</td>
</tr>
<!-- Show a message if there are no widgets -->
<tr *ngIf="!(widgets$ | async)?.length">
<td colspan="5" class="text-center py-4">No widgets found.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Registered Widgets List -->
<div class="mt-8 flex flex-col">
<div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Name</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Component</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Default Size</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
<tr *ngFor="let widget of registeredWidgets">
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="flex items-center">
<div class="ml-4">
<div class="font-medium text-gray-900">{{ widget.thName }}</div>
<div class="text-gray-500">{{ widget.engName }}</div>
</div>
</div>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<span class="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 text-green-800">{{ widget.component }}</span>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{{ widget.cols }}x{{ widget.rows }}</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
<!-- Preview button can be added here later -->
</td>
</tr>
<tr *ngIf="registeredWidgets.length === 0">
<td colspan="4" class="whitespace-nowrap px-3 py-4 text-sm text-center text-gray-500">No widgets registered yet.</td>
</tr>
</tbody>
\ No newline at end of file
import { Component, OnInit, Type } from '@angular/core';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { Observable } from 'rxjs';
import { CommonModule, TitleCasePipe } from '@angular/common';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { NgComponentOutlet } from '@angular/common';
import { Store } from '@ngrx/store';
import { WidgetModel } from '../models/widgets.model';
// Import all the widget components
// Import all widget components for preview
import { CompanyInfoWidgetComponent } from '../widgets/company-info-widget.component';
import { HeadcountWidgetComponent } from '../widgets/headcount-widget.component';
import { AttendanceOverviewWidgetComponent } from '../widgets/attendance-overview-widget.component';
import { PayrollSummaryWidgetComponent } from '../widgets/payroll-summary-widget.component';
import { EmployeeDirectoryWidgetComponent } from '../widgets/employee-directory-widget.component';
import { KpiWidgetComponent } from '../widgets/kpi-widget/kpi-widget.component';
import { WelcomeWidgetComponent } from '../widgets/welcome-widget/welcome-widget.component';
import { ChartWidgetComponent } from '../widgets/chart-widget/chart-widget.component';
import { QuickLinksWidgetComponent } from '../widgets/quick-links-widget/quick-links-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 { SyncfusionChartWidgetComponent } from '../widgets/syncfusion-chart-widget/syncfusion-chart-widget.component';
import { NotificationService } from '../../shared/services/notification.service';
import { WidgetModel } from '../models/widgets.model';
import { WidgetService } from '../services/widgets.service';
import { SimpleKpiWidgetComponent } from '../widgets/simple-kpi-widget/simple-kpi-widget.component';
@Component({
selector: 'app-widget-list',
standalone: true,
imports: [CommonModule, RouterModule, TitleCasePipe, NgComponentOutlet, CompanyInfoWidgetComponent, HeadcountWidgetComponent, AttendanceOverviewWidgetComponent, PayrollSummaryWidgetComponent, EmployeeDirectoryWidgetComponent, KpiWidgetComponent, WelcomeWidgetComponent, ChartWidgetComponent, QuickLinksWidgetComponent, SyncfusionDatagridWidgetComponent, SyncfusionPivotWidgetComponent, SyncfusionChartWidgetComponent],
templateUrl: './widget-list.component.html',
imports: [
CommonModule,
FormsModule,
MatDialogModule,
NgComponentOutlet,
// Import widgets to be available for NgComponentOutlet
CompanyInfoWidgetComponent, HeadcountWidgetComponent, AttendanceOverviewWidgetComponent, PayrollSummaryWidgetComponent, EmployeeDirectoryWidgetComponent, WelcomeWidgetComponent, QuickLinksWidgetComponent, SyncfusionDatagridWidgetComponent, SyncfusionPivotWidgetComponent, SyncfusionChartWidgetComponent, SimpleKpiWidgetComponent
],
templateUrl: './widget-list.component.html'
})
export class WidgetListComponent implements OnInit {
widgets$!: Observable<WidgetModel[]>;
appName: string = '';
// Map string names to actual component classes
public registeredWidgets: WidgetModel[] = [];
// This map is crucial for mapping component string names to actual component types for previewing.
private widgetComponentMap: { [key: string]: Type<any> } = {
'CompanyInfoWidgetComponent': CompanyInfoWidgetComponent,
'HeadcountWidgetComponent': HeadcountWidgetComponent,
'AttendanceOverviewWidgetComponent': AttendanceOverviewWidgetComponent,
'PayrollSummaryWidgetComponent': PayrollSummaryWidgetComponent,
'EmployeeDirectoryWidgetComponent': EmployeeDirectoryWidgetComponent,
'KpiWidgetComponent': KpiWidgetComponent,
'WelcomeWidgetComponent': WelcomeWidgetComponent,
'ChartWidgetComponent': ChartWidgetComponent,
'QuickLinksWidgetComponent': QuickLinksWidgetComponent,
'SyncfusionDatagridWidgetComponent': SyncfusionDatagridWidgetComponent,
'SyncfusionPivotWidgetComponent': SyncfusionPivotWidgetComponent,
'SyncfusionChartWidgetComponent': SyncfusionChartWidgetComponent
CompanyInfoWidgetComponent,
HeadcountWidgetComponent,
AttendanceOverviewWidgetComponent,
PayrollSummaryWidgetComponent,
EmployeeDirectoryWidgetComponent,
WelcomeWidgetComponent,
QuickLinksWidgetComponent,
SyncfusionDatagridWidgetComponent,
SyncfusionPivotWidgetComponent,
SyncfusionChartWidgetComponent,
SimpleKpiWidgetComponent,
};
constructor(
private store: Store,
private route: ActivatedRoute,
private router: Router
private widgetService: WidgetService,
private notificationService: NotificationService,
private dialog: MatDialog
) { }
ngOnInit(): void {
// this.store.dispatch(DashboardActions.loadWidgets());
// this.widgets$ = this.store.select(DashboardSelectors.selectAllWidgets);
}
getComponentType(componentName: string): Type<any> | null {
return this.widgetComponentMap[componentName] || null;
this.loadRegisteredWidgets();
}
editWidget(widgetId: string): void {
this.router.navigate(['/portal-manage', this.appName, 'widget-warehouse', 'edit', widgetId]);
loadRegisteredWidgets(): void {
this.widgetService.getListWidgets().subscribe(widgets => {
this.registeredWidgets = widgets;
});
}
addNewWidget(): void {
this.router.navigate(['/portal-manage', this.appName, 'widget-warehouse', 'edit', 'new']);
getComponentType(componentName: string): Type<any> {
return this.widgetComponentMap[componentName];
}
}
[{"companyId": "1", "itemId": "dataset-1-headcount-bar", "datasetId": "people-analytics-dataset", "widget": {"widgetId": "widget-headcount-bar", "thName": "จำนวนพนักงาน (แผนภูมิแท่ง)", "engName": "Headcount (Bar Chart)", "component": "HeadcountWidgetComponent", "cols": 3, "rows": 2, "x": 0, "y": 0, "config": "{\"title\":\"Headcount by Department\",\"categoryField\":\"department\",\"chartType\":\"bar\"}"}}, {"companyId": "1", "itemId": "dataset-1-headcount-doughnut", "datasetId": "people-analytics-dataset", "widget": {"widgetId": "widget-headcount-doughnut", "thName": "สัดส่วนพนักงาน (แผนภูมิโดนัท)", "engName": "Headcount (Doughnut Chart)", "component": "HeadcountWidgetComponent", "cols": 2, "rows": 2, "x": 0, "y": 0, "config": "{\"title\":\"Headcount by Gender\",\"categoryField\":\"gender\",\"chartType\":\"doughnut\"}"}}, {"companyId": "1", "itemId": "dataset-1-emp-directory", "datasetId": "people-analytics-dataset", "widget": {"widgetId": "widget-emp-directory", "thName": "ทำเนียบพนักงาน", "engName": "Employee Directory", "component": "EmployeeDirectoryWidgetComponent", "cols": 2, "rows": 4, "x": 0, "y": 0, "config": "{\"title\":\"Our Employees\",\"nameField\":\"name\",\"positionField\":\"position\",\"departmentField\":\"department\",\"photoField\":\"photoUrl\"}"}}, {"companyId": "1", "itemId": "dataset-2-kpi", "datasetId": "financial-dataset", "widget": {"widgetId": "widget-revenue-kpi", "thName": "รายได้รวม", "engName": "Total Revenue", "component": "SimpleKpiWidgetComponent", "cols": 1, "rows": 1, "x": 0, "y": 0, "config": "{\"title\":\"Total Revenue\",\"valueField\":\"revenue\",\"aggregation\":\"sum\",\"unit\":\"THB\",\"icon\":\"cash-stack\",\"color\":\"#10b981\"}"}}]}
\ No newline at end of file
[
{
"widgetId": "widget-headcount",
"thName": "จำนวนพนักงาน",
"engName": "Headcount",
"component": "HeadcountWidgetComponent",
"cols": 3,
"rows": 2,
"x": 0,
"y": 0
},
{
"widgetId": "widget-simple-kpi",
"thName": "ตัวชี้วัดอย่างง่าย",
"engName": "Simple KPI",
"component": "SimpleKpiWidgetComponent",
"cols": 1,
"rows": 1,
"x": 0,
"y": 0
},
{
"widgetId": "widget-quick-links",
"thName": "ลิงก์ด่วน",
"engName": "Quick Links",
"component": "QuickLinksWidgetComponent",
"cols": 2,
"rows": 2,
"x": 0,
"y": 0
},
{
"widgetId": "widget-employee-directory",
"thName": "ทำเนียบพนักงาน",
"engName": "Employee Directory",
"component": "EmployeeDirectoryWidgetComponent",
"cols": 3,
"rows": 4,
"x": 0,
"y": 0
}
]
\ No newline at end of file
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