Commit 0e1eb15e by Ooh-Ao

linker

parent e38dbc04
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
"@ngx-translate/core": "^15.0.0", "@ngx-translate/core": "^15.0.0",
"@ngx-translate/http-loader": "^8.0.0", "@ngx-translate/http-loader": "^8.0.0",
"@syncfusion/ej2-angular-base": "^29.2.4", "@syncfusion/ej2-angular-base": "^29.2.4",
"@syncfusion/ej2-angular-buttons": "^29.1.33",
"@syncfusion/ej2-angular-charts": "^29.2.4", "@syncfusion/ej2-angular-charts": "^29.2.4",
"@syncfusion/ej2-angular-circulargauge": "^29.1.33", "@syncfusion/ej2-angular-circulargauge": "^29.1.33",
"@syncfusion/ej2-angular-dropdowns": "^29.2.4", "@syncfusion/ej2-angular-dropdowns": "^29.2.4",
...@@ -6189,6 +6190,55 @@ ...@@ -6189,6 +6190,55 @@
"@syncfusion/ej2-icons": "~29.2.4" "@syncfusion/ej2-icons": "~29.2.4"
} }
}, },
"node_modules/@syncfusion/ej2-angular-buttons": {
"version": "29.1.33",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-buttons/-/ej2-angular-buttons-29.1.33.tgz",
"integrity": "sha512-3eTXpNhVrUcWl8JqZiVZ1mjd0aN9ZUvcvqZpo+zP21KigYs4zV3u8NGrALbTcA1/6f3MoL7se7C2wTiQpsdOhw==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-angular-base": "~29.1.33",
"@syncfusion/ej2-base": "~29.1.33",
"@syncfusion/ej2-buttons": "29.1.33"
}
},
"node_modules/@syncfusion/ej2-angular-buttons/node_modules/@syncfusion/ej2-angular-base": {
"version": "29.1.33",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-base/-/ej2-angular-base-29.1.33.tgz",
"integrity": "sha512-9x/fNmzZiiEL98t0mDrdIyUJZLC+H9h7Uu4kc4Ro44/uIYF/ZJVTxIpYJU1Fl0+F0QnmBakPL5kNpY/gHK1lLg==",
"hasInstallScript": true,
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-base": "~29.1.33",
"@syncfusion/ej2-icons": "~29.1.33"
}
},
"node_modules/@syncfusion/ej2-angular-buttons/node_modules/@syncfusion/ej2-base": {
"version": "29.1.36",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-base/-/ej2-base-29.1.36.tgz",
"integrity": "sha512-XVRrymlbywtzNnxiaf/ByudElO3p7gieJuN2IHcs6FxsQNI60d3A5RdBqEF0znAH/KM0iSiWDFaaP2pqQltiEQ==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-icons": "~29.1.33"
},
"bin": {
"syncfusion-license": "bin/syncfusion-license.js"
}
},
"node_modules/@syncfusion/ej2-angular-buttons/node_modules/@syncfusion/ej2-buttons": {
"version": "29.1.33",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-buttons/-/ej2-buttons-29.1.33.tgz",
"integrity": "sha512-5zEjtrVCYXaUEEFUiY3GZ2mAsy7t6MEQ8IzBVHR8ir8t4qLvWy+Fc/e3wZ/uVB7bmffuImusMJ0Q1al3UCrX+g==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-base": "~29.1.33"
}
},
"node_modules/@syncfusion/ej2-angular-buttons/node_modules/@syncfusion/ej2-icons": {
"version": "29.1.33",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-icons/-/ej2-icons-29.1.33.tgz",
"integrity": "sha512-kfhXGZ5QQAIkvqGdPXOnZCkPqKnyw0ieK74vfoFXv3UlJKLiSIAbkBxr8xOWn7k+FtlADDkGnOTAtIKUpyHBfQ==",
"license": "SEE LICENSE IN license"
},
"node_modules/@syncfusion/ej2-angular-charts": { "node_modules/@syncfusion/ej2-angular-charts": {
"version": "29.2.11", "version": "29.2.11",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-charts/-/ej2-angular-charts-29.2.11.tgz", "resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-charts/-/ej2-angular-charts-29.2.11.tgz",
......
...@@ -21,16 +21,14 @@ export const portalManageRoutes: Routes = [ ...@@ -21,16 +21,14 @@ export const portalManageRoutes: Routes = [
path: ':appName/widget-warehouse/edit/:widgetId', path: ':appName/widget-warehouse/edit/:widgetId',
component: WidgetFormComponent, component: WidgetFormComponent,
}, },
{
path: ':appName/widget-linker',
component: DatasetWidgetLinkerComponent,
},
// Route for viewing a specific dashboard, remains unchanged // Route for viewing a specific dashboard, remains unchanged
{ {
path: 'dashboard-viewer/:dashboardId', path: 'dashboard-viewer/:dashboardId',
component: DashboardViewerComponent component: DashboardViewerComponent
},
{
path: 'widget-management',
children: [
{ path: 'linker', component: DatasetWidgetLinkerComponent, title: 'Dataset Widget Linker' }
]
} }
// Optional: A redirect for the base portal-manage path // Optional: A redirect for the base portal-manage path
// { // {
......
...@@ -22,9 +22,9 @@ export class MMenuitemsWidgetService { ...@@ -22,9 +22,9 @@ export class MMenuitemsWidgetService {
* @returns An Observable of MenuItemsWidget[] that are linked to the given datasetId. * @returns An Observable of MenuItemsWidget[] that are linked to the given datasetId.
*/ */
getWidgetsForDataset(datasetId: string): Observable<MenuItemsWidget[]> { getWidgetsForDataset(datasetId: string): Observable<MenuItemsWidget[]> {
const companyId = this.tokenService.getSelectCompany().companyId; // Get companyId from TokenService const companyId = this.tokenService.getSelectCompany().companyId || "DEMO"; // Get companyId from TokenService
// Assuming the backend /search endpoint supports filtering by datasetId and companyId // Assuming the backend /search endpoint supports filtering by datasetId and companyId
return this.http.get<any[]>(`${this.baseUrl}/search`, { params: { datasetId, companyId } }).pipe( return this.http.get<any[]>(`${this.baseUrl}/lists/search`, { params: { companyId : companyId , itemId : datasetId } }).pipe(
map(items => items.map(item => new MenuItemsWidget(item))), map(items => items.map(item => new MenuItemsWidget(item))),
catchError(error => { catchError(error => {
console.error('Error loading linked widgets:', error); console.error('Error loading linked widgets:', error);
......
<div class="p-4 sm:p-6 lg:p-8"> <!-- Main Container -->
<div class="sm:flex sm:items-center"> <div class="grid grid-cols-12 gap-6">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">Dataset Widget Linker</h1> <!-- Left Panel: Widget List -->
<p class="mt-2 text-sm text-gray-700">Link available widgets to specific datasets and configure their default settings.</p> <div class="col-span-12 md:col-span-4">
<div class="p-4 bg-white rounded-lg shadow-md">
<!-- Dataset Dropdown -->
<div class="mb-4">
<label for="dataset-select" class="block text-sm font-medium text-gray-700 mb-1">Select Dataset</label>
<ejs-dropdownlist
id='dataset-select'
[dataSource]="datasets"
[(ngModel)]="selectedDatasetId"
(change)="onDatasetChange()"
[fields]="{ text: 'tdesc', value: 'itemId' }"
placeholder="Select a Dataset"
floatLabelType="Never"
cssClass="w-full">
</ejs-dropdownlist>
</div> </div>
<!-- Linked Widgets List -->
<div class="flex justify-between items-center mb-2">
<h2 class="text-lg font-semibold">Linked Widgets</h2>
<button ejs-button (click)="openAddWidgetModal()" [disabled]="!selectedDatasetId" cssClass="e-small e-success">
<i class="bi bi-plus-lg mr-1"></i> Add Widget
</button>
</div> </div>
<div class="mt-6"> <div *ngIf="!selectedDatasetId" class="text-center text-gray-500 p-4 border-2 border-dashed rounded-lg">
<label for="dataset-select" class="block text-sm font-medium text-gray-700">Select Dataset</label> Please select a dataset to see its widgets.
<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>
<div class="mt-8 grid grid-cols-1 lg:grid-cols-2 gap-8"> <div *ngIf="selectedDatasetId">
<!-- Available Master Widgets --> <ul *ngIf="linkedWidgets.length > 0" class="space-y-2">
<div> <li *ngFor="let widget of linkedWidgets"
<h2 class="text-lg font-semibold text-gray-900">Available Widgets</h2> class="flex items-center justify-between p-2 rounded-md transition-colors cursor-pointer"
<p class="mt-2 text-sm text-gray-700">Widgets registered in the system.</p> [class.bg-sky-100]="widget.itemId === widgetToPreview?.itemId"
<div class="mt-4 border border-gray-200 rounded-lg overflow-hidden shadow-sm"> [class.hover:bg-gray-100]="widget.itemId !== widgetToPreview?.itemId"
<ul role="list" class="divide-y divide-gray-200"> (click)="viewWidget(widget)">
<li *ngFor="let widget of masterWidgets" class="p-4 flex items-center justify-between hover:bg-gray-50"> <span class="font-medium">{{ widget.widget.thName }}</span>
<div> <div class="flex items-center space-x-2">
<p class="text-sm font-medium text-gray-900">{{ widget.thName }}</p> <button (click)="removeWidget(widget, $event)" class="text-red-600 hover:text-red-800" title="Remove">
<p class="text-sm text-gray-500">{{ widget.component }} ({{ widget.cols }}x{{ widget.rows }})</p> <i class="bi bi-trash-fill"></i>
</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> </button>
</li> </div>
<li *ngIf="masterWidgets.length === 0" class="p-4 text-center text-sm text-gray-500">
No master widgets found.
</li> </li>
</ul> </ul>
<div *ngIf="linkedWidgets.length === 0" class="text-center text-gray-500 p-4 border-2 border-dashed rounded-lg">
No widgets linked to this dataset.
</div>
</div>
</div> </div>
</div> </div>
<!-- Linked Widgets for Selected Dataset --> <!-- Right Panel: Widget Viewer/Config -->
<div> <div class="col-span-12 md:col-span-8">
<h2 class="text-lg font-semibold text-gray-900">Linked Widgets ({{ selectedDataset?.tdesc || 'No Dataset Selected' }})</h2> <div class="p-4 bg-white rounded-lg shadow-md min-h-[400px]">
<p class="mt-2 text-sm text-gray-700">Widgets configured for the selected dataset.</p> <h2 class="text-lg font-semibold mb-4">Widget Preview & Configuration</h2>
<div class="mt-4 border border-gray-200 rounded-lg overflow-hidden shadow-sm"> <div *ngIf="!widgetToPreview" class="flex items-center justify-center h-full text-gray-500 border-2 border-dashed rounded-lg">
<ul role="list" class="divide-y divide-gray-200"> <div class="text-center">
<li *ngFor="let linkedWidget of linkedWidgets" class="p-4 flex items-center justify-between hover:bg-gray-50"> <i class="bi bi-eye text-4xl mb-2"></i>
<div> <p>Select a widget from the list to preview it here.</p>
<p class="text-sm font-medium text-gray-900">{{ linkedWidget.widget.thName }}</p> </div>
<p class="text-sm text-gray-500">{{ linkedWidget.widget.component }}</p> </div>
<p *ngIf="linkedWidget.config" class="text-xs text-gray-400">Config: {{ linkedWidget.config | slice:0:50 }}...</p> <div *ngIf="widgetToPreview">
</div> <div class="mb-4 p-4 border rounded-lg">
<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"> <h3 class="text-md font-bold mb-2">{{ widgetToPreview.widget.thName }}</h3>
Unlink <p class="text-sm text-gray-600">{{ widgetToPreview.widget.component }}</p>
</button> </div>
</li> <div class="p-4 border rounded-lg bg-gray-50">
<li *ngIf="linkedWidgets.length === 0" class="p-4 text-center text-sm text-gray-500"> <h3 class="text-md font-bold mb-2">Configuration</h3>
No widgets linked to this dataset. <pre class="bg-gray-900 text-white p-2 rounded-md text-xs">{{ widgetToPreview.config | json }}</pre>
</li> </div>
</ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Add Widget Modal using Syncfusion Dialog -->
<ejs-dialog #addWidgetDialog header='Add Widgets to Dataset' [isModal]='true' [visible]="isModalVisible" (close)="isModalVisible=false" showCloseIcon='true' width='60%'>
<ng-template #content>
<ejs-grid #grid [dataSource]="masterWidgets" [allowPaging]="true" [selectionSettings]="{ type: 'Multiple' }" [pageSettings]="{ pageSize: 10 }">
<e-columns>
<e-column type='checkbox' width='50'></e-column>
<e-column field='widgetId' headerText='Widget ID' width='100'></e-column>
<e-column field='thName' headerText='Name' width='150'></e-column>
<e-column field='component' headerText='Component' width='150'></e-column>
</e-columns>
</ejs-grid>
</ng-template>
<ng-template #footerTemplate>
<button ejs-button (click)="addWidgetDialog.hide()" cssClass="e-normal">Cancel</button>
<button ejs-button (click)="addSelectedWidgets()" cssClass="e-primary">Add Selected</button>
</ng-template>
</ejs-dialog>
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule, JsonPipe } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
// Syncfusion Modules
import { DropDownListModule } from '@syncfusion/ej2-angular-dropdowns';
import { DialogComponent, DialogModule } from '@syncfusion/ej2-angular-popups';
import { GridComponent, GridModule, PageService, SelectionService } from '@syncfusion/ej2-angular-grids';
import { ButtonComponent, ButtonModule } from '@syncfusion/ej2-angular-buttons';
import { NotificationService } from '../../shared/services/notification.service'; import { NotificationService } from '../../shared/services/notification.service';
import { WidgetService } from '../services/widgets.service'; import { WidgetService } from '../services/widgets.service';
import { MMenuitemsWidgetService } from '../services/m-menuitems-widget.service'; import { MMenuitemsWidgetService } from '../services/m-menuitems-widget.service';
...@@ -15,24 +22,42 @@ import { DashboardStateService } from '../services/dashboard-state.service'; ...@@ -15,24 +22,42 @@ import { DashboardStateService } from '../services/dashboard-state.service';
@Component({ @Component({
selector: 'app-dataset-widget-linker', selector: 'app-dataset-widget-linker',
standalone: true, standalone: true,
imports: [CommonModule, FormsModule], imports: [
CommonModule,
FormsModule,
JsonPipe,
DropDownListModule,
DialogModule,
GridModule,
ButtonModule
],
providers: [PageService, SelectionService],
templateUrl: './dataset-widget-linker.component.html', templateUrl: './dataset-widget-linker.component.html',
styleUrls: ['./dataset-widget-linker.component.scss'] styleUrls: ['./dataset-widget-linker.component.scss']
}) })
export class DatasetWidgetLinkerComponent implements OnInit { export class DatasetWidgetLinkerComponent implements OnInit {
// Syncfusion Component References
@ViewChild('addWidgetDialog') addWidgetDialog!: DialogComponent;
@ViewChild('grid') grid!: GridComponent;
// Data lists
public datasets: DatasetModel[] = []; public datasets: DatasetModel[] = [];
public selectedDataset: DatasetModel | null = null;
public masterWidgets: WidgetModel[] = []; public masterWidgets: WidgetModel[] = [];
public linkedWidgets: MenuItemsWidget[] = []; public linkedWidgets: MenuItemsWidget[] = [];
// UI State
public selectedDatasetId: string | null = null;
public widgetToPreview: MenuItemsWidget | null = null;
public isModalVisible = false;
constructor( constructor(
private widgetService: WidgetService, private widgetService: WidgetService,
private mMenuitemsWidgetService: MMenuitemsWidgetService, private mMenuitemsWidgetService: MMenuitemsWidgetService,
private datasetService: DatasetService, private datasetService: DatasetService,
private notificationService: NotificationService, private notificationService: NotificationService,
private dialog: MatDialog, private dialog: MatDialog,
private dashboardStateService: DashboardStateService // To get columns for config private dashboardStateService: DashboardStateService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
...@@ -45,11 +70,11 @@ export class DatasetWidgetLinkerComponent implements OnInit { ...@@ -45,11 +70,11 @@ export class DatasetWidgetLinkerComponent implements OnInit {
}); });
} }
onDatasetSelected(): void { onDatasetChange(): void {
if (this.selectedDataset) { this.widgetToPreview = null;
this.loadLinkedWidgetsForDataset(this.selectedDataset.itemId); if (this.selectedDatasetId) {
// Also select the dataset in the global state to get its columns this.loadLinkedWidgetsForDataset(this.selectedDatasetId);
this.dashboardStateService.selectDataset(this.selectedDataset.itemId); this.dashboardStateService.selectDataset(this.selectedDatasetId);
} else { } else {
this.linkedWidgets = []; this.linkedWidgets = [];
this.dashboardStateService.selectDataset(null); this.dashboardStateService.selectDataset(null);
...@@ -62,57 +87,73 @@ export class DatasetWidgetLinkerComponent implements OnInit { ...@@ -62,57 +87,73 @@ export class DatasetWidgetLinkerComponent implements OnInit {
}); });
} }
linkWidget(widget: WidgetModel): void { viewWidget(widget: MenuItemsWidget): void {
if (!this.selectedDataset) { this.widgetToPreview = widget;
this.notificationService.warning('Warning', 'Please select a dataset first.'); }
removeWidget(widget: MenuItemsWidget, event: MouseEvent): void {
event.stopPropagation(); // Prevent row selection when clicking the button
if (confirm('Are you sure you want to unlink this widget?')) {
this.mMenuitemsWidgetService.deleteLinkedWidget(widget.itemId).subscribe(() => {
this.notificationService.success('Success', 'Widget unlinked successfully!');
if (this.widgetToPreview?.itemId === widget.itemId) {
this.widgetToPreview = null;
}
this.loadLinkedWidgetsForDataset(this.selectedDatasetId!);
});
}
}
openAddWidgetModal(): void {
this.isModalVisible = true;
this.addWidgetDialog.show();
}
addSelectedWidgets(): void {
const selectedWidgets = this.grid.getSelectedRecords() as WidgetModel[];
if (selectedWidgets.length > 0) {
selectedWidgets.forEach(widgetModel => {
this.linkWidget(widgetModel);
});
}
this.addWidgetDialog.hide();
}
private linkWidget(widget: WidgetModel): void {
if (!this.selectedDatasetId) {
this.notificationService.warning('Warning', 'Something went wrong. No dataset selected.');
return; return;
} }
// Check if widget is already linked
if (this.linkedWidgets.some(lw => lw.widget.widgetId === widget.widgetId)) { if (this.linkedWidgets.some(lw => lw.widget.widgetId === widget.widgetId)) {
this.notificationService.info('Info', 'This widget is already linked to the selected dataset.'); this.notificationService.info('Info', `'${widget.thName}' is already linked.`);
return; return;
} }
// Get available columns for the selected dataset to pass to config dialog
this.dashboardStateService.selectedDataset$.pipe(take(1)).subscribe(selectedDataset => { this.dashboardStateService.selectedDataset$.pipe(take(1)).subscribe(selectedDataset => {
const availableColumns = selectedDataset ? selectedDataset.columns : []; const availableColumns = selectedDataset ? selectedDataset.columns : [];
const dialogRef = this.dialog.open(WidgetConfigComponent, { const dialogRef = this.dialog.open(WidgetConfigComponent, {
width: '600px', width: '600px',
data: { data: { widget: widget, availableColumns: availableColumns }
widget: widget, // Pass the master widget template
availableColumns: availableColumns
}
}); });
dialogRef.afterClosed().subscribe(resultConfig => { dialogRef.afterClosed().subscribe(resultConfig => {
if (resultConfig) { if (resultConfig) {
const newLinkedWidget: MenuItemsWidget = { const newLinkedWidget: MenuItemsWidget = {
companyId: 'DEMO', // Placeholder, ideally from user context companyId: 'DEMO', // Placeholder
itemId: this.selectedDataset!.itemId, itemId: this.selectedDatasetId!,
widget: widget, // The base widget definition widget: widget,
// config: JSON.stringify(resultConfig), // Save configured object as string config: JSON.stringify(resultConfig),
config: '', data: '',
data: '', // Not used for linking, but part of model perspective: ''
perspective: '' // Not used for linking, but part of model
}; };
this.mMenuitemsWidgetService.saveLinkedWidget(newLinkedWidget).subscribe(() => { this.mMenuitemsWidgetService.saveLinkedWidget(newLinkedWidget).subscribe(() => {
this.notificationService.success('Success', 'Widget linked successfully!'); this.notificationService.success('Success', `'${widget.thName}' linked successfully!`);
this.loadLinkedWidgetsForDataset(this.selectedDataset!.itemId); this.loadLinkedWidgetsForDataset(this.selectedDatasetId!);
}); });
} }
}); });
}); });
} }
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);
});
}
}
} }
...@@ -109,7 +109,7 @@ export class NavService implements OnDestroy { ...@@ -109,7 +109,7 @@ export class NavService implements OnDestroy {
type: 'link' type: 'link'
}, },
{ {
path: `/portal-manage/widget-management/linker`, path: `/portal-manage/${appName}/widget-linker`,
title: 'เชื่อมโยงวิดเจ็ตกับชุดข้อมูล', title: 'เชื่อมโยงวิดเจ็ตกับชุดข้อมูล',
icon: 'link', icon: 'link',
type: 'link' type: 'link'
......
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