Commit e6b0a69d by Ooh-Ao

widgetslist form

parent bcdcfd00
......@@ -32,9 +32,9 @@ import {
LoadingIndicatorModel,
ExcelExportProperties,
Column,
ColumnMenuClickEventArgs,
GridModule
} from '@syncfusion/ej2-angular-grids';
import { ClickEventArgs, MenuEventArgs } from '@syncfusion/ej2-navigations';
import { Query } from '@syncfusion/ej2-data';
import { L10n, setCulture } from '@syncfusion/ej2-base';
import { NgbModal, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
......@@ -233,7 +233,7 @@ export class DatagridSyncfutionComponent implements OnInit {
this.sendNextPage.emit(args);
}
toolbarClick(args: any): void {
toolbarClick(args: ClickEventArgs): void {
if (args.item.id === 'Grid_excelexport') {
let exportProperties: ExcelExportProperties = {
columns: this.columns.map(col => ({
......@@ -332,12 +332,12 @@ export class DatagridSyncfutionComponent implements OnInit {
}
}
onColumnMenuClick(args: ColumnMenuClickEventArgs): void {
onColumnMenuClick(args: MenuEventArgs): void {
if (!args.item.id) { return; }
// เช็คว่าเป็น aggregate_... ไหม
if (args.item.id.startsWith('aggregate_')) {
const colField = (args.column as any)?.field;
const colField = (args as any)?.column?.field;
if (!colField) { return; }
// ดีบักดูว่า user คลิกอะไร
......
::ng-deep {
#dashboard_viewer.e-dashboardlayout .e-panel {
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
}
#dashboard_viewer.e-dashboardlayout .e-panel .e-panel-header {
display: none !important;
}
}
.dashboard-viewer-container {
width: 100%;
height: 100%;
}
......@@ -4,12 +4,21 @@
<form [formGroup]="widgetForm" (ngSubmit)="onSubmit()" class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<!-- Widget Name -->
<!-- Widget Name (Thai) -->
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="name">
Widget Name
<label class="block text-gray-700 text-sm font-bold mb-2" for="thName">
Widget Name (Thai)
</label>
<input formControlName="name" id="name" type="text" placeholder="e.g., Company News"
<input formControlName="thName" id="thName" type="text" placeholder="เช่น ข่าวสารบริษัท"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<!-- Widget Name (English) -->
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="engName">
Widget Name (English)
</label>
<input formControlName="engName" id="engName" type="text" placeholder="e.g., Company News"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
......
import { Component, OnInit, Type } from '@angular/core';
import { Component, OnInit, Type, Inject } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { NgComponentOutlet } from '@angular/common';
import { Store } from '@ngrx/store';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
// import * as DashboardActions from '../state/dashboard.actions';
import { WidgetModel } from '../models/widgets.model';
......@@ -25,13 +25,12 @@ import { SyncfusionChartWidgetComponent } from '../widgets/syncfusion-chart-widg
@Component({
selector: 'app-widget-form',
standalone: true,
imports: [CommonModule, RouterModule, ReactiveFormsModule, NgComponentOutlet, CompanyInfoWidgetComponent, HeadcountWidgetComponent, AttendanceOverviewWidgetComponent, PayrollSummaryWidgetComponent, EmployeeDirectoryWidgetComponent, KpiWidgetComponent, WelcomeWidgetComponent, ChartWidgetComponent, QuickLinksWidgetComponent, SyncfusionDatagridWidgetComponent, SyncfusionPivotWidgetComponent, SyncfusionChartWidgetComponent],
imports: [CommonModule, ReactiveFormsModule, NgComponentOutlet, CompanyInfoWidgetComponent, HeadcountWidgetComponent, AttendanceOverviewWidgetComponent, PayrollSummaryWidgetComponent, EmployeeDirectoryWidgetComponent, KpiWidgetComponent, WelcomeWidgetComponent, ChartWidgetComponent, QuickLinksWidgetComponent, SyncfusionDatagridWidgetComponent, SyncfusionPivotWidgetComponent, SyncfusionChartWidgetComponent],
templateUrl: './widget-form.component.html',
})
export class WidgetFormComponent implements OnInit {
widgetForm!: FormGroup;
isNew = true;
widgetId: string | null = null;
appName: string = '';
previewComponentType: Type<any> | null = null; // For dynamic preview
......@@ -54,17 +53,17 @@ export class WidgetFormComponent implements OnInit {
constructor(
private fb: FormBuilder,
private store: Store,
private route: ActivatedRoute,
private router: Router
public dialogRef: MatDialogRef<WidgetFormComponent>,
@Inject(MAT_DIALOG_DATA) public data: { widget: WidgetModel, isNew: boolean }
) { }
ngOnInit(): void {
this.widgetId = this.route.snapshot.paramMap.get('widgetId');
this.isNew = this.data.isNew;
this.widgetForm = this.fb.group({
id: [null],
name: ['', Validators.required],
thName: ['', Validators.required],
engName: ['', Validators.required],
component: ['', Validators.required],
cols: [1, [Validators.required, Validators.min(1)]],
rows: [1, [Validators.required, Validators.min(1)]],
......@@ -73,15 +72,9 @@ export class WidgetFormComponent implements OnInit {
data: [null]
});
if (this.widgetId && this.widgetId !== 'new') {
this.isNew = false;
// this.store.dispatch(DashboardActions.loadWidgets());
// this.store.select(DashboardSelectors.selectWidgetById(this.widgetId)).subscribe(widget => {
// if (widget) {
// this.widgetForm.patchValue(widget);
// this.updatePreview(widget.component);
// }
// });
if (!this.isNew && this.data.widget) {
this.widgetForm.patchValue(this.data.widget);
this.updatePreview(this.data.widget.component);
}
// Subscribe to component name changes for live preview
......@@ -98,17 +91,10 @@ export class WidgetFormComponent implements OnInit {
if (this.widgetForm.invalid) {
return;
}
const widgetData: WidgetModel = this.widgetForm.value;
if (this.isNew) {
// this.store.dispatch(DashboardActions.addWidget({ widget: widgetData }));
} else {
// this.store.dispatch(DashboardActions.updateWidget({ widget: widgetData }));
}
this.router.navigate(['/portal-manage/widget-management']);
this.dialogRef.close(this.widgetForm.value);
}
cancel(): void {
this.router.navigate(['/portal-manage', this.appName, 'widget-warehouse']);
this.dialogRef.close();
}
}
<div class="p-4 sm:p-6 lg:p-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex sm:items-center justify-between">
<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>
<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 class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<button routerLink="/portal-manage/widget-management/linker" type="button" class="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto">
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none flex space-x-2">
<button
(click)="openAddWidgetDialog()"
type="button"
class="btn btn-green sm:w-auto"
>
Add New Widget
</button>
<button
routerLink="/portal-manage/widget-management/linker"
type="button"
class="btn btn-indigo sm:w-auto"
>
Manage Dataset Widgets
</button>
</div>
</div>
<!-- Search/Filter Input -->
<div class="mt-6 relative rounded-md shadow-sm">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<!-- Assuming Boxicons or RemixIcons are available. Using a generic search icon class. -->
<i class="bx bx-search text-gray-400"></i>
</div>
<input
type="text"
[(ngModel)]="searchTerm"
(input)="applyFilter()"
placeholder="Search widgets..."
class="block w-full rounded-md border-gray-300 pl-10 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2"
/>
</div>
<!-- Registered Widgets List -->
<div class="mt-8 flex flex-col">
<div class="mt-8 bg-white shadow-lg rounded-lg p-6">
<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">
<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="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>
<span class="sr-only">Edit</span>
<span class="sr-only">Delete</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
<tr *ngFor="let widget of registeredWidgets">
<tr *ngFor="let widget of filteredWidgets" class="hover:bg-gray-50">
<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="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>
<span
class="inline-flex rounded-full bg-blue/50 px-2 text-xs font-semibold leading-5 text-white"
>{{ 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 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"
>
<button
(click)="openEditWidgetDialog(widget)"
class="btn-text-indigo mr-4"
>
Edit
</button>
<button
(click)="confirmDeleteWidget(widget)"
class="btn btn-text-red"
>
Delete
</button>
</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 *ngIf="filteredWidgets.length === 0">
<td
colspan="5"
class="whitespace-nowrap px-3 py-4 text-lg font-semibold text-center text-gray-500"
>
No widgets found.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
/* Add your custom styles for the widget list component here */
/* Base button styles */
.btn {
@apply inline-flex items-center justify-center rounded-md border border-transparent px-4 py-2 text-sm font-medium shadow-sm;
}
/* Solid button color modifiers */
.btn-green {
@apply bg-green text-white hover:bg-green focus:outline-none focus:ring-2 focus:ring-green focus:ring-offset-2;
}
.btn-indigo {
@apply bg-indigo text-white hover:bg-indigo focus:outline-none focus:ring-2 focus:ring-indigo focus:ring-offset-2;
}
/* Text-only button color modifiers */
.btn-text-red {
@apply text-red hover:text-red;
}
.btn-text-indigo {
@apply text-indigo hover:text-indigo;
}
......@@ -20,6 +20,8 @@ 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';
import { WidgetFormComponent } from './widget-form.component';
import { ConfirmModalComponent } from '../../confirm-modal/confirm-modal.component';
@Component({
selector: 'app-widget-list',
......@@ -32,11 +34,14 @@ import { SimpleKpiWidgetComponent } from '../widgets/simple-kpi-widget/simple-kp
// Import widgets to be available for NgComponentOutlet
CompanyInfoWidgetComponent, HeadcountWidgetComponent, AttendanceOverviewWidgetComponent, PayrollSummaryWidgetComponent, EmployeeDirectoryWidgetComponent, WelcomeWidgetComponent, QuickLinksWidgetComponent, SyncfusionDatagridWidgetComponent, SyncfusionPivotWidgetComponent, SyncfusionChartWidgetComponent, SimpleKpiWidgetComponent
],
templateUrl: './widget-list.component.html'
templateUrl: './widget-list.component.html',
styleUrls: ['./widget-list.component.scss']
})
export class WidgetListComponent implements OnInit {
public registeredWidgets: WidgetModel[] = [];
public filteredWidgets: WidgetModel[] = [];
public searchTerm: string = '';
// This map is crucial for mapping component string names to actual component types for previewing.
private widgetComponentMap: { [key: string]: Type<any> } = {
......@@ -66,10 +71,88 @@ export class WidgetListComponent implements OnInit {
loadRegisteredWidgets(): void {
this.widgetService.getListWidgets().subscribe(widgets => {
this.registeredWidgets = widgets;
this.applyFilter(); // Apply filter after loading widgets
});
}
applyFilter(): void {
if (!this.searchTerm) {
this.filteredWidgets = [...this.registeredWidgets];
return;
}
const lowerCaseSearchTerm = this.searchTerm.toLowerCase();
this.filteredWidgets = this.registeredWidgets.filter(widget =>
(widget.thName && widget.thName.toLowerCase().includes(lowerCaseSearchTerm)) ||
(widget.engName && widget.engName.toLowerCase().includes(lowerCaseSearchTerm)) ||
(widget.component && widget.component.toLowerCase().includes(lowerCaseSearchTerm))
);
}
getComponentType(componentName: string): Type<any> {
return this.widgetComponentMap[componentName];
}
openAddWidgetDialog(): void {
const dialogRef = this.dialog.open(WidgetFormComponent, {
width: '800px',
data: { isNew: true } // Indicate that it's a new widget
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Assuming result is the new widget data
this.widgetService.createWidget(result).subscribe(() => {
this.notificationService.success('Success','Widget added successfully!');
this.loadRegisteredWidgets(); // Reload the list
}, error => {
this.notificationService.error('Error','Failed to add widget.');
console.error('Add widget error:', error);
});
}
});
}
openEditWidgetDialog(widget: WidgetModel): void {
const dialogRef = this.dialog.open(WidgetFormComponent, {
width: '800px',
data: { widget: { ...widget }, isNew: false } // Pass a copy of the widget data and indicate it's an edit
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Assuming result is the updated widget data
this.widgetService.createWidget(result).subscribe(() => {
this.notificationService.success('Success','Widget updated successfully!');
this.loadRegisteredWidgets(); // Reload the list
}, error => {
this.notificationService.error('Error','Failed to update widget.');
console.error('Update widget error:', error);
});
}
});
}
confirmDeleteWidget(widget: WidgetModel): void {
const dialogRef = this.dialog.open(ConfirmModalComponent, {
width: '400px',
data: { title: 'Confirm Deletion', message: `Are you sure you want to delete the widget \'${widget.thName || widget.engName}\'?` }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// User confirmed deletion
if (widget.widgetId) {
this.widgetService.deleteWidget(widget).subscribe(() => {
this.notificationService.success('Success','Widget deleted successfully!');
this.loadRegisteredWidgets(); // Reload the list
}, error => {
this.notificationService.error('Error','Failed to delete widget.');
console.error('Delete widget error:', error);
});
} else {
this.notificationService.error('Error', 'Widget ID is missing. Cannot delete.');
}
}
});
}
}
......@@ -5,7 +5,7 @@
<div [style.background]="backgroundColor" class="relative mx-4 -mt-4 rounded-xl bg-clip-border text-white shadow-lg shadow-blue-500/40 p-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<i *ngIf="icon" [style.color]="iconColor" [class]="'bi bi-' + icon + ' text-3xl'"></i>
<i *ngIf="icon" [style.color]="iconColor" [class]="'bi ' + icon + ' text-3xl'"></i>
<h4 class="text-lg font-semibold truncate">{{ title }}</h4>
</div>
<!-- Removed trendValue display -->
......
import { Component, ViewChild, OnInit, OnDestroy, AfterViewInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { GridModule, PageService, SortService, FilterService, GroupService, ToolbarService, ExcelExportService, PdfExportService, GridComponent, ToolbarItems, SearchSettingsModel, GroupSettingsModel, FilterSettingsModel, SelectionSettingsModel, AggregateService, ColumnMenuService, DetailRowService, ReorderService, EditService, ColumnMenuClickEventArgs, PdfExportProperties, ExcelExportProperties, LoadingIndicatorModel, Column,SearchService } from '@syncfusion/ej2-angular-grids';
import { GridModule, PageService, SortService, FilterService, GroupService, ToolbarService, ExcelExportService, PdfExportService, GridComponent, ToolbarItems, SearchSettingsModel, GroupSettingsModel, FilterSettingsModel, SelectionSettingsModel, AggregateService, ColumnMenuService, DetailRowService, ReorderService, EditService, PdfExportProperties, ExcelExportProperties, LoadingIndicatorModel, Column,SearchService } from '@syncfusion/ej2-angular-grids';
import { MenuEventArgs } from '@syncfusion/ej2-navigations';
import { ClickEventArgs } from '@syncfusion/ej2-navigations';
import { DataManager, Query } from '@syncfusion/ej2-data';
import { DashboardStateService } from '../../services/dashboard-state.service';
......@@ -253,11 +254,11 @@ export class SyncfusionDatagridWidgetComponent extends BaseWidgetComponent imple
// or can be used for custom logic.
}
onColumnMenuClick(args: ColumnMenuClickEventArgs): void {
onColumnMenuClick(args: MenuEventArgs): void {
if (!args.item.id) { return; }
if (args.item.id.startsWith('aggregate_')) {
const colField = (args.column as any)?.field;
const colField = (args as any)?.column?.field;
if (!colField) { return; }
const selectedAgg = args.item.id.split('_')[1];
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -7,9 +7,9 @@ const plugin = require("tailwindcss/plugin");
module.exports = {
darkMode: "class",
content: ["./src/**/*.{html,ts}",
'./src/app/**/**/**/*.{html,ts}',
'./src/app/**/**/**/**/*.{html,ts}',
content: ["./src/**/*.{html,ts,scss}",
'./src/app/**/**/**/*.{html,ts,scss}',
'./src/app/**/**/**/**/*.{html,ts,scss}',
'node_modules/preline/dist/*.js'],
theme: {
screens: {
......
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