Commit affd9160 by Ooh-Ao

config

parent cd412f68
...@@ -624,11 +624,17 @@ export class DashboardManagementComponent implements OnInit { ...@@ -624,11 +624,17 @@ export class DashboardManagementComponent implements OnInit {
mapWidgetsToPanels(widgets: WidgetModel[]): DashboardPanel[] { mapWidgetsToPanels(widgets: WidgetModel[]): DashboardPanel[] {
return widgets.map(widget => { return widgets.map(widget => {
// Always pass the entire widget.config as the input to the component let widgetConfig: any = {};
// Ensure widget.config exists, initialize if not. if (typeof widget.config === 'string') {
const widgetConfig = widget.config || {}; try {
widgetConfig = JSON.parse(widget.config);
} catch (e) {
console.error('Error parsing widget config string:', widget.config, e);
widgetConfig = {}; // Default to empty object on error
}
} else {
widgetConfig = widget.config || {};
}
return { return {
id: widget.widgetId, id: widget.widgetId,
...@@ -852,6 +858,7 @@ export class DashboardManagementComponent implements OnInit { ...@@ -852,6 +858,7 @@ export class DashboardManagementComponent implements OnInit {
openWidgetConfigDialog(panel: PanelModel & { componentType: Type<any>, componentInputs?: { [key: string]: any }, originalWidget: WidgetModel }): void { openWidgetConfigDialog(panel: PanelModel & { componentType: Type<any>, componentInputs?: { [key: string]: any }, originalWidget: WidgetModel }): void {
const widget = panel.originalWidget; const widget = panel.originalWidget;
console.log('Opening config dialog for widget:', widget);
this.dashboardStateService.selectedDataset$.subscribe((selectedDataset: SelectedDataset | null) => { this.dashboardStateService.selectedDataset$.subscribe((selectedDataset: SelectedDataset | null) => {
const availableColumns = selectedDataset ? selectedDataset.columns : []; const availableColumns = selectedDataset ? selectedDataset.columns : [];
......
...@@ -129,7 +129,18 @@ export class DashboardViewerComponent implements OnInit { ...@@ -129,7 +129,18 @@ export class DashboardViewerComponent implements OnInit {
mapWidgetsToPanels(widgets: WidgetModel[]): DashboardPanel[] { mapWidgetsToPanels(widgets: WidgetModel[]): DashboardPanel[] {
return widgets.map(widget => { return widgets.map(widget => {
const widgetConfig = widget.config || {}; let widgetConfig: any = {};
if (typeof widget.config === 'string') {
try {
widgetConfig = JSON.parse(widget.config);
} catch (e) {
console.error('Error parsing widget config string:', widget.config, e);
widgetConfig = {}; // Default to empty object on error
}
} else {
widgetConfig = widget.config || {};
}
return { return {
id: widget.widgetId, id: widget.widgetId,
header: widget.thName, header: widget.thName,
......
...@@ -26,22 +26,38 @@ export class SimpleKpiWidgetComponent extends BaseWidgetComponent { ...@@ -26,22 +26,38 @@ export class SimpleKpiWidgetComponent extends BaseWidgetComponent {
this.unit = this.config.unit || ''; this.unit = this.config.unit || '';
this.trend = this.config.trend || 'neutral'; this.trend = this.config.trend || 'neutral';
this.trendValue = this.config.trendValue || ''; this.trendValue = this.config.trendValue || '';
this.value = '...'; // Loading indicator this.value = '-'; // Initial state before data loads
} }
onDataUpdate(data: any[]): void { onDataUpdate(data: any[]): void {
if (data.length > 0) { console.log('SimpleKpiWidget onDataUpdate config:', this.config);
let kpiValue = 0; // Handle count aggregation separately as it doesn't need a valueField
if (this.config.aggregation === 'count') { if (this.config.aggregation === 'count') {
kpiValue = data.length; this.value = (data?.length || 0).toLocaleString();
} else if (this.config.aggregation === 'sum') { return;
kpiValue = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0);
} else {
// Default to first value if no aggregation
kpiValue = data[0][this.config.valueField];
}
this.value = kpiValue.toLocaleString();
} }
// For other aggregations, valueField is required
if (!this.config.valueField) {
this.value = 'N/A'; // Indicate a configuration error
console.error('SimpleKpiWidget Error: valueField is not configured for this widget.', this.config);
return;
}
// If data is empty, result is 0
if (!data || data.length === 0) {
this.value = '0';
return;
}
let kpiValue = 0;
if (this.config.aggregation === 'sum') {
kpiValue = data.reduce((sum, item) => sum + (item[this.config.valueField] || 0), 0);
} else {
// Default to first value if no aggregation is specified
kpiValue = data[0][this.config.valueField] || 0;
}
this.value = kpiValue.toLocaleString();
} }
onReset(): void { onReset(): void {
......
1. Global Filtering (การกรองข้อมูลทั้งหน้า): ปัจจุบัน Slicer widget ยังไม่มีผลกับ widget อื่นๆ เราสามารถพัฒนาให้ DashboardStateService รับค่าจาก Slicer แล้วนำไปกรองข้อมูลชุดหลักได้ ซึ่งจะทำให้เมื่อผู้ใช้เลือกค่าใน Slicer วิดเจ็ตอื่นๆ ทั้งหมดในหน้าก็จะอัปเดตตามไปด้วย เป็นการสร้าง dashboard ที่ interactive อย่างแท้จริง
1. Global Filtering (การกรองข้อมูลทั้งหน้า): ปัจจุบัน Slicer widget ยังไม่มีผลกับ widget อื่นๆ เราสามารถพัฒนาให้ DashboardStateService รับค่าจาก Slicer แล้วนำไปกรองข้อมูลชุดหลักได้ ซึ่งจะทำให้เมื่อผู้ใช้เลือกค่าใน Slicer วิดเจ็ตอื่นๆ ทั้งหมดในหน้าก็จะอัปเดตตามไปด้วย เป็นการสร้าง dashboard ที่ interactive อย่างแท้จริง
2. ปรับปรุงโครงสร้างเชิงสถาปัตยกรรม:
* Widget Registry Service: สร้าง Service กลางสำหรับลงทะเบียนวิดเจ็ต เพื่อลดการแก้ไขโค้ดใน dashboard-management.component ทุกครั้งที่เพิ่มวิดเจ็ตใหม่ 2. บันทึกการตั้งค่าแยกราย Widget: ตอนนี้การตั้งค่าของ widget (เช่น การเลือกคอลัมน์ในตาราง) จะถูกบันทึกรวมไปกับ layout เราสามารถพัฒนาให้ผู้ใช้สามารถบันทึกการตั้งค่าเฉพาะของแต่ละ widget ได้เอง ซึ่งจะเพิ่มความยืดหยุ่นให้ผู้ใช้มากขึ้น
* Dynamic Configuration Forms: แยกฟอร์มการตั้งค่าของแต่ละวิดเจ็ตออกจาก widget-config.component เพื่อให้ดูแลรักษาง่ายขึ้น
3. Real-Time Updates: สำหรับข้อมูลที่ต้องการความสดใหม่ตลอดเวลา เช่นข้อมูลจากระบบ IoT หรือข้อมูลทางการเงิน เราสามารถใช้เทคโนโลยีอย่าง WebSockets เพื่อผลัก (push) ข้อมูลใหม่มาที่หน้า dashboard ได้ทันทีโดยไม่ต้องรอให้ผู้ใช้รีเฟรช
4. เพิ่มประสิทธิภาพสำหรับข้อมูลขนาดใหญ่: หากในอนาคตต้องแสดงผลข้อมูลจำนวนมหาศาล (หลักแสนหรือล้านรายการ) อาจพิจารณาปรับปรุงการดึงข้อมูลให้เป็นการแบ่งหน้า (Pagination) จากฝั่ง API หรือใช้เทคนิค Virtual Scrolling กับตาราง เพื่อลดภาระการแสดงผลของฝั่งเบราว์เซอร์
ข้อเสนอแนะเหล่านี้เป็นเพียงแนวทางในการพัฒนาต่อยอดครับ ซึ่งจะช่วยให้ระบบ dashboard ของคุณมีความสามารถสูงขึ้นและตอบโจทย์ผู้ใช้ได้ดียิ่งขึ้นในอนาคตครับ
1. ปรับโครงสร้างฟอร์มตั้งค่าให้เป็นแบบ Dynamic (Dynamic Forms)
* ปัญหาปัจจุบัน: WidgetConfigComponent ที่ใช้แสดงฟอร์มตั้งค่าในปัจจุบัน น่าจะมีการใช้ *ngIf หรือ *ngSwitch เพื่อสลับฟอร์มไปตามประเภทของวิดเจ็ต ซึ่งเมื่อมีวิดเจ็ตประเภทใหม่ๆ เพิ่มขึ้น จะต้องกลับมาแก้ไขที่ไฟล์นี้เสมอ ทำให้ดูแลรักษายากและขยายระบบได้ช้า
* ข้อเสนอแนะ: ควรปรับสถาปัตยกรรมให้เป็นแบบ Dynamic Form Architecture
* สร้าง Config Component แยกสำหรับวิดเจ็ตแต่ละประเภทไปเลย เช่น BarChartConfigComponent, KpiConfigComponent เป็นต้น
* จากนั้นใน WidgetConfigComponent หลัก ให้ใช้ ngComponentOutlet เพื่อโหลดฟอร์มตั้งค่าที่ถูกต้องขึ้นมาแสดงผลตามประเภทของวิดเจ็ตที่ถูกส่งเข้ามา
* ผลลัพธ์: วิธีนี้จะทำให้โค้ดแยกส่วนกันชัดเจน เมื่อต้องการเพิ่มวิดเจ็ตใหม่ ก็แค่สร้าง Config Component ใหม่สำหรับวิดเจ็ตนั้นๆ โดยไม่ต้องไปยุ่งกับโค้dเดิม ทำให้ง่ายต่อการพัฒนาและดูแลรักษาในระยะยาว
2. เพิ่มระบบ Live Preview ขณะตั้งค่า
* ปัญหาปัจจุบัน: ผู้ใช้ต้องกดบันทึกและปิดหน้าต่างตั้งค่าก่อน จึงจะเห็นผลลัพธ์การเปลี่ยนแปลงบนวิดเจ็ตจริง
* ข้อเสนอแนะ: ควรทำให้ผู้ใช้เห็นผลลัพธ์ทันที (Live Preview)
* ในขณะที่ผู้ใช้กำลังแก้ไขค่าในฟอร์มตั้งค่า ให้ฟอร์มส่ง event การเปลี่ยนแปลงออกมา
* หน้า dashboard-management จะดักจับ event นี้ แล้วอัปเดตการแสดงผลของวิดเจ็ตนั้นๆ ในพื้นหลังทันที
* ผลลัพธ์: ผู้ใช้จะเห็นภาพสุดท้ายของวิดเจ็ตไปพร้อมๆ กับการตั้งค่า ทำให้ใช้งานง่ายและสะดวกขึ้นมาก
3. ทำให้การเลือกฟิลด์ข้อมูล (Field Selection) เป็นมิตรมากขึ้น
* ปัญหาปัจจุบัน: Dropdown สำหรับเลือกฟิลด์ข้อมูล แสดงชื่อคอลัมน์ดิบจากฐานข้อมูล (เช่น salesAmount, emp_id) ซึ่งอาจไม่เป็นมิตรกับผู้ใช้ทั่วไป
* ข้อเสนอแนะ: ควรมีชั้นของ Metadata หรือ Data Dictionary เข้ามาช่วย
* ในข้อมูล Dataset ที่ส่งมาจาก API ควรมี property เพิ่มสำหรับ "ชื่อที่ใช้แสดงผล" (Display Name) ของแต่ละคอลัมน์มาด้วย เช่น {"field": "salesAmount", "displayName": "ยอดขายรวม"}
* ในหน้าตั้งค่า ให้ Dropdown แสดง "ชื่อที่ใช้แสดงผล" ที่สวยงาม แต่ตอนบันทึกให้เก็บค่า field ที่เป็นชื่อดิบไว้เหมือนเดิม
* ผลลัพธ์: ผู้ใช้ทางธุรกิจหรือผู้ใช้ทั่วไปจะสามารถตั้งค่าวิดเจ็ตได้เองง่ายขึ้นมาก
4. ระบบบันทึกการตั้งค่าเป็น Template (ขั้นสูง)
* ปัญหาปัจจุบัน: การตั้งค่าวิดเจ็ตจะผูกอยู่กับ dashboard นั้นๆ เท่านั้น หากผู้ใช้ตั้งค่า KPI ที่ซับซ้อนและมีประโยชน์มากๆ ก็ไม่สามารถนำไปใช้ซ้ำใน dashboard อื่นได้
* ข้อเสนอแนะ: สร้างฟีเจอร์ "บันทึกการตั้งค่า" หรือ "Widget Template"
* เพิ่มปุ่มให้ผู้ใช้สามารถ "บันทึกการตั้งค่านี้เป็น Template" ได้
* เมื่อผู้ใช้จะเพิ่มวิดเจ็ตใหม่ จะมีตัวเลือกระหว่าง "วิดเจ็ตมาตรฐาน" กับ "Template ที่บันทึกไว้"
* ผลลัพธ์: เพิ่มความสามารถในการนำกลับมาใช้ใหม่ (Reusability) ได้อย่างมหาศาล ลดเวลาในการตั้งค่า dashboard ที่มีลักษณะคล้ายๆ กัน
การปรับปรุงตามข้อเสนอแนะเหล่านี้ จะช่วยยกระดับประสบการณ์การใช้งาน (UX) และการดูแลรักษาระบบ (Maintainability) ได้อย่างมากครับ
1. ด้าน Layout และการตอบสนอง (Responsiveness)
* กำหนดความกว้างสูงสุด (Max Width): ในหน้าจอขนาดใหญ่มากๆ (เช่น จอ Ultrawide) layout ปัจจุบันอาจดูกว้างเกินไป ทำให้การอ่านข้อมูลต้องกวาดสายตาเยอะ ผมแนะนำให้กำหนดความกว้างสูงสุดของ content หลัก (เช่น max-w-7xl ใน Tailwind) และจัดให้อยู่ตรงกลาง (mx-auto) จะช่วยให้การแสดงผลดูลงตัวและอ่านง่ายขึ้นบนจอขนาดใหญ่
* ปรับ Layout สำหรับมือถือโดยเฉพาะ: บนหน้าจอมือถือที่มีพื้นที่จำกัด เราสามารถปรับการแสดงผลของบางวิดเจ็ตให้เรียบง่ายขึ้นได้ เช่น สำหรับวิดเจ็ตประเภทกราฟ อาจซ่อนคำอธิบาย (Legend) หรือชื่อแกน (Axes Title) ไปเมื่อแสดงผลบนจอมือถือ (ใช้คลาส hidden sm:block ของ Tailwind) เพื่อให้มีพื้นที่แสดงผลตัวกราฟได้มากขึ้น
* เพิ่มตัวเลือกความหนาแน่น (Dashboard Density): เพิ่มปุ่มให้ผู้ใช้สามารถสลับระหว่างมุมมอง "สบายตา" (Comfortable) กับ "กระทัดรัด" (Compact) ได้ โดยการปรับค่า cellSpacing ของ ejs-dashboardlayout ซึ่งจะทำให้ผู้ใช้ที่ต้องการเห็นวิดเจ็ตจำนวนมากในหน้าจอเดียวสามารถทำได้
2. ด้านสีสันและธีม (Color & Theme)
* ใช้ CSS Variables สำหรับการทำ Theming: ตอนนี้สีบางส่วนถูก hardcode ไว้ในโค้ด (เช่น bg-blue-500) ซึ่งทำให้การเปลี่ยนธีมสีโดยรวมทำได้ยาก ผมแนะนำให้ประกาศชุดสีหลักของแอปพลิเคชันด้วย CSS Variables ในไฟล์ styles.scss เช่น:
1 :root {
2 --primary-color: #4f46e5; /* สีหลัก */
3 --background-color: #f3f4f6; /* สีพื้นหลัง */
4 --card-background: #ffffff; /* สีพื้นหลังการ์ด */
5 --text-primary: #1f2937; /* สีตัวอักษรหลัก */
6 }
จากนั้นจึงนำชื่อตัวแปรเหล่านี้ไปใช้ใน tailwind.config.js และในคลาส CSS อื่นๆ วิธีนี้จะทำให้เราสามารถเปลี่ยนธีมสีของทั้งแอปพลิเคชันได้จากที่เดียว และยังเป็นรากฐานสำคัญในการทำ Dark Mode ในอนาคตอีกด้วย
* สร้างชุดสีที่สอดคล้องกัน (Consistent Palette): แนะนำให้เลือกใช้ชุดสีหลัก (Primary Color) สำหรับปุ่มหรือส่วนที่โต้ตอบได้, สีรอง (Accent Color) สำหรับเน้นจุดที่สำคัญ และใช้สีเทาในเฉดต่างๆ สำหรับข้อความและพื้นหลัง เพื่อให้งานออกแบบโดยรวมดูเป็นมืออาชีพและสบายตา
3. ด้านความลื่นไหลและอนิเมชัน (Smoothness & Micro-interactions)
* เพิ่ม Animation ตอนโหลด Widget: ปัจจุบันเมื่อวิดเจ็ตโหลดเสร็จ มันจะปรากฏขึ้นมาทันที ซึ่งดูแข็งกระด้างไปเล็กน้อย เราสามารถใช้ @angular/animations เพื่อเพิ่มอนิเมชันง่ายๆ เช่น fade-in (ค่อยๆ ปรากฏ) หรือ slide-up (เลื่อนขึ้นมา) ตอนที่วิดเจ็ตแสดงผล จะทำให้หน้าจอโดยรวมดูมีชีวิตชีวาและน่าใช้งานขึ้นมาก
* ใช้ Skeleton Loaders: เพื่อยกระดับประสบการณ์ผู้ใช้ไปอีกขั้น แทนที่จะแสดง Spinner หมุนๆ ในการ์ดว่างๆ ขณะรอข้อมูล ผมแนะนำให้ใช้ Skeleton Loader ซึ่งเป็น UI ที่เป็นโครงร่างของข้อมูลที่จะโหลดขึ้นมา (เช่น เป็นแถบสีเทาๆ แทนข้อความ, เป็นวงกลมสีเทาแทนกราฟวงกลม) วิธีนี้จะทำให้ผู้ใช้รู้สึกว่าแอปตอบสนองได้เร็วขึ้นและลดความรู้สึกในการรอ
* เพิ่ม Transition ให้กับปุ่มและเมนู: เพิ่ม transition property ใน CSS ให้กับองค์ประกอบที่ผู้ใช้สามารถโต้ตอบได้ เช่น ปุ่ม, ลิงก์, หรือการ์ดวิดเจ็ต เมื่อผู้ใช้นำเมาส์ไปชี้ (hover) การเปลี่ยนแปลงของสีหรือเงาจะดูนุ่มนวลขึ้น ไม่กระตุก
การปรับปรุงตามข้อเสนอแนะเหล่านี้ จะช่วยขัดเกลาให้หน้าจอ dashboard ของคุณดูสวยงาม ทันสมัย และให้ประสบการณ์การใช้งานที่ยอดเยี่ยมเทียบเท่ากับเว็บแอปพลิเคชันชั้นนำได้เลยครับ
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