Commit b7474aa5 by Ooh-Ao

เชื่อมโยง

parent 9e7c3447
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map, catchError, tap } from 'rxjs/operators'; import { map, catchError } from 'rxjs/operators'; // Removed tap
import { MenuItemsWidget } from '../models/m-menuitems-widget.model'; import { MenuItemsWidget } from '../models/m-menuitems-widget.model';
import { environment } from '../../../environments/environment';
import { TokenService } from '../../shared/services/token.service'; // Import TokenService
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class MMenuitemsWidgetService { export class MMenuitemsWidgetService {
private dataUrl = 'assets/data/d-menuitems-widget.json'; private baseUrl = environment.url + 'mmenuitems-widget'; // Base API URL from user's provided endpoints
private linkedWidgetsCache: MenuItemsWidget[] = []; // In-memory cache for simulation
constructor(private http: HttpClient) { } constructor(private http: HttpClient, private tokenService: TokenService) { } // Inject TokenService
/** /**
* Gets all widget menu items and filters them by the provided datasetId. * Gets linked widgets for a specific dataset from the backend API.
* In a real application, this filtering would ideally be done on the backend. * Uses the /mmenuitems-widget/search endpoint.
* @param datasetId The ID of the dataset to filter widgets for. * @param datasetId The ID of the dataset to filter widgets for.
* @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[]> {
// Simulate fetching from backend or use cache if available const companyId = this.tokenService.getSelectCompany().companyId; // Get companyId from TokenService
if (this.linkedWidgetsCache.length > 0) { // Assuming the backend /search endpoint supports filtering by datasetId and companyId
return of(this.linkedWidgetsCache.filter(item => (item as any).datasetId === datasetId)); return this.http.get<any[]>(`${this.baseUrl}/search`, { params: { datasetId, companyId } }).pipe(
} else { map(items => items.map(item => new MenuItemsWidget(item))),
return this.http.get<any[]>(this.dataUrl).pipe( catchError(error => {
tap(items => { console.error('Error loading linked widgets:', error);
// Populate cache on first load return of([]); // Return an empty array on error
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. * Saves a linked widget to the backend API.
* Updates the in-memory cache and logs the action. * Uses the POST /mmenuitems-widget endpoint for both creation and update.
* @param menuItem The MenuItemsWidget to save. * @param menuItem The MenuItemsWidget to save.
* @returns An Observable of the saved MenuItemsWidget. * @returns An Observable of the saved MenuItemsWidget.
*/ */
saveLinkedWidget(menuItem: MenuItemsWidget): Observable<MenuItemsWidget> { saveLinkedWidget(menuItem: MenuItemsWidget): Observable<MenuItemsWidget> {
// Check if it already exists (for update scenario) // Use POST for both creation and update as per provided API.
const index = this.linkedWidgetsCache.findIndex(item => item.itemId === menuItem.itemId); return this.http.post<MenuItemsWidget>(this.baseUrl, menuItem).pipe(
if (index > -1) { catchError(error => {
this.linkedWidgetsCache[index] = menuItem; console.error('Error saving linked widget:', error);
console.log('Simulating update linked widget:', menuItem); throw error; // Re-throw to propagate error
} 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. * Deletes a linked widget by its itemId from the backend API.
* Updates the in-memory cache and logs the action. * Uses the DELETE /mmenuitems-widget endpoint, passing itemId as a query parameter.
* @param itemId The itemId of the MenuItemsWidget to delete. * @param itemId The itemId of the MenuItemsWidget to delete.
* @returns An Observable indicating success. * @returns An Observable indicating success.
*/ */
deleteLinkedWidget(itemId: string): Observable<any> { deleteLinkedWidget(itemId: string): Observable<any> {
this.linkedWidgetsCache = this.linkedWidgetsCache.filter(item => item.itemId !== itemId); // Assuming DELETE /mmenuitems-widget expects itemId as a query parameter
console.log('Simulating delete linked widget with ID:', itemId); return this.http.delete<any>(this.baseUrl, { params: { itemId } }).pipe(
// In a real app, this would return a status or confirmation. catchError(error => {
return of({ success: true }); console.error('Error deleting linked widget:', error);
throw error; // Re-throw to propagate error
})
);
} }
} }
/* Styles for dataset-widget-linker.component.scss */
:host {
display: block;
background-color: #f9fafb; /* Light background for the whole component */
padding: 1rem; /* Consistent padding around the component */
}
.p-4 {
padding: 1rem !important;
}
.sm\:p-6 {
@media (min-width: 640px) {
padding: 1.5rem !important;
}
}
.lg\:p-8 {
@media (min-width: 1024px) {
padding: 2rem !important;
}
}
/* Styling for the select dropdown */
#dataset-select {
border: 1px solid #d1d5db; /* border-gray-300 */
border-radius: 0.375rem; /* rounded-md */
padding-left: 0.75rem; /* pl-3 */
padding-right: 2.5rem; /* pr-10 */
padding-top: 0.5rem; /* py-2 */
padding-bottom: 0.5rem; /* py-2 */
font-size: 1rem; /* text-base */
line-height: 1.5rem;
width: 100%;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); /* shadow-sm */
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
&:focus {
outline: none;
border-color: #6366f1; /* focus:border-indigo-500 */
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.25); /* focus:ring-indigo-500 */
}
}
/* Styling for the widget list containers */
.border {
border: 1px solid #e5e7eb; /* border-gray-200 */
}
.rounded-lg {
border-radius: 0.5rem;
}
.shadow-sm {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.divide-y > :not([hidden]) ~ :not([hidden]) {
border-top-width: 1px;
border-color: #e5e7eb; /* divide-gray-200 */
}
/* Styling for list items */
ul[role="list"] li {
padding: 1rem; /* p-4 */
display: flex;
align-items: center;
justify-content: space-between;
background-color: #ffffff; /* bg-white */
transition: background-color 0.15s ease-in-out;
&:hover {
background-color: #f9fafb; /* hover:bg-gray-50 */
}
}
/* Styling for buttons */
button {
display: inline-flex;
align-items: center;
padding: 0.375rem 0.75rem; /* px-3 py-1.5 */
border-width: 1px;
border-color: transparent;
font-size: 0.75rem; /* text-xs */
font-weight: 500; /* font-medium */
border-radius: 0.375rem; /* rounded-md */
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
color: #ffffff; /* text-white */
transition: background-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
&:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.25); /* focus:ring-indigo-500 */
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.25); /* focus:ring-red-500 */
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.25), 0 0 0 3px rgba(239, 68, 68, 0.25); /* Combined for both colors */
}
}
.bg-indigo-600 {
background-color: #4f46e5;
&:hover {
background-color: #4338ca; /* hover:bg-indigo-700 */
}
}
.bg-red-600 {
background-color: #dc2626;
&:hover {
background-color: #b91c1c; /* hover:bg-red-700 */
}
}
...@@ -89,10 +89,11 @@ export class DatasetWidgetLinkerComponent implements OnInit { ...@@ -89,10 +89,11 @@ export class DatasetWidgetLinkerComponent implements OnInit {
dialogRef.afterClosed().subscribe(resultConfig => { dialogRef.afterClosed().subscribe(resultConfig => {
if (resultConfig) { if (resultConfig) {
const newLinkedWidget: MenuItemsWidget = { const newLinkedWidget: MenuItemsWidget = {
companyId: '1', // Placeholder, ideally from user context companyId: 'DEMO', // Placeholder, ideally from user context
itemId: `link-${this.selectedDataset!.itemId}-${widget.widgetId}`, itemId: this.selectedDataset!.itemId,
widget: widget, // The base widget definition widget: widget, // The base widget definition
config: JSON.stringify(resultConfig), // Save configured object as string // config: JSON.stringify(resultConfig), // Save configured object as string
config: '',
data: '', // Not used for linking, but part of model data: '', // Not used for linking, but part of model
perspective: '' // Not used for linking, but part of model perspective: '' // Not used for linking, but part of model
}; };
......
...@@ -5,6 +5,11 @@ ...@@ -5,6 +5,11 @@
<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> </div>
<!-- Add/Delete functionality removed as this now points to a real API --> <!-- 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">
Manage Dataset Widgets
</button>
</div>
</div> </div>
<!-- Registered Widgets List --> <!-- Registered Widgets List -->
......
...@@ -108,6 +108,12 @@ export class NavService implements OnDestroy { ...@@ -108,6 +108,12 @@ export class NavService implements OnDestroy {
title: 'คลังวิดเจ็ต', title: 'คลังวิดเจ็ต',
type: 'link' type: 'link'
}, },
{
path: `/portal-manage/widget-management/linker`,
title: 'เชื่อมโยงวิดเจ็ตกับชุดข้อมูล',
icon: 'link',
type: 'link'
},
] ]
}; };
} }
......
จุดเด่น:
* สถาปัตยกรรมยอดเยี่ยม: การออกแบบโดยใช้ DashboardStateService เป็นศูนย์กลางในการจัดการข้อมูล และมี BaseWidgetComponent เป็นคลาสพื้นฐานสำหรับวิดเจ็ตทั้งหมด เป็นรูปแบบที่ทันสมัย (Modern Architecture) ช่วยให้ประสิทธิภาพดี ลดการเรียก API ซ้ำซ้อน และง่ายต่อการบำรุงรักษาและเพิ่มวิดเจ็ตใหม่ๆ ในอนาคต
* การทำงานสมบูรณ์: ระบบแดชบอร์ดมีฟังก์ชันการทำงานที่ครบถ้วน ทั้งการสร้าง/ลบ/แก้ไขแดชบอร์ด, การเพิ่ม/ลบ/ปรับขนาดวิดเจ็ต, และการตั้งค่าวิดเจ็ตแต่ละตัวผ่านไดอะล็อก ซึ่งทั้งหมดทำงานบน ejs-dashboardlayout ของ Syncfusion ได้อย่างลงตัว
* ดีไซน์สวยงามและทันสมัย: การใช้ Tailwind CSS ทำให้ได้ UI ที่เป็น Card-based design ดูสะอาดตาและทันสมัย ในหน้าแสดงผล (Viewer) ได้มีการนำเส้นขอบและหัวข้อของ Panel ออก ทำให้วิดเจ็ตต่างๆ แสดงผลแบบไร้รอยต่อ (Seamless) ซึ่งสวยงามมากครับ
* ประสบการณ์ใช้งานที่ดี (UX): วิดเจ็ตต่างๆ มีสถานะ Loading และ Error แสดงผลชัดเจน เช่น KpiWidgetComponent จะแสดง Spinner ขณะรอข้อมูล และแสดงไอคอนพร้อมข้อความเมื่อมีข้อผิดพลาด ซึ่งเป็นสิ่งที่ดีต่อผู้ใช้งาน
ข้อเสนอแนะเพื่อการปรับปรุง:
ผมมีข้อเสนอแนะเล็กน้อย 2-3 จุดที่สามารถปรับปรุงเพิ่มเติมได้ครับ
1. เปิดการแจ้งเตือนเมื่อบันทึกสำเร็จ: ในไฟล์ dashboard-management.component.ts โค้ดที่ใช้แสดง NotificationService แจ้งเตือนผู้ใช้เมื่อบันทึก Layout หรือชื่อแดชบอร์ดสำเร็จ ถูกคอมเมนต์ปิดไว้ การเปิดใช้งานส่วนนี้จะทำให้ผู้ใช้ได้รับ Feedback ที่ชัดเจนว่าการบันทึกสำเร็จแล้ว
2. ลดความซ้ำซ้อนของโค้ด: widgetComponentMap ซึ่งทำหน้าที่จับคู่ชื่อคอมโพเนนต์กับคลาส มีอยู่ทั้งใน dashboard-management.component.ts และ dashboard-viewer.component.ts เราสามารถย้าย Map นี้ไปไว้ในไฟล์ที่ใช้ร่วมกัน (Shared file/service) เพื่อลดความซ้ำซ้อนและง่ายต่อการจัดการเมื่อมีวิดเจ็ตใหม่ๆ เพิ่มเข้ามา
3. ปรับปรุง UX ของหน้าแดชบอร์ดว่างๆ: เมื่อผู้ใช้สร้างแดชบอร์ดใหม่ หน้าจอจะว่างเปล่า เราอาจเพิ่มข้อความแนะนำ เช่น "ลากวิดเจ็ตจากแถบด้านข้างมาวางที่นี่" เพื่อแนะนำผู้ใช้ในขั้นตอนต่อไป
สรุปโดยรวม:
แดชบอร์ดของคุณทำงานได้อย่างสมบูรณ์ มีสถาปัตยกรรมที่แข็งแกร่ง และดีไซน์ที่สวยงามทันสมัย ข้อเสนอแนะของผมเป็นเพียงการปรับปรุงเล็กๆ น้อยๆ (Minor refinements) เพื่อให้ระบบสมบูรณ์แบบยิ่งขึ้นครับ
ผมสามารถช่วยคุณในการปรับปรุงข้อ 1 และ 2 ได้เลย หากคุณต้องการ ให้ผมเริ่มจากการเปิดใช้งานการแจ้งเตือนเมื่อบันทึกสำเร็จก่อนเลยไหมครับ?
เข้าใจแล้วครับ คุณต้องการระบบจัดการวิดเจ็ต 2 ส่วนหลักๆ คือ:
เข้าใจแล้วครับ คุณต้องการระบบจัดการวิดเจ็ต 2 ส่วนหลักๆ คือ:
1. หน้าลงทะเบียนวิดเจ็ตกลาง (Global Widget Registration):
* เป็นหน้ารวมสำหรับลงทะเบียนวิดเจ็ตพื้นฐานทั้งหมดที่มีในระบบ (Master List)
* มีฟังก์ชันสำหรับดูตัวอย่าง (Preview) วิดเจ็ตได้
* ใช้คอมโพเนนต์ widget-list.component.html
2. หน้าจัดการวิดเจ็ตสำหรับแต่ละ Dataset:
* เป็นหน้าที่ใช้เชื่อมโยงวิดเจ็ตจาก Master List (ข้อ 1) เข้ากับ Dataset ที่เลือก
* สามารถกำหนดค่าเริ่มต้น (Default Config) ของวิดเจ็ตสำหรับ Dataset นั้นๆ ได้โดยเฉพาะ
* ข้อมูลที่จัดการในหน้านี้คือข้อมูลที่ m-menuitems-widget.service.ts ที่เราเพิ่งสร้างไป จะนำไปใช้งาน
นี่เป็นฟีเจอร์ใหญ่ ผมจะวางแผนการทำงานดังนี้:
1. สร้างคอมโพเนนต์ใหม่:
* widget-list.component.ts และ .html สำหรับหน้าลงทะเบียนวิดเจ็ตกลาง
* dataset-widget-linker.component.ts และ .html สำหรับหน้าเชื่อมโยงวิดเจ็ตกับ Dataset
2. เพิ่ม Route: เพิ่มเส้นทางใน portal-manage.routes.ts เพื่อเข้าถึงหน้าจัดการวิดเจ็ตใหม่
3. จำลอง Backend:
* สร้าง master-widgets.json เพื่อจำลองฐานข้อมูลของวิดเจ็ตที่ลงทะเบียนแล้ว
* ปรับปรุง MMenuitemsWidgetService ให้สามารถ "บันทึก" ข้อมูลกลับไปยัง d-menuitems-widget.json ได้
4. พัฒนา Logic ของคอมโพเนนต์:
* `widget-list`: แสดงรายการวิดเจ็ต, เพิ่มวิดเจ็ตใหม่เข้าสู่ระบบ (บันทึกลง master-widgets.json)
* `dataset-widget-linker`: เลือก Dataset, แสดงรายการวิดเจ็ตที่ลงทะเบียนแล้วและวิดเจ็ตที่เชื่อมกับ Dataset, มี UI สำหรับการเชื่อมโยงและตั้งค่าเริ่มต้น
ผมจะเริ่มจากส่วนที่ 1 คือ การสร้างหน้าลงทะเบียนวิดเจ็ตกลาง (`widget-list.component`) ก่อนครับ
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