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` แล้ว
......@@ -7,6 +7,11 @@ export const moduleAccessGuard: CanActivateFn = (route: ActivatedRouteSnapshot,
const permissionService = inject(CorePermissionService);
const router = inject(Router);
// ไม่ต้องเช็ค permission สำหรับ dashboard-management และ widget-warehouse
if (state.url.includes('dashboard-management') || state.url.includes('widget-warehouse')) {
return true;
}
// Get the module name from the route parameter :appName or from the route path
let moduleName = route.params['appName'];
......
import { Component } from '@angular/core';
@Component({
selector: 'app-company-department',
template: '<p>company-department works!</p>',
standalone: true
})
export class CompanyDepartmentComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-company-emp',
template: '<p>company-emp works!</p>',
standalone: true
})
export class CompanyEmpComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-company-info',
template: '<p>company-info works!</p>',
standalone: true
})
export class CompanyInfoComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-company-location',
template: '<p>company-location works!</p>',
standalone: true
})
export class CompanyLocationComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-company-management',
templateUrl: './company-management.component.html',
styleUrls: ['./company-management.component.scss'],
standalone: true
})
export class CompanyManagementComponent {
}
import { Routes } from '@angular/router';
import { CompanyManagementComponent } from './company-management.component';
export const COMPANY_MANAGEMENT_ROUTES: Routes = [
{
path: '',
component: CompanyManagementComponent,
children: [
{
path: 'company-info',
loadComponent: () => import('./company-info/company-info.component').then(m => m.CompanyInfoComponent)
},
{
path: 'company-department',
loadComponent: () => import('./company-department/company-department.component').then(m => m.CompanyDepartmentComponent)
},
{
path: 'company-position',
loadComponent: () => import('./company-position/company-position.component').then(m => m.CompanyPositionComponent)
},
{
path: 'company-emp',
loadComponent: () => import('./company-emp/company-emp.component').then(m => m.CompanyEmpComponent)
},
{
path: 'company-location',
loadComponent: () => import('./company-location/company-location.component').then(m => m.CompanyLocationComponent)
},
{
path: 'timestamp-log',
loadComponent: () => import('./timestamp-log/timestamp-log.component').then(m => m.TimestampLogComponent)
},
{
path: 'warning-timetamp',
loadComponent: () => import('./warning-timetamp/warning-timetamp.component').then(m => m.WarningTimetampComponent)
},
{
path: 'enroll-face',
loadComponent: () => import('./enroll-face/enroll-face.component').then(m => m.EnrollFaceComponent)
},
{
path: 'home-installer',
loadComponent: () => import('./home-installer/home-installer.component').then(m => m.HomeInstallerComponent)
},
{
path: '',
redirectTo: 'company-info',
pathMatch: 'full'
}
]
}
];
import { Component } from '@angular/core';
@Component({
selector: 'app-company-position',
template: '<p>company-position works!</p>',
standalone: true
})
export class CompanyPositionComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-enroll-face',
template: '<p>enroll-face works!</p>',
standalone: true
})
export class EnrollFaceComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-home-installer',
template: '<p>home-installer works!</p>',
standalone: true
})
export class HomeInstallerComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-timestamp-log',
template: '<p>timestamp-log works!</p>',
standalone: true
})
export class TimestampLogComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-warning-timetamp',
template: '<p>warning-timetamp works!</p>',
standalone: true
})
export class WarningTimetampComponent {
}
......@@ -40,155 +40,34 @@ import { SyncfusionPivotWidgetComponent } from './widgets/syncfusion-pivot-widge
import { TreemapWidgetComponent } from './widgets/treemap-widget/treemap-widget.component';
import { WaterfallChartWidgetComponent } from './widgets/waterfall-chart-widget/waterfall-chart-widget.component';
import { WelcomeWidgetComponent } from './widgets/welcome-widget/welcome-widget.component';
// New Syncfusion-based widgets
import { CalendarWidgetComponent } from './widgets/calendar-widget/calendar-widget.component';
import { NotificationWidgetComponent } from './widgets/notification-widget/notification-widget.component';
import { WeatherWidgetComponent } from './widgets/weather-widget/weather-widget.component';
import { ClockWidgetComponent } from './widgets/clock-widget/clock-widget.component';
export const routes: Routes = [
{
path: '',
component: DashboardManagementComponent,
children: [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
},
{
path: 'dashboard',
component: DashboardManagementComponent,
title: 'แดชบอร์ดหลัก'
},
{
path: 'viewer/:dashboardId',
component: DashboardViewerComponent,
title: 'ดูแดชบอร์ด'
},
{
path: 'widget-management',
children: [
{
path: '',
component: WidgetListComponent,
title: 'รายการวิดเจ็ต'
},
{
path: 'edit/:widgetId',
component: WidgetFormComponent,
title: 'แก้ไขวิดเจ็ต'
},
{
path: 'linker',
component: DatasetWidgetLinkerComponent,
title: 'เชื่อมโยงข้อมูลกับวิดเจ็ต'
}
]
},
{
path: 'widget-config',
component: WidgetConfigComponent,
title: 'ตั้งค่าวิดเจ็ต'
},
{
path: 'dataset-picker',
component: DatasetPickerComponent,
title: 'เลือกชุดข้อมูล'
},
// Widget preview routes
{
path: 'widget-preview/:widgetType',
component: DashboardViewerComponent,
title: 'ตัวอย่างวิดเจ็ต'
}
]
path: 'dashboard-home',
component: DashboardManagementComponent
},
// Routes for dynamic app-based routing
// {
// path: 'widget-config',
// component: WidgetConfigComponent
// },
{
path: ':appName/dashboard-management',
component: DashboardManagementComponent,
children: [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
},
{
path: 'dashboard',
component: DashboardManagementComponent,
title: 'แดชบอร์ดหลัก'
},
{
path: 'viewer/:dashboardId',
component: DashboardViewerComponent,
title: 'ดูแดชบอร์ด'
},
{
path: 'widget-management',
children: [
{
path: '',
component: WidgetListComponent,
title: 'รายการวิดเจ็ต'
},
{
path: 'edit/:widgetId',
component: WidgetFormComponent,
title: 'แก้ไขวิดเจ็ต'
},
{
path: 'linker',
component: DatasetWidgetLinkerComponent,
title: 'เชื่อมโยงข้อมูลกับวิดเจ็ต'
}
]
},
{
path: 'widget-config',
component: WidgetConfigComponent,
title: 'ตั้งค่าวิดเจ็ต'
},
{
path: 'dataset-picker',
component: DatasetPickerComponent,
title: 'เลือกชุดข้อมูล'
},
// Widget preview routes
{
path: 'widget-preview/:widgetType',
component: DashboardViewerComponent,
title: 'ตัวอย่างวิดเจ็ต'
}
]
path: 'dashboard-viewer',
component: DashboardViewerComponent
},
// Routes for widget warehouse
{
path: ':appName/widget-warehouse',
children: [
{
path: '',
component: WidgetListComponent,
title: 'คลังวิดเจ็ต'
},
{
path: 'edit/:widgetId',
component: WidgetFormComponent,
title: 'แก้ไขวิดเจ็ต'
},
{
path: 'preview/:widgetType',
component: DashboardViewerComponent,
title: 'ตัวอย่างวิดเจ็ต'
}
]
path: 'widget-list',
component: WidgetListComponent
},
{
path: ':appName/widget-linker',
component: DatasetWidgetLinkerComponent,
title: 'เชื่อมโยงข้อมูลกับวิดเจ็ต'
path: 'dataset-widget-linker',
component: DatasetWidgetLinkerComponent
},
// Direct dashboard viewer route
{
path: 'dashboard-viewer/:dashboardId',
component: DashboardViewerComponent,
title: 'ดูแดชบอร์ด'
}
];
@NgModule({
......@@ -232,7 +111,12 @@ export const routes: Routes = [
SyncfusionPivotWidgetComponent,
TreemapWidgetComponent,
WaterfallChartWidgetComponent,
WelcomeWidgetComponent
WelcomeWidgetComponent,
// New Syncfusion-based widgets
CalendarWidgetComponent,
NotificationWidgetComponent,
WeatherWidgetComponent,
ClockWidgetComponent
],
exports: []
})
......
......@@ -35,11 +35,13 @@ import { WidgetService } from '../services/widgets.service';
import { SimpleKpiWidgetComponent } from '../widgets/simple-kpi-widget/simple-kpi-widget.component';
import { WidgetFormComponent } from './widget-form.component';
import { ClickEventArgs } from '@syncfusion/ej2-angular-navigations';
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-widget-list',
standalone: true,
imports: [
RouterModule,
CommonModule,
FormsModule,
MatDialogModule,
......
# New Syncfusion-Based Widgets
## Overview
This document describes the newly added widgets that use Syncfusion components as their foundation.
## Added Widgets
### 1. Calendar Widget (`calendar-widget`)
**Location:** `src/app/portal-manage/dashboard-management/widgets/calendar-widget/`
**Syncfusion Components Used:**
- `CalendarModule` from `@syncfusion/ej2-angular-calendars`
**Features:**
- Interactive calendar display
- Event management and display
- Configurable calendar settings
- Multi-selection support
- RTL support
- Week number display
- Custom CSS classes
**Configuration Options:**
```typescript
{
title: string;
enableMultiSelection: boolean;
enableRtl: boolean;
showWeekNumber: boolean;
start: string; // 'Year', 'Month', 'Decade'
depth: string; // 'Year', 'Month', 'Decade'
cssClass: string;
dateField: string; // Data field for dates
titleField: string; // Data field for event titles
descriptionField: string; // Data field for event descriptions
typeField: string; // Data field for event types
}
```
### 2. Notification Widget (`notification-widget`)
**Location:** `src/app/portal-manage/dashboard-management/widgets/notification-widget/`
**Syncfusion Components Used:**
- `ToastModule` from `@syncfusion/ej2-angular-notifications`
- `MessageModule` from `@syncfusion/ej2-angular-notifications`
**Features:**
- Real-time notification display
- Toast notifications
- Message notifications
- Unread count tracking
- Mark as read functionality
- Delete notifications
- Priority-based styling
- Type-based styling (success, warning, error, info)
**Configuration Options:**
```typescript
{
title: string;
toastPosition: { X: string, Y: string };
showCloseButton: boolean;
showProgressBar: boolean;
timeOut: number;
newestOnTop: boolean;
cssClass: string;
severity: string; // 'Normal', 'Success', 'Warning', 'Error'
variant: string; // 'Filled', 'Outlined'
showIcon: boolean;
showCloseIcon: boolean;
// Data field mappings
idField: string;
titleField: string;
messageField: string;
typeField: string;
timestampField: string;
isReadField: string;
priorityField: string;
}
```
### 3. Weather Widget (`weather-widget`)
**Location:** `src/app/portal-manage/dashboard-management/widgets/weather-widget/`
**Syncfusion Components Used:**
- `CardModule` from `@syncfusion/ej2-angular-layouts`
**Features:**
- Current weather display
- 5-day forecast
- Weather details (humidity, wind, pressure)
- Temperature color coding
- Weather icons
- Location display
- Auto-refresh capability
- Responsive design
**Configuration Options:**
```typescript
{
title: string;
location: string;
refreshInterval: number;
// Data field mappings
temperatureField: string;
humidityField: string;
windSpeedField: string;
pressureField: string;
descriptionField: string;
iconField: string;
feelsLikeField: string;
dayField: string;
highField: string;
lowField: string;
forecastDescriptionField: string;
forecastIconField: string;
}
```
### 4. Clock Widget (`clock-widget`)
**Location:** `src/app/portal-manage/dashboard-management/widgets/clock-widget/`
**Syncfusion Components Used:**
- `ProgressBarModule` from `@syncfusion/ej2-angular-progressbar`
- `CircularGaugeModule` from `@syncfusion/ej2-angular-circulargauge`
**Features:**
- Multiple clock types (digital, analog, gauge)
- Real-time updates
- Timezone support
- 12/24 hour format toggle
- Date display
- Progress bars for time tracking
- Circular gauge clock
- Interactive controls
**Configuration Options:**
```typescript
{
title: string;
timezone: string;
clockType: string; // 'digital', 'analog', 'gauge'
showSeconds: boolean;
showDate: boolean;
showTimezone: boolean;
format24Hour: boolean;
updateInterval: number;
// Data field mappings
timezoneField: string;
clockTypeField: string;
format24HourField: string;
}
```
## Usage Examples
### Calendar Widget
```html
<app-calendar-widget
[config]="{
title: 'Company Calendar',
enableMultiSelection: false,
showWeekNumber: true,
dateField: 'eventDate',
titleField: 'eventTitle',
descriptionField: 'eventDescription'
}">
</app-calendar-widget>
```
### Notification Widget
```html
<app-notification-widget
[config]="{
title: 'System Notifications',
toastPosition: { X: 'Right', Y: 'Top' },
severity: 'Normal',
variant: 'Filled',
titleField: 'notificationTitle',
messageField: 'notificationMessage',
typeField: 'notificationType'
}">
</app-notification-widget>
```
### Weather Widget
```html
<app-weather-widget
[config]="{
title: 'Weather Forecast',
location: 'Bangkok, Thailand',
refreshInterval: 300000,
temperatureField: 'temp',
humidityField: 'humidity',
descriptionField: 'description'
}">
</app-weather-widget>
```
### Clock Widget
```html
<app-clock-widget
[config]="{
title: 'World Clock',
timezone: 'Asia/Bangkok',
clockType: 'analog',
showSeconds: true,
format24Hour: true
}">
</app-clock-widget>
```
## Integration Notes
1. **Module Imports:** All widgets are already imported in `dashboard-management.module.ts`
2. **Standalone Components:** All widgets are standalone components
3. **Base Widget:** All widgets extend `BaseWidgetComponent` for consistency
4. **Data Integration:** All widgets support dynamic data binding through `DashboardStateService`
5. **Responsive Design:** All widgets include responsive CSS for mobile devices
6. **Error Handling:** All widgets include proper error handling and loading states
## Dependencies Required
Make sure these Syncfusion packages are installed:
```bash
npm install @syncfusion/ej2-angular-calendars
npm install @syncfusion/ej2-angular-notifications
npm install @syncfusion/ej2-angular-layouts
npm install @syncfusion/ej2-angular-progressbar
npm install @syncfusion/ej2-angular-circulargauge
```
## Styling
All widgets include:
- Consistent styling with existing widgets
- Dark/light theme support
- Responsive design for mobile devices
- Custom CSS classes for theming
- Hover effects and transitions
## Performance Considerations
1. **Real-time Updates:** Clock widget updates every second, consider reducing frequency if needed
2. **Weather Refresh:** Weather widget can be configured with custom refresh intervals
3. **Notification Limits:** Consider limiting the number of notifications displayed
4. **Calendar Events:** Large numbers of events may impact performance
## Future Enhancements
1. **Calendar Widget:**
- Event creation/editing
- Recurring events
- Multiple calendar support
2. **Notification Widget:**
- Push notification support
- Sound notifications
- Notification categories
3. **Weather Widget:**
- Multiple location support
- Weather alerts
- Historical weather data
4. **Clock Widget:**
- Multiple timezone display
- Stopwatch/timer functionality
- Alarm features
<div class="calendar-widget">
<div class="widget-header">
<h3 class="widget-title">{{ title }}</h3>
</div>
<div class="widget-content">
<div class="loading" *ngIf="isLoading">
<div class="spinner"></div>
<p>Loading calendar...</p>
</div>
<div class="error" *ngIf="hasError">
<div class="error-icon">⚠️</div>
<p>{{ errorMessage }}</p>
</div>
<div class="calendar-container" *ngIf="!isLoading && !hasError">
<ejs-calendar
[value]="selectedDate"
[enableRtl]="calendarSettings.enableRtl"
[start]="calendarSettings.start"
[depth]="calendarSettings.depth"
[cssClass]="calendarSettings.cssClass"
(change)="onDateChange($event)">
</ejs-calendar>
<!-- Events display -->
<div class="events-section" *ngIf="events.length > 0">
<h4>Upcoming Events</h4>
<div class="events-list">
<div class="event-item"
*ngFor="let event of getEventsForDate(selectedDate)"
[class]="'event-' + event.type">
<div class="event-title">{{ event.title }}</div>
<div class="event-description">{{ event.description }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
.calendar-widget {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.widget-header {
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
.widget-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #343a40;
}
}
.widget-content {
flex: 1;
padding: 16px;
overflow-y: auto;
.loading, .error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #6c757d;
.spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.error-icon {
font-size: 32px;
margin-bottom: 8px;
}
}
.calendar-container {
.e-calendar {
border: none;
box-shadow: none;
}
.events-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e9ecef;
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: #495057;
}
.events-list {
.event-item {
padding: 8px 12px;
margin-bottom: 8px;
border-radius: 6px;
background: #f8f9fa;
border-left: 4px solid #007bff;
&.event-meeting {
border-left-color: #28a745;
}
&.event-deadline {
border-left-color: #dc3545;
}
&.event-reminder {
border-left-color: #ffc107;
}
.event-title {
font-weight: 600;
font-size: 13px;
color: #343a40;
margin-bottom: 2px;
}
.event-description {
font-size: 12px;
color: #6c757d;
}
}
}
}
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// Responsive design
@media (max-width: 768px) {
.calendar-widget {
.widget-content {
padding: 12px;
.calendar-container {
.events-section {
.events-list {
.event-item {
padding: 6px 10px;
}
}
}
}
}
}
}
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CalendarModule } from '@syncfusion/ej2-angular-calendars';
import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({
selector: 'app-calendar-widget',
standalone: true,
imports: [CommonModule, CalendarModule],
templateUrl: './calendar-widget.component.html',
styleUrls: ['./calendar-widget.component.scss']
})
export class CalendarWidgetComponent extends BaseWidgetComponent {
public selectedDate: Date = new Date();
public events: any[] = [];
public calendarSettings: any = {};
constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
}
applyInitialConfig(): void {
this.title = this.config.title || 'Calendar';
this.calendarSettings = {
enableRtl: this.config.enableRtl || false,
start: this.config.start || 'Year',
depth: this.config.depth || 'Year',
cssClass: this.config.cssClass || ''
};
this.events = [];
}
onDataUpdate(data: any[]): void {
if (data && data.length > 0) {
// Map data to events format
this.events = data.map(item => ({
date: new Date(item[this.config.dateField || 'date']),
title: item[this.config.titleField || 'title'],
description: item[this.config.descriptionField || 'description'],
type: item[this.config.typeField || 'type'] || 'default'
}));
}
}
onReset(): void {
this.title = 'Calendar (Default)';
this.selectedDate = new Date();
this.events = [
{ date: new Date(), title: 'Meeting', description: 'Team meeting', type: 'meeting' },
{ date: new Date(Date.now() + 86400000), title: 'Deadline', description: 'Project deadline', type: 'deadline' }
];
this.calendarSettings = {
enableRtl: false,
start: 'Year',
depth: 'Year'
};
}
onDateChange(event: any): void {
this.selectedDate = event.value;
// Emit date change event if needed
console.log('Selected date:', this.selectedDate);
}
getEventsForDate(date: Date): any[] {
return this.events.filter(event =>
event.date.toDateString() === date.toDateString()
);
}
}
<div class="clock-widget">
<div class="widget-header">
<h3 class="widget-title">{{ title }}</h3>
<div class="clock-actions">
<button class="btn-toggle" (click)="toggleClockType()" title="Toggle clock type">
{{ clockType === 'analog' ? '🕐' : '⏰' }}
</button>
<button class="btn-format" (click)="toggleFormat()" title="Toggle 12/24 hour">
{{ format24Hour ? '24h' : '12h' }}
</button>
</div>
</div>
<div class="widget-content">
<div class="loading" *ngIf="isLoading">
<div class="spinner"></div>
<p>Loading clock...</p>
</div>
<div class="error" *ngIf="hasError">
<div class="error-icon">⚠️</div>
<p>{{ errorMessage }}</p>
</div>
<div class="clock-container" *ngIf="!isLoading && !hasError">
<!-- Digital Clock -->
<div class="digital-clock" *ngIf="clockType === 'digital'">
<div class="time-display">
{{ getFormattedTime() }}
</div>
<div class="date-display" *ngIf="showDate">
{{ getFormattedDate() }}
</div>
<div class="timezone-display" *ngIf="showTimezone">
{{ getTimezoneDisplay() }}
</div>
</div>
<!-- Analog Clock -->
<div class="analog-clock" *ngIf="clockType === 'analog'">
<div class="clock-face">
<div class="clock-center"></div>
<!-- Hour markers -->
<div class="hour-marker" *ngFor="let hour of [1,2,3,4,5,6,7,8,9,10,11,12]"
[style.transform]="'rotate(' + (hour * 30) + 'deg)'">
<span class="hour-number"
[style.transform]="'rotate(' + (-hour * 30) + 'deg)'">
{{ hour }}
</span>
</div>
<!-- Clock hands -->
<div class="clock-hand hour-hand"
[style.transform]="getAnalogHourRotation()"></div>
<div class="clock-hand minute-hand"
[style.transform]="getAnalogMinuteRotation()"></div>
<div class="clock-hand second-hand"
*ngIf="showSeconds"
[style.transform]="getAnalogSecondRotation()"></div>
</div>
<div class="analog-info">
<div class="date-display" *ngIf="showDate">
{{ getFormattedDate() }}
</div>
<div class="timezone-display" *ngIf="showTimezone">
{{ getTimezoneDisplay() }}
</div>
</div>
</div>
<!-- Circular Gauge Clock -->
<div class="gauge-clock" *ngIf="clockType === 'gauge'">
<ejs-circulargauge
[axes]="gaugeAxes"
[height]="200"
[width]="200">
</ejs-circulargauge>
<div class="gauge-info">
<div class="time-display">{{ getFormattedTime() }}</div>
<div class="date-display" *ngIf="showDate">{{ getFormattedDate() }}</div>
<div class="timezone-display" *ngIf="showTimezone">{{ getTimezoneDisplay() }}</div>
</div>
</div>
<!-- Progress Bars -->
<div class="progress-section">
<h4 class="progress-title">Time Progress</h4>
<div class="progress-item">
<label>Hour</label>
<ejs-progressbar
type="Linear"
[value]="hourProgress"
[showProgressValue]="false"
height="20"
trackThickness="8"
progressThickness="8"
trackColor="#e0e0e0"
progressColor="#007bff">
</ejs-progressbar>
<span class="progress-value">{{ currentTime.getHours() }}/24</span>
</div>
<div class="progress-item">
<label>Minute</label>
<ejs-progressbar
type="Linear"
[value]="minuteProgress"
[showProgressValue]="false"
height="20"
trackThickness="8"
progressThickness="8"
trackColor="#e0e0e0"
progressColor="#28a745">
</ejs-progressbar>
<span class="progress-value">{{ currentTime.getMinutes() }}/60</span>
</div>
<div class="progress-item" *ngIf="showSeconds">
<label>Second</label>
<ejs-progressbar
type="Linear"
[value]="secondProgress"
[showProgressValue]="false"
height="20"
trackThickness="8"
progressThickness="8"
trackColor="#e0e0e0"
progressColor="#dc3545">
</ejs-progressbar>
<span class="progress-value">{{ currentTime.getSeconds() }}/60</span>
</div>
</div>
</div>
</div>
</div>
.clock-widget {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
.widget-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #343a40;
}
.clock-actions {
display: flex;
gap: 8px;
.btn-toggle, .btn-format {
background: #007bff;
color: white;
border: none;
border-radius: 4px;
padding: 6px 10px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.2s;
&:hover {
background: #0056b3;
}
}
.btn-format {
background: #6c757d;
&:hover {
background: #545b62;
}
}
}
}
.widget-content {
flex: 1;
padding: 20px;
overflow-y: auto;
text-align: center;
.loading, .error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #6c757d;
.spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.error-icon {
font-size: 32px;
margin-bottom: 8px;
}
}
.clock-container {
.digital-clock {
.time-display {
font-size: 48px;
font-weight: 300;
color: #343a40;
margin-bottom: 16px;
font-family: 'Courier New', monospace;
letter-spacing: 2px;
}
.date-display {
font-size: 16px;
color: #6c757d;
margin-bottom: 8px;
}
.timezone-display {
font-size: 14px;
color: #007bff;
font-weight: 500;
}
}
.analog-clock {
margin-bottom: 20px;
.clock-face {
position: relative;
width: 200px;
height: 200px;
border: 4px solid #343a40;
border-radius: 50%;
margin: 0 auto 16px;
background: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
.clock-center {
position: absolute;
top: 50%;
left: 50%;
width: 12px;
height: 12px;
background: #343a40;
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.hour-marker {
position: absolute;
top: 50%;
left: 50%;
width: 2px;
height: 90px;
transform-origin: bottom center;
transform: translateX(-50%) translateY(-100%);
.hour-number {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
font-size: 14px;
font-weight: 600;
color: #343a40;
}
}
.clock-hand {
position: absolute;
top: 50%;
left: 50%;
transform-origin: bottom center;
transform: translateX(-50%) translateY(-100%);
border-radius: 2px;
&.hour-hand {
width: 4px;
height: 50px;
background: #343a40;
z-index: 3;
}
&.minute-hand {
width: 3px;
height: 70px;
background: #007bff;
z-index: 2;
}
&.second-hand {
width: 2px;
height: 80px;
background: #dc3545;
z-index: 1;
}
}
}
.analog-info {
.date-display {
font-size: 16px;
color: #6c757d;
margin-bottom: 8px;
}
.timezone-display {
font-size: 14px;
color: #007bff;
font-weight: 500;
}
}
}
.gauge-clock {
margin-bottom: 20px;
.gauge-info {
margin-top: 16px;
.time-display {
font-size: 24px;
font-weight: 600;
color: #343a40;
margin-bottom: 8px;
font-family: 'Courier New', monospace;
}
.date-display {
font-size: 14px;
color: #6c757d;
margin-bottom: 4px;
}
.timezone-display {
font-size: 12px;
color: #007bff;
font-weight: 500;
}
}
}
.progress-section {
margin-top: 24px;
text-align: left;
.progress-title {
font-size: 14px;
font-weight: 600;
color: #343a40;
margin: 0 0 16px 0;
text-align: center;
}
.progress-item {
display: flex;
align-items: center;
margin-bottom: 12px;
gap: 12px;
label {
flex: 0 0 60px;
font-size: 13px;
font-weight: 500;
color: #495057;
}
ejs-progressbar {
flex: 1;
}
.progress-value {
flex: 0 0 50px;
font-size: 12px;
color: #6c757d;
text-align: right;
font-family: 'Courier New', monospace;
}
}
}
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// Responsive design
@media (max-width: 768px) {
.clock-widget {
.widget-content {
padding: 16px;
.clock-container {
.digital-clock {
.time-display {
font-size: 36px;
}
.date-display {
font-size: 14px;
}
}
.analog-clock {
.clock-face {
width: 160px;
height: 160px;
.hour-marker {
height: 70px;
.hour-number {
font-size: 12px;
}
}
.clock-hand {
&.hour-hand {
height: 40px;
}
&.minute-hand {
height: 55px;
}
&.second-hand {
height: 65px;
}
}
}
}
.gauge-clock {
ejs-circulargauge {
height: 160px !important;
width: 160px !important;
}
}
.progress-section {
.progress-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
label {
flex: none;
}
ejs-progressbar {
width: 100%;
}
.progress-value {
flex: none;
text-align: left;
}
}
}
}
}
}
}
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProgressBarModule } from '@syncfusion/ej2-angular-progressbar';
import { CircularGaugeModule, GaugeTooltipService, AnnotationsService } from '@syncfusion/ej2-angular-circulargauge';
import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({
selector: 'app-clock-widget',
standalone: true,
imports: [CommonModule, ProgressBarModule, CircularGaugeModule],
providers: [GaugeTooltipService, AnnotationsService],
templateUrl: './clock-widget.component.html',
styleUrls: ['./clock-widget.component.scss']
})
export class ClockWidgetComponent extends BaseWidgetComponent implements OnInit, OnDestroy {
public currentTime: Date = new Date();
public timezone: string = 'Asia/Bangkok';
public clockType: string = 'analog'; // 'analog' or 'digital'
public showSeconds: boolean = true;
public showDate: boolean = true;
public showTimezone: boolean = true;
public format24Hour: boolean = true;
// Analog clock properties
public hourHand: number = 0;
public minuteHand: number = 0;
public secondHand: number = 0;
// Progress bar properties
public hourProgress: number = 0;
public minuteProgress: number = 0;
public secondProgress: number = 0;
// Circular gauge properties
public gaugeAxes: any[] = [];
private timer: any;
private updateInterval: number = 1000; // 1 second
constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
}
override ngOnInit(): void {
super.ngOnInit();
this.startClock();
}
override ngOnDestroy(): void {
super.ngOnDestroy();
this.stopClock();
}
applyInitialConfig(): void {
this.title = this.config.title || 'Clock';
this.timezone = this.config.timezone || 'Asia/Bangkok';
this.clockType = this.config.clockType || 'analog';
this.showSeconds = this.config.showSeconds !== false;
this.showDate = this.config.showDate !== false;
this.showTimezone = this.config.showTimezone !== false;
this.format24Hour = this.config.format24Hour !== false;
this.updateInterval = this.config.updateInterval || 1000;
this.updateClock();
}
onDataUpdate(data: any[]): void {
if (data && data.length > 0) {
const timeData = data[0];
this.timezone = timeData[this.config.timezoneField || 'timezone'] || this.timezone;
this.clockType = timeData[this.config.clockTypeField || 'clockType'] || this.clockType;
this.format24Hour = timeData[this.config.format24HourField || 'format24Hour'] !== false;
this.updateClock();
}
}
onReset(): void {
this.title = 'Clock (Default)';
this.timezone = 'Asia/Bangkok';
this.clockType = 'analog';
this.showSeconds = true;
this.showDate = true;
this.showTimezone = true;
this.format24Hour = true;
this.updateClock();
}
private startClock(): void {
this.updateClock();
this.timer = setInterval(() => {
this.updateClock();
}, this.updateInterval);
}
private stopClock(): void {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
private updateClock(): void {
this.currentTime = new Date();
// Update analog clock hands
const hours = this.currentTime.getHours();
const minutes = this.currentTime.getMinutes();
const seconds = this.currentTime.getSeconds();
this.hourHand = (hours % 12) * 30 + (minutes / 60) * 30;
this.minuteHand = minutes * 6 + (seconds / 60) * 6;
this.secondHand = seconds * 6;
// Update progress bars
this.hourProgress = (hours / 24) * 100;
this.minuteProgress = (minutes / 60) * 100;
this.secondProgress = (seconds / 60) * 100;
// Update gauge
this.updateGauge();
}
private updateGauge(): void {
const time = this.currentTime;
const hour = time.getHours();
const minute = time.getMinutes();
const second = time.getSeconds();
this.gaugeAxes = [{
startAngle: 270,
endAngle: 90,
minimum: 0,
maximum: 12,
lineStyle: { width: 2, color: '#e0e0e0' },
labelStyle: {
font: { size: '12px', fontFamily: 'Segoe UI' },
position: 'Outside'
},
majorTicks: {
height: 8,
width: 2,
color: '#666666'
},
minorTicks: {
height: 4,
width: 1,
color: '#999999'
},
pointers: [{
value: hour + (minute / 60),
radius: '70%',
pointerWidth: 4,
cap: { radius: 6, color: '#007bff' },
needleTail: { length: '15%' },
color: '#007bff'
}, {
value: minute + (second / 60),
radius: '80%',
pointerWidth: 3,
cap: { radius: 4, color: '#28a745' },
needleTail: { length: '10%' },
color: '#28a745'
}, {
value: second,
radius: '90%',
pointerWidth: 2,
cap: { radius: 3, color: '#dc3545' },
needleTail: { length: '5%' },
color: '#dc3545'
}],
ranges: [{
start: 0, end: 12,
startWidth: 2, endWidth: 2,
color: 'transparent'
}]
}];
}
getFormattedTime(): string {
const options: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
second: this.showSeconds ? '2-digit' : undefined,
hour12: !this.format24Hour,
timeZone: this.timezone
};
return this.currentTime.toLocaleTimeString('en-US', options);
}
getFormattedDate(): string {
const options: Intl.DateTimeFormatOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: this.timezone
};
return this.currentTime.toLocaleDateString('en-US', options);
}
getTimezoneDisplay(): string {
if (!this.showTimezone) return '';
const offset = this.currentTime.getTimezoneOffset();
const offsetHours = Math.floor(Math.abs(offset) / 60);
const offsetMinutes = Math.abs(offset) % 60;
const sign = offset <= 0 ? '+' : '-';
return `GMT${sign}${offsetHours.toString().padStart(2, '0')}:${offsetMinutes.toString().padStart(2, '0')}`;
}
getAnalogHourRotation(): string {
return `rotate(${this.hourHand}deg)`;
}
getAnalogMinuteRotation(): string {
return `rotate(${this.minuteHand}deg)`;
}
getAnalogSecondRotation(): string {
return `rotate(${this.secondHand}deg)`;
}
toggleClockType(): void {
this.clockType = this.clockType === 'analog' ? 'digital' : 'analog';
}
toggleFormat(): void {
this.format24Hour = !this.format24Hour;
}
}
<div class="notification-widget">
<div class="widget-header">
<h3 class="widget-title">{{ title }}</h3>
<div class="notification-actions">
<span class="unread-count" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
<button class="btn-mark-all"
*ngIf="unreadCount > 0"
(click)="markAllAsRead()"
title="Mark all as read">
</button>
</div>
</div>
<div class="widget-content">
<div class="loading" *ngIf="isLoading">
<div class="spinner"></div>
<p>Loading notifications...</p>
</div>
<div class="error" *ngIf="hasError">
<div class="error-icon">⚠️</div>
<p>{{ errorMessage }}</p>
</div>
<div class="notifications-container" *ngIf="!isLoading && !hasError">
<div class="notifications-list" *ngIf="notifications.length > 0">
<div class="notification-item"
*ngFor="let notification of notifications"
[class.unread]="!notification.isRead"
[class]="getTypeClass(notification.type) + ' ' + getPriorityClass(notification.priority)">
<div class="notification-icon">
<span [class]="getIconClass(notification.type)"></span>
</div>
<div class="notification-content">
<div class="notification-header">
<h4 class="notification-title">{{ notification.title }}</h4>
<div class="notification-time">
{{ notification.timestamp | date:'short' }}
</div>
</div>
<p class="notification-message">{{ notification.message }}</p>
</div>
<div class="notification-actions">
<button class="btn-read"
*ngIf="!notification.isRead"
(click)="markAsRead(notification)"
title="Mark as read">
</button>
<button class="btn-show"
(click)="showToast(notification)"
title="Show toast">
📢
</button>
<button class="btn-delete"
(click)="deleteNotification(notification)"
title="Delete">
</button>
</div>
</div>
</div>
<div class="no-notifications" *ngIf="notifications.length === 0">
<div class="no-notifications-icon">📭</div>
<p>No notifications</p>
</div>
</div>
</div>
<!-- Syncfusion Toast Component -->
<ejs-toast #toast
[position]="toastSettings.position"
[showCloseButton]="toastSettings.showCloseButton"
[showProgressBar]="toastSettings.showProgressBar"
[timeOut]="toastSettings.timeOut"
[newestOnTop]="toastSettings.newestOnTop"
[cssClass]="toastSettings.cssClass">
</ejs-toast>
<!-- Syncfusion Message Component -->
<ejs-message #message
[severity]="messageSettings.severity"
[variant]="messageSettings.variant"
[showIcon]="messageSettings.showIcon"
[showCloseIcon]="messageSettings.showCloseIcon">
</ejs-message>
</div>
.notification-widget {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
.widget-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #343a40;
}
.notification-actions {
display: flex;
align-items: center;
gap: 8px;
.unread-count {
background: #dc3545;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
min-width: 20px;
text-align: center;
}
.btn-mark-all {
background: #28a745;
color: white;
border: none;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.2s;
&:hover {
background: #218838;
}
}
}
}
.widget-content {
flex: 1;
overflow-y: auto;
.loading, .error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #6c757d;
.spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.error-icon {
font-size: 32px;
margin-bottom: 8px;
}
}
.notifications-container {
.notifications-list {
.notification-item {
display: flex;
align-items: flex-start;
padding: 12px 16px;
border-bottom: 1px solid #f1f3f4;
transition: background-color 0.2s;
position: relative;
&.unread {
background: #f8f9ff;
border-left: 4px solid #007bff;
}
&:hover {
background: #f8f9fa;
}
&.notification-success {
border-left-color: #28a745;
}
&.notification-warning {
border-left-color: #ffc107;
}
&.notification-error {
border-left-color: #dc3545;
}
&.notification-info {
border-left-color: #17a2b8;
}
&.priority-high {
border-left-width: 6px;
}
.notification-icon {
margin-right: 12px;
margin-top: 2px;
span {
display: inline-block;
width: 24px;
height: 24px;
border-radius: 50%;
text-align: center;
line-height: 24px;
font-size: 12px;
font-weight: 600;
&.e-success {
background: #28a745;
color: white;
}
&.e-warning {
background: #ffc107;
color: #212529;
}
&.e-error {
background: #dc3545;
color: white;
}
&.e-info {
background: #17a2b8;
color: white;
}
}
}
.notification-content {
flex: 1;
min-width: 0;
.notification-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 4px;
.notification-title {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #343a40;
line-height: 1.3;
}
.notification-time {
font-size: 12px;
color: #6c757d;
white-space: nowrap;
margin-left: 8px;
}
}
.notification-message {
margin: 0;
font-size: 13px;
color: #495057;
line-height: 1.4;
word-wrap: break-word;
}
}
.notification-actions {
display: flex;
gap: 4px;
margin-left: 8px;
button {
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
font-size: 12px;
transition: background-color 0.2s;
&:hover {
background: #e9ecef;
}
&.btn-read {
color: #28a745;
}
&.btn-show {
color: #007bff;
}
&.btn-delete {
color: #dc3545;
}
}
}
}
}
.no-notifications {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #6c757d;
.no-notifications-icon {
font-size: 48px;
margin-bottom: 12px;
}
p {
margin: 0;
font-size: 14px;
}
}
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// Responsive design
@media (max-width: 768px) {
.notification-widget {
.widget-header {
padding: 12px;
.notification-actions {
.btn-mark-all {
padding: 6px 10px;
font-size: 14px;
}
}
}
.widget-content {
.notifications-container {
.notifications-list {
.notification-item {
padding: 10px 12px;
.notification-content {
.notification-header {
flex-direction: column;
align-items: flex-start;
.notification-time {
margin-left: 0;
margin-top: 2px;
}
}
}
}
}
}
}
}
}
import { Component, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ToastModule, ToastComponent } from '@syncfusion/ej2-angular-notifications';
import { MessageModule, MessageComponent } from '@syncfusion/ej2-angular-notifications';
import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({
selector: 'app-notification-widget',
standalone: true,
imports: [CommonModule, ToastModule, MessageModule],
templateUrl: './notification-widget.component.html',
styleUrls: ['./notification-widget.component.scss']
})
export class NotificationWidgetComponent extends BaseWidgetComponent {
@ViewChild('toast') toast!: ToastComponent;
@ViewChild('message') message!: MessageComponent;
public notifications: any[] = [];
public unreadCount: number = 0;
public toastSettings: any = {};
public messageSettings: any = {};
constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
}
applyInitialConfig(): void {
this.title = this.config.title || 'Notifications';
this.toastSettings = {
position: this.config.toastPosition || { X: 'Right', Y: 'Top' },
showCloseButton: this.config.showCloseButton !== false,
showProgressBar: this.config.showProgressBar !== false,
timeOut: this.config.timeOut || 4000,
newestOnTop: this.config.newestOnTop !== false,
cssClass: this.config.cssClass || ''
};
this.messageSettings = {
severity: this.config.severity || 'Normal',
variant: this.config.variant || 'Filled',
showIcon: this.config.showIcon !== false,
showCloseIcon: this.config.showCloseIcon !== false
};
this.notifications = [];
this.unreadCount = 0;
}
onDataUpdate(data: any[]): void {
if (data && data.length > 0) {
this.notifications = data.map(item => ({
id: item[this.config.idField || 'id'],
title: item[this.config.titleField || 'title'],
message: item[this.config.messageField || 'message'],
type: item[this.config.typeField || 'type'] || 'info',
timestamp: new Date(item[this.config.timestampField || 'timestamp']),
isRead: item[this.config.isReadField || 'isRead'] || false,
priority: item[this.config.priorityField || 'priority'] || 'normal'
})).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
this.updateUnreadCount();
}
}
onReset(): void {
this.title = 'Notifications (Default)';
this.notifications = [
{
id: 1,
title: 'System Update',
message: 'Your system has been updated successfully',
type: 'success',
timestamp: new Date(),
isRead: false,
priority: 'high'
},
{
id: 2,
title: 'Meeting Reminder',
message: 'You have a meeting in 30 minutes',
type: 'warning',
timestamp: new Date(Date.now() - 3600000),
isRead: false,
priority: 'medium'
},
{
id: 3,
title: 'New Message',
message: 'You received a new message from John Doe',
type: 'info',
timestamp: new Date(Date.now() - 7200000),
isRead: true,
priority: 'normal'
}
];
this.updateUnreadCount();
}
updateUnreadCount(): void {
this.unreadCount = this.notifications.filter(n => !n.isRead).length;
}
markAsRead(notification: any): void {
notification.isRead = true;
this.updateUnreadCount();
}
markAllAsRead(): void {
this.notifications.forEach(n => n.isRead = true);
this.updateUnreadCount();
}
deleteNotification(notification: any): void {
const index = this.notifications.findIndex(n => n.id === notification.id);
if (index > -1) {
this.notifications.splice(index, 1);
this.updateUnreadCount();
}
}
showToast(notification: any): void {
if (this.toast) {
this.toast.show({
title: notification.title,
content: notification.message,
cssClass: `e-toast-${notification.type}`,
icon: this.getIconClass(notification.type)
});
}
}
getIconClass(type: string): string {
const icons: { [key: string]: string } = {
'success': 'e-success',
'warning': 'e-warning',
'error': 'e-error',
'info': 'e-info'
};
return icons[type] || 'e-info';
}
getPriorityClass(priority: string): string {
return `priority-${priority}`;
}
getTypeClass(type: string): string {
return `notification-${type}`;
}
}
<div class="weather-widget">
<div class="widget-header">
<h3 class="widget-title">{{ title }}</h3>
<div class="weather-actions">
<button class="btn-refresh" (click)="refreshWeather()" title="Refresh">
🔄
</button>
</div>
</div>
<div class="widget-content">
<div class="loading" *ngIf="isLoading">
<div class="spinner"></div>
<p>Loading weather...</p>
</div>
<div class="error" *ngIf="hasError">
<div class="error-icon">⚠️</div>
<p>{{ errorMessage }}</p>
</div>
<div class="weather-container" *ngIf="!isLoading && !hasError">
<!-- Current Weather Card -->
<div class="current-weather-card" *ngIf="currentWeather.temperature">
<div class="weather-card-header">
<div class="weather-location">{{ location }}</div>
<div class="weather-updated">Updated: {{ lastUpdated | date:'short' }}</div>
</div>
<div class="weather-card-content">
<div class="current-weather">
<div class="weather-main">
<div class="weather-icon">
{{ getWeatherIcon(currentWeather.icon) }}
</div>
<div class="weather-temp" [class]="getTemperatureColor(currentWeather.temperature)">
{{ formatTemperature(currentWeather.temperature) }}
</div>
</div>
<div class="weather-description">
{{ currentWeather.description }}
</div>
<div class="weather-feels-like" *ngIf="currentWeather.feelsLike">
Feels like {{ formatTemperature(currentWeather.feelsLike) }}
</div>
</div>
</div>
</div>
<!-- Weather Details -->
<div class="weather-details" *ngIf="currentWeather.temperature">
<div class="detail-item">
<div class="detail-icon">💧</div>
<div class="detail-info">
<div class="detail-label">Humidity</div>
<div class="detail-value">{{ formatHumidity(currentWeather.humidity) }}</div>
</div>
</div>
<div class="detail-item">
<div class="detail-icon">💨</div>
<div class="detail-info">
<div class="detail-label">Wind</div>
<div class="detail-value">{{ formatWindSpeed(currentWeather.windSpeed) }}</div>
</div>
</div>
<div class="detail-item">
<div class="detail-icon">📊</div>
<div class="detail-info">
<div class="detail-label">Pressure</div>
<div class="detail-value">{{ formatPressure(currentWeather.pressure) }}</div>
</div>
</div>
</div>
<!-- Forecast -->
<div class="weather-forecast" *ngIf="forecast.length > 0">
<h4 class="forecast-title">5-Day Forecast</h4>
<div class="forecast-list">
<div class="forecast-item" *ngFor="let day of forecast">
<div class="forecast-day">{{ day.day }}</div>
<div class="forecast-icon">{{ getWeatherIcon(day.icon) }}</div>
<div class="forecast-temps">
<span class="temp-high">{{ formatTemperature(day.high) }}</span>
<span class="temp-low">{{ formatTemperature(day.low) }}</span>
</div>
<div class="forecast-desc">{{ day.description }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
.weather-widget {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
.widget-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #343a40;
}
.weather-actions {
.btn-refresh {
background: #007bff;
color: white;
border: none;
border-radius: 4px;
padding: 6px 10px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
&:hover {
background: #0056b3;
}
}
}
}
.widget-content {
flex: 1;
padding: 16px;
overflow-y: auto;
.loading, .error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #6c757d;
.spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.error-icon {
font-size: 32px;
margin-bottom: 8px;
}
}
.weather-container {
.current-weather-card {
margin-bottom: 16px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.weather-card-header {
padding: 12px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
.weather-location {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.weather-updated {
font-size: 12px;
opacity: 0.8;
}
}
.weather-card-content {
padding: 20px 16px;
background: white;
.current-weather {
text-align: center;
.weather-main {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
.weather-icon {
font-size: 48px;
margin-right: 16px;
}
.weather-temp {
font-size: 48px;
font-weight: 300;
line-height: 1;
&.hot {
color: #dc3545;
}
&.warm {
color: #fd7e14;
}
&.mild {
color: #28a745;
}
&.cold {
color: #007bff;
}
}
}
.weather-description {
font-size: 16px;
color: #495057;
margin-bottom: 8px;
text-transform: capitalize;
}
.weather-feels-like {
font-size: 14px;
color: #6c757d;
}
}
}
}
.weather-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
margin-bottom: 20px;
.detail-item {
display: flex;
align-items: center;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #007bff;
.detail-icon {
font-size: 20px;
margin-right: 8px;
}
.detail-info {
.detail-label {
font-size: 12px;
color: #6c757d;
margin-bottom: 2px;
}
.detail-value {
font-size: 14px;
font-weight: 600;
color: #343a40;
}
}
}
}
.weather-forecast {
.forecast-title {
font-size: 14px;
font-weight: 600;
color: #343a40;
margin: 0 0 12px 0;
padding-bottom: 8px;
border-bottom: 1px solid #e9ecef;
}
.forecast-list {
display: flex;
flex-direction: column;
gap: 8px;
.forecast-item {
display: flex;
align-items: center;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
transition: background-color 0.2s;
&:hover {
background: #e9ecef;
}
.forecast-day {
flex: 0 0 80px;
font-size: 13px;
font-weight: 600;
color: #343a40;
}
.forecast-icon {
flex: 0 0 32px;
text-align: center;
font-size: 20px;
}
.forecast-temps {
flex: 0 0 80px;
display: flex;
justify-content: space-between;
margin: 0 12px;
.temp-high {
font-size: 13px;
font-weight: 600;
color: #343a40;
}
.temp-low {
font-size: 13px;
color: #6c757d;
}
}
.forecast-desc {
flex: 1;
font-size: 12px;
color: #6c757d;
text-transform: capitalize;
}
}
}
}
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// Responsive design
@media (max-width: 768px) {
.weather-widget {
.widget-content {
padding: 12px;
.weather-container {
.current-weather-card {
.e-card-content {
.current-weather {
.weather-main {
.weather-icon {
font-size: 36px;
margin-right: 12px;
}
.weather-temp {
font-size: 36px;
}
}
}
}
}
.weather-details {
grid-template-columns: 1fr;
gap: 8px;
.detail-item {
padding: 10px;
}
}
.weather-forecast {
.forecast-list {
.forecast-item {
padding: 10px;
.forecast-day {
flex: 0 0 60px;
font-size: 12px;
}
.forecast-icon {
flex: 0 0 24px;
font-size: 16px;
}
.forecast-temps {
flex: 0 0 60px;
margin: 0 8px;
.temp-high, .temp-low {
font-size: 12px;
}
}
.forecast-desc {
font-size: 11px;
}
}
}
}
}
}
}
}
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DashboardStateService } from '../../services/dashboard-state.service';
import { BaseWidgetComponent } from '../base-widget.component';
@Component({
selector: 'app-weather-widget',
standalone: true,
imports: [CommonModule],
templateUrl: './weather-widget.component.html',
styleUrls: ['./weather-widget.component.scss']
})
export class WeatherWidgetComponent extends BaseWidgetComponent {
public weatherData: any = {};
public currentWeather: any = {};
public forecast: any[] = [];
public location: string = '';
public lastUpdated: Date = new Date();
public refreshInterval: number = 300000; // 5 minutes
constructor(protected override dashboardStateService: DashboardStateService) {
super(dashboardStateService);
}
applyInitialConfig(): void {
this.title = this.config.title || 'Weather';
this.location = this.config.location || 'Bangkok, Thailand';
this.refreshInterval = this.config.refreshInterval || 300000;
this.weatherData = {};
this.currentWeather = {};
this.forecast = [];
}
onDataUpdate(data: any[]): void {
if (data && data.length > 0) {
// Assume data contains weather information
const weatherItem = data[0];
this.currentWeather = {
temperature: weatherItem[this.config.temperatureField || 'temperature'],
humidity: weatherItem[this.config.humidityField || 'humidity'],
windSpeed: weatherItem[this.config.windSpeedField || 'windSpeed'],
pressure: weatherItem[this.config.pressureField || 'pressure'],
description: weatherItem[this.config.descriptionField || 'description'],
icon: weatherItem[this.config.iconField || 'icon'],
feelsLike: weatherItem[this.config.feelsLikeField || 'feelsLike']
};
// Process forecast data if available
if (data.length > 1) {
this.forecast = data.slice(1).map(item => ({
day: item[this.config.dayField || 'day'],
high: item[this.config.highField || 'high'],
low: item[this.config.lowField || 'low'],
description: item[this.config.forecastDescriptionField || 'description'],
icon: item[this.config.forecastIconField || 'icon']
}));
}
this.lastUpdated = new Date();
}
}
onReset(): void {
this.title = 'Weather (Default)';
this.location = 'Bangkok, Thailand';
this.currentWeather = {
temperature: 32,
humidity: 75,
windSpeed: 12,
pressure: 1013,
description: 'Partly Cloudy',
icon: 'partly-cloudy',
feelsLike: 35
};
this.forecast = [
{ day: 'Tomorrow', high: 34, low: 26, description: 'Sunny', icon: 'sunny' },
{ day: 'Wed', high: 33, low: 25, description: 'Cloudy', icon: 'cloudy' },
{ day: 'Thu', high: 31, low: 24, description: 'Rainy', icon: 'rainy' },
{ day: 'Fri', high: 30, low: 23, description: 'Thunderstorm', icon: 'thunderstorm' }
];
this.lastUpdated = new Date();
}
getWeatherIcon(iconType: string): string {
const iconMap: { [key: string]: string } = {
'sunny': '☀️',
'cloudy': '☁️',
'partly-cloudy': '⛅',
'rainy': '🌧️',
'thunderstorm': '⛈️',
'snowy': '❄️',
'foggy': '🌫️'
};
return iconMap[iconType] || '🌤️';
}
getTemperatureColor(temperature: number): string {
if (temperature >= 35) return 'hot';
if (temperature >= 25) return 'warm';
if (temperature >= 15) return 'mild';
return 'cold';
}
getWindDirection(windSpeed: number): string {
if (windSpeed < 5) return 'Calm';
if (windSpeed < 15) return 'Light';
if (windSpeed < 25) return 'Moderate';
return 'Strong';
}
formatTemperature(temp: number): string {
return `${Math.round(temp)}°C`;
}
formatPressure(pressure: number): string {
return `${pressure} hPa`;
}
formatHumidity(humidity: number): string {
return `${humidity}%`;
}
formatWindSpeed(speed: number): string {
return `${speed} km/h`;
}
refreshWeather(): void {
// Trigger data refresh
this.isLoading = true;
// In real implementation, this would call the weather API
setTimeout(() => {
this.isLoading = false;
this.lastUpdated = new Date();
}, 1000);
}
}
import { Component } from '@angular/core';
@Component({
selector: 'app-dashboard-viewer',
template: '<p>dashboard-viewer works!</p>',
standalone: true
})
export class DashboardViewerComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-dashboard',
template: '<p>dashboard works!</p>',
standalone: true
})
export class DashboardComponent {
}
......@@ -7,7 +7,7 @@ import { Router, RouterModule } from '@angular/router';
import { CourseService } from '../../services/course.service';
import { DocumentService } from '../../services/document.service';
import { ExcelService } from '../../services/excel.service';
import { WidgetService } from '../../services/widgets.service';
import { WidgetService } from '../../dashboard-management/services/widgets.service';
import { SharedModule } from '../../../shared/shared.module';
import { DatasourseTableService } from '../../services/datasourse-table.service';
import { DatasourceTableModel, MyDatasourceTableModel } from '../../models/datasource-table.model';
......
......@@ -8,11 +8,11 @@ import { SharedModule } from '../../../shared/shared.module';
import { CourseService } from '../../services/course.service';
import { DocumentService } from '../../services/document.service';
import { ExcelService } from '../../services/excel.service';
import { WidgetService } from '../../services/widgets.service';
import { CompanyModel } from '../../models/company.model';
import { TokenService } from '../../../shared/services/token.service';
import { DatasourceTableModel, MyDatasourceTableModel } from '../../models/datasource-table.model';
import { DatasourseTableService } from '../../services/datasourse-table.service';
import { WidgetService } from '../../dashboard-management/services/widgets.service';
......
......@@ -2,78 +2,72 @@ import { Routes } from '@angular/router';
import { MyPortalComponent } from './my-portal.component';
import { moduleAccessGuard } from '../../core/guards/module-access.guard';
// Import components (you may need to adjust these imports based on actual component names)
// import { CreateCategoryComponent } from './create-category/create-category.component';
// import { CategoryListComponent } from './category-list/category-list.component';
// import { CategoryListApproveComponent } from './category-list-approve/category-list-approve.component';
// import { ApprovedListComponent } from './approved-list/approved-list.component';
// import { ExcelListComponent } from './excel-list/excel-list.component';
// import { ExcelReportComponent } from './excel-report/excel-report.component';
// import { ExcelReportToggleComponent } from './excel-report-toggle/excel-report-toggle.component';
// import { ViewListExcelComponent } from './view-list-excel/view-list-excel.component';
// import { OpenImageComponent } from './open-image/open-image.component';
// import { DatasourceTableComponent } from './datasource-table/datasource-table.component';
// import { AlertModalComponent } from './alert-modal/alert-modal.component';
export const MY_PORTAL_ROUTES: Routes = [
{
path: '',
component: MyPortalComponent,
canActivate: [moduleAccessGuard]
},
// {
// path: 'create-category',
// component: CreateCategoryComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'category-list',
// component: CategoryListComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'category-list-approve',
// component: CategoryListApproveComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'approved-list',
// component: ApprovedListComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'excel-list',
// component: ExcelListComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'excel-report',
// component: ExcelReportComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'excel-report-toggle',
// component: ExcelReportToggleComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'view-list-excel',
// component: ViewListExcelComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'open-image',
// component: OpenImageComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'datasource-table',
// component: DatasourceTableComponent,
// canActivate: [moduleAccessGuard]
// },
// {
// path: 'alert-modal',
// component: AlertModalComponent,
// canActivate: [moduleAccessGuard]
// }
canActivate: [moduleAccessGuard],
children: [
{
path: 'create-category',
loadComponent: () => import('./create-category/create-category.component').then(m => m.CreateCategoryComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'category-list',
loadComponent: () => import('./category-list/category-list.component').then(m => m.CategorylistComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'category-list-approve',
loadComponent: () => import('./category-list-approve/category-list-approve.component').then(m => m.CategoryListApproveComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'approved-list',
loadComponent: () => import('./approved-list/approved-list.component').then(m => m.ApprovedListComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'excel-list',
loadComponent: () => import('./excel-list/excel-list.component').then(m => m.ExcelListComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'excel-report',
loadComponent: () => import('./excel-report/excel-report.component').then(m => m.ExcelReportComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'excel-report-toggle',
loadComponent: () => import('./excel-report-toggle/excel-report-toggle.component').then(m => m.ExcelReportToggleComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'view-list-excel',
loadComponent: () => import('./view-list-excel/view-list-excel.component').then(m => m.ViewListExcelComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'open-image',
loadComponent: () => import('./open-image/open-image.component').then(m => m.OpenImageComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'datasource-table',
loadComponent: () => import('./datasource-table/datasource-table.component').then(m => m.DatasourceTableComponent),
canActivate: [moduleAccessGuard]
},
{
path: 'alert-modal',
loadComponent: () => import('./alert-modal/alert-modal.component').then(m => m.AlertModalComponent),
canActivate: [moduleAccessGuard]
},
{
path: '',
redirectTo: 'category-list', // Assuming a default child route
pathMatch: 'full'
}
]
}
];
<div class="view-list-excel">
<div class="header">
<h2>View List Excel</h2>
</div>
<div class="content">
<p>Excel view component placeholder</p>
</div>
</div>
.view-list-excel {
padding: 20px;
.header {
margin-bottom: 20px;
h2 {
margin: 0;
color: #333;
}
}
.content {
p {
color: #666;
}
}
}
......@@ -8,10 +8,11 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
import swal from 'sweetalert';
import { saveAs } from 'file-saver';
import { SharedModule } from '../../../../shared/shared.module';
import { ExcelContentModel } from '../../../models/excel-content.model';
import { ExcelService } from '../../../services/excel.service';
import { OpenImageComponent } from '../../../open-image/open-image.component';
import { ExcelContentModel } from '../../models/excel-content.model';
import { SharedModule } from '../../../shared/shared.module';
import { ExcelService } from '../../services/excel.service';
import { OpenImageComponent } from '../open-image/open-image.component';
@Component({
selector: 'app-view-list-excel',
......@@ -148,4 +149,4 @@ export class ViewListExcelComponent implements OnInit {
coverDate(date: string) {
return date.split('-').reverse().join('/');
}
}
\ No newline at end of file
}
import { Component } from '@angular/core';
@Component({
selector: 'app-attendance-location',
template: '<p>attendance-location works!</p>',
standalone: true
})
export class AttendanceLocationComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-attendance-settings',
template: '<p>attendance-settings works!</p>',
standalone: true
})
export class AttendanceSettingsComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-face-enrollment',
template: '<p>face-enrollment works!</p>',
standalone: true
})
export class FaceEnrollmentComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-face-verification',
template: '<p>face-verification works!</p>',
standalone: true
})
export class FaceVerificationComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-security-report',
template: '<p>security-report works!</p>',
standalone: true
})
export class SecurityReportComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-attendance-reports',
template: '<p>attendance-reports works!</p>',
standalone: true
})
export class AttendanceReportsComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-company-settings',
template: '<p>company-settings works!</p>',
standalone: true
})
export class CompanySettingsComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-department-settings',
template: '<p>department-settings works!</p>',
standalone: true
})
export class DepartmentSettingsComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-employee-documents',
template: '<p>employee-documents works!</p>',
standalone: true
})
export class EmployeeDocumentsComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-employee-profile',
template: '<p>employee-profile works!</p>',
standalone: true
})
export class EmployeeProfileComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-employee-reports',
template: '<p>employee-reports works!</p>',
standalone: true
})
export class EmployeeReportsComponent {
}
......@@ -20,6 +20,34 @@ export const MYHR_LITE_ROUTES: Routes = [
loadComponent: () => import('./reports/myhr-lite-reports.component').then(m => m.MyhrLiteReportsComponent)
},
{
path: 'employee-profile',
loadComponent: () => import('./employee-profile/employee-profile.component').then(m => m.EmployeeProfileComponent)
},
{
path: 'employee-documents',
loadComponent: () => import('./employee-documents/employee-documents.component').then(m => m.EmployeeDocumentsComponent)
},
{
path: 'company-settings',
loadComponent: () => import('./company-settings/company-settings.component').then(m => m.CompanySettingsComponent)
},
{
path: 'department-settings',
loadComponent: () => import('./department-settings/department-settings.component').then(m => m.DepartmentSettingsComponent)
},
{
path: 'position-settings',
loadComponent: () => import('./position-settings/position-settings.component').then(m => m.PositionSettingsComponent)
},
{
path: 'employee-reports',
loadComponent: () => import('./employee-reports/employee-reports.component').then(m => m.EmployeeReportsComponent)
},
{
path: 'attendance-reports',
loadComponent: () => import('./attendance-reports/attendance-reports.component').then(m => m.AttendanceReportsComponent)
},
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
......
import { Component } from '@angular/core';
@Component({
selector: 'app-position-settings',
template: '<p>position-settings works!</p>',
standalone: true
})
export class PositionSettingsComponent {
}
......@@ -52,6 +52,10 @@ export const MYHR_PLUS_ROUTES: Routes = [
loadComponent: () => import('./reports/myhr-plus-excel-report.component').then(m => m.MyhrPlusExcelReportComponent)
},
{
path: 'widget-warehouse',
loadChildren: () => import('../dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
},
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
......
......@@ -13,7 +13,53 @@ export const MYJOB_ROUTES: Routes = [
},
{
path: 'pdpa-manage',
loadComponent: () => import('./pdpa/myjob-pdpa-manage.component').then(m => m.MyjobPdpaManageComponent)
loadComponent: () => import('./pdpa-manage/pdpa-manage.component').then(m => m.PdpaManageComponent)
},
{
path: 'admin-manage',
loadComponent: () => import('./admin-manage/admin-manage.component').then(m => m.AdminManageComponent)
},
{
path: 'article-manage',
loadComponent: () => import('./article-manage/article-manage.component').then(m => m.ArticleManageComponent)
},
{
path: 'company-department',
loadComponent: () => import('./company-department/company-department.component').then(m => m.CompanyDepartmentComponent)
},
{
path: 'company-manage',
loadComponent: () => import('./company-manage/company-manage.component').then(m => m.CompanyManageComponent)
},
{
path: 'home-common',
loadComponent: () => import('./home-common/home-common.component').then(m => m.HomeCommonComponent)
},
{
path: 'employee',
children: [
{
path: 'department',
loadComponent: () => import('./employee/department/department.component').then(m => m.DepartmentComponent)
},
{
path: 'position',
loadComponent: () => import('./employee/position/position.component').then(m => m.PositionComponent)
}
]
},
{
path: 'user-management',
children: [
{
path: '',
loadComponent: () => import('./user-management/user-management/user-management.component').then(m => m.UserManagementComponent)
},
{
path: 'user-setting',
loadComponent: () => import('./user-management/user-setting/user-setting.component').then(m => m.UserSettingComponent)
}
]
},
{
path: 'manage-articles',
......
import { Component } from '@angular/core';
@Component({
selector: 'app-admin-manage',
template: '<p>admin-manage works!</p>',
standalone: true
})
export class AdminManageComponent {
}
......@@ -12,6 +12,14 @@ export const MYSKILL_X_ROUTES: Routes = [
component: MyskillXDashboardComponent
},
{
path: 'admin-manage',
loadComponent: () => import('./admin-manage/admin-manage.component').then(m => m.AdminManageComponent)
},
{
path: 'user-management',
loadComponent: () => import('./user-management/user-management.component').then(m => m.UserManagementComponent)
},
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
......
import { Component } from '@angular/core';
@Component({
selector: 'app-user-management',
template: '<p>user-management works!</p>',
standalone: true
})
export class UserManagementComponent {
}
......@@ -12,7 +12,6 @@ export const portalManageRoutes: Routes = [
// myHR-Plus Module
{
path: 'myhr-plus',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./myhr-plus/myhr-plus.routes').then(m => m.MYHR_PLUS_ROUTES)
},
......@@ -58,21 +57,21 @@ export const portalManageRoutes: Routes = [
loadChildren: () => import('./myskill-x/myskill-x.routes').then(m => m.MYSKILL_X_ROUTES)
},
// Company Management Module
{
path: 'company-management',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./company-management/company-management.routes').then(m => m.COMPANY_MANAGEMENT_ROUTES)
},
// === การบริการ ===
// Dashboard Management (รวม widget management)
{
path: 'dashboard-management',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
},
// Dashboard (alias สำหรับ backward compatibility)
{
path: 'dashboard',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
},
// Meeting Booking
{
......@@ -104,34 +103,20 @@ export const portalManageRoutes: Routes = [
loadChildren: () => import('./my-portal/my-portal.routes').then(m => m.MY_PORTAL_ROUTES)
},
// === Generic App Routes ===
// These routes are for simple apps that don't need special module-level services.
// Dynamic route for dashboard management per application
{
path: ':appName/dashboard-management',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
},
// Dynamic routes for widget warehouse per application
// Dashboard (Old - for compatibility)
{
path: ':appName/widget-warehouse',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
path: 'dashboard',
component: HomeComponent, // Assuming HomeComponent is a generic dashboard or a placeholder
canActivate: [moduleAccessGuard]
},
{
path: ':appName/widget-linker',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
path: 'dashboard-viewer',
loadComponent: () => import('./dashboard-viewer/dashboard-viewer.component').then(m => m.DashboardViewerComponent),
canActivate: [moduleAccessGuard]
},
// Route for viewing a specific dashboard
{
path: 'dashboard-viewer/:dashboardId',
canActivate: [moduleAccessGuard],
loadChildren: () => import('./dashboard-management/dashboard-management.module').then(m => m.DashboardManagementModule)
},
// === Generic App Routes ===
// These routes are for simple apps that don't need special module-level services.
// Redirect for unknown routes
{
......
......@@ -43,6 +43,11 @@ export class MenuPermissionService {
* ตรวจสอบสิทธิ์การเข้าถึงเมนู
*/
canAccessMenu(menuPath: string, permission: 'view' | 'create' | 'edit' | 'delete' | 'export' | 'import' = 'view'): Observable<boolean> {
// ไม่ต้องเช็ค permission สำหรับ dashboard-management และ widget-warehouse
if (menuPath.includes('dashboard-management') || menuPath.includes('widget-warehouse')) {
return of(true);
}
return this.menuPermissions$.pipe(
map(menus => {
const menu = this.findMenuByPath(menus, menuPath);
......@@ -173,7 +178,7 @@ export class MenuPermissionService {
{
id: 'widget-warehouse',
name: 'คลังวิดเจ็ต',
path: '/portal-manage/dashboard/widget-warehouse',
path: '/portal-manage/myhr-plus/widget-warehouse',
order: 2,
isVisible: true,
permissions: {
......
......@@ -447,7 +447,22 @@ export class SidebarComponent {
if (ele.path == this.currentUrl) {
element.active = true;
element.selected = true;
ele.active = true;
ele.selected = true;
}
// ตรวจสอบ path สำหรับ dashboard management routes
if (this.isDashboardManagementRoute && ele.path && this.currentUrl.startsWith(ele.path.split('?')[0])) {
const currentUrlBase = this.currentUrl.split('?')[0];
const elePathBase = ele.path.split('?')[0];
if (currentUrlBase === elePathBase || this.currentUrl.includes(elePathBase)) {
element.active = true;
element.selected = true;
ele.active = true;
ele.selected = true;
}
}
// ตรวจสอบ path สำหรับ widget routes
if ((this.isWidgetWarehouseRoute || this.isWidgetLinkerRoute) && ele.path && this.currentUrl.startsWith(ele.path.split('?')[0])) {
element.active = true;
......@@ -455,6 +470,7 @@ export class SidebarComponent {
ele.active = true;
ele.selected = true;
}
// ตรวจสอบ path สำหรับ Excel Report ที่มี query parameters
if ((this.isMyportalRoute||this.isMyPortalRoute||this.isInstallerRoute)&&ele.path && this.currentUrl.startsWith(ele.path.split('?')[0])) {
const currentUrlBase = this.currentUrl.split('?')[0];
......@@ -475,7 +491,24 @@ export class SidebarComponent {
element.selected = true;
ele.active = true;
ele.selected = true;
child1.active = true;
child1.selected = true;
}
// ตรวจสอบ path สำหรับ dashboard management routes ในระดับที่ 3
if (this.isDashboardManagementRoute && child1.path && this.currentUrl.startsWith(child1.path.split('?')[0])) {
const currentUrlBase = this.currentUrl.split('?')[0];
const child1PathBase = child1.path.split('?')[0];
if (currentUrlBase === child1PathBase || this.currentUrl.includes(child1PathBase)) {
element.active = true;
element.selected = true;
ele.active = true;
ele.selected = true;
child1.active = true;
child1.selected = true;
}
}
// ตรวจสอบ path สำหรับ widget routes ในระดับที่ 3
if ((this.isWidgetWarehouseRoute || this.isWidgetLinkerRoute) && child1.path && this.currentUrl.startsWith(child1.path.split('?')[0])) {
element.active = true;
......@@ -485,6 +518,7 @@ export class SidebarComponent {
child1.active = true;
child1.selected = true;
}
// ตรวจสอบ path สำหรับ Excel Report ที่มี query parameters ในระดับที่ 3
if ((this.isMyportalRoute||this.isMyPortalRoute||this.isInstallerRoute)&&child1.path && this.currentUrl.startsWith(child1.path.split('?')[0])) {
const currentUrlBase = this.currentUrl.split('?')[0];
......
......@@ -29,7 +29,7 @@ export const authen: Routes = [
]
@NgModule({
imports: [RouterModule.forRoot(admin)],
imports: [RouterModule.forRoot(authen)],
exports: [RouterModule]
})
export class AuthenticationsRoutingModule { }
\ No newline at end of file
......@@ -14,7 +14,7 @@ export const landing: Routes = [
];
@NgModule({
imports: [RouterModule.forRoot(admin)],
imports: [RouterModule.forRoot(landing)],
exports: [RouterModule]
})
export class landingpageRoutingModule { }
......@@ -99,14 +99,50 @@ export class NavService implements OnDestroy {
active: false,
children: [
{
path: `/portal-manage/${appName}/dashboard-management/dashboard`,
path: `/portal-manage/dashboard-management/dashboard-home`,
title: 'แดชบอร์ดหลัก',
type: 'link'
},
{
path: `/portal-manage/${appName}/dashboard-management/widget-management`,
title: 'จัดการวิดเจ็ต',
type: 'link'
icon: 'widget',
type: 'sub',
active: false,
children: [
{
path: `/portal-manage/dashboard-management/widget-list`,
title: 'รายการวิดเจ็ต',
type: 'link'
},
// {
// path: `/portal-manage/dashboard-management/widget-management/edit`,
// title: 'เพิ่มวิดเจ็ตใหม่',
// type: 'link'
// },
{
path: `/portal-manage/dashboard-management/dataset-widget-linker`,
title: 'เชื่อมโยงข้อมูลกับวิดเจ็ต',
type: 'link'
},
// {
// path: `/portal-manage/dashboard-management/widget-config`,
// title: 'ตั้งค่าวิดเจ็ต',
// type: 'link'
// }
]
},
{
title: 'ดูแดชบอร์ด',
icon: 'eye',
type: 'sub',
active: false,
children: [
{
path: `/portal-manage/dashboard-management/dashboard-viewer`,
title: 'ดูแดชบอร์ด',
type: 'link'
}
]
},
{
path: `/portal-manage/${appName}/widget-warehouse`,
......@@ -117,7 +153,7 @@ export class NavService implements OnDestroy {
path: `/portal-manage/${appName}/widget-linker`,
title: 'เชื่อมโยงวิดเจ็ตกับชุดข้อมูล',
type: 'link'
},
}
]
};
}
......@@ -558,10 +594,11 @@ export class NavService implements OnDestroy {
type: 'sub',
active: false,
children: [
{ path: '/portal-manage/dashboard-management/dashboard', title: 'แดชบอร์ดหลัก', type: 'link' },
{ path: '/portal-manage/dashboard-management/widget-management', title: 'จัดการวิดเจ็ต', type: 'link' },
{ path: '/portal-manage/dashboard-management/dashboard-home', title: 'แดชบอร์ดหลัก', type: 'link' },
{ path: '/portal-manage/dashboard-management/widget-list', title: 'รายการวิดเจ็ต', type: 'link' },
{ path: '/portal-manage/dashboard-management/dataset-widget-linker', title: 'เชื่อมโยงข้อมูลกับวิดเจ็ต', type: 'link' },
{ path: '/portal-manage/dashboard-management/widget-config', title: 'ตั้งค่าวิดเจ็ต', type: 'link' },
{ path: '/portal-manage/dashboard-management/dataset-picker', title: 'เลือกชุดข้อมูล', type: 'link' },
{ path: '/portal-manage/dashboard-management/dashboard-viewer', title: 'ดูแดชบอร์ด', type: 'link' },
],
},
{
......@@ -583,7 +620,7 @@ export class NavService implements OnDestroy {
{
title: 'แดชบอร์ดหลัก',
icon: 'dashboard',
path: '/portal-manage/dashboard-management/dashboard',
path: '/portal-manage/dashboard-management/dashboard-home',
type: 'link',
},
{
......@@ -593,94 +630,27 @@ export class NavService implements OnDestroy {
active: false,
children: [
{
path: '/portal-manage/dashboard-management/widget-management',
path: '/portal-manage/dashboard-management/widget-list',
title: 'รายการวิดเจ็ต',
type: 'link'
},
// {
// path: '/portal-manage/dashboard-management/widget-management/edit',
// title: 'เพิ่มวิดเจ็ตใหม่',
// type: 'link'
// },
{
path: '/portal-manage/dashboard-management/widget-management/edit',
title: 'เพิ่มวิดเจ็ตใหม่',
type: 'link'
},
{
path: '/portal-manage/dashboard-management/widget-management/linker',
title: 'เชื่อมโยงข้อมูล',
type: 'link'
},
{
path: '/portal-manage/dashboard-management/widget-config',
title: 'ตั้งค่าวิดเจ็ต',
type: 'link'
},
],
},
{
title: 'คลังวิดเจ็ตแอป',
icon: 'package',
type: 'sub',
active: false,
children: [
{
path: '/portal-manage/myhr-plus/widget-warehouse',
title: 'คลังวิดเจ็ต myHR-Plus',
type: 'link'
},
{
path: '/portal-manage/myhr-lite/widget-warehouse',
title: 'คลังวิดเจ็ต myHR-Lite',
type: 'link'
},
{
path: '/portal-manage/myjob/widget-warehouse',
title: 'คลังวิดเจ็ต MyJob',
type: 'link'
},
{
path: '/portal-manage/mylearn/widget-warehouse',
title: 'คลังวิดเจ็ต MyLearn',
path: '/portal-manage/dashboard-management/dataset-widget-linker',
title: 'เชื่อมโยงข้อมูลกับวิดเจ็ต',
type: 'link'
},
// {
// path: '/portal-manage/dashboard-management/widget-config',
// title: 'ตั้งค่าวิดเจ็ต',
// type: 'link'
// },
],
},
{
title: 'จัดการข้อมูล',
icon: 'database',
type: 'sub',
active: false,
children: [
{
path: '/portal-manage/dashboard-management/dataset-picker',
title: 'เลือกชุดข้อมูล',
type: 'link'
},
{
path: '/portal-manage/myhr-plus/widget-linker',
title: 'เชื่อมโยงข้อมูล myHR-Plus',
type: 'link'
},
{
path: '/portal-manage/myhr-lite/widget-linker',
title: 'เชื่อมโยงข้อมูล myHR-Lite',
type: 'link'
},
{
path: '/portal-manage/myjob/widget-linker',
title: 'เชื่อมโยงข้อมูล MyJob',
type: 'link'
},
{
path: '/portal-manage/mylearn/widget-linker',
title: 'เชื่อมโยงข้อมูล MyLearn',
type: 'link'
},
],
},
{
title: 'ดูแดชบอร์ด',
icon: 'eye',
path: '/portal-manage/dashboard-management/viewer',
type: 'link',
}
];
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment