Commit 0e1eb15e by Ooh-Ao

linker

parent e38dbc04
......@@ -34,6 +34,7 @@
"@ngx-translate/core": "^15.0.0",
"@ngx-translate/http-loader": "^8.0.0",
"@syncfusion/ej2-angular-base": "^29.2.4",
"@syncfusion/ej2-angular-buttons": "^29.1.33",
"@syncfusion/ej2-angular-charts": "^29.2.4",
"@syncfusion/ej2-angular-circulargauge": "^29.1.33",
"@syncfusion/ej2-angular-dropdowns": "^29.2.4",
......@@ -6189,6 +6190,55 @@
"@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": {
"version": "29.2.11",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-charts/-/ej2-angular-charts-29.2.11.tgz",
......
......@@ -21,16 +21,14 @@ export const portalManageRoutes: Routes = [
path: ':appName/widget-warehouse/edit/:widgetId',
component: WidgetFormComponent,
},
{
path: ':appName/widget-linker',
component: DatasetWidgetLinkerComponent,
},
// Route for viewing a specific dashboard, remains unchanged
{
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
// {
......
......@@ -22,9 +22,9 @@ export class MMenuitemsWidgetService {
* @returns An Observable of MenuItemsWidget[] that are linked to the given datasetId.
*/
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
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))),
catchError(error => {
console.error('Error loading linked widgets:', error);
......
<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>
<!-- Main Container -->
<div class="grid grid-cols-12 gap-6">
<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>
<!-- Left Panel: Widget List -->
<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>
<!-- 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 *ngIf="!selectedDatasetId" class="text-center text-gray-500 p-4 border-2 border-dashed rounded-lg">
Please select a dataset to see its widgets.
</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 *ngIf="selectedDatasetId">
<ul *ngIf="linkedWidgets.length > 0" class="space-y-2">
<li *ngFor="let widget of linkedWidgets"
class="flex items-center justify-between p-2 rounded-md transition-colors cursor-pointer"
[class.bg-sky-100]="widget.itemId === widgetToPreview?.itemId"
[class.hover:bg-gray-100]="widget.itemId !== widgetToPreview?.itemId"
(click)="viewWidget(widget)">
<span class="font-medium">{{ widget.widget.thName }}</span>
<div class="flex items-center space-x-2">
<button (click)="removeWidget(widget, $event)" class="text-red-600 hover:text-red-800" title="Remove">
<i class="bi bi-trash-fill"></i>
</button>
</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 *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>
<!-- 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>
<!-- Right Panel: Widget Viewer/Config -->
<div class="col-span-12 md:col-span-8">
<div class="p-4 bg-white rounded-lg shadow-md min-h-[400px]">
<h2 class="text-lg font-semibold mb-4">Widget Preview & Configuration</h2>
<div *ngIf="!widgetToPreview" class="flex items-center justify-center h-full text-gray-500 border-2 border-dashed rounded-lg">
<div class="text-center">
<i class="bi bi-eye text-4xl mb-2"></i>
<p>Select a widget from the list to preview it here.</p>
</div>
</div>
<div *ngIf="widgetToPreview">
<div class="mb-4 p-4 border rounded-lg">
<h3 class="text-md font-bold mb-2">{{ widgetToPreview.widget.thName }}</h3>
<p class="text-sm text-gray-600">{{ widgetToPreview.widget.component }}</p>
</div>
<div class="p-4 border rounded-lg bg-gray-50">
<h3 class="text-md font-bold mb-2">Configuration</h3>
<pre class="bg-gray-900 text-white p-2 rounded-md text-xs">{{ widgetToPreview.config | json }}</pre>
</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 { CommonModule } from '@angular/common';
import { Component, OnInit, ViewChild } from '@angular/core';
import { CommonModule, JsonPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
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 { WidgetService } from '../services/widgets.service';
import { MMenuitemsWidgetService } from '../services/m-menuitems-widget.service';
......@@ -15,24 +22,42 @@ import { DashboardStateService } from '../services/dashboard-state.service';
@Component({
selector: 'app-dataset-widget-linker',
standalone: true,
imports: [CommonModule, FormsModule],
imports: [
CommonModule,
FormsModule,
JsonPipe,
DropDownListModule,
DialogModule,
GridModule,
ButtonModule
],
providers: [PageService, SelectionService],
templateUrl: './dataset-widget-linker.component.html',
styleUrls: ['./dataset-widget-linker.component.scss']
})
export class DatasetWidgetLinkerComponent implements OnInit {
// Syncfusion Component References
@ViewChild('addWidgetDialog') addWidgetDialog!: DialogComponent;
@ViewChild('grid') grid!: GridComponent;
// Data lists
public datasets: DatasetModel[] = [];
public selectedDataset: DatasetModel | null = null;
public masterWidgets: WidgetModel[] = [];
public linkedWidgets: MenuItemsWidget[] = [];
// UI State
public selectedDatasetId: string | null = null;
public widgetToPreview: MenuItemsWidget | null = null;
public isModalVisible = false;
constructor(
private widgetService: WidgetService,
private mMenuitemsWidgetService: MMenuitemsWidgetService,
private datasetService: DatasetService,
private notificationService: NotificationService,
private dialog: MatDialog,
private dashboardStateService: DashboardStateService // To get columns for config
private dashboardStateService: DashboardStateService
) { }
ngOnInit(): void {
......@@ -45,11 +70,11 @@ export class DatasetWidgetLinkerComponent implements OnInit {
});
}
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);
onDatasetChange(): void {
this.widgetToPreview = null;
if (this.selectedDatasetId) {
this.loadLinkedWidgetsForDataset(this.selectedDatasetId);
this.dashboardStateService.selectDataset(this.selectedDatasetId);
} else {
this.linkedWidgets = [];
this.dashboardStateService.selectDataset(null);
......@@ -62,57 +87,73 @@ export class DatasetWidgetLinkerComponent implements OnInit {
});
}
linkWidget(widget: WidgetModel): void {
if (!this.selectedDataset) {
this.notificationService.warning('Warning', 'Please select a dataset first.');
viewWidget(widget: MenuItemsWidget): void {
this.widgetToPreview = widget;
}
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;
}
// 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.');
this.notificationService.info('Info', `'${widget.thName}' is already linked.`);
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
}
data: { widget: widget, availableColumns: availableColumns }
});
dialogRef.afterClosed().subscribe(resultConfig => {
if (resultConfig) {
const newLinkedWidget: MenuItemsWidget = {
companyId: 'DEMO', // Placeholder, ideally from user context
itemId: this.selectedDataset!.itemId,
widget: widget, // The base widget definition
// config: JSON.stringify(resultConfig), // Save configured object as string
config: '',
data: '', // Not used for linking, but part of model
perspective: '' // Not used for linking, but part of model
companyId: 'DEMO', // Placeholder
itemId: this.selectedDatasetId!,
widget: widget,
config: JSON.stringify(resultConfig),
data: '',
perspective: ''
};
this.mMenuitemsWidgetService.saveLinkedWidget(newLinkedWidget).subscribe(() => {
this.notificationService.success('Success', 'Widget linked successfully!');
this.loadLinkedWidgetsForDataset(this.selectedDataset!.itemId);
this.notificationService.success('Success', `'${widget.thName}' linked successfully!`);
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 {
type: 'link'
},
{
path: `/portal-manage/widget-management/linker`,
path: `/portal-manage/${appName}/widget-linker`,
title: 'เชื่อมโยงวิดเจ็ตกับชุดข้อมูล',
icon: '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