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 @@ ...@@ -35,6 +35,7 @@
"@ngx-translate/http-loader": "^8.0.0", "@ngx-translate/http-loader": "^8.0.0",
"@syncfusion/ej2-angular-base": "^31.1.17", "@syncfusion/ej2-angular-base": "^31.1.17",
"@syncfusion/ej2-angular-buttons": "^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-charts": "^31.1.17",
"@syncfusion/ej2-angular-circulargauge": "^31.1.17", "@syncfusion/ej2-angular-circulargauge": "^31.1.17",
"@syncfusion/ej2-angular-dropdowns": "^31.1.17", "@syncfusion/ej2-angular-dropdowns": "^31.1.17",
...@@ -43,8 +44,10 @@ ...@@ -43,8 +44,10 @@
"@syncfusion/ej2-angular-layouts": "^31.1.17", "@syncfusion/ej2-angular-layouts": "^31.1.17",
"@syncfusion/ej2-angular-maps": "^31.1.17", "@syncfusion/ej2-angular-maps": "^31.1.17",
"@syncfusion/ej2-angular-navigations": "^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-pivotview": "^31.1.17",
"@syncfusion/ej2-angular-popups": "^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-angular-treemap": "^31.1.17",
"@syncfusion/ej2-base": "^31.1.17", "@syncfusion/ej2-base": "^31.1.17",
"@syncfusion/ej2-buttons": "^31.1.17", "@syncfusion/ej2-buttons": "^31.1.17",
...@@ -130,6 +133,7 @@ ...@@ -130,6 +133,7 @@
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
...@@ -615,6 +619,7 @@ ...@@ -615,6 +619,7 @@
"version": "17.3.12", "version": "17.3.12",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.12.tgz", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.12.tgz",
"integrity": "sha512-1F8M7nWfChzurb7obbvuE7mJXlHtY1UG58pcwcomVtpPb+kPavgAO8OEvJHYBMV+bzSxkXt5UIwL9lt9jHUxZA==", "integrity": "sha512-1F8M7nWfChzurb7obbvuE7mJXlHtY1UG58pcwcomVtpPb+kPavgAO8OEvJHYBMV+bzSxkXt5UIwL9lt9jHUxZA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/core": "7.23.9", "@babel/core": "7.23.9",
...@@ -643,6 +648,7 @@ ...@@ -643,6 +648,7 @@
"version": "7.23.9", "version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
"integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
...@@ -673,12 +679,14 @@ ...@@ -673,12 +679,14 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
...@@ -1090,6 +1098,7 @@ ...@@ -1090,6 +1098,7 @@
"version": "7.26.10", "version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
...@@ -1120,12 +1129,14 @@ ...@@ -1120,12 +1129,14 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@babel/core/node_modules/semver": { "node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
...@@ -3779,16 +3790,6 @@ ...@@ -3779,16 +3790,6 @@
"@fortawesome/fontawesome-svg-core": "~1.2.27 || ~1.3.0-beta2 || ^6.1.0" "@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": { "node_modules/@fortawesome/fontawesome-free": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.0.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.0.tgz",
...@@ -3798,19 +3799,6 @@ ...@@ -3798,19 +3799,6 @@
"node": ">=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": { "node_modules/@grpc/grpc-js": {
"version": "1.9.15", "version": "1.9.15",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz",
...@@ -3885,6 +3873,7 @@ ...@@ -3885,6 +3873,7 @@
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"string-width": "^5.1.2", "string-width": "^5.1.2",
...@@ -3902,6 +3891,7 @@ ...@@ -3902,6 +3891,7 @@
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
...@@ -3914,6 +3904,7 @@ ...@@ -3914,6 +3904,7 @@
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
...@@ -3926,12 +3917,14 @@ ...@@ -3926,12 +3917,14 @@
"version": "9.2.2", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@isaacs/cliui/node_modules/string-width": { "node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"eastasianwidth": "^0.2.0", "eastasianwidth": "^0.2.0",
...@@ -3949,6 +3942,7 @@ ...@@ -3949,6 +3942,7 @@
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^6.0.1" "ansi-regex": "^6.0.1"
...@@ -3964,6 +3958,7 @@ ...@@ -3964,6 +3958,7 @@
"version": "8.1.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^6.1.0", "ansi-styles": "^6.1.0",
...@@ -4083,13 +4078,6 @@ ...@@ -4083,13 +4078,6 @@
"rxjs": ">=7.5.0" "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": { "node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
...@@ -5700,6 +5688,7 @@ ...@@ -5700,6 +5688,7 @@
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"engines": { "engines": {
...@@ -6202,6 +6191,17 @@ ...@@ -6202,6 +6191,17 @@
"@syncfusion/ej2-buttons": "31.1.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": { "node_modules/@syncfusion/ej2-angular-charts": {
"version": "31.1.19", "version": "31.1.19",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-charts/-/ej2-angular-charts-31.1.19.tgz", "resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-charts/-/ej2-angular-charts-31.1.19.tgz",
...@@ -6290,6 +6290,17 @@ ...@@ -6290,6 +6290,17 @@
"@syncfusion/ej2-navigations": "31.1.18" "@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": { "node_modules/@syncfusion/ej2-angular-pivotview": {
"version": "31.1.17", "version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-pivotview/-/ej2-angular-pivotview-31.1.17.tgz", "resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-pivotview/-/ej2-angular-pivotview-31.1.17.tgz",
...@@ -6312,6 +6323,17 @@ ...@@ -6312,6 +6323,17 @@
"@syncfusion/ej2-popups": "31.1.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": { "node_modules/@syncfusion/ej2-angular-treemap": {
"version": "31.1.17", "version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-treemap/-/ej2-angular-treemap-31.1.17.tgz", "resolved": "https://registry.npmjs.org/@syncfusion/ej2-angular-treemap/-/ej2-angular-treemap-31.1.17.tgz",
...@@ -6578,6 +6600,17 @@ ...@@ -6578,6 +6600,17 @@
"@syncfusion/ej2-buttons": "~31.1.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": { "node_modules/@syncfusion/ej2-splitbuttons": {
"version": "31.1.17", "version": "31.1.17",
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-splitbuttons/-/ej2-splitbuttons-31.1.17.tgz", "resolved": "https://registry.npmjs.org/@syncfusion/ej2-splitbuttons/-/ej2-splitbuttons-31.1.17.tgz",
...@@ -6888,16 +6921,6 @@ ...@@ -6888,16 +6921,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
...@@ -7024,13 +7047,6 @@ ...@@ -7024,13 +7047,6 @@
"@types/send": "*" "@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": { "node_modules/@types/sockjs": {
"version": "0.3.36", "version": "0.3.36",
"resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz",
...@@ -7560,6 +7576,7 @@ ...@@ -7560,6 +7576,7 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/anymatch": { "node_modules/anymatch": {
...@@ -7591,6 +7608,7 @@ ...@@ -7591,6 +7608,7 @@
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/argparse": { "node_modules/argparse": {
...@@ -7744,6 +7762,7 @@ ...@@ -7744,6 +7762,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-js": { "node_modules/base64-js": {
...@@ -8127,6 +8146,7 @@ ...@@ -8127,6 +8146,7 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
...@@ -8175,19 +8195,6 @@ ...@@ -8175,19 +8195,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/chokidar": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
...@@ -8405,6 +8412,7 @@ ...@@ -8405,6 +8412,7 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
...@@ -8543,6 +8551,7 @@ ...@@ -8543,6 +8551,7 @@
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": { "node_modules/cookie": {
...@@ -8721,6 +8730,7 @@ ...@@ -8721,6 +8730,7 @@
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
...@@ -8735,6 +8745,7 @@ ...@@ -8735,6 +8745,7 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
...@@ -8816,6 +8827,7 @@ ...@@ -8816,6 +8827,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"cssesc": "bin/cssesc" "cssesc": "bin/cssesc"
...@@ -9058,6 +9070,7 @@ ...@@ -9058,6 +9070,7 @@
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/dir-glob": { "node_modules/dir-glob": {
...@@ -9077,6 +9090,7 @@ ...@@ -9077,6 +9090,7 @@
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/dns-packet": { "node_modules/dns-packet": {
...@@ -9197,6 +9211,7 @@ ...@@ -9197,6 +9211,7 @@
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/echarts": { "node_modules/echarts": {
...@@ -9264,6 +9279,7 @@ ...@@ -9264,6 +9279,7 @@
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
...@@ -9274,6 +9290,7 @@ ...@@ -9274,6 +9290,7 @@
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
...@@ -10135,6 +10152,7 @@ ...@@ -10135,6 +10152,7 @@
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
...@@ -10151,6 +10169,7 @@ ...@@ -10151,6 +10169,7 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
...@@ -11243,6 +11262,7 @@ ...@@ -11243,6 +11262,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/isobject": { "node_modules/isobject": {
...@@ -11350,6 +11370,7 @@ ...@@ -11350,6 +11370,7 @@
"version": "3.4.3", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/cliui": "^8.0.2" "@isaacs/cliui": "^8.0.2"
...@@ -11403,6 +11424,7 @@ ...@@ -11403,6 +11424,7 @@
"version": "1.21.7", "version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
...@@ -11892,6 +11914,7 @@ ...@@ -11892,6 +11914,7 @@
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/loader-runner": { "node_modules/loader-runner": {
...@@ -12302,6 +12325,7 @@ ...@@ -12302,6 +12325,7 @@
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
...@@ -12568,6 +12592,7 @@ ...@@ -12568,6 +12592,7 @@
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"any-promise": "^1.0.0", "any-promise": "^1.0.0",
...@@ -13173,13 +13198,6 @@ ...@@ -13173,13 +13198,6 @@
"node": ">=0.10.0" "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": { "node_modules/npm": {
"version": "10.9.3", "version": "10.9.3",
"resolved": "https://registry.npmjs.org/npm/-/npm-10.9.3.tgz", "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.3.tgz",
...@@ -15795,6 +15813,7 @@ ...@@ -15795,6 +15813,7 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
...@@ -15804,6 +15823,7 @@ ...@@ -15804,6 +15823,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
...@@ -16050,6 +16070,7 @@ ...@@ -16050,6 +16070,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true,
"license": "BlueOak-1.0.0" "license": "BlueOak-1.0.0"
}, },
"node_modules/pacote": { "node_modules/pacote": {
...@@ -16228,6 +16249,7 @@ ...@@ -16228,6 +16249,7 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
...@@ -16243,6 +16265,7 @@ ...@@ -16243,6 +16265,7 @@
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"lru-cache": "^10.2.0", "lru-cache": "^10.2.0",
...@@ -16259,6 +16282,7 @@ ...@@ -16259,6 +16282,7 @@
"version": "10.4.3", "version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
...@@ -16309,6 +16333,7 @@ ...@@ -16309,6 +16333,7 @@
"version": "4.0.7", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
...@@ -16545,6 +16570,7 @@ ...@@ -16545,6 +16570,7 @@
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"camelcase-css": "^2.0.1" "camelcase-css": "^2.0.1"
...@@ -16705,6 +16731,7 @@ ...@@ -16705,6 +16731,7 @@
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
...@@ -16730,6 +16757,7 @@ ...@@ -16730,6 +16757,7 @@
"version": "6.1.2", "version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cssesc": "^3.0.0", "cssesc": "^3.0.0",
...@@ -17270,6 +17298,7 @@ ...@@ -17270,6 +17298,7 @@
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/regenerate": { "node_modules/regenerate": {
...@@ -18095,6 +18124,7 @@ ...@@ -18095,6 +18124,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"shebang-regex": "^3.0.0" "shebang-regex": "^3.0.0"
...@@ -18107,6 +18137,7 @@ ...@@ -18107,6 +18137,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
...@@ -18657,6 +18688,7 @@ ...@@ -18657,6 +18688,7 @@
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
...@@ -18684,6 +18716,7 @@ ...@@ -18684,6 +18716,7 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
...@@ -18706,6 +18739,7 @@ ...@@ -18706,6 +18739,7 @@
"version": "3.35.0", "version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/gen-mapping": "^0.3.2",
...@@ -18728,6 +18762,7 @@ ...@@ -18728,6 +18762,7 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
...@@ -18737,6 +18772,7 @@ ...@@ -18737,6 +18772,7 @@
"version": "10.4.5", "version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"foreground-child": "^3.1.0", "foreground-child": "^3.1.0",
...@@ -18757,6 +18793,7 @@ ...@@ -18757,6 +18793,7 @@
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
...@@ -18857,6 +18894,7 @@ ...@@ -18857,6 +18894,7 @@
"version": "3.4.17", "version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
...@@ -18894,6 +18932,7 @@ ...@@ -18894,6 +18932,7 @@
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"is-glob": "^4.0.3" "is-glob": "^4.0.3"
...@@ -18906,6 +18945,7 @@ ...@@ -18906,6 +18945,7 @@
"version": "15.1.0", "version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"postcss-value-parser": "^4.0.0", "postcss-value-parser": "^4.0.0",
...@@ -18923,6 +18963,7 @@ ...@@ -18923,6 +18963,7 @@
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
...@@ -18958,6 +18999,7 @@ ...@@ -18958,6 +18999,7 @@
"version": "6.1.2", "version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cssesc": "^3.0.0", "cssesc": "^3.0.0",
...@@ -19173,6 +19215,7 @@ ...@@ -19173,6 +19215,7 @@
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"any-promise": "^1.0.0" "any-promise": "^1.0.0"
...@@ -19182,6 +19225,7 @@ ...@@ -19182,6 +19225,7 @@
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"thenify": ">= 3.1.0 < 4" "thenify": ">= 3.1.0 < 4"
...@@ -19295,6 +19339,7 @@ ...@@ -19295,6 +19339,7 @@
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/tslib": { "node_modules/tslib": {
...@@ -19355,6 +19400,7 @@ ...@@ -19355,6 +19400,7 @@
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
...@@ -20543,6 +20589,7 @@ ...@@ -20543,6 +20589,7 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^4.0.0", "ansi-styles": "^4.0.0",
......
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
"@ngx-translate/http-loader": "^8.0.0", "@ngx-translate/http-loader": "^8.0.0",
"@syncfusion/ej2-angular-base": "^31.1.17", "@syncfusion/ej2-angular-base": "^31.1.17",
"@syncfusion/ej2-angular-buttons": "^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-charts": "^31.1.17",
"@syncfusion/ej2-angular-circulargauge": "^31.1.17", "@syncfusion/ej2-angular-circulargauge": "^31.1.17",
"@syncfusion/ej2-angular-dropdowns": "^31.1.17", "@syncfusion/ej2-angular-dropdowns": "^31.1.17",
...@@ -48,8 +49,10 @@ ...@@ -48,8 +49,10 @@
"@syncfusion/ej2-angular-layouts": "^31.1.17", "@syncfusion/ej2-angular-layouts": "^31.1.17",
"@syncfusion/ej2-angular-maps": "^31.1.17", "@syncfusion/ej2-angular-maps": "^31.1.17",
"@syncfusion/ej2-angular-navigations": "^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-pivotview": "^31.1.17",
"@syncfusion/ej2-angular-popups": "^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-angular-treemap": "^31.1.17",
"@syncfusion/ej2-base": "^31.1.17", "@syncfusion/ej2-base": "^31.1.17",
"@syncfusion/ej2-buttons": "^31.1.17", "@syncfusion/ej2-buttons": "^31.1.17",
......
...@@ -7,6 +7,11 @@ export const moduleAccessGuard: CanActivateFn = (route: ActivatedRouteSnapshot, ...@@ -7,6 +7,11 @@ export const moduleAccessGuard: CanActivateFn = (route: ActivatedRouteSnapshot,
const permissionService = inject(CorePermissionService); const permissionService = inject(CorePermissionService);
const router = inject(Router); 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 // Get the module name from the route parameter :appName or from the route path
let moduleName = route.params['appName']; 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 ...@@ -40,155 +40,34 @@ import { SyncfusionPivotWidgetComponent } from './widgets/syncfusion-pivot-widge
import { TreemapWidgetComponent } from './widgets/treemap-widget/treemap-widget.component'; import { TreemapWidgetComponent } from './widgets/treemap-widget/treemap-widget.component';
import { WaterfallChartWidgetComponent } from './widgets/waterfall-chart-widget/waterfall-chart-widget.component'; import { WaterfallChartWidgetComponent } from './widgets/waterfall-chart-widget/waterfall-chart-widget.component';
import { WelcomeWidgetComponent } from './widgets/welcome-widget/welcome-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 = [ export const routes: Routes = [
{ {
path: '', path: 'dashboard-home',
component: DashboardManagementComponent, 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: 'ตัวอย่างวิดเจ็ต'
}
]
}, },
// Routes for dynamic app-based routing // {
// path: 'widget-config',
// component: WidgetConfigComponent
// },
{ {
path: ':appName/dashboard-management', path: 'dashboard-viewer',
component: DashboardManagementComponent, component: DashboardViewerComponent
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: 'ตัวอย่างวิดเจ็ต'
}
]
}, },
// Routes for widget warehouse
{ {
path: ':appName/widget-warehouse', path: 'widget-list',
children: [ component: WidgetListComponent
{
path: '',
component: WidgetListComponent,
title: 'คลังวิดเจ็ต'
},
{
path: 'edit/:widgetId',
component: WidgetFormComponent,
title: 'แก้ไขวิดเจ็ต'
},
{
path: 'preview/:widgetType',
component: DashboardViewerComponent,
title: 'ตัวอย่างวิดเจ็ต'
}
]
}, },
{ {
path: ':appName/widget-linker', path: 'dataset-widget-linker',
component: DatasetWidgetLinkerComponent, component: DatasetWidgetLinkerComponent
title: 'เชื่อมโยงข้อมูลกับวิดเจ็ต'
}, },
// Direct dashboard viewer route
{
path: 'dashboard-viewer/:dashboardId',
component: DashboardViewerComponent,
title: 'ดูแดชบอร์ด'
}
]; ];
@NgModule({ @NgModule({
...@@ -232,7 +111,12 @@ export const routes: Routes = [ ...@@ -232,7 +111,12 @@ export const routes: Routes = [
SyncfusionPivotWidgetComponent, SyncfusionPivotWidgetComponent,
TreemapWidgetComponent, TreemapWidgetComponent,
WaterfallChartWidgetComponent, WaterfallChartWidgetComponent,
WelcomeWidgetComponent WelcomeWidgetComponent,
// New Syncfusion-based widgets
CalendarWidgetComponent,
NotificationWidgetComponent,
WeatherWidgetComponent,
ClockWidgetComponent
], ],
exports: [] exports: []
}) })
......
...@@ -35,11 +35,13 @@ import { WidgetService } from '../services/widgets.service'; ...@@ -35,11 +35,13 @@ import { WidgetService } from '../services/widgets.service';
import { SimpleKpiWidgetComponent } from '../widgets/simple-kpi-widget/simple-kpi-widget.component'; import { SimpleKpiWidgetComponent } from '../widgets/simple-kpi-widget/simple-kpi-widget.component';
import { WidgetFormComponent } from './widget-form.component'; import { WidgetFormComponent } from './widget-form.component';
import { ClickEventArgs } from '@syncfusion/ej2-angular-navigations'; import { ClickEventArgs } from '@syncfusion/ej2-angular-navigations';
import { RouterModule } from '@angular/router';
@Component({ @Component({
selector: 'app-widget-list', selector: 'app-widget-list',
standalone: true, standalone: true,
imports: [ imports: [
RouterModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
MatDialogModule, 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'; ...@@ -7,7 +7,7 @@ import { Router, RouterModule } from '@angular/router';
import { CourseService } from '../../services/course.service'; import { CourseService } from '../../services/course.service';
import { DocumentService } from '../../services/document.service'; import { DocumentService } from '../../services/document.service';
import { ExcelService } from '../../services/excel.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 { SharedModule } from '../../../shared/shared.module';
import { DatasourseTableService } from '../../services/datasourse-table.service'; import { DatasourseTableService } from '../../services/datasourse-table.service';
import { DatasourceTableModel, MyDatasourceTableModel } from '../../models/datasource-table.model'; import { DatasourceTableModel, MyDatasourceTableModel } from '../../models/datasource-table.model';
......
...@@ -8,11 +8,11 @@ import { SharedModule } from '../../../shared/shared.module'; ...@@ -8,11 +8,11 @@ import { SharedModule } from '../../../shared/shared.module';
import { CourseService } from '../../services/course.service'; import { CourseService } from '../../services/course.service';
import { DocumentService } from '../../services/document.service'; import { DocumentService } from '../../services/document.service';
import { ExcelService } from '../../services/excel.service'; import { ExcelService } from '../../services/excel.service';
import { WidgetService } from '../../services/widgets.service';
import { CompanyModel } from '../../models/company.model'; import { CompanyModel } from '../../models/company.model';
import { TokenService } from '../../../shared/services/token.service'; import { TokenService } from '../../../shared/services/token.service';
import { DatasourceTableModel, MyDatasourceTableModel } from '../../models/datasource-table.model'; import { DatasourceTableModel, MyDatasourceTableModel } from '../../models/datasource-table.model';
import { DatasourseTableService } from '../../services/datasourse-table.service'; import { DatasourseTableService } from '../../services/datasourse-table.service';
import { WidgetService } from '../../dashboard-management/services/widgets.service';
......
...@@ -2,78 +2,72 @@ import { Routes } from '@angular/router'; ...@@ -2,78 +2,72 @@ import { Routes } from '@angular/router';
import { MyPortalComponent } from './my-portal.component'; import { MyPortalComponent } from './my-portal.component';
import { moduleAccessGuard } from '../../core/guards/module-access.guard'; 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 = [ export const MY_PORTAL_ROUTES: Routes = [
{ {
path: '', path: '',
component: MyPortalComponent, component: MyPortalComponent,
canActivate: [moduleAccessGuard] canActivate: [moduleAccessGuard],
}, children: [
// { {
// path: 'create-category', path: 'create-category',
// component: CreateCategoryComponent, loadComponent: () => import('./create-category/create-category.component').then(m => m.CreateCategoryComponent),
// canActivate: [moduleAccessGuard] canActivate: [moduleAccessGuard]
// }, },
// { {
// path: 'category-list', path: 'category-list',
// component: CategoryListComponent, loadComponent: () => import('./category-list/category-list.component').then(m => m.CategorylistComponent),
// canActivate: [moduleAccessGuard] canActivate: [moduleAccessGuard]
// }, },
// { {
// path: 'category-list-approve', path: 'category-list-approve',
// component: CategoryListApproveComponent, loadComponent: () => import('./category-list-approve/category-list-approve.component').then(m => m.CategoryListApproveComponent),
// canActivate: [moduleAccessGuard] canActivate: [moduleAccessGuard]
// }, },
// { {
// path: 'approved-list', path: 'approved-list',
// component: ApprovedListComponent, loadComponent: () => import('./approved-list/approved-list.component').then(m => m.ApprovedListComponent),
// canActivate: [moduleAccessGuard] canActivate: [moduleAccessGuard]
// }, },
// { {
// path: 'excel-list', path: 'excel-list',
// component: ExcelListComponent, loadComponent: () => import('./excel-list/excel-list.component').then(m => m.ExcelListComponent),
// canActivate: [moduleAccessGuard] canActivate: [moduleAccessGuard]
// }, },
// { {
// path: 'excel-report', path: 'excel-report',
// component: ExcelReportComponent, loadComponent: () => import('./excel-report/excel-report.component').then(m => m.ExcelReportComponent),
// canActivate: [moduleAccessGuard] canActivate: [moduleAccessGuard]
// }, },
// { {
// path: 'excel-report-toggle', path: 'excel-report-toggle',
// component: ExcelReportToggleComponent, loadComponent: () => import('./excel-report-toggle/excel-report-toggle.component').then(m => m.ExcelReportToggleComponent),
// canActivate: [moduleAccessGuard] canActivate: [moduleAccessGuard]
// }, },
// { {
// path: 'view-list-excel', path: 'view-list-excel',
// component: ViewListExcelComponent, loadComponent: () => import('./view-list-excel/view-list-excel.component').then(m => m.ViewListExcelComponent),
// canActivate: [moduleAccessGuard] canActivate: [moduleAccessGuard]
// }, },
// { {
// path: 'open-image', path: 'open-image',
// component: OpenImageComponent, loadComponent: () => import('./open-image/open-image.component').then(m => m.OpenImageComponent),
// canActivate: [moduleAccessGuard] canActivate: [moduleAccessGuard]
// }, },
// { {
// path: 'datasource-table', path: 'datasource-table',
// component: DatasourceTableComponent, loadComponent: () => import('./datasource-table/datasource-table.component').then(m => m.DatasourceTableComponent),
// canActivate: [moduleAccessGuard] canActivate: [moduleAccessGuard]
// }, },
// { {
// path: 'alert-modal', path: 'alert-modal',
// component: AlertModalComponent, loadComponent: () => import('./alert-modal/alert-modal.component').then(m => m.AlertModalComponent),
// canActivate: [moduleAccessGuard] 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'; ...@@ -8,10 +8,11 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import swal from 'sweetalert'; import swal from 'sweetalert';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import { SharedModule } from '../../../../shared/shared.module'; import { ExcelContentModel } from '../../models/excel-content.model';
import { ExcelContentModel } from '../../../models/excel-content.model'; import { SharedModule } from '../../../shared/shared.module';
import { ExcelService } from '../../../services/excel.service'; import { ExcelService } from '../../services/excel.service';
import { OpenImageComponent } from '../../../open-image/open-image.component'; import { OpenImageComponent } from '../open-image/open-image.component';
@Component({ @Component({
selector: 'app-view-list-excel', selector: 'app-view-list-excel',
...@@ -148,4 +149,4 @@ export class ViewListExcelComponent implements OnInit { ...@@ -148,4 +149,4 @@ export class ViewListExcelComponent implements OnInit {
coverDate(date: string) { coverDate(date: string) {
return date.split('-').reverse().join('/'); 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 = [ ...@@ -20,6 +20,34 @@ export const MYHR_LITE_ROUTES: Routes = [
loadComponent: () => import('./reports/myhr-lite-reports.component').then(m => m.MyhrLiteReportsComponent) 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: '', path: '',
redirectTo: 'dashboard', redirectTo: 'dashboard',
pathMatch: 'full' 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 = [ ...@@ -52,6 +52,10 @@ export const MYHR_PLUS_ROUTES: Routes = [
loadComponent: () => import('./reports/myhr-plus-excel-report.component').then(m => m.MyhrPlusExcelReportComponent) 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: '', path: '',
redirectTo: 'dashboard', redirectTo: 'dashboard',
pathMatch: 'full' pathMatch: 'full'
......
...@@ -13,7 +13,53 @@ export const MYJOB_ROUTES: Routes = [ ...@@ -13,7 +13,53 @@ export const MYJOB_ROUTES: Routes = [
}, },
{ {
path: 'pdpa-manage', 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', 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 = [ ...@@ -12,6 +12,14 @@ export const MYSKILL_X_ROUTES: Routes = [
component: MyskillXDashboardComponent 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: '', path: '',
redirectTo: 'dashboard', redirectTo: 'dashboard',
pathMatch: 'full' 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 = [ ...@@ -12,7 +12,6 @@ export const portalManageRoutes: Routes = [
// myHR-Plus Module // myHR-Plus Module
{ {
path: 'myhr-plus', path: 'myhr-plus',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./myhr-plus/myhr-plus.routes').then(m => m.MYHR_PLUS_ROUTES) loadChildren: () => import('./myhr-plus/myhr-plus.routes').then(m => m.MYHR_PLUS_ROUTES)
}, },
...@@ -58,21 +57,21 @@ export const portalManageRoutes: Routes = [ ...@@ -58,21 +57,21 @@ export const portalManageRoutes: Routes = [
loadChildren: () => import('./myskill-x/myskill-x.routes').then(m => m.MYSKILL_X_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) // Dashboard Management (รวม widget management)
{ {
path: 'dashboard-management', path: 'dashboard-management',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule) 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 // Meeting Booking
{ {
...@@ -104,34 +103,20 @@ export const portalManageRoutes: Routes = [ ...@@ -104,34 +103,20 @@ export const portalManageRoutes: Routes = [
loadChildren: () => import('./my-portal/my-portal.routes').then(m => m.MY_PORTAL_ROUTES) loadChildren: () => import('./my-portal/my-portal.routes').then(m => m.MY_PORTAL_ROUTES)
}, },
// === Generic App Routes === // Dashboard (Old - for compatibility)
// 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
{ {
path: ':appName/widget-warehouse', path: 'dashboard',
canActivate: [moduleAccessGuard], component: HomeComponent, // Assuming HomeComponent is a generic dashboard or a placeholder
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule) canActivate: [moduleAccessGuard]
}, },
{ {
path: ':appName/widget-linker', path: 'dashboard-viewer',
canActivate: [moduleAccessGuard], loadComponent: () => import('./dashboard-viewer/dashboard-viewer.component').then(m => m.DashboardViewerComponent),
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule) canActivate: [moduleAccessGuard]
}, },
// Route for viewing a specific dashboard // === Generic App Routes ===
{ // These routes are for simple apps that don't need special module-level services.
path: 'dashboard-viewer/:dashboardId',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
},
// Redirect for unknown routes // Redirect for unknown routes
{ {
......
...@@ -43,6 +43,11 @@ export class MenuPermissionService { ...@@ -43,6 +43,11 @@ export class MenuPermissionService {
* ตรวจสอบสิทธิ์การเข้าถึงเมนู * ตรวจสอบสิทธิ์การเข้าถึงเมนู
*/ */
canAccessMenu(menuPath: string, permission: 'view' | 'create' | 'edit' | 'delete' | 'export' | 'import' = 'view'): Observable<boolean> { 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( return this.menuPermissions$.pipe(
map(menus => { map(menus => {
const menu = this.findMenuByPath(menus, menuPath); const menu = this.findMenuByPath(menus, menuPath);
...@@ -173,7 +178,7 @@ export class MenuPermissionService { ...@@ -173,7 +178,7 @@ export class MenuPermissionService {
{ {
id: 'widget-warehouse', id: 'widget-warehouse',
name: 'คลังวิดเจ็ต', name: 'คลังวิดเจ็ต',
path: '/portal-manage/dashboard/widget-warehouse', path: '/portal-manage/myhr-plus/widget-warehouse',
order: 2, order: 2,
isVisible: true, isVisible: true,
permissions: { permissions: {
......
...@@ -447,7 +447,22 @@ export class SidebarComponent { ...@@ -447,7 +447,22 @@ export class SidebarComponent {
if (ele.path == this.currentUrl) { if (ele.path == this.currentUrl) {
element.active = true; element.active = true;
element.selected = 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 // ตรวจสอบ path สำหรับ widget routes
if ((this.isWidgetWarehouseRoute || this.isWidgetLinkerRoute) && ele.path && this.currentUrl.startsWith(ele.path.split('?')[0])) { if ((this.isWidgetWarehouseRoute || this.isWidgetLinkerRoute) && ele.path && this.currentUrl.startsWith(ele.path.split('?')[0])) {
element.active = true; element.active = true;
...@@ -455,6 +470,7 @@ export class SidebarComponent { ...@@ -455,6 +470,7 @@ export class SidebarComponent {
ele.active = true; ele.active = true;
ele.selected = true; ele.selected = true;
} }
// ตรวจสอบ path สำหรับ Excel Report ที่มี query parameters // ตรวจสอบ path สำหรับ Excel Report ที่มี query parameters
if ((this.isMyportalRoute||this.isMyPortalRoute||this.isInstallerRoute)&&ele.path && this.currentUrl.startsWith(ele.path.split('?')[0])) { if ((this.isMyportalRoute||this.isMyPortalRoute||this.isInstallerRoute)&&ele.path && this.currentUrl.startsWith(ele.path.split('?')[0])) {
const currentUrlBase = this.currentUrl.split('?')[0]; const currentUrlBase = this.currentUrl.split('?')[0];
...@@ -475,7 +491,24 @@ export class SidebarComponent { ...@@ -475,7 +491,24 @@ export class SidebarComponent {
element.selected = true; element.selected = true;
ele.active = true; ele.active = true;
ele.selected = 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 // ตรวจสอบ path สำหรับ widget routes ในระดับที่ 3
if ((this.isWidgetWarehouseRoute || this.isWidgetLinkerRoute) && child1.path && this.currentUrl.startsWith(child1.path.split('?')[0])) { if ((this.isWidgetWarehouseRoute || this.isWidgetLinkerRoute) && child1.path && this.currentUrl.startsWith(child1.path.split('?')[0])) {
element.active = true; element.active = true;
...@@ -485,6 +518,7 @@ export class SidebarComponent { ...@@ -485,6 +518,7 @@ export class SidebarComponent {
child1.active = true; child1.active = true;
child1.selected = true; child1.selected = true;
} }
// ตรวจสอบ path สำหรับ Excel Report ที่มี query parameters ในระดับที่ 3 // ตรวจสอบ path สำหรับ Excel Report ที่มี query parameters ในระดับที่ 3
if ((this.isMyportalRoute||this.isMyPortalRoute||this.isInstallerRoute)&&child1.path && this.currentUrl.startsWith(child1.path.split('?')[0])) { if ((this.isMyportalRoute||this.isMyPortalRoute||this.isInstallerRoute)&&child1.path && this.currentUrl.startsWith(child1.path.split('?')[0])) {
const currentUrlBase = this.currentUrl.split('?')[0]; const currentUrlBase = this.currentUrl.split('?')[0];
......
...@@ -29,7 +29,7 @@ export const authen: Routes = [ ...@@ -29,7 +29,7 @@ export const authen: Routes = [
] ]
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(admin)], imports: [RouterModule.forRoot(authen)],
exports: [RouterModule] exports: [RouterModule]
}) })
export class AuthenticationsRoutingModule { } export class AuthenticationsRoutingModule { }
\ No newline at end of file
...@@ -14,7 +14,7 @@ export const landing: Routes = [ ...@@ -14,7 +14,7 @@ export const landing: Routes = [
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(admin)], imports: [RouterModule.forRoot(landing)],
exports: [RouterModule] exports: [RouterModule]
}) })
export class landingpageRoutingModule { } export class landingpageRoutingModule { }
...@@ -99,14 +99,50 @@ export class NavService implements OnDestroy { ...@@ -99,14 +99,50 @@ export class NavService implements OnDestroy {
active: false, active: false,
children: [ children: [
{ {
path: `/portal-manage/${appName}/dashboard-management/dashboard`, path: `/portal-manage/dashboard-management/dashboard-home`,
title: 'แดชบอร์ดหลัก', title: 'แดชบอร์ดหลัก',
type: 'link' type: 'link'
}, },
{ {
path: `/portal-manage/${appName}/dashboard-management/widget-management`,
title: 'จัดการวิดเจ็ต', 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`, path: `/portal-manage/${appName}/widget-warehouse`,
...@@ -117,7 +153,7 @@ export class NavService implements OnDestroy { ...@@ -117,7 +153,7 @@ export class NavService implements OnDestroy {
path: `/portal-manage/${appName}/widget-linker`, path: `/portal-manage/${appName}/widget-linker`,
title: 'เชื่อมโยงวิดเจ็ตกับชุดข้อมูล', title: 'เชื่อมโยงวิดเจ็ตกับชุดข้อมูล',
type: 'link' type: 'link'
}, }
] ]
}; };
} }
...@@ -558,10 +594,11 @@ export class NavService implements OnDestroy { ...@@ -558,10 +594,11 @@ export class NavService implements OnDestroy {
type: 'sub', type: 'sub',
active: false, active: false,
children: [ children: [
{ path: '/portal-manage/dashboard-management/dashboard', title: 'แดชบอร์ดหลัก', type: 'link' }, { path: '/portal-manage/dashboard-management/dashboard-home', title: 'แดชบอร์ดหลัก', type: 'link' },
{ path: '/portal-manage/dashboard-management/widget-management', 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/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 { ...@@ -583,7 +620,7 @@ export class NavService implements OnDestroy {
{ {
title: 'แดชบอร์ดหลัก', title: 'แดชบอร์ดหลัก',
icon: 'dashboard', icon: 'dashboard',
path: '/portal-manage/dashboard-management/dashboard', path: '/portal-manage/dashboard-management/dashboard-home',
type: 'link', type: 'link',
}, },
{ {
...@@ -593,94 +630,27 @@ export class NavService implements OnDestroy { ...@@ -593,94 +630,27 @@ export class NavService implements OnDestroy {
active: false, active: false,
children: [ children: [
{ {
path: '/portal-manage/dashboard-management/widget-management', path: '/portal-manage/dashboard-management/widget-list',
title: 'รายการวิดเจ็ต', title: 'รายการวิดเจ็ต',
type: 'link' type: 'link'
}, },
// {
// path: '/portal-manage/dashboard-management/widget-management/edit',
// title: 'เพิ่มวิดเจ็ตใหม่',
// type: 'link'
// },
{ {
path: '/portal-manage/dashboard-management/widget-management/edit', path: '/portal-manage/dashboard-management/dataset-widget-linker',
title: 'เพิ่มวิดเจ็ตใหม่', 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',
type: 'link' 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