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 { 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