Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
portal-apps-manage
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Registry
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
angular
portal-apps-manage
Commits
affd9160
Commit
affd9160
authored
Sep 02, 2025
by
Ooh-Ao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
config
parent
cd412f68
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
140 additions
and
22 deletions
+140
-22
dashboard-management.component.ts
...ge/dashboard-management/dashboard-management.component.ts
+12
-5
dashboard-viewer.component.ts
...tal-manage/dashboard-viewer/dashboard-viewer.component.ts
+12
-1
simple-kpi-widget.component.ts
.../widgets/simple-kpi-widget/simple-kpi-widget.component.ts
+28
-12
ข้อเสนอแนะเพื่อการปรับปรุง.txt
ข้อเสนอแนะเพื่อการปรับปรุง.txt
+88
-4
No files found.
src/app/portal-manage/dashboard-management/dashboard-management.component.ts
View file @
affd9160
...
@@ -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
:
[];
...
...
src/app/portal-manage/dashboard-viewer/dashboard-viewer.component.ts
View file @
affd9160
...
@@ -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
,
...
...
src/app/portal-manage/widgets/simple-kpi-widget/simple-kpi-widget.component.ts
View file @
affd9160
...
@@ -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
{
...
...
ข้อเสนอแนะเพื่อการปรับปรุง.txt
View file @
affd9160
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 ของคุณดูสวยงาม ทันสมัย และให้ประสบการณ์การใช้งานที่ยอดเยี่ยมเทียบเท่ากับเว็บแอปพลิเคชันชั้นนำได้เลยครับ
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment