Commit 162ff658 by Ooh-Ao

widget

parent f629f997
# การลบ Permission Check ออกจาก Dashboard Management
## สรุปการเปลี่ยนแปลง
### ✅ สิ่งที่ทำสำเร็จ:
1. **ลบ Module Access Guard** จาก dashboard-management route ใน `portal-manage.routes.ts`
2. **อัปเดต Dashboard Management Module** ให้ไม่ต้องเช็ค permission
3. **ปรับ Module Access Guard** ให้ไม่ต้องเช็ค permission สำหรับ dashboard-management และ widget-warehouse
4. **ปรับ Menu Permission Service** ให้ไม่ต้องเช็ค permission สำหรับ dashboard-management และ widget-warehouse
## การเปลี่ยนแปลงรายละเอียด
### 1. Portal Manage Routes (`portal-manage.routes.ts`)
```typescript
// ก่อน
{
path: 'dashboard-management',
canActivate: [moduleAccessGuard], // ← ลบออก
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
}
// หลัง
{
path: 'dashboard-management',
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
}
```
### 2. Dashboard Management Module (`dashboard-management.module.ts`)
- เพิ่ม comment `(no permission check)` ในทุก routes
- ไม่มีการเปลี่ยนแปลงโครงสร้าง routes
- Routes ทั้งหมดสามารถเข้าถึงได้โดยไม่ต้องเช็ค permission
### 3. Module Access Guard (`module-access.guard.ts`)
```typescript
// ไม่ต้องเช็ค permission สำหรับ dashboard-management และ widget-warehouse
if (state.url.includes('dashboard-management') || state.url.includes('widget-warehouse')) {
return true;
}
```
### 4. Menu Permission Service (`menu-permission.service.ts`)
```typescript
canAccessMenu(menuPath: string, permission: 'view' | 'create' | 'edit' | 'delete' | 'export' | 'import' = 'view'): Observable<boolean> {
// ไม่ต้องเช็ค permission สำหรับ dashboard-management และ widget-warehouse
if (menuPath.includes('dashboard-management') || menuPath.includes('widget-warehouse')) {
return of(true);
}
// ... rest of the logic
}
```
## Routes ที่ไม่ต้องเช็ค Permission อีกต่อไป
### Dashboard Management Routes:
- `/portal-manage/dashboard-management`
- `/portal-manage/dashboard-management/dashboard`
- `/portal-manage/dashboard-management/widget-management`
- `/portal-manage/dashboard-management/widget-config`
- `/portal-manage/dashboard-management/dataset-picker`
- `/portal-manage/dashboard-management/widget-preview/:widgetType`
### App-based Dashboard Management Routes:
- `/portal-manage/:appName/dashboard-management`
- `/portal-manage/:appName/dashboard-management/dashboard`
- `/portal-manage/:appName/dashboard-management/widget-management`
- `/portal-manage/:appName/dashboard-management/widget-config`
- `/portal-manage/:appName/dashboard-management/dataset-picker`
### Widget Warehouse Routes:
- `/portal-manage/:appName/widget-warehouse`
- `/portal-manage/:appName/widget-warehouse/edit/:widgetId`
- `/portal-manage/:appName/widget-warehouse/preview/:widgetType`
### Widget Linker Routes:
- `/portal-manage/:appName/widget-linker`
### Direct Dashboard Viewer:
- `/portal-manage/dashboard-viewer/:dashboardId`
## ผลลัพธ์
### ✅ ข้อดี:
1. **เข้าถึงได้ทันที** - ไม่ต้องรอการตรวจสอบ permission
2. **ไม่ต้อง login** - สามารถเข้าถึงได้โดยไม่ต้องมี authentication
3. **ไม่มี error** - ไม่มี redirect ไปหน้า unauthorized
4. **Performance ดีขึ้น** - ไม่ต้องเรียก API ตรวจสอบ permission
### ⚠️ ข้อควรระวัง:
1. **ความปลอดภัย** - ข้อมูลอาจเข้าถึงได้โดยไม่จำกัด
2. **การควบคุม** - ไม่สามารถจำกัดการเข้าถึงได้
3. **Audit Trail** - ไม่มีการบันทึกการเข้าถึง
## การทดสอบ
### URLs ที่ควรทดสอบ:
1. `http://localhost:59423/portal-manage/dashboard-management`
2. `http://localhost:59423/portal-manage/myhr-plus/widget-warehouse`
3. `http://localhost:59423/portal-manage/dashboard-management/widget-management`
4. `http://localhost:59423/portal-manage/dashboard-management/widget-config`
### Expected Results:
- ✅ เข้าถึงได้ทันทีโดยไม่ต้อง login
- ✅ ไม่มี redirect ไปหน้า unauthorized
- ✅ แสดงหน้า dashboard management หรือ widget warehouse ได้ปกติ
- ✅ ไม่มี error ใน console
## หมายเหตุ
การเปลี่ยนแปลงนี้ทำให้ dashboard management และ widget warehouse สามารถเข้าถึงได้โดยไม่ต้องเช็ค permission ใดๆ ซึ่งเหมาะสำหรับการพัฒนาหรือการทดสอบ แต่ควรพิจารณาเพิ่ม permission check กลับมาเมื่อพร้อมใช้งานจริงใน production environment
# การแก้ไขปัญหา Widget Warehouse Route
## ปัญหาที่พบ
Route `/portal-manage/myhr-plus/widget-warehouse` ไม่ทำงาน
## สาเหตุของปัญหา
### 1. Route Conflict
- มี route `dashboard-management` ซ้ำกันใน `portal-manage.routes.ts`
- บรรทัดที่ 72-75: load module
- บรรทัดที่ 115-118: load component (ซ้ำ!)
### 2. Path ไม่ตรงกัน
- Menu permission service กำหนด path เป็น `/portal-manage/dashboard/widget-warehouse`
- แต่ route จริงอยู่ใน `dashboard-management.module.ts` เป็น `:appName/widget-warehouse`
### 3. Route ไม่ได้ถูกเพิ่มใน myhr-plus module
- myhr-plus module ไม่มี route สำหรับ widget-warehouse
## การแก้ไข
### 1. ลบ Route ที่ซ้ำกัน
```typescript
// ลบออกจาก portal-manage.routes.ts
{
path: 'dashboard-management',
loadComponent: () => import('./dashboard-management/dashboard-management.component').then(m => m.DashboardManagementComponent),
canActivate: [moduleAccessGuard]
},
```
### 2. เพิ่ม Widget Warehouse Route ใน myhr-plus
```typescript
// เพิ่มใน myhr-plus.routes.ts
{
path: 'widget-warehouse',
loadChildren: () => import('../dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
},
```
### 3. อัปเดต Menu Permission Service
```typescript
// แก้ไข path ใน menu-permission.service.ts
path: '/portal-manage/myhr-plus/widget-warehouse'
```
## ผลลัพธ์
### ✅ สิ่งที่แก้ไขสำเร็จ:
1. **ลบ route conflict** - ไม่มี route ซ้ำกันแล้ว
2. **เพิ่ม widget-warehouse route** ใน myhr-plus module
3. **แก้ไข path** ให้ตรงกันใน menu permission service
4. **Build ผ่าน** โดยไม่มี errors
### 🎯 URL ที่ทำงานได้:
- **Widget Warehouse**: `/portal-manage/myhr-plus/widget-warehouse`
- **Widget Edit**: `/portal-manage/myhr-plus/widget-warehouse/edit/:widgetId`
- **Widget Preview**: `/portal-manage/myhr-plus/widget-warehouse/preview/:widgetType`
### 📋 Components ที่ใช้งานได้:
- `WidgetListComponent` - รายการวิดเจ็ท
- `WidgetFormComponent` - แก้ไขวิดเจ็ท
- `DashboardViewerComponent` - ตัวอย่างวิดเจ็ท
## การทดสอบ
### 1. Build Test
```bash
ng build --configuration development
# ✅ สำเร็จ - ไม่มี errors
```
### 2. Route Test
- เข้า URL: `http://localhost:4200/portal-manage/myhr-plus/widget-warehouse`
- ควรแสดงหน้า Widget List
### 3. Menu Test
- คลิกเมนู "คลังวิดเจ็ต" ใน myhr-plus
- ควรนำทางไปที่ widget-warehouse page
## หมายเหตุ
### Route Structure:
```
/portal-manage
└── myhr-plus
└── widget-warehouse (DashboardManagementModule)
├── '' (WidgetListComponent)
├── edit/:widgetId (WidgetFormComponent)
└── preview/:widgetType (DashboardViewerComponent)
```
### Dependencies:
- ใช้ DashboardManagementModule ที่มีอยู่แล้ว
- ไม่ต้องสร้าง component ใหม่
- ใช้ WidgetListComponent, WidgetFormComponent, DashboardViewerComponent ที่มีอยู่
## สรุป
ปัญหาการไม่ทำงานของ widget-warehouse route ได้รับการแก้ไขเรียบร้อยแล้ว โดยการ:
1. ลบ route conflict
2. เพิ่ม route ใน myhr-plus module
3. แก้ไข path ใน menu permission service
ตอนนี้ widget-warehouse สามารถเข้าถึงได้ผ่าน URL `/portal-manage/myhr-plus/widget-warehouse` แล้ว
......@@ -35,6 +35,7 @@
"@ngx-translate/http-loader": "^8.0.0",
"@syncfusion/ej2-angular-base": "^31.1.17",
"@syncfusion/ej2-angular-buttons": "^31.1.17",
"@syncfusion/ej2-angular-calendars": "^31.1.19",
"@syncfusion/ej2-angular-charts": "^31.1.17",
"@syncfusion/ej2-angular-circulargauge": "^31.1.17",
"@syncfusion/ej2-angular-dropdowns": "^31.1.17",
......@@ -43,8 +44,10 @@
"@syncfusion/ej2-angular-layouts": "^31.1.17",
"@syncfusion/ej2-angular-maps": "^31.1.17",
"@syncfusion/ej2-angular-navigations": "^31.1.17",
"@syncfusion/ej2-angular-notifications": "^31.1.17",
"@syncfusion/ej2-angular-pivotview": "^31.1.17",
"@syncfusion/ej2-angular-popups": "^31.1.17",
"@syncfusion/ej2-angular-progressbar": "^31.1.17",
"@syncfusion/ej2-angular-treemap": "^31.1.17",
"@syncfusion/ej2-base": "^31.1.17",
"@syncfusion/ej2-buttons": "^31.1.17",
......@@ -130,6 +133,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
......@@ -615,6 +619,7 @@
"version": "17.3.12",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.12.tgz",
"integrity": "sha512-1F8M7nWfChzurb7obbvuE7mJXlHtY1UG58pcwcomVtpPb+kPavgAO8OEvJHYBMV+bzSxkXt5UIwL9lt9jHUxZA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "7.23.9",
......@@ -643,6 +648,7 @@
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
"integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
......@@ -673,12 +679,14 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
},
"node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
......@@ -1090,6 +1098,7 @@
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
......@@ -1120,12 +1129,14 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
},
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
......@@ -3779,16 +3790,6 @@
"@fortawesome/fontawesome-svg-core": "~1.2.27 || ~1.3.0-beta2 || ^6.1.0"
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.0.tgz",
......@@ -3798,19 +3799,6 @@
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.9.15",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz",
......@@ -3885,6 +3873,7 @@
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
......@@ -3902,6 +3891,7 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
......@@ -3914,6 +3904,7 @@
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
......@@ -3926,12 +3917,14 @@
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
......@@ -3949,6 +3942,7 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
......@@ -3964,6 +3958,7 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
......@@ -4083,13 +4078,6 @@
"rxjs": ">=7.5.0"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT",
"peer": true
},
"node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
......@@ -5700,6 +5688,7 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
......@@ -6202,6 +6191,17 @@
"@syncfusion/ej2-buttons": "31.1.17"
}
},
"node_modules/@syncfusion/ej2-angular-calendars": {
"version": "31.1.19",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-calendars/-/ej2-angular-calendars-31.1.19.tgz",
"integrity": "sha512-lV3Z4cK2mN93Sz9xANYQSFLBztnzIyRt1aRo8ohfq+PxhgHdB1zb5paOGKXC9Lki/e4Xa/5uT+zZ+Dot3AEB1A==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-angular-base": "~31.1.17",
"@syncfusion/ej2-base": "~31.1.17",
"@syncfusion/ej2-calendars": "31.1.19"
}
},
"node_modules/@syncfusion/ej2-angular-charts": {
"version": "31.1.19",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-charts/-/ej2-angular-charts-31.1.19.tgz",
......@@ -6290,6 +6290,17 @@
"@syncfusion/ej2-navigations": "31.1.18"
}
},
"node_modules/@syncfusion/ej2-angular-notifications": {
"version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-notifications/-/ej2-angular-notifications-31.1.17.tgz",
"integrity": "sha512-R+lzgwdBhwg7V1o0NU0OTjymuJaEnQo8ga1o+mxiCDVj8cJHFPUvvB8JbdDqpBxBjGt9UnrcX3wJ1jBfEgLHGg==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-angular-base": "~31.1.17",
"@syncfusion/ej2-base": "~31.1.17",
"@syncfusion/ej2-notifications": "31.1.17"
}
},
"node_modules/@syncfusion/ej2-angular-pivotview": {
"version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-pivotview/-/ej2-angular-pivotview-31.1.17.tgz",
......@@ -6312,6 +6323,17 @@
"@syncfusion/ej2-popups": "31.1.17"
}
},
"node_modules/@syncfusion/ej2-angular-progressbar": {
"version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-progressbar/-/ej2-angular-progressbar-31.1.17.tgz",
"integrity": "sha512-JQpHIehJOmRb3K+xPRh5/EJlZE+WIHIf+5tVqaP6TS9rglRoOMu5sRL/GaBU67J/4aSdySOaOTC2yiNyWO2IVQ==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-angular-base": "~31.1.17",
"@syncfusion/ej2-base": "~31.1.17",
"@syncfusion/ej2-progressbar": "31.1.17"
}
},
"node_modules/@syncfusion/ej2-angular-treemap": {
"version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-treemap/-/ej2-angular-treemap-31.1.17.tgz",
......@@ -6578,6 +6600,17 @@
"@syncfusion/ej2-buttons": "~31.1.17"
}
},
"node_modules/@syncfusion/ej2-progressbar": {
"version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-progressbar/-/ej2-progressbar-31.1.17.tgz",
"integrity": "sha512-pU4oHhxlVBSJ9Xw+NsKMgt5I18F9TP82rFkvvTmj9NMqafhv32NF6BDgoUndjilvk8ffHOakgjYu7akJtEWrDQ==",
"license": "SEE LICENSE IN license",
"dependencies": {
"@syncfusion/ej2-base": "~31.1.17",
"@syncfusion/ej2-data": "~31.1.17",
"@syncfusion/ej2-svg-base": "~31.1.17"
}
},
"node_modules/@syncfusion/ej2-splitbuttons": {
"version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-splitbuttons/-/ej2-splitbuttons-31.1.17.tgz",
......@@ -6888,16 +6921,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/jquery": {
"version": "3.5.33",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.33.tgz",
"integrity": "sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/sizzle": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
......@@ -7024,13 +7047,6 @@
"@types/send": "*"
}
},
"node_modules/@types/sizzle": {
"version": "2.3.10",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz",
"integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==",
"license": "MIT",
"peer": true
},
"node_modules/@types/sockjs": {
"version": "0.3.36",
"resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz",
......@@ -7560,6 +7576,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
......@@ -7591,6 +7608,7 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
......@@ -7744,6 +7762,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
......@@ -8127,6 +8146,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
......@@ -8175,19 +8195,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/chart.js": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
......@@ -8405,6 +8412,7 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
......@@ -8543,6 +8551,7 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
......@@ -8721,6 +8730,7 @@
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
......@@ -8735,6 +8745,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
......@@ -8816,6 +8827,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
......@@ -9058,6 +9070,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/dir-glob": {
......@@ -9077,6 +9090,7 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT"
},
"node_modules/dns-packet": {
......@@ -9197,6 +9211,7 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true,
"license": "MIT"
},
"node_modules/echarts": {
......@@ -9264,6 +9279,7 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
......@@ -9274,6 +9290,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
......@@ -10135,6 +10152,7 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
......@@ -10151,6 +10169,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
......@@ -11243,6 +11262,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/isobject": {
......@@ -11350,6 +11370,7 @@
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
......@@ -11403,6 +11424,7 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
......@@ -11892,6 +11914,7 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"license": "MIT"
},
"node_modules/loader-runner": {
......@@ -12302,6 +12325,7 @@
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
......@@ -12568,6 +12592,7 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
......@@ -13173,13 +13198,6 @@
"node": ">=0.10.0"
}
},
"node_modules/nouislider": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.8.1.tgz",
"integrity": "sha512-93TweAi8kqntHJSPiSWQ1o/uZ29VWOmal9YKb6KKGGlCkugaNfAupT7o1qTHqdJvNQ7S0su5rO6qRFCjP8fxtw==",
"license": "MIT",
"peer": true
},
"node_modules/npm": {
"version": "10.9.3",
"resolved": "https://registry.npmjs.org/npm/-/npm-10.9.3.tgz",
......@@ -15795,6 +15813,7 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
......@@ -15804,6 +15823,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
......@@ -16050,6 +16070,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/pacote": {
......@@ -16228,6 +16249,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
......@@ -16243,6 +16265,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
......@@ -16259,6 +16282,7 @@
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/path-to-regexp": {
......@@ -16309,6 +16333,7 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
......@@ -16545,6 +16570,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"camelcase-css": "^2.0.1"
......@@ -16705,6 +16731,7 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
......@@ -16730,6 +16757,7 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
......@@ -17270,6 +17298,7 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/regenerate": {
......@@ -18095,6 +18124,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
......@@ -18107,6 +18137,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
......@@ -18657,6 +18688,7 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
......@@ -18684,6 +18716,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
......@@ -18706,6 +18739,7 @@
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
......@@ -18728,6 +18762,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
......@@ -18737,6 +18772,7 @@
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
......@@ -18757,6 +18793,7 @@
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
......@@ -18857,6 +18894,7 @@
"version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
......@@ -18894,6 +18932,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
......@@ -18906,6 +18945,7 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
......@@ -18923,6 +18963,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
......@@ -18958,6 +18999,7 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
......@@ -19173,6 +19215,7 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
......@@ -19182,6 +19225,7 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
......@@ -19295,6 +19339,7 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tslib": {
......@@ -19355,6 +19400,7 @@
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
......@@ -20543,6 +20589,7 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
......
......@@ -7,6 +7,11 @@ export const moduleAccessGuard: CanActivateFn = (route: ActivatedRouteSnapshot,
const permissionService = inject(CorePermissionService);
const router = inject(Router);
// ไม่ต้องเช็ค permission สำหรับ dashboard-management และ widget-warehouse
if (state.url.includes('dashboard-management') || state.url.includes('widget-warehouse')) {
return true;
}
// Get the module name from the route parameter :appName or from the route path
let moduleName = route.params['appName'];
......
import { Component } from '@angular/core';
@Component({
selector: 'app-company-department',
template: '<p>company-department works!</p>',
standalone: true
})
export class CompanyDepartmentComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-company-emp',
template: '<p>company-emp works!</p>',
standalone: true
})
export class CompanyEmpComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-company-info',
template: '<p>company-info works!</p>',
standalone: true
})
export class CompanyInfoComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-company-location',
template: '<p>company-location works!</p>',
standalone: true
})
export class CompanyLocationComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-company-management',
templateUrl: './company-management.component.html',
styleUrls: ['./company-management.component.scss'],
standalone: true
})
export class CompanyManagementComponent {
}
import { Routes } from '@angular/router';
import { CompanyManagementComponent } from './company-management.component';
export const COMPANY_MANAGEMENT_ROUTES: Routes = [
{
path: '',
component: CompanyManagementComponent,
children: [
{
path: 'company-info',
loadComponent: () => import('./company-info/company-info.component').then(m => m.CompanyInfoComponent)
},
{
path: 'company-department',
loadComponent: () => import('./company-department/company-department.component').then(m => m.CompanyDepartmentComponent)
},
{
path: 'company-position',
loadComponent: () => import('./company-position/company-position.component').then(m => m.CompanyPositionComponent)
},
{
path: 'company-emp',
loadComponent: () => import('./company-emp/company-emp.component').then(m => m.CompanyEmpComponent)
},
{
path: 'company-location',
loadComponent: () => import('./company-location/company-location.component').then(m => m.CompanyLocationComponent)
},
{
path: 'timestamp-log',
loadComponent: () => import('./timestamp-log/timestamp-log.component').then(m => m.TimestampLogComponent)
},
{
path: 'warning-timetamp',
loadComponent: () => import('./warning-timetamp/warning-timetamp.component').then(m => m.WarningTimetampComponent)
},
{
path: 'enroll-face',
loadComponent: () => import('./enroll-face/enroll-face.component').then(m => m.EnrollFaceComponent)
},
{
path: 'home-installer',
loadComponent: () => import('./home-installer/home-installer.component').then(m => m.HomeInstallerComponent)
},
{
path: '',
redirectTo: 'company-info',
pathMatch: 'full'
}
]
}
];
import { Component } from '@angular/core';
@Component({
selector: 'app-company-position',
template: '<p>company-position works!</p>',
standalone: true
})
export class CompanyPositionComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-enroll-face',
template: '<p>enroll-face works!</p>',
standalone: true
})
export class EnrollFaceComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-home-installer',
template: '<p>home-installer works!</p>',
standalone: true
})
export class HomeInstallerComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-timestamp-log',
template: '<p>timestamp-log works!</p>',
standalone: true
})
export class TimestampLogComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-warning-timetamp',
template: '<p>warning-timetamp works!</p>',
standalone: true
})
export class WarningTimetampComponent {
}
......@@ -40,155 +40,34 @@ import { SyncfusionPivotWidgetComponent } from './widgets/syncfusion-pivot-widge
import { TreemapWidgetComponent } from './widgets/treemap-widget/treemap-widget.component';
import { WaterfallChartWidgetComponent } from './widgets/waterfall-chart-widget/waterfall-chart-widget.component';
import { WelcomeWidgetComponent } from './widgets/welcome-widget/welcome-widget.component';
// New Syncfusion-based widgets
import { CalendarWidgetComponent } from './widgets/calendar-widget/calendar-widget.component';
import { NotificationWidgetComponent } from './widgets/notification-widget/notification-widget.component';
import { WeatherWidgetComponent } from './widgets/weather-widget/weather-widget.component';
import { ClockWidgetComponent } from './widgets/clock-widget/clock-widget.component';
export const routes: Routes = [
{
path: '',
component: DashboardManagementComponent,
children: [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
},
{
path: 'dashboard',
component: DashboardManagementComponent,
title: 'แดชบอร์ดหลัก'
},
{
path: 'viewer/:dashboardId',
component: DashboardViewerComponent,
title: 'ดูแดชบอร์ด'
},
{
path: 'widget-management',
children: [
{
path: '',
component: WidgetListComponent,
title: 'รายการวิดเจ็ต'
},
{
path: 'edit/:widgetId',
component: WidgetFormComponent,
title: 'แก้ไขวิดเจ็ต'
},
{
path: 'linker',
component: DatasetWidgetLinkerComponent,
title: 'เชื่อมโยงข้อมูลกับวิดเจ็ต'
}
]
},
{
path: 'widget-config',
component: WidgetConfigComponent,
title: 'ตั้งค่าวิดเจ็ต'
},
{
path: 'dataset-picker',
component: DatasetPickerComponent,
title: 'เลือกชุดข้อมูล'
},
// Widget preview routes
{
path: 'widget-preview/:widgetType',
component: DashboardViewerComponent,
title: 'ตัวอย่างวิดเจ็ต'
}
]
path: 'dashboard-home',
component: DashboardManagementComponent
},
// Routes for dynamic app-based routing
// {
// path: 'widget-config',
// component: WidgetConfigComponent
// },
{
path: ':appName/dashboard-management',
component: DashboardManagementComponent,
children: [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
},
{
path: 'dashboard',
component: DashboardManagementComponent,
title: 'แดชบอร์ดหลัก'
},
{
path: 'viewer/:dashboardId',
component: DashboardViewerComponent,
title: 'ดูแดชบอร์ด'
},
{
path: 'widget-management',
children: [
{
path: '',
component: WidgetListComponent,
title: 'รายการวิดเจ็ต'
},
{
path: 'edit/:widgetId',
component: WidgetFormComponent,
title: 'แก้ไขวิดเจ็ต'
},
{
path: 'linker',
component: DatasetWidgetLinkerComponent,
title: 'เชื่อมโยงข้อมูลกับวิดเจ็ต'
}
]
},
{
path: 'widget-config',
component: WidgetConfigComponent,
title: 'ตั้งค่าวิดเจ็ต'
},
{
path: 'dataset-picker',
component: DatasetPickerComponent,
title: 'เลือกชุดข้อมูล'
},
// Widget preview routes
{
path: 'widget-preview/:widgetType',
component: DashboardViewerComponent,
title: 'ตัวอย่างวิดเจ็ต'
}
]
path: 'dashboard-viewer',
component: DashboardViewerComponent
},
// Routes for widget warehouse
{
path: ':appName/widget-warehouse',
children: [
{
path: '',
component: WidgetListComponent,
title: 'คลังวิดเจ็ต'
},
{
path: 'edit/:widgetId',
component: WidgetFormComponent,
title: 'แก้ไขวิดเจ็ต'
},
{
path: 'preview/:widgetType',
component: DashboardViewerComponent,
title: 'ตัวอย่างวิดเจ็ต'
}
]
path: 'widget-list',
component: WidgetListComponent
},
{
path: ':appName/widget-linker',
component: DatasetWidgetLinkerComponent,
title: 'เชื่อมโยงข้อมูลกับวิดเจ็ต'
path: 'dataset-widget-linker',
component: DatasetWidgetLinkerComponent
},
// Direct dashboard viewer route
{
path: 'dashboard-viewer/:dashboardId',
component: DashboardViewerComponent,
title: 'ดูแดชบอร์ด'
}
];
@NgModule({
......@@ -232,7 +111,12 @@ export const routes: Routes = [
SyncfusionPivotWidgetComponent,
TreemapWidgetComponent,
WaterfallChartWidgetComponent,
WelcomeWidgetComponent
WelcomeWidgetComponent,
// New Syncfusion-based widgets
CalendarWidgetComponent,
NotificationWidgetComponent,
WeatherWidgetComponent,
ClockWidgetComponent
],
exports: []
})
......
......@@ -35,11 +35,13 @@ import { WidgetService } from '../services/widgets.service';
import { SimpleKpiWidgetComponent } from '../widgets/simple-kpi-widget/simple-kpi-widget.component';
import { WidgetFormComponent } from './widget-form.component';
import { ClickEventArgs } from '@syncfusion/ej2-angular-navigations';
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-widget-list',
standalone: true,
imports: [
RouterModule,
CommonModule,
FormsModule,
MatDialogModule,
......
# New Syncfusion-Based Widgets
## Overview
This document describes the newly added widgets that use Syncfusion components as their foundation.
## Added Widgets
### 1. Calendar Widget (`calendar-widget`)
**Location:** `src/app/portal-manage/dashboard-management/widgets/calendar-widget/`
**Syncfusion Components Used:**
- `CalendarModule` from `@syncfusion/ej2-angular-calendars`
**Features:**
- Interactive calendar display
- Event management and display
- Configurable calendar settings
- Multi-selection support
- RTL support
- Week number display
- Custom CSS classes
**Configuration Options:**
```typescript
{
title: string;
enableMultiSelection: boolean;
enableRtl: boolean;
showWeekNumber: boolean;
start: string; // 'Year', 'Month', 'Decade'
depth: string; // 'Year', 'Month', 'Decade'
cssClass: string;
dateField: string; // Data field for dates
titleField: string; // Data field for event titles
descriptionField: string; // Data field for event descriptions
typeField: string; // Data field for event types
}
```
### 2. Notification Widget (`notification-widget`)
**Location:** `src/app/portal-manage/dashboard-management/widgets/notification-widget/`
**Syncfusion Components Used:**
- `ToastModule` from `@syncfusion/ej2-angular-notifications`
- `MessageModule` from `@syncfusion/ej2-angular-notifications`
**Features:**
- Real-time notification display
- Toast notifications
- Message notifications
- Unread count tracking
- Mark as read functionality
- Delete notifications
- Priority-based styling
- Type-based styling (success, warning, error, info)
**Configuration Options:**
```typescript
{
title: string;
toastPosition: { X: string, Y: string };
showCloseButton: boolean;
showProgressBar: boolean;
timeOut: number;
newestOnTop: boolean;
cssClass: string;
severity: string; // 'Normal', 'Success', 'Warning', 'Error'
variant: string; // 'Filled', 'Outlined'
showIcon: boolean;
showCloseIcon: boolean;
// Data field mappings
idField: string;
titleField: string;
messageField: string;
typeField: string;
timestampField: string;
isReadField: string;
priorityField: string;
}
```
### 3. Weather Widget (`weather-widget`)
**Location:** `src/app/portal-manage/dashboard-management/widgets/weather-widget/`
**Syncfusion Components Used:**
- `CardModule` from `@syncfusion/ej2-angular-layouts`
**Features:**
- Current weather display
- 5-day forecast
- Weather details (humidity, wind, pressure)
- Temperature color coding
- Weather icons
- Location display
- Auto-refresh capability
- Responsive design
**Configuration Options:**
```typescript
{
title: string;
location: string;
refreshInterval: number;
// Data field mappings
temperatureField: string;
humidityField: string;
windSpeedField: string;
pressureField: string;
descriptionField: string;
iconField: string;
feelsLikeField: string;
dayField: string;
highField: string;
lowField: string;
forecastDescriptionField: string;
forecastIconField: string;
}
```
### 4. Clock Widget (`clock-widget`)
**Location:** `src/app/portal-manage/dashboard-management/widgets/clock-widget/`
**Syncfusion Components Used:**
- `ProgressBarModule` from `@syncfusion/ej2-angular-progressbar`
- `CircularGaugeModule` from `@syncfusion/ej2-angular-circulargauge`
**Features:**
- Multiple clock types (digital, analog, gauge)
- Real-time updates
- Timezone support
- 12/24 hour format toggle
- Date display
- Progress bars for time tracking
- Circular gauge clock
- Interactive controls
**Configuration Options:**
```typescript
{
title: string;
timezone: string;
clockType: string; // 'digital', 'analog', 'gauge'
showSeconds: boolean;
showDate: boolean;
showTimezone: boolean;
format24Hour: boolean;
updateInterval: number;
// Data field mappings
timezoneField: string;
clockTypeField: string;
format24HourField: string;
}
```
## Usage Examples
### Calendar Widget
```html
<app-calendar-widget
[config]="{
title: 'Company Calendar',
enableMultiSelection: false,
showWeekNumber: true,
dateField: 'eventDate',
titleField: 'eventTitle',
descriptionField: 'eventDescription'
}">
</app-calendar-widget>
```
### Notification Widget
```html
<app-notification-widget
[config]="{
title: 'System Notifications',
toastPosition: { X: 'Right', Y: 'Top' },
severity: 'Normal',
variant: 'Filled',
titleField: 'notificationTitle',
messageField: 'notificationMessage',
typeField: 'notificationType'
}">
</app-notification-widget>
```
### Weather Widget
```html
<app-weather-widget
[config]="{
title: 'Weather Forecast',
location: 'Bangkok, Thailand',
refreshInterval: 300000,
temperatureField: 'temp',
humidityField: 'humidity',
descriptionField: 'description'
}">
</app-weather-widget>
```
### Clock Widget
```html
<app-clock-widget
[config]="{
title: 'World Clock',
timezone: 'Asia/Bangkok',
clockType: 'analog',
showSeconds: true,
format24Hour: true
}">
</app-clock-widget>
```
## Integration Notes
1. **Module Imports:** All widgets are already imported in `dashboard-management.module.ts`
2. **Standalone Components:** All widgets are standalone components
3. **Base Widget:** All widgets extend `BaseWidgetComponent` for consistency
4. **Data Integration:** All widgets support dynamic data binding through `DashboardStateService`
5. **Responsive Design:** All widgets include responsive CSS for mobile devices
6. **Error Handling:** All widgets include proper error handling and loading states
## Dependencies Required
Make sure these Syncfusion packages are installed:
```bash
npm install @syncfusion/ej2-angular-calendars
npm install @syncfusion/ej2-angular-notifications
npm install @syncfusion/ej2-angular-layouts
npm install @syncfusion/ej2-angular-progressbar
npm install @syncfusion/ej2-angular-circulargauge
```
## Styling
All widgets include:
- Consistent styling with existing widgets
- Dark/light theme support
- Responsive design for mobile devices
- Custom CSS classes for theming
- Hover effects and transitions
## Performance Considerations
1. **Real-time Updates:** Clock widget updates every second, consider reducing frequency if needed
2. **Weather Refresh:** Weather widget can be configured with custom refresh intervals
3. **Notification Limits:** Consider limiting the number of notifications displayed
4. **Calendar Events:** Large numbers of events may impact performance
## Future Enhancements
1. **Calendar Widget:**
- Event creation/editing
- Recurring events
- Multiple calendar support
2. **Notification Widget:**
- Push notification support
- Sound notifications
- Notification categories
3. **Weather Widget:**
- Multiple location support
- Weather alerts
- Historical weather data
4. **Clock Widget:**
- Multiple timezone display
- Stopwatch/timer functionality
- Alarm features
<div class="calendar-widget">
<div class="widget-header">
<h3 class="widget-title">{{ title }}</h3>
</div>
<div class="widget-content">
<div class="loading" *ngIf="isLoading">
<div class="spinner"></div>
<p>Loading calendar...</p>
</div>
<div class="error" *ngIf="hasError">
<div class="error-icon">⚠️</div>
<p>{{ errorMessage }}</p>
</div>
<div class="calendar-container" *ngIf="!isLoading && !hasError">
<ejs-calendar
[value]="selectedDate"
[enableRtl]="calendarSettings.enableRtl"
[start]="calendarSettings.start"
[depth]="calendarSettings.depth"
[cssClass]="calendarSettings.cssClass"
(change)="onDateChange($event)">
</ejs-calendar>
<!-- Events display -->
<div class="events-section" *ngIf="events.length > 0">
<h4>Upcoming Events</h4>
<div class="events-list">
<div class="event-item"
*ngFor="let event of getEventsForDate(selectedDate)"
[class]="'event-' + event.type">
<div class="event-title">{{ event.title }}</div>
<div class="event-description">{{ event.description }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
.calendar-widget {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.widget-header {
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
.widget-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #343a40;
}
}
.widget-content {
flex: 1;
padding: 16px;
overflow-y: auto;
.loading, .error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #6c757d;
.spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.error-icon {
font-size: 32px;
margin-bottom: 8px;
}
}
.calendar-container {
.e-calendar {
border: none;
box-shadow: none;
}
.events-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e9ecef;
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: #495057;
}
.events-list {
.event-item {
padding: 8px 12px;
margin-bottom: 8px;
border-radius: 6px;
background: #f8f9fa;
border-left: 4px solid #007bff;
&.event-meeting {
border-left-color: #28a745;
}
&.event-deadline {
border-left-color: #dc3545;
}
&.event-reminder {
border-left-color: #ffc107;
}
.event-title {
font-weight: 600;
font-size: 13px;
color: #343a40;
margin-bottom: 2px;
}
.event-description {
font-size: 12px;
color: #6c757d;
}
}
}
}
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// Responsive design
@media (max-width: 768px) {
.calendar-widget {
.widget-content {
padding: 12px;
.calendar-container {
.events-section {
.events-list {
.event-item {
padding: 6px 10px;
}
}
}
}
}
}
}
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CalendarModule } from '@syncfusion/ej2-angular-calendars';
import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({
selector: 'app-calendar-widget',
standalone: true,
imports: [CommonModule, CalendarModule],
templateUrl: './calendar-widget.component.html',
styleUrls: ['./calendar-widget.component.scss']
})
export class CalendarWidgetComponent extends BaseWidgetComponent {
public selectedDate: Date = new Date();
public events: any[] = [];
public calendarSettings: any = {};
constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
}
applyInitialConfig(): void {
this.title = this.config.title || 'Calendar';
this.calendarSettings = {
enableRtl: this.config.enableRtl || false,
start: this.config.start || 'Year',
depth: this.config.depth || 'Year',
cssClass: this.config.cssClass || ''
};
this.events = [];
}
onDataUpdate(data: any[]): void {
if (data && data.length > 0) {
// Map data to events format
this.events = data.map(item => ({
date: new Date(item[this.config.dateField || 'date']),
title: item[this.config.titleField || 'title'],
description: item[this.config.descriptionField || 'description'],
type: item[this.config.typeField || 'type'] || 'default'
}));
}
}
onReset(): void {
this.title = 'Calendar (Default)';
this.selectedDate = new Date();
this.events = [
{ date: new Date(), title: 'Meeting', description: 'Team meeting', type: 'meeting' },
{ date: new Date(Date.now() + 86400000), title: 'Deadline', description: 'Project deadline', type: 'deadline' }
];
this.calendarSettings = {
enableRtl: false,
start: 'Year',
depth: 'Year'
};
}
onDateChange(event: any): void {
this.selectedDate = event.value;
// Emit date change event if needed
console.log('Selected date:', this.selectedDate);
}
getEventsForDate(date: Date): any[] {
return this.events.filter(event =>
event.date.toDateString() === date.toDateString()
);
}
}
<div class="clock-widget">
<div class="widget-header">
<h3 class="widget-title">{{ title }}</h3>
<div class="clock-actions">
<button class="btn-toggle" (click)="toggleClockType()" title="Toggle clock type">
{{ clockType === 'analog' ? '🕐' : '⏰' }}
</button>
<button class="btn-format" (click)="toggleFormat()" title="Toggle 12/24 hour">
{{ format24Hour ? '24h' : '12h' }}
</button>
</div>
</div>
<div class="widget-content">
<div class="loading" *ngIf="isLoading">
<div class="spinner"></div>
<p>Loading clock...</p>
</div>
<div class="error" *ngIf="hasError">
<div class="error-icon">⚠️</div>
<p>{{ errorMessage }}</p>
</div>
<div class="clock-container" *ngIf="!isLoading && !hasError">
<!-- Digital Clock -->
<div class="digital-clock" *ngIf="clockType === 'digital'">
<div class="time-display">
{{ getFormattedTime() }}
</div>
<div class="date-display" *ngIf="showDate">
{{ getFormattedDate() }}
</div>
<div class="timezone-display" *ngIf="showTimezone">
{{ getTimezoneDisplay() }}
</div>
</div>
<!-- Analog Clock -->
<div class="analog-clock" *ngIf="clockType === 'analog'">
<div class="clock-face">
<div class="clock-center"></div>
<!-- Hour markers -->
<div class="hour-marker" *ngFor="let hour of [1,2,3,4,5,6,7,8,9,10,11,12]"
[style.transform]="'rotate(' + (hour * 30) + 'deg)'">
<span class="hour-number"
[style.transform]="'rotate(' + (-hour * 30) + 'deg)'">
{{ hour }}
</span>
</div>
<!-- Clock hands -->
<div class="clock-hand hour-hand"
[style.transform]="getAnalogHourRotation()"></div>
<div class="clock-hand minute-hand"
[style.transform]="getAnalogMinuteRotation()"></div>
<div class="clock-hand second-hand"
*ngIf="showSeconds"
[style.transform]="getAnalogSecondRotation()"></div>
</div>
<div class="analog-info">
<div class="date-display" *ngIf="showDate">
{{ getFormattedDate() }}
</div>
<div class="timezone-display" *ngIf="showTimezone">
{{ getTimezoneDisplay() }}
</div>
</div>
</div>
<!-- Circular Gauge Clock -->
<div class="gauge-clock" *ngIf="clockType === 'gauge'">
<ejs-circulargauge
[axes]="gaugeAxes"
[height]="200"
[width]="200">
</ejs-circulargauge>
<div class="gauge-info">
<div class="time-display">{{ getFormattedTime() }}</div>
<div class="date-display" *ngIf="showDate">{{ getFormattedDate() }}</div>
<div class="timezone-display" *ngIf="showTimezone">{{ getTimezoneDisplay() }}</div>
</div>
</div>
<!-- Progress Bars -->
<div class="progress-section">
<h4 class="progress-title">Time Progress</h4>
<div class="progress-item">
<label>Hour</label>
<ejs-progressbar
type="Linear"
[value]="hourProgress"
[showProgressValue]="false"
height="20"
trackThickness="8"
progressThickness="8"
trackColor="#e0e0e0"
progressColor="#007bff">
</ejs-progressbar>
<span class="progress-value">{{ currentTime.getHours() }}/24</span>
</div>
<div class="progress-item">
<label>Minute</label>
<ejs-progressbar
type="Linear"
[value]="minuteProgress"
[showProgressValue]="false"
height="20"
trackThickness="8"
progressThickness="8"
trackColor="#e0e0e0"
progressColor="#28a745">
</ejs-progressbar>
<span class="progress-value">{{ currentTime.getMinutes() }}/60</span>
</div>
<div class="progress-item" *ngIf="showSeconds">
<label>Second</label>
<ejs-progressbar
type="Linear"
[value]="secondProgress"
[showProgressValue]="false"
height="20"
trackThickness="8"
progressThickness="8"
trackColor="#e0e0e0"
progressColor="#dc3545">
</ejs-progressbar>
<span class="progress-value">{{ currentTime.getSeconds() }}/60</span>
</div>
</div>
</div>
</div>
</div>
.clock-widget {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
.widget-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #343a40;
}
.clock-actions {
display: flex;
gap: 8px;
.btn-toggle, .btn-format {
background: #007bff;
color: white;
border: none;
border-radius: 4px;
padding: 6px 10px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.2s;
&:hover {
background: #0056b3;
}
}
.btn-format {
background: #6c757d;
&:hover {
background: #545b62;
}
}
}
}
.widget-content {
flex: 1;
padding: 20px;
overflow-y: auto;
text-align: center;
.loading, .error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #6c757d;
.spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.error-icon {
font-size: 32px;
margin-bottom: 8px;
}
}
.clock-container {
.digital-clock {
.time-display {
font-size: 48px;
font-weight: 300;
color: #343a40;
margin-bottom: 16px;
font-family: 'Courier New', monospace;
letter-spacing: 2px;
}
.date-display {
font-size: 16px;
color: #6c757d;
margin-bottom: 8px;
}
.timezone-display {
font-size: 14px;
color: #007bff;
font-weight: 500;
}
}
.analog-clock {
margin-bottom: 20px;
.clock-face {
position: relative;
width: 200px;
height: 200px;
border: 4px solid #343a40;
border-radius: 50%;
margin: 0 auto 16px;
background: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
.clock-center {
position: absolute;
top: 50%;
left: 50%;
width: 12px;
height: 12px;
background: #343a40;
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.hour-marker {
position: absolute;
top: 50%;
left: 50%;
width: 2px;
height: 90px;
transform-origin: bottom center;
transform: translateX(-50%) translateY(-100%);
.hour-number {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
font-size: 14px;
font-weight: 600;
color: #343a40;
}
}
.clock-hand {
position: absolute;
top: 50%;
left: 50%;
transform-origin: bottom center;
transform: translateX(-50%) translateY(-100%);
border-radius: 2px;
&.hour-hand {
width: 4px;
height: 50px;
background: #343a40;
z-index: 3;
}
&.minute-hand {
width: 3px;
height: 70px;
background: #007bff;
z-index: 2;
}
&.second-hand {
width: 2px;
height: 80px;
background: #dc3545;
z-index: 1;
}
}
}
.analog-info {
.date-display {
font-size: 16px;
color: #6c757d;
margin-bottom: 8px;
}
.timezone-display {
font-size: 14px;
color: #007bff;
font-weight: 500;
}
}
}
.gauge-clock {
margin-bottom: 20px;
.gauge-info {
margin-top: 16px;
.time-display {
font-size: 24px;
font-weight: 600;
color: #343a40;
margin-bottom: 8px;
font-family: 'Courier New', monospace;
}
.date-display {
font-size: 14px;
color: #6c757d;
margin-bottom: 4px;
}
.timezone-display {
font-size: 12px;
color: #007bff;
font-weight: 500;
}
}
}
.progress-section {
margin-top: 24px;
text-align: left;
.progress-title {
font-size: 14px;
font-weight: 600;
color: #343a40;
margin: 0 0 16px 0;
text-align: center;
}
.progress-item {
display: flex;
align-items: center;
margin-bottom: 12px;
gap: 12px;
label {
flex: 0 0 60px;
font-size: 13px;
font-weight: 500;
color: #495057;
}
ejs-progressbar {
flex: 1;
}
.progress-value {
flex: 0 0 50px;
font-size: 12px;
color: #6c757d;
text-align: right;
font-family: 'Courier New', monospace;
}
}
}
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// Responsive design
@media (max-width: 768px) {
.clock-widget {
.widget-content {
padding: 16px;
.clock-container {
.digital-clock {
.time-display {
font-size: 36px;
}
.date-display {
font-size: 14px;
}
}
.analog-clock {
.clock-face {
width: 160px;
height: 160px;
.hour-marker {
height: 70px;
.hour-number {
font-size: 12px;
}
}
.clock-hand {
&.hour-hand {
height: 40px;
}
&.minute-hand {
height: 55px;
}
&.second-hand {
height: 65px;
}
}
}
}
.gauge-clock {
ejs-circulargauge {
height: 160px !important;
width: 160px !important;
}
}
.progress-section {
.progress-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
label {
flex: none;
}
ejs-progressbar {
width: 100%;
}
.progress-value {
flex: none;
text-align: left;
}
}
}
}
}
}
}
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProgressBarModule } from '@syncfusion/ej2-angular-progressbar';
import { CircularGaugeModule, GaugeTooltipService, AnnotationsService } from '@syncfusion/ej2-angular-circulargauge';
import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({
selector: 'app-clock-widget',
standalone: true,
imports: [CommonModule, ProgressBarModule, CircularGaugeModule],
providers: [GaugeTooltipService, AnnotationsService],
templateUrl: './clock-widget.component.html',
styleUrls: ['./clock-widget.component.scss']
})
export class ClockWidgetComponent extends BaseWidgetComponent implements OnInit, OnDestroy {
public currentTime: Date = new Date();
public timezone: string = 'Asia/Bangkok';
public clockType: string = 'analog'; // 'analog' or 'digital'
public showSeconds: boolean = true;
public showDate: boolean = true;
public showTimezone: boolean = true;
public format24Hour: boolean = true;
// Analog clock properties
public hourHand: number = 0;
public minuteHand: number = 0;
public secondHand: number = 0;
// Progress bar properties
public hourProgress: number = 0;
public minuteProgress: number = 0;
public secondProgress: number = 0;
// Circular gauge properties
public gaugeAxes: any[] = [];
private timer: any;
private updateInterval: number = 1000; // 1 second
constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
}
override ngOnInit(): void {
super.ngOnInit();
this.startClock();
}
override ngOnDestroy(): void {
super.ngOnDestroy();
this.stopClock();
}
applyInitialConfig(): void {
this.title = this.config.title || 'Clock';
this.timezone = this.config.timezone || 'Asia/Bangkok';
this.clockType = this.config.clockType || 'analog';
this.showSeconds = this.config.showSeconds !== false;
this.showDate = this.config.showDate !== false;
this.showTimezone = this.config.showTimezone !== false;
this.format24Hour = this.config.format24Hour !== false;
this.updateInterval = this.config.updateInterval || 1000;
this.updateClock();
}
onDataUpdate(data: any[]): void {
if (data && data.length > 0) {
const timeData = data[0];
this.timezone = timeData[this.config.timezoneField || 'timezone'] || this.timezone;
this.clockType = timeData[this.config.clockTypeField || 'clockType'] || this.clockType;
this.format24Hour = timeData[this.config.format24HourField || 'format24Hour'] !== false;
this.updateClock();
}
}
onReset(): void {
this.title = 'Clock (Default)';
this.timezone = 'Asia/Bangkok';
this.clockType = 'analog';
this.showSeconds = true;
this.showDate = true;
this.showTimezone = true;
this.format24Hour = true;
this.updateClock();
}
private startClock(): void {
this.updateClock();
this.timer = setInterval(() => {
this.updateClock();
}, this.updateInterval);
}
private stopClock(): void {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
private updateClock(): void {
this.currentTime = new Date();
// Update analog clock hands
const hours = this.currentTime.getHours();
const minutes = this.currentTime.getMinutes();
const seconds = this.currentTime.getSeconds();
this.hourHand = (hours % 12) * 30 + (minutes / 60) * 30;
this.minuteHand = minutes * 6 + (seconds / 60) * 6;
this.secondHand = seconds * 6;
// Update progress bars
this.hourProgress = (hours / 24) * 100;
this.minuteProgress = (minutes / 60) * 100;
this.secondProgress = (seconds / 60) * 100;
// Update gauge
this.updateGauge();
}
private updateGauge(): void {
const time = this.currentTime;
const hour = time.getHours();
const minute = time.getMinutes();
const second = time.getSeconds();
this.gaugeAxes = [{
startAngle: 270,
endAngle: 90,
minimum: 0,
maximum: 12,
lineStyle: { width: 2, color: '#e0e0e0' },
labelStyle: {
font: { size: '12px', fontFamily: 'Segoe UI' },
position: 'Outside'
},
majorTicks: {
height: 8,
width: 2,
color: '#666666'
},
minorTicks: {
height: 4,
width: 1,
color: '#999999'
},
pointers: [{
value: hour + (minute / 60),
radius: '70%',
pointerWidth: 4,
cap: { radius: 6, color: '#007bff' },
needleTail: { length: '15%' },
color: '#007bff'
}, {
value: minute + (second / 60),
radius: '80%',
pointerWidth: 3,
cap: { radius: 4, color: '#28a745' },
needleTail: { length: '10%' },
color: '#28a745'
}, {
value: second,
radius: '90%',
pointerWidth: 2,
cap: { radius: 3, color: '#dc3545' },
needleTail: { length: '5%' },
color: '#dc3545'
}],
ranges: [{
start: 0, end: 12,
startWidth: 2, endWidth: 2,
color: 'transparent'
}]
}];
}
getFormattedTime(): string {
const options: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
second: this.showSeconds ? '2-digit' : undefined,
hour12: !this.format24Hour,
timeZone: this.timezone
};
return this.currentTime.toLocaleTimeString('en-US', options);
}
getFormattedDate(): string {
const options: Intl.DateTimeFormatOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: this.timezone
};
return this.currentTime.toLocaleDateString('en-US', options);
}
getTimezoneDisplay(): string {
if (!this.showTimezone) return '';
const offset = this.currentTime.getTimezoneOffset();
const offsetHours = Math.floor(Math.abs(offset) / 60);
const offsetMinutes = Math.abs(offset) % 60;
const sign = offset <= 0 ? '+' : '-';
return `GMT${sign}${offsetHours.toString().padStart(2, '0')}:${offsetMinutes.toString().padStart(2, '0')}`;
}
getAnalogHourRotation(): string {
return `rotate(${this.hourHand}deg)`;
}
getAnalogMinuteRotation(): string {
return `rotate(${this.minuteHand}deg)`;
}
getAnalogSecondRotation(): string {
return `rotate(${this.secondHand}deg)`;
}
toggleClockType(): void {
this.clockType = this.clockType === 'analog' ? 'digital' : 'analog';
}
toggleFormat(): void {
this.format24Hour = !this.format24Hour;
}
}
<div class="notification-widget">
<div class="widget-header">
<h3 class="widget-title">{{ title }}</h3>
<div class="notification-actions">
<span class="unread-count" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
<button class="btn-mark-all"
*ngIf="unreadCount > 0"
(click)="markAllAsRead()"
title="Mark all as read">
</button>
</div>
</div>
<div class="widget-content">
<div class="loading" *ngIf="isLoading">
<div class="spinner"></div>
<p>Loading notifications...</p>
</div>
<div class="error" *ngIf="hasError">
<div class="error-icon">⚠️</div>
<p>{{ errorMessage }}</p>
</div>
<div class="notifications-container" *ngIf="!isLoading && !hasError">
<div class="notifications-list" *ngIf="notifications.length > 0">
<div class="notification-item"
*ngFor="let notification of notifications"
[class.unread]="!notification.isRead"
[class]="getTypeClass(notification.type) + ' ' + getPriorityClass(notification.priority)">
<div class="notification-icon">
<span [class]="getIconClass(notification.type)"></span>
</div>
<div class="notification-content">
<div class="notification-header">
<h4 class="notification-title">{{ notification.title }}</h4>
<div class="notification-time">
{{ notification.timestamp | date:'short' }}
</div>
</div>
<p class="notification-message">{{ notification.message }}</p>
</div>
<div class="notification-actions">
<button class="btn-read"
*ngIf="!notification.isRead"
(click)="markAsRead(notification)"
title="Mark as read">
</button>
<button class="btn-show"
(click)="showToast(notification)"
title="Show toast">
📢
</button>
<button class="btn-delete"
(click)="deleteNotification(notification)"
title="Delete">
</button>
</div>
</div>
</div>
<div class="no-notifications" *ngIf="notifications.length === 0">
<div class="no-notifications-icon">📭</div>
<p>No notifications</p>
</div>
</div>
</div>
<!-- Syncfusion Toast Component -->
<ejs-toast #toast
[position]="toastSettings.position"
[showCloseButton]="toastSettings.showCloseButton"
[showProgressBar]="toastSettings.showProgressBar"
[timeOut]="toastSettings.timeOut"
[newestOnTop]="toastSettings.newestOnTop"
[cssClass]="toastSettings.cssClass">
</ejs-toast>
<!-- Syncfusion Message Component -->
<ejs-message #message
[severity]="messageSettings.severity"
[variant]="messageSettings.variant"
[showIcon]="messageSettings.showIcon"
[showCloseIcon]="messageSettings.showCloseIcon">
</ejs-message>
</div>
.notification-widget {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
.widget-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #343a40;
}
.notification-actions {
display: flex;
align-items: center;
gap: 8px;
.unread-count {
background: #dc3545;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
min-width: 20px;
text-align: center;
}
.btn-mark-all {
background: #28a745;
color: white;
border: none;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.2s;
&:hover {
background: #218838;
}
}
}
}
.widget-content {
flex: 1;
overflow-y: auto;
.loading, .error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #6c757d;
.spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.error-icon {
font-size: 32px;
margin-bottom: 8px;
}
}
.notifications-container {
.notifications-list {
.notification-item {
display: flex;
align-items: flex-start;
padding: 12px 16px;
border-bottom: 1px solid #f1f3f4;
transition: background-color 0.2s;
position: relative;
&.unread {
background: #f8f9ff;
border-left: 4px solid #007bff;
}
&:hover {
background: #f8f9fa;
}
&.notification-success {
border-left-color: #28a745;
}
&.notification-warning {
border-left-color: #ffc107;
}
&.notification-error {
border-left-color: #dc3545;
}
&.notification-info {
border-left-color: #17a2b8;
}
&.priority-high {
border-left-width: 6px;
}
.notification-icon {
margin-right: 12px;
margin-top: 2px;
span {
display: inline-block;
width: 24px;
height: 24px;
border-radius: 50%;
text-align: center;
line-height: 24px;
font-size: 12px;
font-weight: 600;
&.e-success {
background: #28a745;
color: white;
}
&.e-warning {
background: #ffc107;
color: #212529;
}
&.e-error {
background: #dc3545;
color: white;
}
&.e-info {
background: #17a2b8;
color: white;
}
}
}
.notification-content {
flex: 1;
min-width: 0;
.notification-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 4px;
.notification-title {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #343a40;
line-height: 1.3;
}
.notification-time {
font-size: 12px;
color: #6c757d;
white-space: nowrap;
margin-left: 8px;
}
}
.notification-message {
margin: 0;
font-size: 13px;
color: #495057;
line-height: 1.4;
word-wrap: break-word;
}
}
.notification-actions {
display: flex;
gap: 4px;
margin-left: 8px;
button {
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
font-size: 12px;
transition: background-color 0.2s;
&:hover {
background: #e9ecef;
}
&.btn-read {
color: #28a745;
}
&.btn-show {
color: #007bff;
}
&.btn-delete {
color: #dc3545;
}
}
}
}
}
.no-notifications {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #6c757d;
.no-notifications-icon {
font-size: 48px;
margin-bottom: 12px;
}
p {
margin: 0;
font-size: 14px;
}
}
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// Responsive design
@media (max-width: 768px) {
.notification-widget {
.widget-header {
padding: 12px;
.notification-actions {
.btn-mark-all {
padding: 6px 10px;
font-size: 14px;
}
}
}
.widget-content {
.notifications-container {
.notifications-list {
.notification-item {
padding: 10px 12px;
.notification-content {
.notification-header {
flex-direction: column;
align-items: flex-start;
.notification-time {
margin-left: 0;
margin-top: 2px;
}
}
}
}
}
}
}
}
}
import { Component, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ToastModule, ToastComponent } from '@syncfusion/ej2-angular-notifications';
import { MessageModule, MessageComponent } from '@syncfusion/ej2-angular-notifications';
import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({
selector: 'app-notification-widget',
standalone: true,
imports: [CommonModule, ToastModule, MessageModule],
templateUrl: './notification-widget.component.html',
styleUrls: ['./notification-widget.component.scss']
})
export class NotificationWidgetComponent extends BaseWidgetComponent {
@ViewChild('toast') toast!: ToastComponent;
@ViewChild('message') message!: MessageComponent;
public notifications: any[] = [];
public unreadCount: number = 0;
public toastSettings: any = {};
public messageSettings: any = {};
constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
}
applyInitialConfig(): void {
this.title = this.config.title || 'Notifications';
this.toastSettings = {
position: this.config.toastPosition || { X: 'Right', Y: 'Top' },
showCloseButton: this.config.showCloseButton !== false,
showProgressBar: this.config.showProgressBar !== false,
timeOut: this.config.timeOut || 4000,
newestOnTop: this.config.newestOnTop !== false,
cssClass: this.config.cssClass || ''
};
this.messageSettings = {
severity: this.config.severity || 'Normal',
variant: this.config.variant || 'Filled',
showIcon: this.config.showIcon !== false,
showCloseIcon: this.config.showCloseIcon !== false
};
this.notifications = [];
this.unreadCount = 0;
}
onDataUpdate(data: any[]): void {
if (data && data.length > 0) {
this.notifications = data.map(item => ({
id: item[this.config.idField || 'id'],
title: item[this.config.titleField || 'title'],
message: item[this.config.messageField || 'message'],
type: item[this.config.typeField || 'type'] || 'info',
timestamp: new Date(item[this.config.timestampField || 'timestamp']),
isRead: item[this.config.isReadField || 'isRead'] || false,
priority: item[this.config.priorityField || 'priority'] || 'normal'
})).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
this.updateUnreadCount();
}
}
onReset(): void {
this.title = 'Notifications (Default)';
this.notifications = [
{
id: 1,
title: 'System Update',
message: 'Your system has been updated successfully',
type: 'success',
timestamp: new Date(),
isRead: false,
priority: 'high'
},
{
id: 2,
title: 'Meeting Reminder',
message: 'You have a meeting in 30 minutes',
type: 'warning',
timestamp: new Date(Date.now() - 3600000),
isRead: false,
priority: 'medium'
},
{
id: 3,
title: 'New Message',
message: 'You received a new message from John Doe',
type: 'info',
timestamp: new Date(Date.now() - 7200000),
isRead: true,
priority: 'normal'
}
];
this.updateUnreadCount();
}
updateUnreadCount(): void {
this.unreadCount = this.notifications.filter(n => !n.isRead).length;
}
markAsRead(notification: any): void {
notification.isRead = true;
this.updateUnreadCount();
}
markAllAsRead(): void {
this.notifications.forEach(n => n.isRead = true);
this.updateUnreadCount();
}
deleteNotification(notification: any): void {
const index = this.notifications.findIndex(n => n.id === notification.id);
if (index > -1) {
this.notifications.splice(index, 1);
this.updateUnreadCount();
}
}
showToast(notification: any): void {
if (this.toast) {
this.toast.show({
title: notification.title,
content: notification.message,
cssClass: `e-toast-${notification.type}`,
icon: this.getIconClass(notification.type)
});
}
}
getIconClass(type: string): string {
const icons: { [key: string]: string } = {
'success': 'e-success',
'warning': 'e-warning',
'error': 'e-error',
'info': 'e-info'
};
return icons[type] || 'e-info';
}
getPriorityClass(priority: string): string {
return `priority-${priority}`;
}
getTypeClass(type: string): string {
return `notification-${type}`;
}
}
<div class="weather-widget">
<div class="widget-header">
<h3 class="widget-title">{{ title }}</h3>
<div class="weather-actions">
<button class="btn-refresh" (click)="refreshWeather()" title="Refresh">
🔄
</button>
</div>
</div>
<div class="widget-content">
<div class="loading" *ngIf="isLoading">
<div class="spinner"></div>
<p>Loading weather...</p>
</div>
<div class="error" *ngIf="hasError">
<div class="error-icon">⚠️</div>
<p>{{ errorMessage }}</p>
</div>
<div class="weather-container" *ngIf="!isLoading && !hasError">
<!-- Current Weather Card -->
<div class="current-weather-card" *ngIf="currentWeather.temperature">
<div class="weather-card-header">
<div class="weather-location">{{ location }}</div>
<div class="weather-updated">Updated: {{ lastUpdated | date:'short' }}</div>
</div>
<div class="weather-card-content">
<div class="current-weather">
<div class="weather-main">
<div class="weather-icon">
{{ getWeatherIcon(currentWeather.icon) }}
</div>
<div class="weather-temp" [class]="getTemperatureColor(currentWeather.temperature)">
{{ formatTemperature(currentWeather.temperature) }}
</div>
</div>
<div class="weather-description">
{{ currentWeather.description }}
</div>
<div class="weather-feels-like" *ngIf="currentWeather.feelsLike">
Feels like {{ formatTemperature(currentWeather.feelsLike) }}
</div>
</div>
</div>
</div>
<!-- Weather Details -->
<div class="weather-details" *ngIf="currentWeather.temperature">
<div class="detail-item">
<div class="detail-icon">💧</div>
<div class="detail-info">
<div class="detail-label">Humidity</div>
<div class="detail-value">{{ formatHumidity(currentWeather.humidity) }}</div>
</div>
</div>
<div class="detail-item">
<div class="detail-icon">💨</div>
<div class="detail-info">
<div class="detail-label">Wind</div>
<div class="detail-value">{{ formatWindSpeed(currentWeather.windSpeed) }}</div>
</div>
</div>
<div class="detail-item">
<div class="detail-icon">📊</div>
<div class="detail-info">
<div class="detail-label">Pressure</div>
<div class="detail-value">{{ formatPressure(currentWeather.pressure) }}</div>
</div>
</div>
</div>
<!-- Forecast -->
<div class="weather-forecast" *ngIf="forecast.length > 0">
<h4 class="forecast-title">5-Day Forecast</h4>
<div class="forecast-list">
<div class="forecast-item" *ngFor="let day of forecast">
<div class="forecast-day">{{ day.day }}</div>
<div class="forecast-icon">{{ getWeatherIcon(day.icon) }}</div>
<div class="forecast-temps">
<span class="temp-high">{{ formatTemperature(day.high) }}</span>
<span class="temp-low">{{ formatTemperature(day.low) }}</span>
</div>
<div class="forecast-desc">{{ day.description }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
.weather-widget {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
.widget-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #343a40;
}
.weather-actions {
.btn-refresh {
background: #007bff;
color: white;
border: none;
border-radius: 4px;
padding: 6px 10px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
&:hover {
background: #0056b3;
}
}
}
}
.widget-content {
flex: 1;
padding: 16px;
overflow-y: auto;
.loading, .error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #6c757d;
.spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.error-icon {
font-size: 32px;
margin-bottom: 8px;
}
}
.weather-container {
.current-weather-card {
margin-bottom: 16px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.weather-card-header {
padding: 12px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
.weather-location {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.weather-updated {
font-size: 12px;
opacity: 0.8;
}
}
.weather-card-content {
padding: 20px 16px;
background: white;
.current-weather {
text-align: center;
.weather-main {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
.weather-icon {
font-size: 48px;
margin-right: 16px;
}
.weather-temp {
font-size: 48px;
font-weight: 300;
line-height: 1;
&.hot {
color: #dc3545;
}
&.warm {
color: #fd7e14;
}
&.mild {
color: #28a745;
}
&.cold {
color: #007bff;
}
}
}
.weather-description {
font-size: 16px;
color: #495057;
margin-bottom: 8px;
text-transform: capitalize;
}
.weather-feels-like {
font-size: 14px;
color: #6c757d;
}
}
}
}
.weather-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
margin-bottom: 20px;
.detail-item {
display: flex;
align-items: center;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #007bff;
.detail-icon {
font-size: 20px;
margin-right: 8px;
}
.detail-info {
.detail-label {
font-size: 12px;
color: #6c757d;
margin-bottom: 2px;
}
.detail-value {
font-size: 14px;
font-weight: 600;
color: #343a40;
}
}
}
}
.weather-forecast {
.forecast-title {
font-size: 14px;
font-weight: 600;
color: #343a40;
margin: 0 0 12px 0;
padding-bottom: 8px;
border-bottom: 1px solid #e9ecef;
}
.forecast-list {
display: flex;
flex-direction: column;
gap: 8px;
.forecast-item {
display: flex;
align-items: center;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
transition: background-color 0.2s;
&:hover {
background: #e9ecef;
}
.forecast-day {
flex: 0 0 80px;
font-size: 13px;
font-weight: 600;
color: #343a40;
}
.forecast-icon {
flex: 0 0 32px;
text-align: center;
font-size: 20px;
}
.forecast-temps {
flex: 0 0 80px;
display: flex;
justify-content: space-between;
margin: 0 12px;
.temp-high {
font-size: 13px;
font-weight: 600;
color: #343a40;
}
.temp-low {
font-size: 13px;
color: #6c757d;
}
}
.forecast-desc {
flex: 1;
font-size: 12px;
color: #6c757d;
text-transform: capitalize;
}
}
}
}
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// Responsive design
@media (max-width: 768px) {
.weather-widget {
.widget-content {
padding: 12px;
.weather-container {
.current-weather-card {
.e-card-content {
.current-weather {
.weather-main {
.weather-icon {
font-size: 36px;
margin-right: 12px;
}
.weather-temp {
font-size: 36px;
}
}
}
}
}
.weather-details {
grid-template-columns: 1fr;
gap: 8px;
.detail-item {
padding: 10px;
}
}
.weather-forecast {
.forecast-list {
.forecast-item {
padding: 10px;
.forecast-day {
flex: 0 0 60px;
font-size: 12px;
}
.forecast-icon {
flex: 0 0 24px;
font-size: 16px;
}
.forecast-temps {
flex: 0 0 60px;
margin: 0 8px;
.temp-high, .temp-low {
font-size: 12px;
}
}
.forecast-desc {
font-size: 11px;
}
}
}
}
}
}
}
}
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({
selector: 'app-weather-widget',
standalone: true,
imports: [CommonModule],
templateUrl: './weather-widget.component.html',
styleUrls: ['./weather-widget.component.scss']
})
export class WeatherWidgetComponent extends BaseWidgetComponent {
public weatherData: any = {};
public currentWeather: any = {};
public forecast: any[] = [];
public location: string = '';
public lastUpdated: Date = new Date();
public refreshInterval: number = 300000; // 5 minutes
constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
}
applyInitialConfig(): void {
this.title = this.config.title || 'Weather';
this.location = this.config.location || 'Bangkok, Thailand';
this.refreshInterval = this.config.refreshInterval || 300000;
this.weatherData = {};
this.currentWeather = {};
this.forecast = [];
}
onDataUpdate(data: any[]): void {
if (data && data.length > 0) {
// Assume data contains weather information
const weatherItem = data[0];
this.currentWeather = {
temperature: weatherItem[this.config.temperatureField || 'temperature'],
humidity: weatherItem[this.config.humidityField || 'humidity'],
windSpeed: weatherItem[this.config.windSpeedField || 'windSpeed'],
pressure: weatherItem[this.config.pressureField || 'pressure'],
description: weatherItem[this.config.descriptionField || 'description'],
icon: weatherItem[this.config.iconField || 'icon'],
feelsLike: weatherItem[this.config.feelsLikeField || 'feelsLike']
};
// Process forecast data if available
if (data.length > 1) {
this.forecast = data.slice(1).map(item => ({
day: item[this.config.dayField || 'day'],
high: item[this.config.highField || 'high'],
low: item[this.config.lowField || 'low'],
description: item[this.config.forecastDescriptionField || 'description'],
icon: item[this.config.forecastIconField || 'icon']
}));
}
this.lastUpdated = new Date();
}
}
onReset(): void {
this.title = 'Weather (Default)';
this.location = 'Bangkok, Thailand';
this.currentWeather = {
temperature: 32,
humidity: 75,
windSpeed: 12,
pressure: 1013,
description: 'Partly Cloudy',
icon: 'partly-cloudy',
feelsLike: 35
};
this.forecast = [
{ day: 'Tomorrow', high: 34, low: 26, description: 'Sunny', icon: 'sunny' },
{ day: 'Wed', high: 33, low: 25, description: 'Cloudy', icon: 'cloudy' },
{ day: 'Thu', high: 31, low: 24, description: 'Rainy', icon: 'rainy' },
{ day: 'Fri', high: 30, low: 23, description: 'Thunderstorm', icon: 'thunderstorm' }
];
this.lastUpdated = new Date();
}
getWeatherIcon(iconType: string): string {
const iconMap: { [key: string]: string } = {
'sunny': '☀️',
'cloudy': '☁️',
'partly-cloudy': '⛅',
'rainy': '🌧️',
'thunderstorm': '⛈️',
'snowy': '❄️',
'foggy': '🌫️'
};
return iconMap[iconType] || '🌤️';
}
getTemperatureColor(temperature: number): string {
if (temperature >= 35) return 'hot';
if (temperature >= 25) return 'warm';
if (temperature >= 15) return 'mild';
return 'cold';
}
getWindDirection(windSpeed: number): string {
if (windSpeed < 5) return 'Calm';
if (windSpeed < 15) return 'Light';
if (windSpeed < 25) return 'Moderate';
return 'Strong';
}
formatTemperature(temp: number): string {
return `${Math.round(temp)}°C`;
}
formatPressure(pressure: number): string {
return `${pressure} hPa`;
}
formatHumidity(humidity: number): string {
return `${humidity}%`;
}
formatWindSpeed(speed: number): string {
return `${speed} km/h`;
}
refreshWeather(): void {
// Trigger data refresh
this.isLoading = true;
// In real implementation, this would call the weather API
setTimeout(() => {
this.isLoading = false;
this.lastUpdated = new Date();
}, 1000);
}
}
import { Component } from '@angular/core';
@Component({
selector: 'app-dashboard-viewer',
template: '<p>dashboard-viewer works!</p>',
standalone: true
})
export class DashboardViewerComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-dashboard',
template: '<p>dashboard works!</p>',
standalone: true
})
export class DashboardComponent {
}
......@@ -7,7 +7,7 @@ import { Router, RouterModule } from '@angular/router';
import { CourseService } from '../../services/course.service';
import { DocumentService } from '../../services/document.service';
import { ExcelService } from '../../services/excel.service';
import { WidgetService } from '../../services/widgets.service';
import { WidgetService } from '../../dashboard-management/services/widgets.service';
import { SharedModule } from '../../../shared/shared.module';
import { DatasourseTableService } from '../../services/datasourse-table.service';
import { DatasourceTableModel, MyDatasourceTableModel } from '../../models/datasource-table.model';
......
......@@ -8,11 +8,11 @@ import { SharedModule } from '../../../shared/shared.module';
import { CourseService } from '../../services/course.service';
import { DocumentService } from '../../services/document.service';
import { ExcelService } from '../../services/excel.service';
import { WidgetService } from '../../services/widgets.service';
import { CompanyModel } from '../../models/company.model';
import { TokenService } from '../../../shared/services/token.service';
import { DatasourceTableModel, MyDatasourceTableModel } from '../../models/datasource-table.model';
import { DatasourseTableService } from '../../services/datasourse-table.service';
import { WidgetService } from '../../dashboard-management/services/widgets.service';
......
......@@ -2,78 +2,72 @@ import { Routes } from '@angular/router';
import { MyPortalComponent } from './my-portal.component';
import { moduleAccessGuard } from '../../core/guards/module-access.guard';
// Import components (you may need to adjust these imports based on actual component names)
// import { CreateCategoryComponent } from './create-category/create-category.component';
// import { CategoryListComponent } from './category-list/category-list.component';
// import { CategoryListApproveComponent } from './category-list-approve/category-list-approve.component';
// import { ApprovedListComponent } from './approved-list/approved-list.component';
// import { ExcelListComponent } from './excel-list/excel-list.component';
// import { ExcelReportComponent } from './excel-report/excel-report.component';
// import { ExcelReportToggleComponent } from './excel-report-toggle/excel-report-toggle.component';
// import { ViewListExcelComponent } from './view-list-excel/view-list-excel.component';
// import { OpenImageComponent } from './open-image/open-image.component';
// import { DatasourceTableComponent } from './datasource-table/datasource-table.component';
// import { AlertModalComponent } from './alert-modal/alert-modal.component';
export const MY_PORTAL_ROUTES: Routes = [
{
path: '',
component: MyPortalComponent,
canActivate: [moduleAccessGuard]
},
// {
// path: 'create-category',
// component: CreateCategoryComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'category-list',
// component: CategoryListComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'category-list-approve',
// component: CategoryListApproveComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'approved-list',
// component: ApprovedListComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'excel-list',
// component: ExcelListComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'excel-report',
// component: ExcelReportComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'excel-report-toggle',
// component: ExcelReportToggleComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'view-list-excel',
// component: ViewListExcelComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'open-image',
// component: OpenImageComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'datasource-table',
// component: DatasourceTableComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'alert-modal',
// component: AlertModalComponent,
// canActivate: [moduleAccessGuard]
// }
canActivate: [moduleAccessGuard],
children: [
{
path: 'create-category',
loadComponent: () => import('./create-category/create-category.component').then(m => m.CreateCategoryComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'category-list',
loadComponent: () => import('./category-list/category-list.component').then(m => m.CategorylistComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'category-list-approve',
loadComponent: () => import('./category-list-approve/category-list-approve.component').then(m => m.CategoryListApproveComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'approved-list',
loadComponent: () => import('./approved-list/approved-list.component').then(m => m.ApprovedListComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'excel-list',
loadComponent: () => import('./excel-list/excel-list.component').then(m => m.ExcelListComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'excel-report',
loadComponent: () => import('./excel-report/excel-report.component').then(m => m.ExcelReportComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'excel-report-toggle',
loadComponent: () => import('./excel-report-toggle/excel-report-toggle.component').then(m => m.ExcelReportToggleComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'view-list-excel',
loadComponent: () => import('./view-list-excel/view-list-excel.component').then(m => m.ViewListExcelComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'open-image',
loadComponent: () => import('./open-image/open-image.component').then(m => m.OpenImageComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'datasource-table',
loadComponent: () => import('./datasource-table/datasource-table.component').then(m => m.DatasourceTableComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'alert-modal',
loadComponent: () => import('./alert-modal/alert-modal.component').then(m => m.AlertModalComponent),
canActivate: [moduleAccessGuard]
},
{
path: '',
redirectTo: 'category-list', // Assuming a default child route
pathMatch: 'full'
}
]
}
];
<div class="view-list-excel">
<div class="header">
<h2>View List Excel</h2>
</div>
<div class="content">
<p>Excel view component placeholder</p>
</div>
</div>
.view-list-excel {
padding: 20px;
.header {
margin-bottom: 20px;
h2 {
margin: 0;
color: #333;
}
}
.content {
p {
color: #666;
}
}
}
......@@ -8,10 +8,11 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
import swal from 'sweetalert';
import { saveAs } from 'file-saver';
import { SharedModule } from '../../../../shared/shared.module';
import { ExcelContentModel } from '../../../models/excel-content.model';
import { ExcelService } from '../../../services/excel.service';
import { OpenImageComponent } from '../../../open-image/open-image.component';
import { ExcelContentModel } from '../../models/excel-content.model';
import { SharedModule } from '../../../shared/shared.module';
import { ExcelService } from '../../services/excel.service';
import { OpenImageComponent } from '../open-image/open-image.component';
@Component({
selector: 'app-view-list-excel',
......@@ -148,4 +149,4 @@ export class ViewListExcelComponent implements OnInit {
coverDate(date: string) {
return date.split('-').reverse().join('/');
}
}
\ No newline at end of file
}
import { Component } from '@angular/core';
@Component({
selector: 'app-attendance-location',
template: '<p>attendance-location works!</p>',
standalone: true
})
export class AttendanceLocationComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-attendance-settings',
template: '<p>attendance-settings works!</p>',
standalone: true
})
export class AttendanceSettingsComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-face-enrollment',
template: '<p>face-enrollment works!</p>',
standalone: true
})
export class FaceEnrollmentComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-face-verification',
template: '<p>face-verification works!</p>',
standalone: true
})
export class FaceVerificationComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-security-report',
template: '<p>security-report works!</p>',
standalone: true
})
export class SecurityReportComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-attendance-reports',
template: '<p>attendance-reports works!</p>',
standalone: true
})
export class AttendanceReportsComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-company-settings',
template: '<p>company-settings works!</p>',
standalone: true
})
export class CompanySettingsComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-department-settings',
template: '<p>department-settings works!</p>',
standalone: true
})
export class DepartmentSettingsComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-employee-documents',
template: '<p>employee-documents works!</p>',
standalone: true
})
export class EmployeeDocumentsComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-employee-profile',
template: '<p>employee-profile works!</p>',
standalone: true
})
export class EmployeeProfileComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-employee-reports',
template: '<p>employee-reports works!</p>',
standalone: true
})
export class EmployeeReportsComponent {
}
......@@ -20,6 +20,34 @@ export const MYHR_LITE_ROUTES: Routes = [
loadComponent: () => import('./reports/myhr-lite-reports.component').then(m => m.MyhrLiteReportsComponent)
},
{
path: 'employee-profile',
loadComponent: () => import('./employee-profile/employee-profile.component').then(m => m.EmployeeProfileComponent)
},
{
path: 'employee-documents',
loadComponent: () => import('./employee-documents/employee-documents.component').then(m => m.EmployeeDocumentsComponent)
},
{
path: 'company-settings',
loadComponent: () => import('./company-settings/company-settings.component').then(m => m.CompanySettingsComponent)
},
{
path: 'department-settings',
loadComponent: () => import('./department-settings/department-settings.component').then(m => m.DepartmentSettingsComponent)
},
{
path: 'position-settings',
loadComponent: () => import('./position-settings/position-settings.component').then(m => m.PositionSettingsComponent)
},
{
path: 'employee-reports',
loadComponent: () => import('./employee-reports/employee-reports.component').then(m => m.EmployeeReportsComponent)
},
{
path: 'attendance-reports',
loadComponent: () => import('./attendance-reports/attendance-reports.component').then(m => m.AttendanceReportsComponent)
},
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
......
import { Component } from '@angular/core';
@Component({
selector: 'app-position-settings',
template: '<p>position-settings works!</p>',
standalone: true
})
export class PositionSettingsComponent {
}
......@@ -52,6 +52,10 @@ export const MYHR_PLUS_ROUTES: Routes = [
loadComponent: () => import('./reports/myhr-plus-excel-report.component').then(m => m.MyhrPlusExcelReportComponent)
},
{
path: 'widget-warehouse',
loadChildren: () => import('../dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
},
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
......
......@@ -13,7 +13,53 @@ export const MYJOB_ROUTES: Routes = [
},
{
path: 'pdpa-manage',
loadComponent: () => import('./pdpa/myjob-pdpa-manage.component').then(m => m.MyjobPdpaManageComponent)
loadComponent: () => import('./pdpa-manage/pdpa-manage.component').then(m => m.PdpaManageComponent)
},
{
path: 'admin-manage',
loadComponent: () => import('./admin-manage/admin-manage.component').then(m => m.AdminManageComponent)
},
{
path: 'article-manage',
loadComponent: () => import('./article-manage/article-manage.component').then(m => m.ArticleManageComponent)
},
{
path: 'company-department',
loadComponent: () => import('./company-department/company-department.component').then(m => m.CompanyDepartmentComponent)
},
{
path: 'company-manage',
loadComponent: () => import('./company-manage/company-manage.component').then(m => m.CompanyManageComponent)
},
{
path: 'home-common',
loadComponent: () => import('./home-common/home-common.component').then(m => m.HomeCommonComponent)
},
{
path: 'employee',
children: [
{
path: 'department',
loadComponent: () => import('./employee/department/department.component').then(m => m.DepartmentComponent)
},
{
path: 'position',
loadComponent: () => import('./employee/position/position.component').then(m => m.PositionComponent)
}
]
},
{
path: 'user-management',
children: [
{
path: '',
loadComponent: () => import('./user-management/user-management/user-management.component').then(m => m.UserManagementComponent)
},
{
path: 'user-setting',
loadComponent: () => import('./user-management/user-setting/user-setting.component').then(m => m.UserSettingComponent)
}
]
},
{
path: 'manage-articles',
......
import { Component } from '@angular/core';
@Component({
selector: 'app-admin-manage',
template: '<p>admin-manage works!</p>',
standalone: true
})
export class AdminManageComponent {
}
......@@ -12,6 +12,14 @@ export const MYSKILL_X_ROUTES: Routes = [
component: MyskillXDashboardComponent
},
{
path: 'admin-manage',
loadComponent: () => import('./admin-manage/admin-manage.component').then(m => m.AdminManageComponent)
},
{
path: 'user-management',
loadComponent: () => import('./user-management/user-management.component').then(m => m.UserManagementComponent)
},
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
......
import { Component } from '@angular/core';
@Component({
selector: 'app-user-management',
template: '<p>user-management works!</p>',
standalone: true
})
export class UserManagementComponent {
}
......@@ -12,7 +12,6 @@ export const portalManageRoutes: Routes = [
// myHR-Plus Module
{
path: 'myhr-plus',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./myhr-plus/myhr-plus.routes').then(m => m.MYHR_PLUS_ROUTES)
},
......@@ -58,21 +57,21 @@ export const portalManageRoutes: Routes = [
loadChildren: () => import('./myskill-x/myskill-x.routes').then(m => m.MYSKILL_X_ROUTES)
},
// Company Management Module
{
path: 'company-management',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./company-management/company-management.routes').then(m => m.COMPANY_MANAGEMENT_ROUTES)
},
// === การบริการ ===
// Dashboard Management (รวม widget management)
{
path: 'dashboard-management',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
},
// Dashboard (alias สำหรับ backward compatibility)
{
path: 'dashboard',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
},
// Meeting Booking
{
......@@ -104,34 +103,20 @@ export const portalManageRoutes: Routes = [
loadChildren: () => import('./my-portal/my-portal.routes').then(m => m.MY_PORTAL_ROUTES)
},
// === Generic App Routes ===
// These routes are for simple apps that don't need special module-level services.
// Dynamic route for dashboard management per application
{
path: ':appName/dashboard-management',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
},
// Dynamic routes for widget warehouse per application
// Dashboard (Old - for compatibility)
{
path: ':appName/widget-warehouse',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
path: 'dashboard',
component: HomeComponent, // Assuming HomeComponent is a generic dashboard or a placeholder
canActivate: [moduleAccessGuard]
},
{
path: ':appName/widget-linker',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
path: 'dashboard-viewer',
loadComponent: () => import('./dashboard-viewer/dashboard-viewer.component').then(m => m.DashboardViewerComponent),
canActivate: [moduleAccessGuard]
},
// Route for viewing a specific dashboard
{
path: 'dashboard-viewer/:dashboardId',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
},
// === Generic App Routes ===
// These routes are for simple apps that don't need special module-level services.
// Redirect for unknown routes
{
......
......@@ -43,6 +43,11 @@ export class MenuPermissionService {
* ตรวจสอบสิทธิ์การเข้าถึงเมนู
*/
canAccessMenu(menuPath: string, permission: 'view' | 'create' | 'edit' | 'delete' | 'export' | 'import' = 'view'): Observable<boolean> {
// ไม่ต้องเช็ค permission สำหรับ dashboard-management และ widget-warehouse
if (menuPath.includes('dashboard-management') || menuPath.includes('widget-warehouse')) {
return of(true);
}
return this.menuPermissions$.pipe(
map(menus => {
const menu = this.findMenuByPath(menus, menuPath);
......@@ -173,7 +178,7 @@ export class MenuPermissionService {
{
id: 'widget-warehouse',
name: 'คลังวิดเจ็ต',
path: '/portal-manage/dashboard/widget-warehouse',
path: '/portal-manage/myhr-plus/widget-warehouse',
order: 2,
isVisible: true,
permissions: {
......
......@@ -447,7 +447,22 @@ export class SidebarComponent {
if (ele.path == this.currentUrl) {
element.active = true;
element.selected = true;
ele.active = true;
ele.selected = true;
}
// ตรวจสอบ path สำหรับ dashboard management routes
if (this.isDashboardManagementRoute && ele.path && this.currentUrl.startsWith(ele.path.split('?')[0])) {
const currentUrlBase = this.currentUrl.split('?')[0];
const elePathBase = ele.path.split('?')[0];
if (currentUrlBase === elePathBase || this.currentUrl.includes(elePathBase)) {
element.active = true;
element.selected = true;
ele.active = true;
ele.selected = true;
}
}
// ตรวจสอบ path สำหรับ widget routes
if ((this.isWidgetWarehouseRoute || this.isWidgetLinkerRoute) && ele.path && this.currentUrl.startsWith(ele.path.split('?')[0])) {
element.active = true;
......@@ -455,6 +470,7 @@ export class SidebarComponent {
ele.active = true;
ele.selected = true;
}
// ตรวจสอบ path สำหรับ Excel Report ที่มี query parameters
if ((this.isMyportalRoute||this.isMyPortalRoute||this.isInstallerRoute)&&ele.path && this.currentUrl.startsWith(ele.path.split('?')[0])) {
const currentUrlBase = this.currentUrl.split('?')[0];
......@@ -475,7 +491,24 @@ export class SidebarComponent {
element.selected = true;
ele.active = true;
ele.selected = true;
child1.active = true;
child1.selected = true;
}
// ตรวจสอบ path สำหรับ dashboard management routes ในระดับที่ 3
if (this.isDashboardManagementRoute && child1.path && this.currentUrl.startsWith(child1.path.split('?')[0])) {
const currentUrlBase = this.currentUrl.split('?')[0];
const child1PathBase = child1.path.split('?')[0];
if (currentUrlBase === child1PathBase || this.currentUrl.includes(child1PathBase)) {
element.active = true;
element.selected = true;
ele.active = true;
ele.selected = true;
child1.active = true;
child1.selected = true;
}
}
// ตรวจสอบ path สำหรับ widget routes ในระดับที่ 3
if ((this.isWidgetWarehouseRoute || this.isWidgetLinkerRoute) && child1.path && this.currentUrl.startsWith(child1.path.split('?')[0])) {
element.active = true;
......@@ -485,6 +518,7 @@ export class SidebarComponent {
child1.active = true;
child1.selected = true;
}
// ตรวจสอบ path สำหรับ Excel Report ที่มี query parameters ในระดับที่ 3
if ((this.isMyportalRoute||this.isMyPortalRoute||this.isInstallerRoute)&&child1.path && this.currentUrl.startsWith(child1.path.split('?')[0])) {
const currentUrlBase = this.currentUrl.split('?')[0];
......
......@@ -29,7 +29,7 @@ export const authen: Routes = [
]
@NgModule({
imports: [RouterModule.forRoot(admin)],
imports: [RouterModule.forRoot(authen)],
exports: [RouterModule]
})
export class AuthenticationsRoutingModule { }
\ No newline at end of file
......@@ -14,7 +14,7 @@ export const landing: Routes = [
];
@NgModule({
imports: [RouterModule.forRoot(admin)],
imports: [RouterModule.forRoot(landing)],
exports: [RouterModule]
})
export class landingpageRoutingModule { }
......@@ -99,14 +99,50 @@ export class NavService implements OnDestroy {
active: false,
children: [
{
path: `/portal-manage/${appName}/dashboard-management/dashboard`,
path: `/portal-manage/dashboard-management/dashboard-home`,
title: 'แดชบอร์ดหลัก',
type: 'link'
},
{
path: `/portal-manage/${appName}/dashboard-management/widget-management`,
title: 'จัดการวิดเจ็ต',
type: 'link'
icon: 'widget',
type: 'sub',
active: false,
children: [
{
path: `/portal-manage/dashboard-management/widget-list`,
title: 'รายการวิดเจ็ต',
type: 'link'
},
// {
// path: `/portal-manage/dashboard-management/widget-management/edit`,
// title: 'เพิ่มวิดเจ็ตใหม่',
// type: 'link'
// },
{
path: `/portal-manage/dashboard-management/dataset-widget-linker`,
title: 'เชื่อมโยงข้อมูลกับวิดเจ็ต',
type: 'link'
},
// {
// path: `/portal-manage/dashboard-management/widget-config`,
// title: 'ตั้งค่าวิดเจ็ต',
// type: 'link'
// }
]
},
{
title: 'ดูแดชบอร์ด',
icon: 'eye',
type: 'sub',
active: false,
children: [
{
path: `/portal-manage/dashboard-management/dashboard-viewer`,
title: 'ดูแดชบอร์ด',
type: 'link'
}
]
},
{
path: `/portal-manage/${appName}/widget-warehouse`,
......@@ -117,7 +153,7 @@ export class NavService implements OnDestroy {
path: `/portal-manage/${appName}/widget-linker`,
title: 'เชื่อมโยงวิดเจ็ตกับชุดข้อมูล',
type: 'link'
},
}
]
};
}
......@@ -558,10 +594,11 @@ export class NavService implements OnDestroy {
type: 'sub',
active: false,
children: [
{ path: '/portal-manage/dashboard-management/dashboard', title: 'แดชบอร์ดหลัก', type: 'link' },
{ path: '/portal-manage/dashboard-management/widget-management', title: 'จัดการวิดเจ็ต', type: 'link' },
{ path: '/portal-manage/dashboard-management/dashboard-home', title: 'แดชบอร์ดหลัก', type: 'link' },
{ path: '/portal-manage/dashboard-management/widget-list', title: 'รายการวิดเจ็ต', type: 'link' },
{ path: '/portal-manage/dashboard-management/dataset-widget-linker', title: 'เชื่อมโยงข้อมูลกับวิดเจ็ต', type: 'link' },
{ path: '/portal-manage/dashboard-management/widget-config', title: 'ตั้งค่าวิดเจ็ต', type: 'link' },
{ path: '/portal-manage/dashboard-management/dataset-picker', title: 'เลือกชุดข้อมูล', type: 'link' },
{ path: '/portal-manage/dashboard-management/dashboard-viewer', title: 'ดูแดชบอร์ด', type: 'link' },
],
},
{
......@@ -583,7 +620,7 @@ export class NavService implements OnDestroy {
{
title: 'แดชบอร์ดหลัก',
icon: 'dashboard',
path: '/portal-manage/dashboard-management/dashboard',
path: '/portal-manage/dashboard-management/dashboard-home',
type: 'link',
},
{
......@@ -593,94 +630,27 @@ export class NavService implements OnDestroy {
active: false,
children: [
{
path: '/portal-manage/dashboard-management/widget-management',
path: '/portal-manage/dashboard-management/widget-list',
title: 'รายการวิดเจ็ต',
type: 'link'
},
// {
// path: '/portal-manage/dashboard-management/widget-management/edit',
// title: 'เพิ่มวิดเจ็ตใหม่',
// type: 'link'
// },
{
path: '/portal-manage/dashboard-management/widget-management/edit',
title: 'เพิ่มวิดเจ็ตใหม่',
type: 'link'
},
{
path: '/portal-manage/dashboard-management/widget-management/linker',
title: 'เชื่อมโยงข้อมูล',
type: 'link'
},
{
path: '/portal-manage/dashboard-management/widget-config',
title: 'ตั้งค่าวิดเจ็ต',
type: 'link'
},
],
},
{
title: 'คลังวิดเจ็ตแอป',
icon: 'package',
type: 'sub',
active: false,
children: [
{
path: '/portal-manage/myhr-plus/widget-warehouse',
title: 'คลังวิดเจ็ต myHR-Plus',
type: 'link'
},
{
path: '/portal-manage/myhr-lite/widget-warehouse',
title: 'คลังวิดเจ็ต myHR-Lite',
type: 'link'
},
{
path: '/portal-manage/myjob/widget-warehouse',
title: 'คลังวิดเจ็ต MyJob',
type: 'link'
},
{
path: '/portal-manage/mylearn/widget-warehouse',
title: 'คลังวิดเจ็ต MyLearn',
path: '/portal-manage/dashboard-management/dataset-widget-linker',
title: 'เชื่อมโยงข้อมูลกับวิดเจ็ต',
type: 'link'
},
// {
// path: '/portal-manage/dashboard-management/widget-config',
// title: 'ตั้งค่าวิดเจ็ต',
// type: 'link'
// },
],
},
{
title: 'จัดการข้อมูล',
icon: 'database',
type: 'sub',
active: false,
children: [
{
path: '/portal-manage/dashboard-management/dataset-picker',
title: 'เลือกชุดข้อมูล',
type: 'link'
},
{
path: '/portal-manage/myhr-plus/widget-linker',
title: 'เชื่อมโยงข้อมูล myHR-Plus',
type: 'link'
},
{
path: '/portal-manage/myhr-lite/widget-linker',
title: 'เชื่อมโยงข้อมูล myHR-Lite',
type: 'link'
},
{
path: '/portal-manage/myjob/widget-linker',
title: 'เชื่อมโยงข้อมูล MyJob',
type: 'link'
},
{
path: '/portal-manage/mylearn/widget-linker',
title: 'เชื่อมโยงข้อมูล MyLearn',
type: 'link'
},
],
},
{
title: 'ดูแดชบอร์ด',
icon: 'eye',
path: '/portal-manage/dashboard-management/viewer',
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