Commit 63435617 by Ooh-Ao

เมนู

parent 41948ca4
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { MenuPermissionService } from '../../portal-manage/services/menu-permission.service';
@Injectable({
providedIn: 'root'
})
export class MenuPermissionGuard implements CanActivate {
constructor(
private menuPermissionService: MenuPermissionService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
const requiredPermission = route.data['permission'] || 'view';
const menuPath = state.url;
return this.menuPermissionService.canAccessMenu(menuPath, requiredPermission).pipe(
map(hasPermission => {
if (hasPermission) {
return true;
} else {
// Redirect to unauthorized page or dashboard
this.router.navigate(['/portal-manage/unauthorized']);
return false;
}
}),
catchError(() => {
// On error, redirect to dashboard
this.router.navigate(['/portal-manage/dashboard']);
return of(false);
})
);
}
}
<div class="container mx-auto px-4 py-8">
<div class="flex justify-end mb-6">
<button class="hs-dropdown-toggle ti-btn ti-btn-danger-full me-2" (click)="logout()">Logout</button>
</div>
<h1 class="text-4xl font-bold text-indigo-900 mb-10">Application</h1>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8">
<a class="card bg-white rounded-xl p-8 text-center shadow-lg transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:bg-gradient-to-br from-blue-50 to-blue-100"
(click)="checkAppToken('myhr-plus')">
<div class="card-icon text-5xl text-indigo-700 mb-4 transition-transform duration-300 ease-in-out group-hover:scale-110">
<img src="./assets/images/logoallHR/myhr-plus.jpg" alt="miscrosoft"
class="leading-[1.75] text-2xl !h-[5rem] align-middle flex justify-center mx-auto">
</div>
<!-- <h3 class="card-title text-2xl font-semibold text-gray-800 mb-2">myHR-Plus</h3> -->
<p class="card-description text-gray-600 text-sm">myHR-Plus</p>
</a>
<a class="card bg-white rounded-xl p-8 text-center shadow-lg transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:bg-gradient-to-br from-blue-50 to-blue-100"
(click)="checkAppToken('myhr-lite')">
<div class="card-icon text-5xl text-indigo-700 mb-4 transition-transform duration-300 ease-in-out group-hover:scale-110">
<img src="./assets/images/logoallHR/myHR-Lite-logo-new.png" alt="miscrosoft"
class="leading-[1.75] text-2xl !h-[5rem] align-middle flex justify-center mx-auto">
</div>
<!-- <h3 class="card-title text-2xl font-semibold text-gray-800 mb-2">myHR-Lite</h3> -->
<p class="card-description text-gray-600 text-sm">myHR-Lite</p>
</a>
<a class="card bg-white rounded-xl p-8 text-center shadow-lg transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:bg-gradient-to-br from-blue-50 to-blue-100"
(click)="checkAppToken('zeeme')">
<div class="card-icon text-5xl text-indigo-700 mb-4 transition-transform duration-300 ease-in-out group-hover:scale-110">
<img src="./assets/images/logoallHR/zeemePlus.png" alt="miscrosoft"
class="leading-[1.75] text-2xl !h-[5rem] align-middle flex justify-center mx-auto">
</div>
<!-- <h3 class="card-title text-2xl font-semibold text-gray-800 mb-2">Zeeme Plus</h3> -->
<p class="card-description text-gray-600 text-sm">Zeeme Plus</p>
</a>
<a class="card bg-white rounded-xl p-8 text-center shadow-lg transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:bg-gradient-to-br from-blue-50 to-blue-100"
(click)="checkAppToken('myface')">
<div class="card-icon text-5xl text-indigo-700 mb-4 transition-transform duration-300 ease-in-out group-hover:scale-110">
<img src="./assets/images/logoallHR/logo_myface.png" alt="miscrosoft"
class="leading-[1.75] text-2xl !h-[5rem] align-middle flex justify-center mx-auto">
</div>
<!-- <h3 class="card-title text-2xl font-semibold text-gray-800 mb-2">myFace</h3> -->
<p class="card-description text-gray-600 text-sm">myFace</p>
</a>
<a class="card bg-white rounded-xl p-8 text-center shadow-lg transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:bg-gradient-to-br from-blue-50 to-blue-100"
(click)="checkAppToken('mylearn')">
<div class="card-icon text-5xl text-indigo-700 mb-4 transition-transform duration-300 ease-in-out group-hover:scale-110">
<img src="./assets/images/logoallHR/mylearn-logo.png" alt="miscrosoft"
class="leading-[1.75] text-2xl !h-[5rem] align-middle flex justify-center mx-auto">
</div>
<!-- <h3 class="card-title text-2xl font-semibold text-gray-800 mb-2">myLearn</h3> -->
<p class="card-description text-gray-600 text-sm">myLearn</p>
</a>
<a class="card bg-white rounded-xl p-8 text-center shadow-lg transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:bg-gradient-to-br from-blue-50 to-blue-100"
(click)="checkAppToken('myjob')">
<div class="card-icon text-5xl text-indigo-700 mb-4 transition-transform duration-300 ease-in-out group-hover:scale-110">
<img src="./assets/images/logoallHR/logo_myjob.png" alt="miscrosoft"
class="leading-[1.75] text-2xl !h-[5rem] align-middle flex justify-center mx-auto">
</div>
<!-- <h3 class="card-title text-2xl font-semibold text-gray-800 mb-2">myJob</h3> -->
<p class="card-description text-gray-600 text-sm">myJob</p>
</a>
<a class="card bg-white rounded-xl p-8 text-center shadow-lg transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:bg-gradient-to-br from-blue-50 to-blue-100"
(click)="checkAppToken('myskill-x')">
<div class="card-icon text-5xl text-indigo-700 mb-4 transition-transform duration-300 ease-in-out group-hover:scale-110">
<img src="./assets/images/logoallHR/mySkill-x.png" alt="miscrosoft"
class="leading-[1.75] text-2xl !h-[5rem] align-middle flex justify-center mx-auto">
</div>
<!-- <h3 class="card-title text-2xl font-semibold text-gray-800 mb-2">mySkill-X</h3> -->
<p class="card-description text-gray-600 text-sm">mySkill-X</p>
</a>
<a class="card bg-white rounded-xl p-8 text-center shadow-lg transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:bg-gradient-to-br from-gray-50 to-gray-100"
[routerLink]="['/portal-manage/permission-management']">
<div class="card-icon text-5xl text-gray-700 mb-4 transition-transform duration-300 ease-in-out group-hover:scale-110">
<img src="./assets/images/icons/widget.png" alt="Settings"
class="leading-[1.75] text-2xl !h-[5rem] align-middle flex justify-center mx-auto">
</div>
<p class="card-description text-gray-600 text-sm">Permission Management</p>
</a>
<div class="min-h-screen relative overflow-hidden">
<!-- Main Background Gradient -->
<div class="absolute inset-0 company-logo-gradient"></div>
<!-- Animated Background Elements -->
<div class="absolute inset-0 overflow-hidden background-animations">
<!-- Floating Circles -->
<div class="absolute -top-40 -right-40 w-80 h-80 company-blue-circle rounded-full blur-3xl animate-pulse"></div>
<div class="absolute top-1/2 -left-40 w-96 h-96 company-red-circle rounded-full blur-3xl animate-pulse"></div>
<div class="absolute bottom-0 right-1/4 w-64 h-64 company-blue-light rounded-full blur-3xl animate-pulse"></div>
<!-- Geometric Patterns -->
<div class="absolute top-20 left-10 w-32 h-32 border border-blue-300/40 rounded-full"></div>
<div class="absolute top-40 right-20 w-24 h-24 border border-red-300/40 rounded-full"></div>
<div class="absolute bottom-40 left-1/4 w-40 h-40 border border-blue-200/30 rounded-full"></div>
<!-- Grid Pattern -->
<div class="absolute inset-0 opacity-30 company-grid-pattern"></div>
<!-- Wave Pattern -->
<div class="absolute bottom-0 left-0 right-0 h-32 company-wave-gradient"></div>
<!-- Particle Effects -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute top-1/4 left-1/4 w-2 h-2 bg-blue-500/50 rounded-full animate-ping particle-1"></div>
<div class="absolute top-1/3 right-1/3 w-1 h-1 bg-red-500/60 rounded-full animate-ping particle-2"></div>
<div class="absolute bottom-1/3 left-1/3 w-1.5 h-1.5 bg-blue-400/50 rounded-full animate-ping particle-3"></div>
<div class="absolute top-2/3 right-1/4 w-1 h-1 bg-red-400/60 rounded-full animate-ping particle-4"></div>
<div class="absolute bottom-1/4 right-1/2 w-2 h-2 bg-blue-600/40 rounded-full animate-ping particle-5"></div>
</div>
<!-- Glassmorphism Overlay -->
<div class="absolute inset-0 glassmorphism-overlay backdrop-blur-sm"></div>
</div>
<div class="relative container mx-auto px-4 py-8">
<!-- Header Section -->
<div class="flex flex-col lg:flex-row justify-between items-start lg:items-center mb-12">
<div class="flex-1 mb-6 lg:mb-0">
<div class="inline-flex items-center px-4 py-2 rounded-full bg-white/80 backdrop-blur-sm border border-white/20 shadow-lg mb-4">
<div class="w-3 h-3 bg-green-500 rounded-full animate-pulse mr-3"></div>
<span class="text-sm font-medium text-gray-700">ระบบออนไลน์</span>
</div>
<h1 class="text-5xl lg:text-6xl font-bold company-title-gradient bg-clip-text text-transparent mb-4 leading-tight">
ระบบจัดการพอร์ทัล
</h1>
<p class="text-xl text-gray-600 max-w-2xl leading-relaxed">
ยินดีต้อนรับสู่ระบบจัดการพอร์ทัลสำหรับพนักงาน
<span class="text-indigo-600 font-semibold">ที่ทันสมัยและใช้งานง่าย</span>
</p>
</div>
<!-- User Info Card -->
<div *ngIf="userInfo$ | async as userInfo" class="bg-white/80 backdrop-blur-sm rounded-2xl p-6 shadow-xl border border-white/20 hover:shadow-2xl transition-all duration-300">
<div class="flex items-center space-x-4">
<div class="w-16 h-16 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-full flex items-center justify-center text-white text-2xl font-bold shadow-lg">
{{ userInfo.fullName.charAt(0) }}
</div>
<div class="flex-1">
<div class="text-sm text-gray-500 mb-1">ยินดีต้อนรับ</div>
<div class="font-bold text-gray-800 text-lg">{{ userInfo.fullName }}</div>
<div class="text-sm text-gray-600">{{ userInfo.department }}</div>
<div class="text-xs text-gray-500">{{ userInfo.position }}</div>
</div>
</div>
<button
class="mt-4 w-full bg-gradient-to-r from-red-500 to-pink-600 hover:from-red-600 hover:to-pink-700 text-white font-semibold py-3 px-4 rounded-xl transition-all duration-300 transform hover:scale-105 shadow-lg hover:shadow-xl flex items-center justify-center space-x-2"
(click)="logout()">
<i class="ri-logout-box-line text-lg"></i>
<span>ออกจากระบบ</span>
</button>
</div>
</div>
<!-- Quick Stats -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
<div class="group bg-white/80 backdrop-blur-sm rounded-2xl p-6 shadow-xl border border-white/20 hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="text-3xl font-bold text-gray-800 mb-2">{{ (accessibleApps$ | async)?.length || 0 }}</div>
<div class="text-gray-600 font-medium">แอปพลิเคชันที่เข้าถึงได้</div>
<div class="text-sm text-gray-500 mt-1">ตามสิทธิ์ของคุณ</div>
</div>
<div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl flex items-center justify-center text-white text-2xl shadow-lg group-hover:scale-110 transition-transform duration-300">
<i class="ri-apps-line"></i>
</div>
</div>
<div class="mt-4 w-full bg-gray-200 rounded-full h-2">
<div class="bg-gradient-to-r from-blue-500 to-blue-600 h-2 rounded-full transition-all duration-500"
[style.width.%]="((accessibleApps$ | async)?.length || 0) * 8.33"></div>
</div>
</div>
<div class="group bg-white/80 backdrop-blur-sm rounded-2xl p-6 shadow-xl border border-white/20 hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="text-3xl font-bold text-gray-800 mb-2">12</div>
<div class="text-gray-600 font-medium">โมดูลทั้งหมด</div>
<div class="text-sm text-gray-500 mt-1">ในระบบ</div>
</div>
<div class="w-16 h-16 bg-gradient-to-br from-green-500 to-green-600 rounded-2xl flex items-center justify-center text-white text-2xl shadow-lg group-hover:scale-110 transition-transform duration-300">
<i class="ri-module-line"></i>
</div>
</div>
<div class="mt-4 flex items-center text-sm text-green-600">
<i class="ri-arrow-up-line mr-1"></i>
<span>+2 จากเดือนที่แล้ว</span>
</div>
</div>
<div class="group bg-white/80 backdrop-blur-sm rounded-2xl p-6 shadow-xl border border-white/20 hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="text-3xl font-bold text-gray-800 mb-2">5</div>
<div class="text-gray-600 font-medium">การจองวันนี้</div>
<div class="text-sm text-gray-500 mt-1">ห้องประชุม</div>
</div>
<div class="w-16 h-16 bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl flex items-center justify-center text-white text-2xl shadow-lg group-hover:scale-110 transition-transform duration-300">
<i class="ri-calendar-check-line"></i>
</div>
</div>
<div class="mt-4 flex items-center text-sm text-purple-600">
<i class="ri-time-line mr-1"></i>
<span>3 ครั้งที่รอดำเนินการ</span>
</div>
</div>
<div class="group bg-white/80 backdrop-blur-sm rounded-2xl p-6 shadow-xl border border-white/20 hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="text-3xl font-bold text-gray-800 mb-2">3</div>
<div class="text-gray-600 font-medium">การแจ้งเตือน</div>
<div class="text-sm text-gray-500 mt-1">ใหม่</div>
</div>
<div class="w-16 h-16 bg-gradient-to-br from-orange-500 to-orange-600 rounded-2xl flex items-center justify-center text-white text-2xl shadow-lg group-hover:scale-110 transition-transform duration-300">
<i class="ri-notification-line"></i>
</div>
</div>
<div class="mt-4 flex items-center text-sm text-orange-600">
<div class="w-2 h-2 bg-orange-500 rounded-full mr-2 animate-pulse"></div>
<span>2 ข้อความใหม่</span>
</div>
</div>
</div>
<!-- Applications Grid -->
<div class="mb-12">
<div class="flex items-center justify-between mb-8">
<div>
<h2 class="text-3xl font-bold bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent mb-2">
แอปพลิเคชันที่เข้าถึงได้
</h2>
<p class="text-gray-600">เลือกแอปพลิเคชันที่คุณต้องการใช้งาน</p>
</div>
<div class="flex items-center space-x-2">
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span class="text-sm text-gray-600">{{ (accessibleApps$ | async)?.length || 0 }} แอปพร้อมใช้งาน</span>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<div *ngFor="let app of accessibleApps$ | async; let i = index"
class="group bg-white/80 backdrop-blur-sm rounded-2xl p-6 shadow-xl border border-white/20 hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2 cursor-pointer"
(click)="navigateToApp(app)"
[style.animation-delay]="(i * 100) + 'ms'">
<!-- App Icon -->
<div class="relative mb-6">
<div class="w-20 h-20 mx-auto bg-gradient-to-br from-indigo-100 to-purple-100 rounded-2xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform duration-300">
<img [src]="app.icon"
alt="App Icon"
title="App Icon"
class="w-12 h-12 object-contain rounded-lg"
loading="lazy">
</div>
<!-- Online Indicator -->
<div class="absolute -top-1 -right-1 w-6 h-6 bg-green-500 rounded-full border-2 border-white flex items-center justify-center">
<div class="w-2 h-2 bg-white rounded-full"></div>
</div>
</div>
<!-- App Info -->
<div class="text-center mb-4">
<h3 class="text-xl font-bold text-gray-800 mb-2 group-hover:text-indigo-600 transition-colors duration-300">
{{ app.displayName }}
</h3>
<p class="text-gray-600 text-sm leading-relaxed">
{{ app.description }}
</p>
</div>
<!-- Permission Badges -->
<div class="flex flex-wrap gap-2 justify-center mb-4">
<span *ngIf="app.permissions.create"
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700 border border-emerald-200">
<i class="ri-add-line mr-1"></i>สร้าง
</span>
<span *ngIf="app.permissions.edit"
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-700 border border-blue-200">
<i class="ri-edit-line mr-1"></i>แก้ไข
</span>
<span *ngIf="app.permissions.delete"
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-red-100 text-red-700 border border-red-200">
<i class="ri-delete-bin-line mr-1"></i>ลบ
</span>
<span *ngIf="app.permissions.export"
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-700 border border-purple-200">
<i class="ri-download-line mr-1"></i>ส่งออก
</span>
</div>
<!-- Action Button -->
<div class="flex items-center justify-center">
<div class="inline-flex items-center px-4 py-2 bg-gradient-to-r from-indigo-500 to-purple-600 text-white text-sm font-semibold rounded-xl group-hover:from-indigo-600 group-hover:to-purple-700 transition-all duration-300 transform group-hover:scale-105">
<span>เข้าสู่ระบบ</span>
<i class="ri-arrow-right-line ml-2 group-hover:translate-x-1 transition-transform duration-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Activities -->
<div class="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl border border-white/20 p-8">
<div class="flex items-center justify-between mb-6">
<h3 class="text-2xl font-bold bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent">
กิจกรรมล่าสุด
</h3>
<button class="text-indigo-600 hover:text-indigo-800 font-medium text-sm flex items-center space-x-1 transition-colors duration-300">
<span>ดูทั้งหมด</span>
<i class="ri-arrow-right-line"></i>
</button>
</div>
<div class="space-y-4">
<div class="group flex items-center space-x-4 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-100 hover:shadow-lg transition-all duration-300 cursor-pointer">
<div class="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center text-white shadow-lg group-hover:scale-110 transition-transform duration-300">
<i class="ri-calendar-line text-lg"></i>
</div>
<div class="flex-1">
<div class="font-semibold text-gray-800 group-hover:text-blue-600 transition-colors duration-300">จองห้องประชุม A1</div>
<div class="text-sm text-gray-600">วันนี้ 14:00 - 15:00 น.</div>
<div class="text-xs text-blue-600 font-medium">การประชุมทีมพัฒนา</div>
</div>
<div class="text-right">
<div class="text-xs text-gray-500">2 ชั่วโมงที่แล้ว</div>
<div class="w-2 h-2 bg-blue-500 rounded-full mt-1"></div>
</div>
</div>
<div class="group flex items-center space-x-4 p-4 bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl border border-green-100 hover:shadow-lg transition-all duration-300 cursor-pointer">
<div class="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center text-white shadow-lg group-hover:scale-110 transition-transform duration-300">
<i class="ri-user-add-line text-lg"></i>
</div>
<div class="flex-1">
<div class="font-semibold text-gray-800 group-hover:text-green-600 transition-colors duration-300">เพิ่มผู้ใช้ใหม่</div>
<div class="text-sm text-gray-600">สมชาย ใจดี - ตำแหน่ง Developer</div>
<div class="text-xs text-green-600 font-medium">แผนก IT</div>
</div>
<div class="text-right">
<div class="text-xs text-gray-500">4 ชั่วโมงที่แล้ว</div>
<div class="w-2 h-2 bg-green-500 rounded-full mt-1"></div>
</div>
</div>
<div class="group flex items-center space-x-4 p-4 bg-gradient-to-r from-purple-50 to-violet-50 rounded-xl border border-purple-100 hover:shadow-lg transition-all duration-300 cursor-pointer">
<div class="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center text-white shadow-lg group-hover:scale-110 transition-transform duration-300">
<i class="ri-settings-line text-lg"></i>
</div>
<div class="flex-1">
<div class="font-semibold text-gray-800 group-hover:text-purple-600 transition-colors duration-300">อัปเดตสิทธิ์เมนู</div>
<div class="text-sm text-gray-600">บทบาท Manager - เพิ่มสิทธิ์การจัดการ</div>
<div class="text-xs text-purple-600 font-medium">การจัดการสิทธิ์</div>
</div>
<div class="text-right">
<div class="text-xs text-gray-500">1 วันที่แล้ว</div>
<div class="w-2 h-2 bg-purple-500 rounded-full mt-1"></div>
</div>
</div>
</div>
</div>
</div>
</div>
// Modern Home Component Styles
.container {
max-width: 1400px;
}
// Background animations
@keyframes float {
0%, 100% {
transform: translateY(0px) scale(1);
opacity: 0.9;
}
25% {
transform: translateY(-5px) scale(1.01);
opacity: 1;
}
50% {
transform: translateY(-10px) scale(1.02);
opacity: 1;
}
75% {
transform: translateY(-5px) scale(1.01);
opacity: 1;
}
}
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 0 20px rgba(99, 102, 241, 0.3);
transform: scale(1);
opacity: 0.8;
}
25% {
box-shadow: 0 0 25px rgba(99, 102, 241, 0.4);
transform: scale(1.02);
opacity: 0.9;
}
50% {
box-shadow: 0 0 30px rgba(99, 102, 241, 0.6);
transform: scale(1.05);
opacity: 1;
}
75% {
box-shadow: 0 0 25px rgba(99, 102, 241, 0.5);
transform: scale(1.03);
opacity: 0.95;
}
}
@keyframes slideInFromLeft {
0% {
transform: translateX(-100px) scale(0.9);
opacity: 0;
}
50% {
transform: translateX(-20px) scale(0.95);
opacity: 0.5;
}
100% {
transform: translateX(0) scale(1);
opacity: 1;
}
}
@keyframes slideInFromRight {
0% {
transform: translateX(100px) scale(0.9);
opacity: 0;
}
50% {
transform: translateX(20px) scale(0.95);
opacity: 0.5;
}
100% {
transform: translateX(0) scale(1);
opacity: 1;
}
}
@keyframes fadeInScale {
0% {
transform: scale(0.8) translateY(20px);
opacity: 0;
}
50% {
transform: scale(0.95) translateY(10px);
opacity: 0.7;
}
100% {
transform: scale(1) translateY(0);
opacity: 1;
}
}
// Glass morphism effects
.glass-effect {
background: rgba(255, 255, 255, 0.25);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
}
// Card animations
.group {
animation: fadeInScale 0.6s ease-out;
&:nth-child(1) { animation-delay: 0.1s; }
&:nth-child(2) { animation-delay: 0.2s; }
&:nth-child(3) { animation-delay: 0.3s; }
&:nth-child(4) { animation-delay: 0.4s; }
&:nth-child(5) { animation-delay: 0.5s; }
&:nth-child(6) { animation-delay: 0.6s; }
&:nth-child(7) { animation-delay: 0.7s; }
&:nth-child(8) { animation-delay: 0.8s; }
}
// Hover effects with enhanced animations
.group:hover {
animation: float 2s ease-in-out infinite;
}
// Gradient text effects
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
// Enhanced button styles
button {
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
&:hover::before {
left: 100%;
}
}
// Stats cards with enhanced effects
.bg-white\/80 {
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
border-radius: inherit;
pointer-events: none;
}
}
// Progress bar animations
.bg-gradient-to-r {
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transform: translateX(-100%);
animation: shimmer 2s infinite;
}
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
// Icon animations
i {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.group:hover i {
transform: scale(1.1) rotate(5deg);
}
// Badge hover effects
.inline-flex {
position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
// Activity items with enhanced interactions
.space-y-4 > * {
position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(to bottom, #3b82f6, #8b5cf6);
border-radius: 0 2px 2px 0;
transform: scaleY(0);
transition: transform 0.3s ease;
}
&:hover::before {
transform: scaleY(1);
}
&:hover {
transform: translateX(8px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
}
// Enhanced responsive design
@media (max-width: 1024px) {
.container {
padding: 1.5rem;
}
.grid {
gap: 1.5rem;
}
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.grid {
gap: 1rem;
}
.text-5xl {
font-size: 2.5rem;
}
.text-6xl {
font-size: 3rem;
}
.text-3xl {
font-size: 1.875rem;
}
.text-2xl {
font-size: 1.5rem;
}
.group {
padding: 1.5rem;
}
}
@media (max-width: 640px) {
.flex.justify-between {
flex-direction: column;
align-items: flex-start;
gap: 1.5rem;
}
.text-right {
text-align: left;
}
.grid-cols-1 {
grid-template-columns: 1fr;
}
.space-y-4 > * {
padding: 1rem;
}
}
// Loading states with skeleton
.loading {
opacity: 0.6;
pointer-events: none;
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
animation: loading 1.5s infinite;
}
}
@keyframes loading {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
// Empty states with enhanced styling
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: #6b7280;
position: relative;
&::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 200px;
background: radial-gradient(circle, rgba(99, 102, 241, 0.1) 0%, transparent 70%);
border-radius: 50%;
animation: pulse 2s infinite;
}
.text-6xl {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
animation: float 3s ease-in-out infinite;
}
}
@keyframes pulse {
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.7; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.3; }
}
// Enhanced focus states
.group:focus {
outline: 2px solid #3b82f6;
outline-offset: 4px;
border-radius: 1rem;
}
button:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
// Custom scrollbar with enhanced styling
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
border-radius: 4px;
transition: background 0.3s ease;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #2563eb, #7c3aed);
}
// Print styles
@media print {
.ti-btn, button {
display: none;
}
.group {
break-inside: avoid;
box-shadow: none;
border: 1px solid #000;
background: white !important;
}
.bg-gradient-to-r, .bg-gradient-to-br {
background: white !important;
color: black !important;
}
}
// Dark mode support (if needed)
@media (prefers-color-scheme: dark) {
.bg-white\/80 {
background: rgba(31, 41, 55, 0.8);
color: white;
}
.text-gray-800 {
color: #f9fafb;
}
.text-gray-600 {
color: #d1d5db;
}
}
// Additional Background Animations - Smooth versions
@keyframes floatUpDown {
0%, 100% {
transform: translateY(0px) rotate(0deg) scale(1);
opacity: 0.8;
}
25% {
transform: translateY(-15px) rotate(0.5deg) scale(1.02);
opacity: 0.9;
}
50% {
transform: translateY(-25px) rotate(1deg) scale(1.05);
opacity: 1;
}
75% {
transform: translateY(-10px) rotate(0.5deg) scale(1.02);
opacity: 0.9;
}
}
@keyframes drift {
0% {
transform: translateX(0px) translateY(0px) scale(1);
opacity: 0.7;
}
25% {
transform: translateX(20px) translateY(-20px) scale(1.03);
opacity: 0.8;
}
50% {
transform: translateX(35px) translateY(-35px) scale(1.05);
opacity: 0.9;
}
75% {
transform: translateX(15px) translateY(-15px) scale(1.02);
opacity: 0.8;
}
100% {
transform: translateX(0px) translateY(0px) scale(1);
opacity: 0.7;
}
}
@keyframes rotate {
0% {
transform: rotate(0deg) scale(1);
opacity: 0.6;
}
50% {
transform: rotate(180deg) scale(1.1);
opacity: 0.8;
}
100% {
transform: rotate(360deg) scale(1);
opacity: 0.6;
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
opacity: 0.5;
}
50% {
background-position: 0% 0;
opacity: 0.8;
}
100% {
background-position: 200% 0;
opacity: 0.5;
}
}
// Smooth particle animations
@keyframes particleFloat {
0%, 100% {
transform: translateY(0px) translateX(0px) scale(1);
opacity: 0.4;
}
25% {
transform: translateY(-8px) translateX(3px) scale(1.05);
opacity: 0.7;
}
50% {
transform: translateY(-12px) translateX(-2px) scale(1.1);
opacity: 0.9;
}
75% {
transform: translateY(-6px) translateX(4px) scale(1.03);
opacity: 0.6;
}
}
@keyframes particleGlow {
0%, 100% {
box-shadow: 0 0 5px currentColor;
transform: scale(1);
}
50% {
box-shadow: 0 0 20px currentColor, 0 0 30px currentColor;
transform: scale(1.1);
}
}
// Background Element Animations - Only for background elements
.background-animations .absolute {
&:nth-child(1) {
animation: floatUpDown 12s cubic-bezier(0.4, 0, 0.2, 1) infinite;
will-change: transform, opacity;
}
&:nth-child(2) {
animation: drift 16s cubic-bezier(0.4, 0, 0.2, 1) infinite;
will-change: transform, opacity;
}
&:nth-child(3) {
animation: floatUpDown 14s cubic-bezier(0.4, 0, 0.2, 1) infinite reverse;
will-change: transform, opacity;
}
}
// Enhanced floating circles - Only for background circles
.background-animations .absolute.-top-40 {
animation: floatUpDown 20s cubic-bezier(0.4, 0, 0.2, 1) infinite;
will-change: transform, opacity;
}
.background-animations .absolute.top-1\/2 {
animation: drift 24s cubic-bezier(0.4, 0, 0.2, 1) infinite;
will-change: transform, opacity;
}
.background-animations .absolute.bottom-0 {
animation: floatUpDown 22s cubic-bezier(0.4, 0, 0.2, 1) infinite reverse;
will-change: transform, opacity;
}
// Geometric pattern animations - Only for background borders
.background-animations .border {
animation: rotate 40s cubic-bezier(0.4, 0, 0.2, 1) infinite;
will-change: transform, opacity;
&:nth-child(2) {
animation: rotate 35s cubic-bezier(0.4, 0, 0.2, 1) infinite reverse;
will-change: transform, opacity;
}
&:nth-child(3) {
animation: rotate 45s cubic-bezier(0.4, 0, 0.2, 1) infinite;
will-change: transform, opacity;
}
}
// Particle animations
@keyframes particleFloat {
0%, 100% {
transform: translateY(0px) translateX(0px) scale(1);
opacity: 0.4;
}
25% {
transform: translateY(-10px) translateX(5px) scale(1.1);
opacity: 0.8;
}
50% {
transform: translateY(-5px) translateX(-5px) scale(0.9);
opacity: 0.6;
}
75% {
transform: translateY(-15px) translateX(3px) scale(1.05);
opacity: 0.7;
}
}
@keyframes particleGlow {
0%, 100% {
box-shadow: 0 0 5px currentColor;
}
50% {
box-shadow: 0 0 20px currentColor, 0 0 30px currentColor;
}
}
// Apply particle animations - Smooth versions
.absolute.inset-0 .absolute {
animation: particleFloat 8s cubic-bezier(0.4, 0, 0.2, 1) infinite;
will-change: transform, opacity;
&:nth-child(1) {
animation: particleFloat 10s cubic-bezier(0.4, 0, 0.2, 1) infinite;
will-change: transform, opacity;
}
&:nth-child(2) {
animation: particleFloat 9s cubic-bezier(0.4, 0, 0.2, 1) infinite reverse;
will-change: transform, opacity;
}
&:nth-child(3) {
animation: particleFloat 11s cubic-bezier(0.4, 0, 0.2, 1) infinite;
will-change: transform, opacity;
}
&:nth-child(4) {
animation: particleFloat 8.5s cubic-bezier(0.4, 0, 0.2, 1) infinite reverse;
will-change: transform, opacity;
}
&:nth-child(5) {
animation: particleFloat 10.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
will-change: transform, opacity;
}
&:hover {
animation: particleGlow 3s cubic-bezier(0.4, 0, 0.2, 1) infinite;
will-change: transform, opacity, box-shadow;
}
}
// Enhanced glassmorphism
.backdrop-blur-sm {
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
}
// Smooth transitions for all background elements
.absolute {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform, opacity;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
transform-style: preserve-3d;
-webkit-transform-style: preserve-3d;
}
// Global smooth animation settings
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
// Hardware acceleration for smooth animations
.background-animations,
.background-animations .absolute,
.group,
.absolute.inset-0 .absolute {
transform: translateZ(0);
-webkit-transform: translateZ(0);
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
// App card animations - Smooth versions
.group {
animation: fadeInScale 0.8s cubic-bezier(0.4, 0, 0.2, 1);
transform: none;
will-change: transform, opacity;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
animation: float 3s cubic-bezier(0.4, 0, 0.2, 1) infinite;
transform: translateY(-8px) scale(1.02);
will-change: transform, opacity;
}
}
// Animation delay classes
.floating-circle-2 {
animation-delay: 2s;
}
.floating-circle-3 {
animation-delay: 4s;
}
.particle-1 {
animation-delay: 0s;
}
.particle-2 {
animation-delay: 1s;
}
.particle-3 {
animation-delay: 2s;
}
.particle-4 {
animation-delay: 3s;
}
.particle-5 {
animation-delay: 4s;
}
// Grid pattern
.grid-pattern {
background-image: radial-gradient(circle at 1px 1px, rgba(99, 102, 241, 0.15) 1px, transparent 0);
background-size: 40px 40px;
}
// Wave gradient
.wave-gradient {
background: linear-gradient(to top, rgba(99, 102, 241, 0.1), transparent);
background-image: linear-gradient(to top, rgba(99, 102, 241, 0.1), transparent);
}
// Additional gradient classes for better compatibility
.main-bg-gradient {
background: linear-gradient(135deg, #e0e7ff 0%, #ffffff 50%, #ecfeff 100%);
}
.floating-circle-1 {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(147, 51, 234, 0.2) 100%);
}
.floating-circle-2 {
background: linear-gradient(135deg, rgba(244, 114, 182, 0.2) 0%, rgba(249, 115, 22, 0.2) 100%);
}
.floating-circle-3 {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2) 0%, rgba(20, 184, 166, 0.2) 100%);
}
.glassmorphism-overlay {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
}
// Glassmorphism card styles
.glass-card {
background: rgba(255, 255, 255, 0.8);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
transform: translateY(-8px);
}
}
// Glassmorphism status indicator
.glass-status {
background: rgba(255, 255, 255, 0.8);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
border-radius: 9999px;
padding: 0.5rem 1rem;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
border: 1px solid rgba(255, 255, 255, 0.18);
}
// Tailwind compatibility classes
.bg-white\/80 {
background-color: rgba(255, 255, 255, 0.8);
}
.backdrop-blur-sm {
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
}
.rounded-2xl {
border-radius: 1rem;
}
.p-6 {
padding: 1.5rem;
}
.shadow-xl {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.border-white\/20 {
border-color: rgba(255, 255, 255, 0.2);
}
.hover\:shadow-2xl:hover {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
}
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.duration-300 {
transition-duration: 300ms;
}
.transform {
transform: translateX(var(--tw-translate-x, 0)) translateY(var(--tw-translate-y, 0)) rotate(var(--tw-rotate, 0)) skewX(var(--tw-skew-x, 0)) skewY(var(--tw-skew-y, 0)) scaleX(var(--tw-scale-x, 1)) scaleY(var(--tw-scale-y, 1));
}
.hover\:-translate-y-2:hover {
--tw-translate-y: -0.5rem;
transform: translateX(var(--tw-translate-x, 0)) translateY(var(--tw-translate-y, 0)) rotate(var(--tw-rotate, 0)) skewX(var(--tw-skew-x, 0)) skewY(var(--tw-skew-y, 0)) scaleX(var(--tw-scale-x, 1)) scaleY(var(--tw-scale-y, 1));
}
// Additional Tailwind compatibility classes
.w-16 {
width: 4rem;
}
.h-16 {
height: 4rem;
}
.bg-gradient-to-br {
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
}
.from-blue-500 {
--tw-gradient-from: #3b82f6;
--tw-gradient-to: rgba(59, 130, 246, 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-blue-600 {
--tw-gradient-to: #2563eb;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.text-white {
color: #ffffff;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.shadow-lg {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.group:hover .group-hover\:scale-110 {
--tw-scale-x: 1.1;
--tw-scale-y: 1.1;
transform: translateX(var(--tw-translate-x, 0)) translateY(var(--tw-translate-y, 0)) rotate(var(--tw-rotate, 0)) skewX(var(--tw-skew-x, 0)) skewY(var(--tw-skew-y, 0)) scaleX(var(--tw-scale-x, 1)) scaleY(var(--tw-scale-y, 1));
}
.transition-transform {
transition-property: transform;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
// Gradient color stops
:root {
--tw-gradient-from: #3b82f6;
--tw-gradient-to: rgba(59, 130, 246, 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
// Additional gradient colors
.from-green-500 {
--tw-gradient-from: #10b981;
--tw-gradient-to: rgba(16, 185, 129, 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-green-600 {
--tw-gradient-to: #059669;
}
.from-purple-500 {
--tw-gradient-from: #8b5cf6;
--tw-gradient-to: rgba(139, 92, 246, 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-purple-600 {
--tw-gradient-to: #7c3aed;
}
.from-orange-500 {
--tw-gradient-from: #f97316;
--tw-gradient-to: rgba(249, 115, 22, 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-orange-600 {
--tw-gradient-to: #ea580c;
}
.from-indigo-500 {
--tw-gradient-from: #6366f1;
--tw-gradient-to: rgba(99, 102, 241, 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-indigo-600 {
--tw-gradient-to: #4f46e5;
}
// Additional size classes
.w-12 {
width: 3rem;
}
.h-12 {
height: 3rem;
}
.w-20 {
width: 5rem;
}
.h-20 {
height: 5rem;
}
.w-24 {
width: 6rem;
}
.h-24 {
height: 6rem;
}
.w-32 {
width: 8rem;
}
.h-32 {
height: 8rem;
}
.w-40 {
width: 10rem;
}
.h-40 {
height: 10rem;
}
.w-64 {
width: 16rem;
}
.h-64 {
height: 16rem;
}
.w-80 {
width: 20rem;
}
.h-80 {
height: 20rem;
}
.w-96 {
width: 24rem;
}
.h-96 {
height: 24rem;
}
// Company Logo Color Scheme
.company-logo-gradient {
background: linear-gradient(135deg, #1e40af 0%, #3b82f6 25%, #ffffff 50%, #dc2626 75%, #ef4444 100%);
}
.company-blue-circle {
background: linear-gradient(135deg, rgba(30, 64, 175, 0.3) 0%, rgba(59, 130, 246, 0.2) 100%);
}
.company-red-circle {
background: linear-gradient(135deg, rgba(220, 38, 38, 0.3) 0%, rgba(239, 68, 68, 0.2) 100%);
}
.company-blue-light {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(30, 64, 175, 0.1) 100%);
}
.company-grid-pattern {
background-image: radial-gradient(circle at 1px 1px, rgba(30, 64, 175, 0.2) 1px, transparent 0);
background-size: 40px 40px;
}
.company-wave-gradient {
background: linear-gradient(to top, rgba(30, 64, 175, 0.15), transparent);
background-image: linear-gradient(to top, rgba(30, 64, 175, 0.15), transparent);
}
// Company brand colors
.company-blue {
color: #1e40af;
}
.company-red {
color: #dc2626;
}
.company-blue-bg {
background-color: #1e40af;
}
.company-red-bg {
background-color: #dc2626;
}
.company-blue-light-bg {
background-color: #3b82f6;
}
.company-red-light-bg {
background-color: #ef4444;
}
// Company title gradient
.company-title-gradient {
background: linear-gradient(135deg, #1e40af 0%, #3b82f6 25%, #dc2626 75%, #ef4444 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
// Additional Tailwind compatibility classes for button
.mt-4 {
margin-top: 1rem;
}
.w-full {
width: 100%;
}
.bg-gradient-to-r {
background-image: linear-gradient(to right, var(--tw-gradient-stops));
}
.from-red-500 {
--tw-gradient-from: #ef4444;
--tw-gradient-to: rgba(239, 68, 68, 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-pink-600 {
--tw-gradient-to: #db2777;
}
.hover\:from-red-600:hover {
--tw-gradient-from: #dc2626;
--tw-gradient-to: rgba(220, 38, 38, 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.hover\:to-pink-700:hover {
--tw-gradient-to: #be185d;
}
.text-white {
color: #ffffff;
}
.font-semibold {
font-weight: 600;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.rounded-xl {
border-radius: 0.75rem;
}
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.duration-300 {
transition-duration: 300ms;
}
.transform {
transform: translateX(var(--tw-translate-x, 0)) translateY(var(--tw-translate-y, 0)) rotate(var(--tw-rotate, 0)) skewX(var(--tw-skew-x, 0)) skewY(var(--tw-skew-y, 0)) scaleX(var(--tw-scale-x, 1)) scaleY(var(--tw-scale-y, 1));
}
.hover\:scale-105:hover {
--tw-scale-x: 1.05;
--tw-scale-y: 1.05;
transform: translateX(var(--tw-translate-x, 0)) translateY(var(--tw-translate-y, 0)) rotate(var(--tw-rotate, 0)) skewX(var(--tw-skew-x, 0)) skewY(var(--tw-skew-y, 0)) scaleX(var(--tw-scale-x, 1)) scaleY(var(--tw-scale-y, 1));
}
.shadow-lg {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.hover\:shadow-xl:hover {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
margin-left: 0.5rem;
}
// Additional gradient colors for red and pink
.from-red-600 {
--tw-gradient-from: #dc2626;
--tw-gradient-to: rgba(220, 38, 38, 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-pink-700 {
--tw-gradient-to: #be185d;
}
// Update root variables for red-pink gradient
:root {
--tw-gradient-from: #ef4444;
--tw-gradient-to: rgba(239, 68, 68, 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
// Additional text and spacing classes
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.font-medium {
font-weight: 500;
}
.font-bold {
font-weight: 700;
}
// Additional margin and padding classes
.mb-1 {
margin-bottom: 0.25rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mb-8 {
margin-bottom: 2rem;
}
.mb-12 {
margin-bottom: 3rem;
}
.mr-1 {
margin-right: 0.25rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.mr-3 {
margin-right: 0.75rem;
}
.ml-1 {
margin-left: 0.25rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.ml-3 {
margin-left: 0.75rem;
}
// Additional flexbox classes
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
margin-left: 0.25rem;
}
.space-x-3 > :not([hidden]) ~ :not([hidden]) {
margin-left: 0.75rem;
}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
margin-left: 1rem;
}
// Additional border radius classes
.rounded-full {
border-radius: 9999px;
}
.rounded-lg {
border-radius: 0.5rem;
}
.rounded-md {
border-radius: 0.375rem;
}
.rounded-sm {
border-radius: 0.25rem;
}
// Company brand text colors
.text-company-blue {
color: #1e40af;
}
.text-company-blue-light {
color: #3b82f6;
}
.text-company-blue-dark {
color: #1e3a8a;
}
// Override gray-800 with company blue
.text-gray-800 {
color: #1e40af;
}
// Additional company color variations
.text-company-red {
color: #dc2626;
}
.text-company-red-light {
color: #ef4444;
}
// Additional gray color overrides with company colors
.text-gray-600 {
color: #3b82f6;
}
.text-gray-700 {
color: #1e40af;
}
.text-gray-900 {
color: #1e3a8a;
}
// Background color overrides
.bg-gray-50 {
background-color: #eff6ff;
}
.bg-gray-100 {
background-color: #dbeafe;
}
.bg-gray-200 {
background-color: #bfdbfe;
}
// Border color overrides
.border-gray-200 {
border-color: #bfdbfe;
}
.border-gray-300 {
border-color: #93c5fd;
}
// Fix for Remix Icons
.ri-module-line {
font-family: "remixicon" !important;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: inline-block;
vertical-align: middle;
}
.ri-module-line::before {
content: "\f1c0";
}
// Ensure all remix icons work properly
[class^="ri-"],
[class*=" ri-"] {
font-family: "remixicon" !important;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: inline-block;
vertical-align: middle;
line-height: 1;
}
// Fallback for missing icons
.ri-module-line:not([class*="ri-"])::before {
content: "📦";
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
font-size: 1.2em;
}
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Router, RouterLink, RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { Observable, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { TokenService } from '../../shared/services/token.service';
import { MenuPermissionService } from '../services/menu-permission.service';
import { UserRoleService } from '../services/user-role.service';
interface AppModule {
id: string;
name: string;
displayName: string;
description: string;
icon: string;
path: string;
isVisible: boolean;
permissions: {
view: boolean;
create: boolean;
edit: boolean;
delete: boolean;
export: boolean;
import: boolean;
};
}
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
standalone: true,
imports: [RouterModule]
imports: [RouterModule, CommonModule]
})
export class HomeComponent {
export class HomeComponent implements OnInit {
accessibleApps$: Observable<AppModule[]>;
userInfo$: Observable<any>;
constructor(
private router: Router,
private tokenService: TokenService,
private menuPermissionService: MenuPermissionService,
private userRoleService: UserRoleService
) { }
ngOnInit(): void {
this.loadAccessibleApps();
this.loadUserInfo();
}
private loadAccessibleApps(): void {
this.accessibleApps$ = combineLatest([
this.menuPermissionService.getAccessibleMenus(),
this.menuPermissionService.menuPermissions$
]).pipe(
map(([accessibleMenus, allMenus]) => {
return this.getAppModules().filter(app => {
const menu = this.findMenuByPath(allMenus, app.path);
return menu && menu.isVisible && menu.permissions.view;
});
})
);
}
private loadUserInfo(): void {
// Load current user info
this.userInfo$ = this.userRoleService.getUserById('current-user').pipe(
map(user => user || {
fullName: 'ผู้ใช้ปัจจุบัน',
email: 'user@company.com',
department: 'IT',
position: 'System User'
})
);
}
private getAppModules(): AppModule[] {
return [
{
id: 'myhr-plus',
name: 'myhr-plus',
displayName: 'myHR-Plus',
description: 'ระบบจัดการทรัพยากรบุคคลขั้นสูง',
icon: './assets/images/logoallHR/myhr-plus.jpg',
path: '/myhr-plus',
isVisible: true,
permissions: { view: true, create: false, edit: false, delete: false, export: false, import: false }
},
{
id: 'myhr-lite',
name: 'myhr-lite',
displayName: 'myHR-Lite',
description: 'ระบบจัดการทรัพยากรบุคคลพื้นฐาน',
icon: './assets/images/logoallHR/myHR-Lite-logo-new.png',
path: '/myhr-lite',
isVisible: true,
permissions: { view: true, create: false, edit: false, delete: false, export: false, import: false }
},
{
id: 'zeeme',
name: 'zeeme',
displayName: 'Zeeme Plus',
description: 'ระบบจัดการเวลาและลงเวลา',
icon: './assets/images/logoallHR/zeemePlus.png',
path: '/zeeme',
isVisible: true,
permissions: { view: true, create: false, edit: false, delete: false, export: false, import: false }
},
{
id: 'myface',
name: 'myface',
displayName: 'myFace',
description: 'ระบบจัดการใบหน้าและความปลอดภัย',
icon: './assets/images/logoallHR/logo_myface.png',
path: '/myface',
isVisible: true,
permissions: { view: true, create: false, edit: false, delete: false, export: false, import: false }
},
{
id: 'mylearn',
name: 'mylearn',
displayName: 'myLearn',
description: 'ระบบจัดการการเรียนรู้และฝึกอบรม',
icon: './assets/images/logoallHR/mylearn-logo.png',
path: '/mylearn',
isVisible: true,
permissions: { view: true, create: false, edit: false, delete: false, export: false, import: false }
},
{
id: 'myjob',
name: 'myjob',
displayName: 'myJob',
description: 'ระบบจัดการงานและโครงการ',
icon: './assets/images/logoallHR/logo_myjob.png',
path: '/myjob',
isVisible: true,
permissions: { view: true, create: false, edit: false, delete: false, export: false, import: false }
},
{
id: 'myskill-x',
name: 'myskill-x',
displayName: 'mySkill-X',
description: 'ระบบจัดการทักษะและความสามารถ',
icon: './assets/images/logoallHR/mySkill-x.png',
path: '/myskill-x',
isVisible: true,
permissions: { view: true, create: false, edit: false, delete: false, export: false, import: false }
},
{
id: 'permission-management',
name: 'permission-management',
displayName: 'Permission Management',
description: 'ระบบจัดการสิทธิ์และบทบาท',
icon: './assets/images/icons/widget.png',
path: '/portal-manage/permission-management',
isVisible: true,
permissions: { view: true, create: false, edit: false, delete: false, export: false, import: false }
},
{
id: 'menu-permission-management',
name: 'menu-permission-management',
displayName: 'Menu Permission',
description: 'ระบบจัดการสิทธิ์เมนู',
icon: './assets/images/icons/menu.png',
path: '/portal-manage/menu-permission-management',
isVisible: true,
permissions: { view: true, create: false, edit: false, delete: false, export: false, import: false }
},
{
id: 'user-role-management',
name: 'user-role-management',
displayName: 'User & Role Management',
description: 'ระบบจัดการผู้ใช้และบทบาท',
icon: './assets/images/icons/users.png',
path: '/portal-manage/user-role-management',
isVisible: true,
permissions: { view: true, create: false, edit: false, delete: false, export: false, import: false }
},
{
id: 'meeting-booking',
name: 'meeting-booking',
displayName: 'Meeting Booking',
description: 'ระบบจองห้องประชุม',
icon: './assets/images/icons/calendar.png',
path: '/portal-manage/meeting-booking',
isVisible: true,
permissions: { view: true, create: false, edit: false, delete: false, export: false, import: false }
},
{
id: 'company-management',
name: 'company-management',
displayName: 'Company Management',
description: 'ระบบจัดการบริษัท',
icon: './assets/images/icons/building.png',
path: '/portal-manage/company-management',
isVisible: true,
permissions: { view: true, create: false, edit: false, delete: false, export: false, import: false }
},
{
id: 'dashboard-management',
name: 'dashboard-management',
displayName: 'Dashboard Management',
description: 'ระบบจัดการแดชบอร์ด',
icon: './assets/images/icons/dashboard.png',
path: '/portal-manage/dashboard-management',
isVisible: true,
permissions: { view: true, create: false, edit: false, delete: false, export: false, import: false }
}
];
}
private findMenuByPath(menus: any[], path: string): any {
for (const menu of menus) {
if (menu.path === path) {
return menu;
}
if (menu.children) {
const found = this.findMenuByPath(menu.children, path);
if (found) return found;
}
}
return null;
}
logout() {
// localStorage.removeItem('authToken'); // Clear the authentication token
// this.router.navigate(['/auth/login']); // Navigate to the login page
this.tokenService.signOut();
}
checkAppToken(appmodule:string){
this.tokenService.saveAppToken(appmodule)
this.router.navigate(['/'+appmodule])
checkAppToken(appmodule: string) {
this.tokenService.saveAppToken(appmodule);
this.router.navigate(['/' + appmodule]);
}
navigateToApp(app: AppModule) {
if (app.path.startsWith('/portal-manage')) {
this.router.navigate([app.path]);
} else {
this.checkAppToken(app.id);
}
}
getAppCardClass(app: AppModule): string {
const baseClass = 'card bg-white rounded-xl p-8 text-center shadow-lg transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl';
if (app.id.includes('management') || app.id.includes('permission')) {
return `${baseClass} hover:bg-gradient-to-br from-gray-50 to-gray-100`;
} else {
return `${baseClass} hover:bg-gradient-to-br from-blue-50 to-blue-100`;
}
}
}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">ระบบจองห้องประชุม</h4>
<p class="card-subtitle">จองห้องประชุมและจัดการการประชุมของคุณ</p>
</div>
<div class="card-body">
<mat-tab-group>
<!-- Booking Tab -->
<mat-tab label="จองห้องประชุม">
<div class="row mt-3">
<div class="col-md-8">
<mat-card>
<mat-card-header>
<mat-card-title>ข้อมูลการจอง</mat-card-title>
</mat-card-header>
<mat-card-content>
<form [formGroup]="bookingForm" (ngSubmit)="createBooking()">
<div class="row">
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>ห้องประชุม</mat-label>
<mat-select formControlName="roomId" (selectionChange)="onRoomChange()">
<mat-option *ngFor="let room of rooms$ | async" [value]="room.id">
{{ room.name }} ({{ room.capacity }} ที่นั่ง) - {{ room.location }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>วันที่</mat-label>
<input matInput [matDatepicker]="picker"
[(ngModel)]="selectedDate"
(dateChange)="onDateChange()"
formControlName="startDateTime">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>หัวข้อการประชุม</mat-label>
<input matInput formControlName="title" placeholder="ระบุหัวข้อการประชุม">
</mat-form-field>
</div>
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>รายละเอียด</mat-label>
<input matInput formControlName="description" placeholder="รายละเอียดเพิ่มเติม (ไม่บังคับ)">
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>เวลาเริ่มต้น</mat-label>
<input matInput formControlName="startDateTime" type="datetime-local">
</mat-form-field>
</div>
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>เวลาสิ้นสุด</mat-label>
<input matInput formControlName="endDateTime" type="datetime-local">
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-12">
<mat-form-field appearance="outline" class="w-100">
<mat-label>ผู้เข้าร่วมประชุม</mat-label>
<mat-chip-grid #chipGrid>
<mat-chip-row *ngFor="let attendee of attendees" (removed)="removeAttendee(attendee)">
{{ attendee }}
<button matChipRemove>
<mat-icon>cancel</mat-icon>
</button>
</mat-chip-row>
<input placeholder="เพิ่มผู้เข้าร่วมประชุม"
[matChipInputFor]="chipGrid"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addAttendee($event)">
</mat-chip-grid>
</mat-form-field>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<button mat-raised-button color="primary" type="submit"
[disabled]="!bookingForm.valid">
<mat-icon>event</mat-icon>
จองห้องประชุม
</button>
<button mat-button type="button" (click)="bookingForm.reset()">
<mat-icon>refresh</mat-icon>
รีเซ็ต
</button>
</div>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
<div class="col-md-4">
<mat-card>
<mat-card-header>
<mat-card-title>ช่วงเวลาที่ว่าง</mat-card-title>
<mat-card-subtitle *ngIf="selectedRoom">ห้อง: {{ (rooms$ | async)?.find(r => r.id === selectedRoom)?.name }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div *ngIf="isLoading" class="text-center">
<mat-spinner diameter="30"></mat-spinner>
<p>กำลังโหลด...</p>
</div>
<div *ngIf="!isLoading && timeSlots.length > 0" class="time-slots">
<div *ngFor="let slot of timeSlots"
class="time-slot"
[class.available]="slot.isAvailable"
[class.unavailable]="!slot.isAvailable"
(click)="selectTimeSlot(slot)">
<span class="time">{{ slot.startTime }} - {{ slot.endTime }}</span>
<mat-icon *ngIf="slot.isAvailable">check_circle</mat-icon>
<mat-icon *ngIf="!slot.isAvailable">cancel</mat-icon>
</div>
</div>
<div *ngIf="!isLoading && timeSlots.length === 0" class="text-center text-muted">
<mat-icon>event_busy</mat-icon>
<p>ไม่มีช่วงเวลาที่ว่างในวันนี้</p>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</mat-tab>
<!-- My Bookings Tab -->
<mat-tab label="การจองของฉัน">
<div class="row mt-3">
<div class="col-12">
<mat-card>
<mat-card-header>
<mat-card-title>รายการการจองของฉัน</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="table-responsive">
<table mat-table [dataSource]="bookings$ | async" class="w-100">
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef>หัวข้อ</th>
<td mat-cell *matCellDef="let booking">{{ booking.title }}</td>
</ng-container>
<ng-container matColumnDef="room">
<th mat-header-cell *matHeaderCellDef>ห้องประชุม</th>
<td mat-cell *matCellDef="let booking">{{ booking.roomName }}</td>
</ng-container>
<ng-container matColumnDef="startTime">
<th mat-header-cell *matHeaderCellDef>เวลาเริ่มต้น</th>
<td mat-cell *matCellDef="let booking">
{{ booking.startDateTime | date:'dd/MM/yyyy HH:mm' }}
</td>
</ng-container>
<ng-container matColumnDef="endTime">
<th mat-header-cell *matHeaderCellDef>เวลาสิ้นสุด</th>
<td mat-cell *matCellDef="let booking">
{{ booking.endDateTime | date:'dd/MM/yyyy HH:mm' }}
</td>
</ng-container>
<ng-container matColumnDef="organizer">
<th mat-header-cell *matHeaderCellDef>ผู้จัด</th>
<td mat-cell *matCellDef="let booking">{{ booking.organizerName }}</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef>สถานะ</th>
<td mat-cell *matCellDef="let booking">
<span class="badge badge-{{ getStatusColor(booking.status) }}">
{{ getStatusText(booking.status) }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>การดำเนินการ</th>
<td mat-cell *matCellDef="let booking">
<button mat-icon-button
*ngIf="booking.status === 'pending' || booking.status === 'confirmed'"
(click)="cancelBooking(booking.id)"
matTooltip="ยกเลิกการจอง">
<mat-icon>cancel</mat-icon>
</button>
<button mat-icon-button matTooltip="ดูรายละเอียด">
<mat-icon>visibility</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</mat-tab>
<!-- Statistics Tab -->
<mat-tab label="สถิติ">
<div class="row mt-3">
<div class="col-12">
<mat-card>
<mat-card-header>
<mat-card-title>สถิติการจองห้องประชุม</mat-card-title>
<mat-card-subtitle>ข้อมูลสถิติการใช้งานห้องประชุม</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div *ngIf="statistics$ | async as stats" class="row">
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">{{ stats.totalBookings }}</div>
<div class="stat-label">การจองทั้งหมด</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">{{ stats.confirmedBookings }}</div>
<div class="stat-label">ยืนยันแล้ว</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">{{ stats.pendingBookings }}</div>
<div class="stat-label">รอดำเนินการ</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">{{ stats.averageBookingDuration | number:'1.0-0' }} นาที</div>
<div class="stat-label">ระยะเวลาเฉลี่ย</div>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
</div>
</div>
</div>
</div>
.time-slots {
max-height: 400px;
overflow-y: auto;
.time-slot {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
margin-bottom: 4px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
&.available {
background-color: #e8f5e8;
border: 1px solid #4caf50;
color: #2e7d32;
&:hover {
background-color: #c8e6c9;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
}
&.unavailable {
background-color: #ffebee;
border: 1px solid #f44336;
color: #c62828;
cursor: not-allowed;
opacity: 0.6;
}
.time {
font-weight: 500;
}
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
}
.stat-card {
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px;
margin-bottom: 16px;
.stat-number {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 8px;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.9;
}
}
.badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
&.badge-success {
background-color: #4caf50;
color: white;
}
&.badge-warning {
background-color: #ff9800;
color: white;
}
&.badge-danger {
background-color: #f44336;
color: white;
}
&.badge-info {
background-color: #2196f3;
color: white;
}
&.badge-secondary {
background-color: #6c757d;
color: white;
}
}
.table-responsive {
overflow-x: auto;
}
mat-card {
margin-bottom: 16px;
}
mat-card-header {
margin-bottom: 16px;
}
mat-form-field {
margin-bottom: 16px;
}
// Time slot grid
.time-slots {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 8px;
}
// Responsive design
@media (max-width: 768px) {
.time-slots {
grid-template-columns: 1fr;
}
.stat-card {
margin-bottom: 12px;
.stat-number {
font-size: 2rem;
}
}
}
// Form styling
form {
.row {
margin-bottom: 16px;
}
}
// Button styling
button[mat-raised-button] {
margin-right: 8px;
}
// Loading spinner
mat-spinner {
margin: 0 auto;
}
// Empty state
.text-center {
text-align: center;
}
.text-muted {
color: #6c757d;
}
// Card hover effects
mat-card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transition: box-shadow 0.2s ease;
}
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from '@angular/material/core';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatChipsModule } from '@angular/material/chips';
import { MatChipInputEvent } from '@angular/material/chips';
import { MatDialogModule, MatDialog } from '@angular/material/dialog';
import { MatTabsModule } from '@angular/material/tabs';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { MeetingBookingService } from '../services/meeting-booking.service';
import { MeetingRoom, MeetingBooking, BookingTimeSlot, BookingStatistics } from '../models/meeting-booking.model';
@Component({
selector: 'app-meeting-booking',
standalone: true,
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatDatepickerModule,
MatNativeDateModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
MatChipsModule,
MatDialogModule,
MatTabsModule,
MatSnackBarModule,
MatProgressSpinnerModule,
TranslateModule
],
templateUrl: './meeting-booking.component.html',
styleUrls: ['./meeting-booking.component.scss']
})
export class MeetingBookingComponent implements OnInit {
rooms$: Observable<MeetingRoom[]>;
bookings$: Observable<MeetingBooking[]>;
statistics$: Observable<BookingStatistics>;
selectedDate = new Date();
selectedRoom: string = '';
timeSlots: BookingTimeSlot[] = [];
isLoading = false;
bookingForm: FormGroup;
attendees: string[] = [];
displayedColumns: string[] = ['title', 'room', 'startTime', 'endTime', 'organizer', 'status', 'actions'];
constructor(
private meetingBookingService: MeetingBookingService,
private fb: FormBuilder,
private dialog: MatDialog,
private snackBar: MatSnackBar
) {
this.rooms$ = this.meetingBookingService.rooms$;
this.bookings$ = this.meetingBookingService.bookings$;
this.bookingForm = this.fb.group({
roomId: ['', Validators.required],
title: ['', Validators.required],
description: [''],
startDateTime: ['', Validators.required],
endDateTime: ['', Validators.required],
attendees: [[]]
});
this.statistics$ = this.meetingBookingService.getBookingStatistics(
new Date(new Date().setMonth(new Date().getMonth() - 1)),
new Date()
);
}
ngOnInit(): void {
this.loadTimeSlots();
}
onDateChange(): void {
this.loadTimeSlots();
}
onRoomChange(): void {
this.loadTimeSlots();
}
loadTimeSlots(): void {
if (this.selectedRoom && this.selectedDate) {
this.isLoading = true;
this.meetingBookingService.getAvailableTimeSlots(this.selectedRoom, this.selectedDate)
.subscribe({
next: (slots) => {
this.timeSlots = slots;
this.isLoading = false;
},
error: (error) => {
console.error('Error loading time slots:', error);
this.isLoading = false;
}
});
}
}
selectTimeSlot(slot: BookingTimeSlot): void {
if (slot.isAvailable) {
const startDateTime = new Date(this.selectedDate);
const [hours, minutes] = slot.startTime.split(':').map(Number);
startDateTime.setHours(hours, minutes, 0, 0);
const endDateTime = new Date(this.selectedDate);
const [endHours, endMinutes] = slot.endTime.split(':').map(Number);
endDateTime.setHours(endHours, endMinutes, 0, 0);
this.bookingForm.patchValue({
startDateTime,
endDateTime
});
}
}
addAttendee(event: MatChipInputEvent): void {
const value = (event.value || '').trim();
if (value) {
this.attendees.push(value);
this.bookingForm.patchValue({ attendees: this.attendees });
}
event.chipInput!.clear();
}
removeAttendee(attendee: string): void {
const index = this.attendees.indexOf(attendee);
if (index >= 0) {
this.attendees.splice(index, 1);
this.bookingForm.patchValue({ attendees: this.attendees });
}
}
createBooking(): void {
if (this.bookingForm.valid) {
const formValue = this.bookingForm.value;
const booking = {
roomId: formValue.roomId,
roomName: '', // Will be filled by service
title: formValue.title,
description: formValue.description,
startDateTime: formValue.startDateTime,
endDateTime: formValue.endDateTime,
organizerId: 'current-user', // From auth service
organizerName: 'Current User', // From auth service
attendees: this.attendees.map(email => ({
userId: '',
userName: email,
email: email,
status: 'pending' as const
})),
status: 'pending' as const
};
this.meetingBookingService.createBooking(booking).subscribe({
next: () => {
this.snackBar.open('จองห้องประชุมสำเร็จ', 'ปิด', { duration: 3000 });
this.bookingForm.reset();
this.attendees = [];
this.loadTimeSlots();
},
error: (error) => {
this.snackBar.open('เกิดข้อผิดพลาดในการจอง: ' + error.message, 'ปิด', { duration: 5000 });
}
});
}
}
cancelBooking(bookingId: string): void {
this.meetingBookingService.cancelBooking(bookingId, 'Cancelled by user').subscribe({
next: () => {
this.snackBar.open('ยกเลิกการจองสำเร็จ', 'ปิด', { duration: 3000 });
this.loadTimeSlots();
},
error: (error) => {
this.snackBar.open('เกิดข้อผิดพลาดในการยกเลิก: ' + error.message, 'ปิด', { duration: 5000 });
}
});
}
getStatusColor(status: string): string {
switch (status) {
case 'confirmed': return 'success';
case 'pending': return 'warning';
case 'cancelled': return 'danger';
case 'completed': return 'info';
default: return 'secondary';
}
}
getStatusText(status: string): string {
switch (status) {
case 'confirmed': return 'ยืนยันแล้ว';
case 'pending': return 'รอดำเนินการ';
case 'cancelled': return 'ยกเลิกแล้ว';
case 'completed': return 'เสร็จสิ้น';
default: return 'ไม่ทราบสถานะ';
}
}
}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">จัดการสิทธิ์เข้าถึงเมนู</h4>
<p class="card-subtitle">กำหนดสิทธิ์การเข้าถึงเมนูต่างๆ สำหรับบทบาทและผู้ใช้</p>
</div>
<div class="card-body">
<mat-tab-group>
<!-- Role Permissions Tab -->
<mat-tab label="สิทธิ์ตามบทบาท">
<div class="row mt-3">
<div class="col-md-4">
<mat-form-field appearance="outline" class="w-100">
<mat-label>เลือกบทบาท</mat-label>
<mat-select [(ngModel)]="selectedRoleId">
<mat-option *ngFor="let role of roles" [value]="role.id">
{{ role.name }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-md-8">
<button mat-raised-button color="primary"
(click)="saveRolePermissions()"
[disabled]="!selectedRoleId"
class="me-2">
<mat-icon>save</mat-icon>
บันทึกสิทธิ์บทบาท
</button>
<button mat-button (click)="resetPermissions()">
<mat-icon>refresh</mat-icon>
รีเซ็ต
</button>
</div>
</div>
<div class="row mt-3" *ngIf="selectedRoleId">
<div class="col-12">
<mat-card>
<mat-card-header>
<mat-card-title>สิทธิ์การเข้าถึงเมนู</mat-card-title>
<mat-card-subtitle>กำหนดสิทธิ์สำหรับบทบาท: {{ roles.find(r => r.id === selectedRoleId)?.name }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" class="permission-tree">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
<div class="permission-node">
<div class="permission-info">
<mat-icon *ngIf="node.icon" class="me-2">{{ node.icon }}</mat-icon>
<span class="permission-name">{{ node.name }}</span>
<span class="permission-path text-muted">({{ node.path }})</span>
</div>
<div class="permission-controls">
<mat-slide-toggle
[(ngModel)]="node.isVisible"
(change)="onVisibilityChange(node, $event.checked)"
class="me-3">
แสดง
</mat-slide-toggle>
<div class="permission-toggles">
<mat-checkbox
*ngFor="let permType of permissionTypes"
[(ngModel)]="node.permissions[permType.key]"
(change)="onPermissionChange(node, permType.key, $event.checked)"
class="me-2">
{{ permType.label }}
</mat-checkbox>
</div>
</div>
</div>
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
<button mat-icon-button matTreeNodeToggle>
<mat-icon class="mat-icon-rtl-mirror">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
<div class="permission-node">
<div class="permission-info">
<mat-icon *ngIf="node.icon" class="me-2">{{ node.icon }}</mat-icon>
<span class="permission-name">{{ node.name }}</span>
<span class="permission-path text-muted">({{ node.path }})</span>
</div>
<div class="permission-controls">
<mat-slide-toggle
[(ngModel)]="node.isVisible"
(change)="onVisibilityChange(node, $event.checked)"
class="me-3">
แสดง
</mat-slide-toggle>
<div class="permission-toggles">
<mat-checkbox
*ngFor="let permType of permissionTypes"
[(ngModel)]="node.permissions[permType.key]"
(change)="onPermissionChange(node, permType.key, $event.checked)"
class="me-2">
{{ permType.label }}
</mat-checkbox>
</div>
</div>
</div>
</mat-tree-node>
</mat-tree>
</mat-card-content>
</mat-card>
</div>
</div>
</mat-tab>
<!-- User Permissions Tab -->
<mat-tab label="สิทธิ์ตามผู้ใช้">
<div class="row mt-3">
<div class="col-md-4">
<mat-form-field appearance="outline" class="w-100">
<mat-label>เลือกผู้ใช้</mat-label>
<mat-select [(ngModel)]="selectedUserId">
<mat-option *ngFor="let user of users" [value]="user.id">
{{ user.name }} ({{ roles.find(r => r.id === user.roleId)?.name }})
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-md-8">
<button mat-raised-button color="primary"
(click)="saveUserPermissions()"
[disabled]="!selectedUserId"
class="me-2">
<mat-icon>save</mat-icon>
บันทึกสิทธิ์ผู้ใช้
</button>
<button mat-button (click)="resetPermissions()">
<mat-icon>refresh</mat-icon>
รีเซ็ต
</button>
</div>
</div>
<div class="row mt-3" *ngIf="selectedUserId">
<div class="col-12">
<mat-card>
<mat-card-header>
<mat-card-title>สิทธิ์การเข้าถึงเมนู</mat-card-title>
<mat-card-subtitle>กำหนดสิทธิ์สำหรับผู้ใช้: {{ users.find(u => u.id === selectedUserId)?.name }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p class="text-info">
<mat-icon>info</mat-icon>
สิทธิ์ผู้ใช้จะแทนที่สิทธิ์บทบาท หากมีการกำหนดไว้
</p>
<!-- Same tree structure as role permissions -->
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" class="permission-tree">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
<div class="permission-node">
<div class="permission-info">
<mat-icon *ngIf="node.icon" class="me-2">{{ node.icon }}</mat-icon>
<span class="permission-name">{{ node.name }}</span>
<span class="permission-path text-muted">({{ node.path }})</span>
</div>
<div class="permission-controls">
<mat-slide-toggle
[(ngModel)]="node.isVisible"
(change)="onVisibilityChange(node, $event.checked)"
class="me-3">
แสดง
</mat-slide-toggle>
<div class="permission-toggles">
<mat-checkbox
*ngFor="let permType of permissionTypes"
[(ngModel)]="node.permissions[permType.key]"
(change)="onPermissionChange(node, permType.key, $event.checked)"
class="me-2">
{{ permType.label }}
</mat-checkbox>
</div>
</div>
</div>
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
<button mat-icon-button matTreeNodeToggle>
<mat-icon class="mat-icon-rtl-mirror">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
<div class="permission-node">
<div class="permission-info">
<mat-icon *ngIf="node.icon" class="me-2">{{ node.icon }}</mat-icon>
<span class="permission-name">{{ node.name }}</span>
<span class="permission-path text-muted">({{ node.path }})</span>
</div>
<div class="permission-controls">
<mat-slide-toggle
[(ngModel)]="node.isVisible"
(change)="onVisibilityChange(node, $event.checked)"
class="me-3">
แสดง
</mat-slide-toggle>
<div class="permission-toggles">
<mat-checkbox
*ngFor="let permType of permissionTypes"
[(ngModel)]="node.permissions[permType.key]"
(change)="onPermissionChange(node, permType.key, $event.checked)"
class="me-2">
{{ permType.label }}
</mat-checkbox>
</div>
</div>
</div>
</mat-tree-node>
</mat-tree>
</mat-card-content>
</mat-card>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
</div>
</div>
</div>
</div>
.permission-tree {
.permission-node {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #e0e0e0;
&:last-child {
border-bottom: none;
}
.permission-info {
display: flex;
align-items: center;
flex: 1;
.permission-name {
font-weight: 500;
margin-right: 8px;
}
.permission-path {
font-size: 0.875rem;
color: #666;
}
}
.permission-controls {
display: flex;
align-items: center;
gap: 16px;
.permission-toggles {
display: flex;
flex-wrap: wrap;
gap: 8px;
mat-checkbox {
font-size: 0.875rem;
}
}
}
}
}
.permission-tree mat-tree-node {
padding-left: 0;
}
.permission-tree mat-tree-node[matTreeNodePadding] {
padding-left: 0;
}
.permission-tree mat-tree-node[matTreeNodePadding] > .mat-tree-node {
padding-left: 0;
}
// Responsive design
@media (max-width: 768px) {
.permission-node {
flex-direction: column;
align-items: flex-start !important;
.permission-controls {
width: 100%;
margin-top: 8px;
justify-content: space-between;
}
}
}
// Material tree styling
.mat-tree {
background: transparent;
}
.mat-tree-node {
min-height: 40px;
}
.mat-tree-node:hover {
background-color: rgba(0, 0, 0, 0.04);
}
// Card styling
mat-card {
margin-bottom: 16px;
}
mat-card-header {
margin-bottom: 16px;
}
// Tab styling
mat-tab-group {
margin-top: 16px;
}
// Form field styling
mat-form-field {
margin-bottom: 16px;
}
// Button styling
button[mat-raised-button] {
margin-right: 8px;
}
// Info text styling
.text-info {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background-color: #e3f2fd;
border-radius: 4px;
margin-bottom: 16px;
color: #1976d2;
}
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MatTreeModule, MatTreeNestedDataSource } from '@angular/material/tree';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatTabsModule } from '@angular/material/tabs';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { NestedTreeControl } from '@angular/cdk/tree';
import { MenuPermissionService } from '../services/menu-permission.service';
import { MenuHierarchy, MenuPermission } from '../models/menu-permission.model';
interface MenuNode extends MenuHierarchy {
expandable: boolean;
level: number;
}
@Component({
selector: 'app-menu-permission-management',
standalone: true,
imports: [
CommonModule,
FormsModule,
MatTreeModule,
MatIconModule,
MatButtonModule,
MatCheckboxModule,
MatSlideToggleModule,
MatCardModule,
MatTabsModule,
MatSelectModule,
MatFormFieldModule,
MatInputModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
MatDialogModule,
MatSnackBarModule,
TranslateModule
],
templateUrl: './menu-permission-management.component.html',
styleUrls: ['./menu-permission-management.component.scss']
})
export class MenuPermissionManagementComponent implements OnInit {
treeControl = new NestedTreeControl<MenuNode>(node => node.children);
dataSource = new MatTreeNestedDataSource<MenuNode>();
selectedRoleId: string = '';
selectedUserId: string = '';
roles: any[] = [];
users: any[] = [];
// Permission types
permissionTypes = [
{ key: 'view', label: 'ดู' },
{ key: 'create', label: 'สร้าง' },
{ key: 'edit', label: 'แก้ไข' },
{ key: 'delete', label: 'ลบ' },
{ key: 'export', label: 'ส่งออก' },
{ key: 'import', label: 'นำเข้า' }
];
constructor(private menuPermissionService: MenuPermissionService) {}
ngOnInit(): void {
this.loadMenuHierarchy();
this.loadRoles();
this.loadUsers();
}
hasChild = (_: number, node: MenuNode) => node.expandable;
loadMenuHierarchy(): void {
this.menuPermissionService.getMenuHierarchy().subscribe(menus => {
this.dataSource.data = this.convertToTreeNodes(menus);
});
}
loadRoles(): void {
// Mock data - in real app, load from API
this.roles = [
{ id: 'admin', name: 'ผู้ดูแลระบบ' },
{ id: 'manager', name: 'ผู้จัดการ' },
{ id: 'user', name: 'ผู้ใช้ทั่วไป' }
];
}
loadUsers(): void {
// Mock data - in real app, load from API
this.users = [
{ id: '1', name: 'สมชาย ใจดี', roleId: 'admin' },
{ id: '2', name: 'สมหญิง รักงาน', roleId: 'manager' },
{ id: '3', name: 'สมศักดิ์ ทำงาน', roleId: 'user' }
];
}
convertToTreeNodes(menus: MenuHierarchy[]): MenuNode[] {
return menus.map(menu => ({
...menu,
expandable: !!(menu.children && menu.children.length > 0),
level: 0,
children: menu.children ? this.convertToTreeNodes(menu.children) : undefined
}));
}
onPermissionChange(node: MenuNode, permissionType: string, isChecked: boolean): void {
if (node.permissions) {
(node.permissions as any)[permissionType] = isChecked;
}
}
onVisibilityChange(node: MenuNode, isVisible: boolean): void {
node.isVisible = isVisible;
}
saveRolePermissions(): void {
if (!this.selectedRoleId) return;
const menuPermissions = this.extractMenuPermissions(this.dataSource.data);
this.menuPermissionService.updateRoleMenuPermissions(this.selectedRoleId, menuPermissions)
.subscribe({
next: () => {
console.log('Role permissions saved successfully');
// Show success message
},
error: (error) => {
console.error('Error saving role permissions:', error);
// Show error message
}
});
}
saveUserPermissions(): void {
if (!this.selectedUserId) return;
const menuPermissions = this.extractMenuPermissions(this.dataSource.data);
this.menuPermissionService.updateUserMenuPermissions(this.selectedUserId, menuPermissions)
.subscribe({
next: () => {
console.log('User permissions saved successfully');
// Show success message
},
error: (error) => {
console.error('Error saving user permissions:', error);
// Show error message
}
});
}
private extractMenuPermissions(nodes: MenuNode[]): MenuPermission[] {
const permissions: MenuPermission[] = [];
nodes.forEach(node => {
permissions.push({
id: node.id,
menuId: node.id,
menuName: node.name,
menuPath: node.path,
parentMenuId: undefined,
icon: node.icon,
order: node.order,
isVisible: node.isVisible,
permissions: node.permissions
});
if (node.children) {
permissions.push(...this.extractMenuPermissions(node.children));
}
});
return permissions;
}
resetPermissions(): void {
this.loadMenuHierarchy();
}
}
export interface MeetingRoom {
id: string;
name: string;
capacity: number;
location: string;
floor: string;
building: string;
amenities: string[];
isActive: boolean;
imageUrl?: string;
description?: string;
}
export interface MeetingBooking {
id: string;
roomId: string;
roomName: string;
title: string;
description?: string;
startDateTime: Date;
endDateTime: Date;
organizerId: string;
organizerName: string;
attendees: MeetingAttendee[];
status: 'pending' | 'confirmed' | 'cancelled' | 'completed';
recurringPattern?: RecurringPattern;
createdAt: Date;
updatedAt: Date;
createdBy: string;
updatedBy: string;
}
export interface MeetingAttendee {
userId: string;
userName: string;
email: string;
status: 'pending' | 'accepted' | 'declined' | 'tentative';
responseDate?: Date;
}
export interface RecurringPattern {
type: 'none' | 'daily' | 'weekly' | 'monthly';
interval: number;
daysOfWeek?: number[]; // 0 = Sunday, 1 = Monday, etc.
endDate?: Date;
occurrences?: number;
}
export interface BookingTimeSlot {
startTime: string;
endTime: string;
isAvailable: boolean;
bookingId?: string;
bookingTitle?: string;
}
export interface BookingConflict {
bookingId: string;
title: string;
startDateTime: Date;
endDateTime: Date;
organizerName: string;
}
export interface BookingFilter {
roomId?: string;
startDate?: Date;
endDate?: Date;
status?: string;
organizerId?: string;
}
export interface BookingStatistics {
totalBookings: number;
confirmedBookings: number;
pendingBookings: number;
cancelledBookings: number;
mostPopularRoom: string;
averageBookingDuration: number; // in minutes
peakHours: string[];
}
export interface MenuPermission {
id: string;
menuId: string;
menuName: string;
menuPath: string;
parentMenuId?: string;
icon?: string;
order: number;
isVisible: boolean;
permissions: {
view: boolean;
create: boolean;
edit: boolean;
delete: boolean;
export: boolean;
import: boolean;
};
}
export interface RoleMenuPermission {
roleId: string;
menuPermissions: MenuPermission[];
}
export interface UserMenuPermission {
userId: string;
roleId: string;
menuPermissions: MenuPermission[];
customPermissions?: {
[menuId: string]: {
view: boolean;
create: boolean;
edit: boolean;
delete: boolean;
export: boolean;
import: boolean;
};
};
}
export interface MenuHierarchy {
id: string;
name: string;
path: string;
icon?: string;
order: number;
isVisible: boolean;
children?: MenuHierarchy[];
permissions: {
view: boolean;
create: boolean;
edit: boolean;
delete: boolean;
export: boolean;
import: boolean;
};
}
export interface User {
id: string;
username: string;
email: string;
firstName: string;
lastName: string;
fullName: string;
avatar?: string;
phone?: string;
department?: string;
position?: string;
isActive: boolean;
lastLogin?: Date;
createdAt: Date;
updatedAt: Date;
createdBy: string;
updatedBy: string;
}
export interface Role {
id: string;
name: string;
displayName: string;
description: string;
isSystem: boolean;
isActive: boolean;
permissions: string[];
createdAt: Date;
updatedAt: Date;
createdBy: string;
updatedBy: string;
}
export interface UserRole {
id: string;
userId: string;
roleId: string;
assignedBy: string;
assignedAt: Date;
expiresAt?: Date;
isActive: boolean;
}
export interface Permission {
id: string;
name: string;
displayName: string;
description: string;
category: string;
resource: string;
action: string;
isSystem: boolean;
}
export interface RolePermission {
roleId: string;
permissionId: string;
granted: boolean;
grantedBy: string;
grantedAt: Date;
}
export interface UserPermission {
userId: string;
permissionId: string;
granted: boolean;
grantedBy: string;
grantedAt: Date;
expiresAt?: Date;
}
export interface Department {
id: string;
name: string;
description?: string;
parentId?: string;
managerId?: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
export interface Position {
id: string;
name: string;
description?: string;
level: number;
departmentId?: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
export interface UserFilter {
search?: string;
departmentId?: string;
positionId?: string;
roleId?: string;
isActive?: boolean;
lastLoginFrom?: Date;
lastLoginTo?: Date;
}
export interface RoleFilter {
search?: string;
isSystem?: boolean;
isActive?: boolean;
}
export interface UserRoleAssignment {
userId: string;
userName: string;
userEmail: string;
roleId: string;
roleName: string;
assignedAt: Date;
expiresAt?: Date;
isActive: boolean;
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, of } from 'rxjs';
import { map, catchError, tap } from 'rxjs/operators';
import {
MeetingRoom,
MeetingBooking,
MeetingAttendee,
RecurringPattern,
BookingTimeSlot,
BookingConflict,
BookingFilter,
BookingStatistics
} from '../models/meeting-booking.model';
@Injectable({
providedIn: 'root'
})
export class MeetingBookingService {
private bookingsSubject = new BehaviorSubject<MeetingBooking[]>([]);
public bookings$ = this.bookingsSubject.asObservable();
private roomsSubject = new BehaviorSubject<MeetingRoom[]>([]);
public rooms$ = this.roomsSubject.asObservable();
private dataUrl = 'assets/data/meeting-booking.json';
constructor(private http: HttpClient) {
this.loadInitialData();
}
private loadInitialData(): void {
this.getRooms().subscribe();
this.getBookings().subscribe();
}
// Meeting Rooms
getRooms(): Observable<MeetingRoom[]> {
return this.http.get<MeetingRoom[]>(`${this.dataUrl}/rooms`).pipe(
catchError(() => of(this.getMockRooms())),
tap(rooms => this.roomsSubject.next(rooms))
);
}
getRoomById(id: string): Observable<MeetingRoom | undefined> {
return this.rooms$.pipe(
map(rooms => rooms.find(room => room.id === id))
);
}
createRoom(room: Omit<MeetingRoom, 'id'>): Observable<MeetingRoom> {
const newRoom: MeetingRoom = {
...room,
id: this.generateId()
};
// In real app, this would make an HTTP POST request
console.log('Creating room:', newRoom);
return of(newRoom);
}
updateRoom(id: string, room: Partial<MeetingRoom>): Observable<MeetingRoom> {
// In real app, this would make an HTTP PUT request
console.log('Updating room:', id, room);
return of({} as MeetingRoom);
}
deleteRoom(id: string): Observable<boolean> {
// In real app, this would make an HTTP DELETE request
console.log('Deleting room:', id);
return of(true);
}
// Meeting Bookings
getBookings(filter?: BookingFilter): Observable<MeetingBooking[]> {
return this.http.get<MeetingBooking[]>(`${this.dataUrl}/bookings`).pipe(
catchError(() => of(this.getMockBookings())),
map(bookings => this.filterBookings(bookings, filter)),
tap(bookings => this.bookingsSubject.next(bookings))
);
}
getBookingById(id: string): Observable<MeetingBooking | undefined> {
return this.bookings$.pipe(
map(bookings => bookings.find(booking => booking.id === id))
);
}
createBooking(booking: Omit<MeetingBooking, 'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy'>): Observable<MeetingBooking> {
const newBooking: MeetingBooking = {
...booking,
id: this.generateId(),
createdAt: new Date(),
updatedAt: new Date(),
createdBy: 'current-user', // This would come from auth service
updatedBy: 'current-user'
};
// Check for conflicts
return this.checkBookingConflicts(newBooking).pipe(
map(conflicts => {
if (conflicts.length > 0) {
throw new Error('Booking conflicts detected');
}
return newBooking;
}),
tap(booking => {
const currentBookings = this.bookingsSubject.value;
this.bookingsSubject.next([...currentBookings, booking]);
})
);
}
updateBooking(id: string, booking: Partial<MeetingBooking>): Observable<MeetingBooking> {
// In real app, this would make an HTTP PUT request
console.log('Updating booking:', id, booking);
return of({} as MeetingBooking);
}
cancelBooking(id: string, reason?: string): Observable<boolean> {
// In real app, this would make an HTTP PATCH request
console.log('Cancelling booking:', id, reason);
return of(true);
}
// Time Slots
getAvailableTimeSlots(roomId: string, date: Date): Observable<BookingTimeSlot[]> {
return this.bookings$.pipe(
map(bookings => {
const dayBookings = bookings.filter(booking =>
booking.roomId === roomId &&
this.isSameDay(new Date(booking.startDateTime), date) &&
booking.status !== 'cancelled'
);
return this.generateTimeSlots(date, dayBookings);
})
);
}
// Conflicts
checkBookingConflicts(booking: MeetingBooking): Observable<BookingConflict[]> {
return this.bookings$.pipe(
map(bookings => {
return bookings.filter(existingBooking =>
existingBooking.roomId === booking.roomId &&
existingBooking.id !== booking.id &&
existingBooking.status !== 'cancelled' &&
this.isTimeOverlapping(
new Date(existingBooking.startDateTime),
new Date(existingBooking.endDateTime),
new Date(booking.startDateTime),
new Date(booking.endDateTime)
)
).map(conflict => ({
bookingId: conflict.id,
title: conflict.title,
startDateTime: new Date(conflict.startDateTime),
endDateTime: new Date(conflict.endDateTime),
organizerName: conflict.organizerName
}));
})
);
}
// Statistics
getBookingStatistics(startDate: Date, endDate: Date): Observable<BookingStatistics> {
return this.bookings$.pipe(
map(bookings => {
const filteredBookings = bookings.filter(booking => {
const bookingDate = new Date(booking.startDateTime);
return bookingDate >= startDate && bookingDate <= endDate;
});
return this.calculateStatistics(filteredBookings);
})
);
}
// Attendees
respondToBooking(bookingId: string, attendeeId: string, response: 'accepted' | 'declined' | 'tentative'): Observable<boolean> {
// In real app, this would make an HTTP PATCH request
console.log('Responding to booking:', bookingId, attendeeId, response);
return of(true);
}
// Private helper methods
private filterBookings(bookings: MeetingBooking[], filter?: BookingFilter): MeetingBooking[] {
if (!filter) return bookings;
return bookings.filter(booking => {
if (filter.roomId && booking.roomId !== filter.roomId) return false;
if (filter.status && booking.status !== filter.status) return false;
if (filter.organizerId && booking.organizerId !== filter.organizerId) return false;
if (filter.startDate || filter.endDate) {
const bookingDate = new Date(booking.startDateTime);
if (filter.startDate && bookingDate < filter.startDate) return false;
if (filter.endDate && bookingDate > filter.endDate) return false;
}
return true;
});
}
private generateTimeSlots(date: Date, bookings: MeetingBooking[]): BookingTimeSlot[] {
const slots: BookingTimeSlot[] = [];
const startHour = 8; // 8 AM
const endHour = 18; // 6 PM
const slotDuration = 30; // 30 minutes
for (let hour = startHour; hour < endHour; hour++) {
for (let minute = 0; minute < 60; minute += slotDuration) {
const startTime = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
const endMinute = minute + slotDuration;
const endHourAdjusted = endMinute >= 60 ? hour + 1 : hour;
const endMinuteAdjusted = endMinute >= 60 ? endMinute - 60 : endMinute;
const endTime = `${endHourAdjusted.toString().padStart(2, '0')}:${endMinuteAdjusted.toString().padStart(2, '0')}`;
const slotStart = new Date(date);
slotStart.setHours(hour, minute, 0, 0);
const slotEnd = new Date(date);
slotEnd.setHours(endHourAdjusted, endMinuteAdjusted, 0, 0);
const isBooked = bookings.some(booking =>
this.isTimeOverlapping(
new Date(booking.startDateTime),
new Date(booking.endDateTime),
slotStart,
slotEnd
)
);
slots.push({
startTime,
endTime,
isAvailable: !isBooked
});
}
}
return slots;
}
private isTimeOverlapping(start1: Date, end1: Date, start2: Date, end2: Date): boolean {
return start1 < end2 && start2 < end1;
}
private isSameDay(date1: Date, date2: Date): boolean {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate();
}
private calculateStatistics(bookings: MeetingBooking[]): BookingStatistics {
const totalBookings = bookings.length;
const confirmedBookings = bookings.filter(b => b.status === 'confirmed').length;
const pendingBookings = bookings.filter(b => b.status === 'pending').length;
const cancelledBookings = bookings.filter(b => b.status === 'cancelled').length;
// Most popular room
const roomCounts = bookings.reduce((acc, booking) => {
acc[booking.roomId] = (acc[booking.roomId] || 0) + 1;
return acc;
}, {} as { [key: string]: number });
const mostPopularRoom = Object.keys(roomCounts).reduce((a, b) =>
roomCounts[a] > roomCounts[b] ? a : b, '');
// Average booking duration
const totalDuration = bookings.reduce((acc, booking) => {
const duration = new Date(booking.endDateTime).getTime() - new Date(booking.startDateTime).getTime();
return acc + duration;
}, 0);
const averageBookingDuration = totalBookings > 0 ? totalDuration / totalBookings / (1000 * 60) : 0;
// Peak hours (simplified)
const peakHours = ['09:00', '10:00', '14:00', '15:00'];
return {
totalBookings,
confirmedBookings,
pendingBookings,
cancelledBookings,
mostPopularRoom,
averageBookingDuration,
peakHours
};
}
private generateId(): string {
return Math.random().toString(36).substr(2, 9);
}
// Mock data
private getMockRooms(): MeetingRoom[] {
return [
{
id: '1',
name: 'ห้องประชุมใหญ่',
capacity: 20,
location: 'ชั้น 5',
floor: '5',
building: 'อาคาร A',
amenities: ['โปรเจคเตอร์', 'ไวท์บอร์ด', 'ระบบเสียง', 'WiFi'],
isActive: true,
description: 'ห้องประชุมขนาดใหญ่เหมาะสำหรับการประชุมสำคัญ'
},
{
id: '2',
name: 'ห้องประชุมเล็ก',
capacity: 8,
location: 'ชั้น 3',
floor: '3',
building: 'อาคาร A',
amenities: ['โปรเจคเตอร์', 'ไวท์บอร์ด', 'WiFi'],
isActive: true,
description: 'ห้องประชุมขนาดเล็กเหมาะสำหรับการประชุมทีม'
},
{
id: '3',
name: 'ห้องประชุม VIP',
capacity: 12,
location: 'ชั้น 10',
floor: '10',
building: 'อาคาร B',
amenities: ['โปรเจคเตอร์', 'ไวท์บอร์ด', 'ระบบเสียง', 'WiFi', 'เครื่องปรับอากาศ', 'โต๊ะประชุมไม้'],
isActive: true,
description: 'ห้องประชุมระดับ VIP สำหรับการประชุมสำคัญ'
}
];
}
private getMockBookings(): MeetingBooking[] {
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
return [
{
id: '1',
roomId: '1',
roomName: 'ห้องประชุมใหญ่',
title: 'ประชุมทีมพัฒนา',
description: 'ประชุมรายสัปดาห์ของทีมพัฒนา',
startDateTime: new Date(tomorrow.setHours(9, 0, 0, 0)),
endDateTime: new Date(tomorrow.setHours(10, 0, 0, 0)),
organizerId: '1',
organizerName: 'สมชาย ใจดี',
attendees: [
{ userId: '2', userName: 'สมหญิง รักงาน', email: 'somying@company.com', status: 'accepted' },
{ userId: '3', userName: 'สมศักดิ์ ทำงาน', email: 'somsak@company.com', status: 'pending' }
],
status: 'confirmed',
createdAt: new Date(),
updatedAt: new Date(),
createdBy: '1',
updatedBy: '1'
}
];
}
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, of } from 'rxjs';
import { map, catchError, tap } from 'rxjs/operators';
import { MenuPermission, RoleMenuPermission, UserMenuPermission, MenuHierarchy } from '../models/menu-permission.model';
@Injectable({
providedIn: 'root'
})
export class MenuPermissionService {
private menuPermissionsSubject = new BehaviorSubject<MenuHierarchy[]>([]);
public menuPermissions$ = this.menuPermissionsSubject.asObservable();
private dataUrl = 'assets/data/menu-permissions.json';
constructor(private http: HttpClient) {
this.loadMenuPermissions();
}
/**
* โหลดข้อมูลสิทธิ์เมนูจาก API หรือ Mock Data
*/
private loadMenuPermissions(): void {
this.getMenuHierarchy().subscribe({
next: (menus) => this.menuPermissionsSubject.next(menus),
error: (error) => console.error('Error loading menu permissions:', error)
});
}
/**
* ดึงข้อมูลโครงสร้างเมนูแบบลำดับชั้น
*/
getMenuHierarchy(): Observable<MenuHierarchy[]> {
return this.http.get<MenuHierarchy[]>(this.dataUrl).pipe(
catchError(() => {
// Fallback to mock data if API fails
return of(this.getMockMenuHierarchy());
})
);
}
/**
* ตรวจสอบสิทธิ์การเข้าถึงเมนู
*/
canAccessMenu(menuPath: string, permission: 'view' | 'create' | 'edit' | 'delete' | 'export' | 'import' = 'view'): Observable<boolean> {
return this.menuPermissions$.pipe(
map(menus => {
const menu = this.findMenuByPath(menus, menuPath);
return menu ? menu.permissions[permission] : false;
})
);
}
/**
* ตรวจสอบสิทธิ์การเข้าถึงเมนูหลายเมนูพร้อมกัน
*/
canAccessMenus(menuPaths: string[], permission: 'view' | 'create' | 'edit' | 'delete' | 'export' | 'import' = 'view'): Observable<{ [path: string]: boolean }> {
return this.menuPermissions$.pipe(
map(menus => {
const result: { [path: string]: boolean } = {};
menuPaths.forEach(path => {
const menu = this.findMenuByPath(menus, path);
result[path] = menu ? menu.permissions[permission] : false;
});
return result;
})
);
}
/**
* ดึงเมนูที่ผู้ใช้สามารถเข้าถึงได้
*/
getAccessibleMenus(): Observable<MenuHierarchy[]> {
return this.menuPermissions$.pipe(
map(menus => this.filterAccessibleMenus(menus))
);
}
/**
* อัปเดตสิทธิ์เมนูสำหรับบทบาท
*/
updateRoleMenuPermissions(roleId: string, menuPermissions: MenuPermission[]): Observable<any> {
const roleMenuPermission: RoleMenuPermission = {
roleId,
menuPermissions
};
// In a real app, this would make an HTTP PUT request
console.log('Updating role menu permissions:', roleMenuPermission);
return of({ success: true });
}
/**
* อัปเดตสิทธิ์เมนูสำหรับผู้ใช้เฉพาะ
*/
updateUserMenuPermissions(userId: string, menuPermissions: MenuPermission[]): Observable<any> {
const userMenuPermission: UserMenuPermission = {
userId,
roleId: '', // This would come from user data
menuPermissions
};
// In a real app, this would make an HTTP PUT request
console.log('Updating user menu permissions:', userMenuPermission);
return of({ success: true });
}
/**
* ค้นหาเมนูตาม path
*/
private findMenuByPath(menus: MenuHierarchy[], path: string): MenuHierarchy | null {
for (const menu of menus) {
if (menu.path === path) {
return menu;
}
if (menu.children) {
const found = this.findMenuByPath(menu.children, path);
if (found) return found;
}
}
return null;
}
/**
* กรองเมนูที่ผู้ใช้สามารถเข้าถึงได้
*/
private filterAccessibleMenus(menus: MenuHierarchy[]): MenuHierarchy[] {
return menus
.filter(menu => menu.isVisible && menu.permissions.view)
.map(menu => ({
...menu,
children: menu.children ? this.filterAccessibleMenus(menu.children) : undefined
}))
.filter(menu => menu.children ? menu.children.length > 0 : true);
}
/**
* Mock data สำหรับการทดสอบ
*/
private getMockMenuHierarchy(): MenuHierarchy[] {
return [
{
id: 'dashboard',
name: 'แดชบอร์ด',
path: '/portal-manage/dashboard',
icon: 'grid-alt',
order: 1,
isVisible: true,
permissions: {
view: true,
create: true,
edit: true,
delete: true,
export: true,
import: false
},
children: [
{
id: 'dashboard-management',
name: 'จัดการแดชบอร์ด',
path: '/portal-manage/dashboard/management',
order: 1,
isVisible: true,
permissions: {
view: true,
create: true,
edit: true,
delete: true,
export: true,
import: false
}
},
{
id: 'widget-warehouse',
name: 'คลังวิดเจ็ต',
path: '/portal-manage/dashboard/widget-warehouse',
order: 2,
isVisible: true,
permissions: {
view: true,
create: true,
edit: true,
delete: true,
export: true,
import: true
}
}
]
},
{
id: 'company-management',
name: 'จัดการบริษัท',
path: '/portal-manage/company-management',
icon: 'building',
order: 2,
isVisible: true,
permissions: {
view: true,
create: true,
edit: true,
delete: true,
export: true,
import: false
}
},
{
id: 'permission-management',
name: 'จัดการสิทธิ์',
path: '/portal-manage/permission-management',
icon: 'shield',
order: 3,
isVisible: true,
permissions: {
view: true,
create: true,
edit: true,
delete: true,
export: true,
import: false
}
},
{
id: 'meeting-booking',
name: 'จองห้องประชุม',
path: '/portal-manage/meeting-booking',
icon: 'calendar',
order: 4,
isVisible: true,
permissions: {
view: true,
create: true,
edit: true,
delete: true,
export: false,
import: false
}
}
];
}
}
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { TokenService } from '../../shared/services/token.service';
import { UserRole, UserRoleModel } from '../models/user-role-model';
import { map, tap, switchMap, filter, reduce } from "rxjs/operators";
import { PageResponseModel, ResponseModel } from '../models/base.model';
import { forkJoin, Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, of } from 'rxjs';
import { map, catchError, tap } from 'rxjs/operators';
import {
User,
Role,
UserRole,
Permission,
RolePermission,
UserPermission,
Department,
Position,
UserFilter,
RoleFilter,
UserRoleAssignment
} from '../models/user-role.model';
@Injectable({
providedIn: 'root'
})
export class UserRoleService {
apiBaseUrl = "/user-role";
constructor(
private http: HttpClient,
private translateService: TranslateService
) { }
getById(id: Number) {
return this.http
.get<UserRoleModel>(this.apiBaseUrl + "/" + id)
.pipe(map((e) => new UserRole(e, this.translateService)));
}
getLists() {
return this.http
.get<UserRoleModel[]>(this.apiBaseUrl + "/lists")
.pipe(
map((e) => e.map((e) => new UserRole(e, this.translateService)))
private usersSubject = new BehaviorSubject<User[]>([]);
public users$ = this.usersSubject.asObservable();
private rolesSubject = new BehaviorSubject<Role[]>([]);
public roles$ = this.rolesSubject.asObservable();
private permissionsSubject = new BehaviorSubject<Permission[]>([]);
public permissions$ = this.permissionsSubject.asObservable();
private departmentsSubject = new BehaviorSubject<Department[]>([]);
public departments$ = this.departmentsSubject.asObservable();
private positionsSubject = new BehaviorSubject<Position[]>([]);
public positions$ = this.positionsSubject.asObservable();
private dataUrl = 'assets/data/user-role.json';
constructor(private http: HttpClient) {
this.loadInitialData();
}
private loadInitialData(): void {
this.getUsers().subscribe();
this.getRoles().subscribe();
this.getPermissions().subscribe();
this.getDepartments().subscribe();
this.getPositions().subscribe();
}
// Users
getUsers(filter?: UserFilter): Observable<User[]> {
return this.http.get<User[]>(`${this.dataUrl}/users`).pipe(
catchError(() => of(this.getMockUsers())),
map(users => this.filterUsers(users, filter)),
tap(users => this.usersSubject.next(users))
);
}
getListByPageSize(body: { page: number; size: number }) {
return this.http
.get<PageResponseModel<UserRoleModel>>(this.apiBaseUrl, {
params: body,
})
.pipe(
map((page) => {
return {
...page,
content: page.content.map(
(e) => new UserRole(e, this.translateService)
),
getUserById(id: string): Observable<User | undefined> {
return this.users$.pipe(
map(users => users.find(user => user.id === id))
);
}
createUser(user: Omit<User, 'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy'>): Observable<User> {
const newUser: User = {
...user,
id: this.generateId(),
createdAt: new Date(),
updatedAt: new Date(),
createdBy: 'current-user',
updatedBy: 'current-user'
};
})
console.log('Creating user:', newUser);
return of(newUser);
}
updateUser(id: string, user: Partial<User>): Observable<User> {
console.log('Updating user:', id, user);
return of({} as User);
}
deleteUser(id: string): Observable<boolean> {
console.log('Deleting user:', id);
return of(true);
}
// Roles
getRoles(filter?: RoleFilter): Observable<Role[]> {
return this.http.get<Role[]>(`${this.dataUrl}/roles`).pipe(
catchError(() => of(this.getMockRoles())),
map(roles => this.filterRoles(roles, filter)),
tap(roles => this.rolesSubject.next(roles))
);
}
getListAllPageSize(): Observable<UserRoleModel[]> {
return this.http
.get<PageResponseModel<UserRoleModel>>(this.apiBaseUrl, {
params: { page: 0, size: 1 },
})
.pipe(
switchMap((checkData: any) => {
//console.log("checkData="+checkData)
const size = 500;
const numOfPages = checkData.totalElements / size;
const parallelList: Observable<PageResponseModel<UserRoleModel>>[] = [];
for (let page = 0; page < numOfPages; page++) {
parallelList.push(
this.getListByPageSize({
page,
size,
})
getRoleById(id: string): Observable<Role | undefined> {
return this.roles$.pipe(
map(roles => roles.find(role => role.id === id))
);
}
return forkJoin(parallelList).pipe(
map((response) => {
let data: UserRoleModel[] = [];
for (let i = 0; i < response.length; i++) {
data = data.concat(response[i].content);
createRole(role: Omit<Role, 'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy'>): Observable<Role> {
const newRole: Role = {
...role,
id: this.generateId(),
createdAt: new Date(),
updatedAt: new Date(),
createdBy: 'current-user',
updatedBy: 'current-user'
};
console.log('Creating role:', newRole);
return of(newRole);
}
return data;
})
updateRole(id: string, role: Partial<Role>): Observable<Role> {
console.log('Updating role:', id, role);
return of({} as Role);
}
deleteRole(id: string): Observable<boolean> {
console.log('Deleting role:', id);
return of(true);
}
// Permissions
getPermissions(): Observable<Permission[]> {
return this.http.get<Permission[]>(`${this.dataUrl}/permissions`).pipe(
catchError(() => of(this.getMockPermissions())),
tap(permissions => this.permissionsSubject.next(permissions))
);
}
// User Roles
getUserRoles(userId: string): Observable<UserRole[]> {
return this.http.get<UserRole[]>(`${this.dataUrl}/user-roles/${userId}`).pipe(
catchError(() => of([]))
);
}
assignRoleToUser(userId: string, roleId: string, expiresAt?: Date): Observable<boolean> {
console.log('Assigning role to user:', userId, roleId, expiresAt);
return of(true);
}
removeRoleFromUser(userId: string, roleId: string): Observable<boolean> {
console.log('Removing role from user:', userId, roleId);
return of(true);
}
// Role Permissions
getRolePermissions(roleId: string): Observable<RolePermission[]> {
return this.http.get<RolePermission[]>(`${this.dataUrl}/role-permissions/${roleId}`).pipe(
catchError(() => of([]))
);
}
})
updateRolePermissions(roleId: string, permissions: RolePermission[]): Observable<boolean> {
console.log('Updating role permissions:', roleId, permissions);
return of(true);
}
// User Permissions
getUserPermissions(userId: string): Observable<UserPermission[]> {
return this.http.get<UserPermission[]>(`${this.dataUrl}/user-permissions/${userId}`).pipe(
catchError(() => of([]))
);
}
updateUserPermissions(userId: string, permissions: UserPermission[]): Observable<boolean> {
console.log('Updating user permissions:', userId, permissions);
return of(true);
}
// Departments
getDepartments(): Observable<Department[]> {
return this.http.get<Department[]>(`${this.dataUrl}/departments`).pipe(
catchError(() => of(this.getMockDepartments())),
tap(departments => this.departmentsSubject.next(departments))
);
}
save(body: UserRoleModel) {
return this.http.post<ResponseModel>(this.apiBaseUrl, new UserRole(body));
createDepartment(department: Omit<Department, 'id' | 'createdAt' | 'updatedAt'>): Observable<Department> {
const newDepartment: Department = {
...department,
id: this.generateId(),
createdAt: new Date(),
updatedAt: new Date()
};
console.log('Creating department:', newDepartment);
return of(newDepartment);
}
// Positions
getPositions(): Observable<Position[]> {
return this.http.get<Position[]>(`${this.dataUrl}/positions`).pipe(
catchError(() => of(this.getMockPositions())),
tap(positions => this.positionsSubject.next(positions))
);
}
delete(body: UserRoleModel) {
const options = {
headers: new HttpHeaders({
"Content-Type": "application/json",
}),
body: new UserRole(body),
createPosition(position: Omit<Position, 'id' | 'createdAt' | 'updatedAt'>): Observable<Position> {
const newPosition: Position = {
...position,
id: this.generateId(),
createdAt: new Date(),
updatedAt: new Date()
};
return this.http.delete<ResponseModel>(this.apiBaseUrl, options);
console.log('Creating position:', newPosition);
return of(newPosition);
}
// Statistics
getUserStatistics(): Observable<any> {
return this.users$.pipe(
map(users => ({
totalUsers: users.length,
activeUsers: users.filter(u => u.isActive).length,
inactiveUsers: users.filter(u => !u.isActive).length,
usersByDepartment: this.groupUsersByDepartment(users),
usersByRole: this.groupUsersByRole(users)
}))
);
}
// Private helper methods
private filterUsers(users: User[], filter?: UserFilter): User[] {
if (!filter) return users;
return users.filter(user => {
if (filter.search) {
const searchTerm = filter.search.toLowerCase();
if (!user.fullName.toLowerCase().includes(searchTerm) &&
!user.email.toLowerCase().includes(searchTerm) &&
!user.username.toLowerCase().includes(searchTerm)) {
return false;
}
}
if (filter.departmentId && user.department !== filter.departmentId) return false;
if (filter.positionId && user.position !== filter.positionId) return false;
if (filter.isActive !== undefined && user.isActive !== filter.isActive) return false;
if (filter.lastLoginFrom || filter.lastLoginTo) {
if (!user.lastLogin) return false;
const lastLogin = new Date(user.lastLogin);
if (filter.lastLoginFrom && lastLogin < filter.lastLoginFrom) return false;
if (filter.lastLoginTo && lastLogin > filter.lastLoginTo) return false;
}
return true;
});
}
private filterRoles(roles: Role[], filter?: RoleFilter): Role[] {
if (!filter) return roles;
return roles.filter(role => {
if (filter.search) {
const searchTerm = filter.search.toLowerCase();
if (!role.name.toLowerCase().includes(searchTerm) &&
!role.displayName.toLowerCase().includes(searchTerm) &&
!role.description.toLowerCase().includes(searchTerm)) {
return false;
}
}
if (filter.isSystem !== undefined && role.isSystem !== filter.isSystem) return false;
if (filter.isActive !== undefined && role.isActive !== filter.isActive) return false;
return true;
});
}
private groupUsersByDepartment(users: User[]): { [key: string]: number } {
return users.reduce((acc, user) => {
const dept = user.department || 'ไม่ระบุ';
acc[dept] = (acc[dept] || 0) + 1;
return acc;
}, {} as { [key: string]: number });
}
private groupUsersByRole(users: User[]): { [key: string]: number } {
// This would need to be implemented with actual role data
return {};
}
private generateId(): string {
return Math.random().toString(36).substr(2, 9);
}
// Mock data
private getMockUsers(): User[] {
return [
{
id: '1',
username: 'admin',
email: 'admin@company.com',
firstName: 'สมชาย',
lastName: 'ใจดี',
fullName: 'สมชาย ใจดี',
phone: '081-234-5678',
department: 'IT',
position: 'System Administrator',
isActive: true,
lastLogin: new Date('2024-01-15T08:30:00Z'),
createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-15T08:30:00Z'),
createdBy: 'system',
updatedBy: 'system'
},
{
id: '2',
username: 'manager',
email: 'manager@company.com',
firstName: 'สมหญิง',
lastName: 'รักงาน',
fullName: 'สมหญิง รักงาน',
phone: '081-234-5679',
department: 'HR',
position: 'HR Manager',
isActive: true,
lastLogin: new Date('2024-01-14T17:00:00Z'),
createdAt: new Date('2024-01-02T00:00:00Z'),
updatedAt: new Date('2024-01-14T17:00:00Z'),
createdBy: '1',
updatedBy: '1'
}
];
}
private getMockRoles(): Role[] {
return [
{
id: 'admin',
name: 'admin',
displayName: 'ผู้ดูแลระบบ',
description: 'มีสิทธิ์เข้าถึงระบบทั้งหมด',
isSystem: true,
isActive: true,
permissions: ['*'],
createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-01T00:00:00Z'),
createdBy: 'system',
updatedBy: 'system'
},
{
id: 'manager',
name: 'manager',
displayName: 'ผู้จัดการ',
description: 'สามารถจัดการทีมและดูรายงาน',
isSystem: false,
isActive: true,
permissions: ['user.view', 'user.edit', 'report.view'],
createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-01T00:00:00Z'),
createdBy: 'system',
updatedBy: 'system'
},
{
id: 'user',
name: 'user',
displayName: 'ผู้ใช้ทั่วไป',
description: 'สิทธิ์พื้นฐานในการใช้งานระบบ',
isSystem: false,
isActive: true,
permissions: ['profile.view', 'profile.edit'],
createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-01T00:00:00Z'),
createdBy: 'system',
updatedBy: 'system'
}
];
}
private getMockPermissions(): Permission[] {
return [
{
id: 'user.view',
name: 'user.view',
displayName: 'ดูข้อมูลผู้ใช้',
description: 'สามารถดูข้อมูลผู้ใช้ได้',
category: 'User Management',
resource: 'user',
action: 'view',
isSystem: true
},
{
id: 'user.edit',
name: 'user.edit',
displayName: 'แก้ไขข้อมูลผู้ใช้',
description: 'สามารถแก้ไขข้อมูลผู้ใช้ได้',
category: 'User Management',
resource: 'user',
action: 'edit',
isSystem: true
},
{
id: 'role.view',
name: 'role.view',
displayName: 'ดูข้อมูลบทบาท',
description: 'สามารถดูข้อมูลบทบาทได้',
category: 'Role Management',
resource: 'role',
action: 'view',
isSystem: true
},
{
id: 'role.edit',
name: 'role.edit',
displayName: 'แก้ไขข้อมูลบทบาท',
description: 'สามารถแก้ไขข้อมูลบทบาทได้',
category: 'Role Management',
resource: 'role',
action: 'edit',
isSystem: true
}
];
}
private getMockDepartments(): Department[] {
return [
{
id: '1',
name: 'IT',
description: 'แผนกเทคโนโลยีสารสนเทศ',
isActive: true,
createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-01T00:00:00Z')
},
{
id: '2',
name: 'HR',
description: 'แผนกทรัพยากรบุคคล',
isActive: true,
createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-01T00:00:00Z')
}
];
}
private getMockPositions(): Position[] {
return [
{
id: '1',
name: 'System Administrator',
description: 'ผู้ดูแลระบบ',
level: 5,
departmentId: '1',
isActive: true,
createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-01T00:00:00Z')
},
{
id: '2',
name: 'HR Manager',
description: 'ผู้จัดการทรัพยากรบุคคล',
level: 4,
departmentId: '2',
isActive: true,
createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-01T00:00:00Z')
}
];
}
}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">จัดการผู้ใช้และบทบาท</h4>
<p class="card-subtitle">จัดการข้อมูลผู้ใช้ บทบาท และสิทธิ์การเข้าถึง</p>
</div>
<div class="card-body">
<mat-tab-group>
<!-- User Management Tab -->
<mat-tab label="จัดการผู้ใช้">
<div class="row mt-3">
<!-- User Filter -->
<div class="col-12">
<mat-card>
<mat-card-header>
<mat-card-title>ค้นหาและกรอง</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="row">
<div class="col-md-3">
<mat-form-field appearance="outline" class="w-100">
<mat-label>ค้นหา</mat-label>
<input matInput [(ngModel)]="userFilter.search"
(ngModelChange)="onUserFilterChange()"
placeholder="ชื่อ, อีเมล, หรือชื่อผู้ใช้">
</mat-form-field>
</div>
<div class="col-md-3">
<mat-form-field appearance="outline" class="w-100">
<mat-label>แผนก</mat-label>
<mat-select [(ngModel)]="userFilter.department"
(selectionChange)="onUserFilterChange()">
<mat-option value="">ทั้งหมด</mat-option>
<mat-option *ngFor="let dept of departments$ | async" [value]="dept.id">
{{ dept.name }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-md-3">
<mat-form-field appearance="outline" class="w-100">
<mat-label>ตำแหน่ง</mat-label>
<mat-select [(ngModel)]="userFilter.position"
(selectionChange)="onUserFilterChange()">
<mat-option value="">ทั้งหมด</mat-option>
<mat-option *ngFor="let pos of positions$ | async" [value]="pos.id">
{{ pos.name }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-md-3">
<mat-form-field appearance="outline" class="w-100">
<mat-label>สถานะ</mat-label>
<mat-select [(ngModel)]="userFilter.isActive"
(selectionChange)="onUserFilterChange()">
<mat-option value="">ทั้งหมด</mat-option>
<mat-option [value]="true">ใช้งาน</mat-option>
<mat-option [value]="false">ไม่ใช้งาน</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
<!-- User Form -->
<div class="col-md-4">
<mat-card>
<mat-card-header>
<mat-card-title>เพิ่มผู้ใช้ใหม่</mat-card-title>
</mat-card-header>
<mat-card-content>
<form [formGroup]="userForm" (ngSubmit)="createUser()">
<div class="row">
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>ชื่อผู้ใช้</mat-label>
<input matInput formControlName="username" placeholder="username">
</mat-form-field>
</div>
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>อีเมล</mat-label>
<input matInput formControlName="email" type="email" placeholder="email@company.com">
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>ชื่อ</mat-label>
<input matInput formControlName="firstName" placeholder="ชื่อ">
</mat-form-field>
</div>
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>นามสกุล</mat-label>
<input matInput formControlName="lastName" placeholder="นามสกุล">
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>เบอร์โทร</mat-label>
<input matInput formControlName="phone" placeholder="081-234-5678">
</mat-form-field>
</div>
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>แผนก</mat-label>
<mat-select formControlName="department">
<mat-option *ngFor="let dept of departments$ | async" [value]="dept.id">
{{ dept.name }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-md-6">
<mat-form-field appearance="outline" class="w-100">
<mat-label>ตำแหน่ง</mat-label>
<mat-select formControlName="position">
<mat-option *ngFor="let pos of positions$ | async" [value]="pos.id">
{{ pos.name }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-md-6">
<mat-slide-toggle formControlName="isActive" class="mt-3">
ใช้งาน
</mat-slide-toggle>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<button mat-raised-button color="primary" type="submit"
[disabled]="!userForm.valid">
<mat-icon>person_add</mat-icon>
เพิ่มผู้ใช้
</button>
<button mat-button type="button" (click)="userForm.reset()">
<mat-icon>refresh</mat-icon>
รีเซ็ต
</button>
</div>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
<!-- User List -->
<div class="col-md-8">
<mat-card>
<mat-card-header>
<mat-card-title>รายการผู้ใช้</mat-card-title>
<mat-card-subtitle>ผู้ใช้ทั้งหมดในระบบ</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div *ngIf="isLoading" class="text-center">
<mat-spinner diameter="30"></mat-spinner>
<p>กำลังโหลด...</p>
</div>
<div *ngIf="!isLoading" class="table-responsive">
<table mat-table [dataSource]="users$ | async" class="w-100">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="masterUserSelectionChange($event.checked)"
[checked]="isAllUsersSelected()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let user">
<mat-checkbox (change)="userSelectionChange(user, $event.checked)"
[checked]="isUserSelected(user)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="fullName">
<th mat-header-cell *matHeaderCellDef>ชื่อ-นามสกุล</th>
<td mat-cell *matCellDef="let user">
<div class="user-info">
<div class="user-name">{{ user.fullName }}</div>
<div class="user-email text-muted">{{ user.email }}</div>
</div>
</td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef>อีเมล</th>
<td mat-cell *matCellDef="let user">{{ user.email }}</td>
</ng-container>
<ng-container matColumnDef="department">
<th mat-header-cell *matHeaderCellDef>แผนก</th>
<td mat-cell *matCellDef="let user">{{ user.department || '-' }}</td>
</ng-container>
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef>ตำแหน่ง</th>
<td mat-cell *matCellDef="let user">{{ user.position || '-' }}</td>
</ng-container>
<ng-container matColumnDef="isActive">
<th mat-header-cell *matHeaderCellDef>สถานะ</th>
<td mat-cell *matCellDef="let user">
<span class="badge badge-{{ getStatusColor(user.isActive) }}">
{{ getStatusText(user.isActive) }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="lastLogin">
<th mat-header-cell *matHeaderCellDef>เข้าสู่ระบบล่าสุด</th>
<td mat-cell *matCellDef="let user">{{ formatDate(user.lastLogin) }}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>การดำเนินการ</th>
<td mat-cell *matCellDef="let user">
<button mat-icon-button matTooltip="แก้ไข">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button matTooltip="ลบ" (click)="deleteUser(user)">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="userDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: userDisplayedColumns;"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</mat-tab>
<!-- Role Management Tab -->
<mat-tab label="จัดการบทบาท">
<div class="row mt-3">
<!-- Role Filter -->
<div class="col-12">
<mat-card>
<mat-card-header>
<mat-card-title>ค้นหาและกรอง</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="row">
<div class="col-md-4">
<mat-form-field appearance="outline" class="w-100">
<mat-label>ค้นหา</mat-label>
<input matInput [(ngModel)]="roleFilter.search"
(ngModelChange)="onRoleFilterChange()"
placeholder="ชื่อบทบาทหรือคำอธิบาย">
</mat-form-field>
</div>
<div class="col-md-4">
<mat-form-field appearance="outline" class="w-100">
<mat-label>ประเภท</mat-label>
<mat-select [(ngModel)]="roleFilter.isSystem"
(selectionChange)="onRoleFilterChange()">
<mat-option value="">ทั้งหมด</mat-option>
<mat-option [value]="true">ระบบ</mat-option>
<mat-option [value]="false">ผู้ใช้สร้าง</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-md-4">
<mat-form-field appearance="outline" class="w-100">
<mat-label>สถานะ</mat-label>
<mat-select [(ngModel)]="roleFilter.isActive"
(selectionChange)="onRoleFilterChange()">
<mat-option value="">ทั้งหมด</mat-option>
<mat-option [value]="true">ใช้งาน</mat-option>
<mat-option [value]="false">ไม่ใช้งาน</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
<!-- Role Form -->
<div class="col-md-4">
<mat-card>
<mat-card-header>
<mat-card-title>เพิ่มบทบาทใหม่</mat-card-title>
</mat-card-header>
<mat-card-content>
<form [formGroup]="roleForm" (ngSubmit)="createRole()">
<div class="row">
<div class="col-12">
<mat-form-field appearance="outline" class="w-100">
<mat-label>ชื่อบทบาท</mat-label>
<input matInput formControlName="name" placeholder="role_name">
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-12">
<mat-form-field appearance="outline" class="w-100">
<mat-label>ชื่อแสดง</mat-label>
<input matInput formControlName="displayName" placeholder="ชื่อบทบาท">
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-12">
<mat-form-field appearance="outline" class="w-100">
<mat-label>คำอธิบาย</mat-label>
<textarea matInput formControlName="description"
placeholder="คำอธิบายบทบาท" rows="3"></textarea>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-md-6">
<mat-slide-toggle formControlName="isSystem" class="mt-3">
บทบาทระบบ
</mat-slide-toggle>
</div>
<div class="col-md-6">
<mat-slide-toggle formControlName="isActive" class="mt-3">
ใช้งาน
</mat-slide-toggle>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<button mat-raised-button color="primary" type="submit"
[disabled]="!roleForm.valid">
<mat-icon>add</mat-icon>
เพิ่มบทบาท
</button>
<button mat-button type="button" (click)="roleForm.reset()">
<mat-icon>refresh</mat-icon>
รีเซ็ต
</button>
</div>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
<!-- Role List -->
<div class="col-md-8">
<mat-card>
<mat-card-header>
<mat-card-title>รายการบทบาท</mat-card-title>
<mat-card-subtitle>บทบาททั้งหมดในระบบ</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="table-responsive">
<table mat-table [dataSource]="roles$ | async" class="w-100">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="masterRoleSelectionChange($event.checked)"
[checked]="isAllRolesSelected()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let role">
<mat-checkbox (change)="roleSelectionChange(role, $event.checked)"
[checked]="isRoleSelected(role)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>ชื่อบทบาท</th>
<td mat-cell *matCellDef="let role">{{ role.name }}</td>
</ng-container>
<ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef>ชื่อแสดง</th>
<td mat-cell *matCellDef="let role">{{ role.displayName }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>คำอธิบาย</th>
<td mat-cell *matCellDef="let role">{{ role.description || '-' }}</td>
</ng-container>
<ng-container matColumnDef="isSystem">
<th mat-header-cell *matHeaderCellDef>ระบบ</th>
<td mat-cell *matCellDef="let role">
<span class="badge" [class.badge-primary]="role.isSystem"
[class.badge-secondary]="!role.isSystem">
{{ role.isSystem ? 'ใช่' : 'ไม่' }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="isActive">
<th mat-header-cell *matHeaderCellDef>สถานะ</th>
<td mat-cell *matCellDef="let role">
<span class="badge badge-{{ getStatusColor(role.isActive) }}">
{{ getStatusText(role.isActive) }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>การดำเนินการ</th>
<td mat-cell *matCellDef="let role">
<button mat-icon-button matTooltip="แก้ไข">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button matTooltip="ลบ" (click)="deleteRole(role)">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="roleDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: roleDisplayedColumns;"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</mat-tab>
<!-- Permission Management Tab -->
<mat-tab label="จัดการสิทธิ์">
<div class="row mt-3">
<div class="col-12">
<mat-card>
<mat-card-header>
<mat-card-title>จัดการสิทธิ์การเข้าถึง</mat-card-title>
<mat-card-subtitle>กำหนดสิทธิ์สำหรับบทบาทและผู้ใช้</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div *ngIf="permissions$ | async as permissions">
<div *ngFor="let category of getPermissionCategories(permissions)" class="permission-category">
<h5>{{ category }}</h5>
<div class="permission-grid">
<div *ngFor="let permission of getPermissionsByCategory(permissions, category)"
class="permission-item">
<mat-checkbox [checked]="false">
{{ permission.displayName }}
</mat-checkbox>
<small class="text-muted">{{ permission.description }}</small>
</div>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</mat-tab>
<!-- Statistics Tab -->
<mat-tab label="สถิติ">
<div class="row mt-3">
<div class="col-12">
<mat-card>
<mat-card-header>
<mat-card-title>สถิติผู้ใช้และบทบาท</mat-card-title>
<mat-card-subtitle>ข้อมูลสถิติการใช้งานระบบ</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div *ngIf="statistics$ | async as stats" class="row">
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">{{ stats.totalUsers }}</div>
<div class="stat-label">ผู้ใช้ทั้งหมด</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">{{ stats.activeUsers }}</div>
<div class="stat-label">ผู้ใช้ที่ใช้งาน</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">{{ stats.inactiveUsers }}</div>
<div class="stat-label">ผู้ใช้ที่ไม่ใช้งาน</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">{{ (roles$ | async)?.length || 0 }}</div>
<div class="stat-label">บทบาททั้งหมด</div>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
</div>
</div>
</div>
</div>
.user-info {
.user-name {
font-weight: 500;
margin-bottom: 2px;
}
.user-email {
font-size: 0.875rem;
}
}
.badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
&.badge-success {
background-color: #4caf50;
color: white;
}
&.badge-danger {
background-color: #f44336;
color: white;
}
&.badge-primary {
background-color: #2196f3;
color: white;
}
&.badge-secondary {
background-color: #6c757d;
color: white;
}
}
.stat-card {
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px;
margin-bottom: 16px;
.stat-number {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 8px;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.9;
}
}
.permission-category {
margin-bottom: 24px;
h5 {
color: #333;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #e0e0e0;
}
.permission-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 12px;
.permission-item {
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #fafafa;
mat-checkbox {
margin-bottom: 4px;
}
small {
display: block;
margin-top: 4px;
}
}
}
}
.table-responsive {
overflow-x: auto;
}
mat-card {
margin-bottom: 16px;
}
mat-card-header {
margin-bottom: 16px;
}
mat-form-field {
margin-bottom: 16px;
}
// Form styling
form {
.row {
margin-bottom: 16px;
}
}
// Button styling
button[mat-raised-button] {
margin-right: 8px;
}
// Loading spinner
mat-spinner {
margin: 0 auto;
}
// Empty state
.text-center {
text-align: center;
}
.text-muted {
color: #6c757d;
}
// Card hover effects
mat-card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transition: box-shadow 0.2s ease;
}
// Responsive design
@media (max-width: 768px) {
.permission-grid {
grid-template-columns: 1fr;
}
.stat-card {
margin-bottom: 12px;
.stat-number {
font-size: 2rem;
}
}
.table-responsive {
font-size: 0.875rem;
}
}
// Material table styling
.mat-mdc-table {
width: 100%;
}
.mat-mdc-header-cell {
font-weight: 600;
color: #333;
}
.mat-mdc-cell {
padding: 8px 12px;
}
// Checkbox styling
mat-checkbox {
margin-right: 8px;
}
// Slide toggle styling
mat-slide-toggle {
margin: 8px 0;
}
// Form field styling
mat-form-field {
.mat-mdc-form-field-subscript-wrapper {
display: none;
}
}
// Tab styling
mat-tab-group {
margin-top: 16px;
}
// Card content spacing
mat-card-content {
padding: 16px;
}
// Row spacing
.row {
margin-bottom: 16px;
}
// Utility classes
.w-100 {
width: 100%;
}
.mt-3 {
margin-top: 16px;
}
.mb-3 {
margin-bottom: 16px;
}
.me-2 {
margin-right: 8px;
}
.ms-2 {
margin-left: 8px;
}
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatDialogModule, MatDialog } from '@angular/material/dialog';
import { MatTabsModule } from '@angular/material/tabs';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatChipInputEvent } from '@angular/material/chips';
import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { UserRoleService } from '../services/user-role.service';
import { User, Role, Permission, Department, Position, UserFilter, RoleFilter } from '../models/user-role.model';
@Component({
selector: 'app-user-role-management',
standalone: true,
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
MatCheckboxModule,
MatChipsModule,
MatDialogModule,
MatTabsModule,
MatSnackBarModule,
MatProgressSpinnerModule,
MatSlideToggleModule,
TranslateModule
],
templateUrl: './user-role-management.component.html',
styleUrls: ['./user-role-management.component.scss']
})
export class UserRoleManagementComponent implements OnInit {
users$: Observable<User[]>;
roles$: Observable<Role[]>;
permissions$: Observable<Permission[]>;
departments$: Observable<Department[]>;
positions$: Observable<Position[]>;
statistics$: Observable<any>;
// User management
userForm: FormGroup;
userFilter: UserFilter = {};
selectedUsers: User[] = [];
userDisplayedColumns: string[] = ['select', 'fullName', 'email', 'department', 'position', 'isActive', 'lastLogin', 'actions'];
// Role management
roleForm: FormGroup;
roleFilter: RoleFilter = {};
selectedRoles: Role[] = [];
roleDisplayedColumns: string[] = ['select', 'name', 'displayName', 'description', 'isSystem', 'isActive', 'actions'];
// Permission management
selectedRolePermissions: { [roleId: string]: string[] } = {};
selectedUserPermissions: { [userId: string]: string[] } = {};
isLoading = false;
constructor(
private userRoleService: UserRoleService,
private fb: FormBuilder,
private dialog: MatDialog,
private snackBar: MatSnackBar
) {
this.users$ = this.userRoleService.users$;
this.roles$ = this.userRoleService.roles$;
this.permissions$ = this.userRoleService.permissions$;
this.departments$ = this.userRoleService.departments$;
this.positions$ = this.userRoleService.positions$;
this.statistics$ = this.userRoleService.getUserStatistics();
this.userForm = this.fb.group({
username: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
firstName: ['', Validators.required],
lastName: ['', Validators.required],
phone: [''],
department: [''],
position: [''],
isActive: [true]
});
this.roleForm = this.fb.group({
name: ['', Validators.required],
displayName: ['', Validators.required],
description: [''],
isSystem: [false],
isActive: [true],
permissions: [[]]
});
}
ngOnInit(): void {
this.loadData();
}
loadData(): void {
this.isLoading = true;
this.userRoleService.getUsers(this.userFilter).subscribe({
next: () => this.isLoading = false,
error: () => this.isLoading = false
});
}
// User management methods
onUserFilterChange(): void {
this.loadData();
}
createUser(): void {
if (this.userForm.valid) {
const formValue = this.userForm.value;
const user = {
username: formValue.username,
email: formValue.email,
firstName: formValue.firstName,
lastName: formValue.lastName,
fullName: `${formValue.firstName} ${formValue.lastName}`,
phone: formValue.phone,
department: formValue.department,
position: formValue.position,
isActive: formValue.isActive
};
this.userRoleService.createUser(user).subscribe({
next: () => {
this.snackBar.open('สร้างผู้ใช้สำเร็จ', 'ปิด', { duration: 3000 });
this.userForm.reset();
this.loadData();
},
error: (error) => {
this.snackBar.open('เกิดข้อผิดพลาด: ' + error.message, 'ปิด', { duration: 5000 });
}
});
}
}
updateUser(user: User): void {
this.userRoleService.updateUser(user.id, user).subscribe({
next: () => {
this.snackBar.open('อัปเดตผู้ใช้สำเร็จ', 'ปิด', { duration: 3000 });
this.loadData();
},
error: (error) => {
this.snackBar.open('เกิดข้อผิดพลาด: ' + error.message, 'ปิด', { duration: 5000 });
}
});
}
deleteUser(user: User): void {
if (confirm(`คุณต้องการลบผู้ใช้ ${user.fullName} หรือไม่?`)) {
this.userRoleService.deleteUser(user.id).subscribe({
next: () => {
this.snackBar.open('ลบผู้ใช้สำเร็จ', 'ปิด', { duration: 3000 });
this.loadData();
},
error: (error) => {
this.snackBar.open('เกิดข้อผิดพลาด: ' + error.message, 'ปิด', { duration: 5000 });
}
});
}
}
// Role management methods
onRoleFilterChange(): void {
this.userRoleService.getRoles(this.roleFilter).subscribe();
}
createRole(): void {
if (this.roleForm.valid) {
const formValue = this.roleForm.value;
const role = {
name: formValue.name,
displayName: formValue.displayName,
description: formValue.description,
isSystem: formValue.isSystem,
isActive: formValue.isActive,
permissions: formValue.permissions || []
};
this.userRoleService.createRole(role).subscribe({
next: () => {
this.snackBar.open('สร้างบทบาทสำเร็จ', 'ปิด', { duration: 3000 });
this.roleForm.reset();
this.loadData();
},
error: (error) => {
this.snackBar.open('เกิดข้อผิดพลาด: ' + error.message, 'ปิด', { duration: 5000 });
}
});
}
}
updateRole(role: Role): void {
this.userRoleService.updateRole(role.id, role).subscribe({
next: () => {
this.snackBar.open('อัปเดตบทบาทสำเร็จ', 'ปิด', { duration: 3000 });
this.loadData();
},
error: (error) => {
this.snackBar.open('เกิดข้อผิดพลาด: ' + error.message, 'ปิด', { duration: 5000 });
}
});
}
deleteRole(role: Role): void {
if (confirm(`คุณต้องการลบบทบาท ${role.displayName} หรือไม่?`)) {
this.userRoleService.deleteRole(role.id).subscribe({
next: () => {
this.snackBar.open('ลบบทบาทสำเร็จ', 'ปิด', { duration: 3000 });
this.loadData();
},
error: (error) => {
this.snackBar.open('เกิดข้อผิดพลาด: ' + error.message, 'ปิด', { duration: 5000 });
}
});
}
}
// Permission management methods
onRolePermissionChange(roleId: string, permissionId: string, isChecked: boolean): void {
if (!this.selectedRolePermissions[roleId]) {
this.selectedRolePermissions[roleId] = [];
}
if (isChecked) {
if (!this.selectedRolePermissions[roleId].includes(permissionId)) {
this.selectedRolePermissions[roleId].push(permissionId);
}
} else {
this.selectedRolePermissions[roleId] = this.selectedRolePermissions[roleId].filter(p => p !== permissionId);
}
}
onUserPermissionChange(userId: string, permissionId: string, isChecked: boolean): void {
if (!this.selectedUserPermissions[userId]) {
this.selectedUserPermissions[userId] = [];
}
if (isChecked) {
if (!this.selectedUserPermissions[userId].includes(permissionId)) {
this.selectedUserPermissions[userId].push(permissionId);
}
} else {
this.selectedUserPermissions[userId] = this.selectedUserPermissions[userId].filter(p => p !== permissionId);
}
}
saveRolePermissions(roleId: string): void {
const permissions = this.selectedRolePermissions[roleId] || [];
// Implementation would save role permissions
this.snackBar.open('บันทึกสิทธิ์บทบาทสำเร็จ', 'ปิด', { duration: 3000 });
}
saveUserPermissions(userId: string): void {
const permissions = this.selectedUserPermissions[userId] || [];
// Implementation would save user permissions
this.snackBar.open('บันทึกสิทธิ์ผู้ใช้สำเร็จ', 'ปิด', { duration: 3000 });
}
// Selection methods
isAllUsersSelected(): boolean {
// Implementation for checking if all users are selected
return false;
}
isAllRolesSelected(): boolean {
// Implementation for checking if all roles are selected
return false;
}
masterUserSelectionChange(isChecked: boolean): void {
// Implementation for master user selection
}
masterRoleSelectionChange(isChecked: boolean): void {
// Implementation for master role selection
}
isUserSelected(user: User): boolean {
return this.selectedUsers.includes(user);
}
isRoleSelected(role: Role): boolean {
return this.selectedRoles.includes(role);
}
userSelectionChange(user: User, isChecked: boolean): void {
if (isChecked) {
if (!this.selectedUsers.includes(user)) {
this.selectedUsers.push(user);
}
} else {
this.selectedUsers = this.selectedUsers.filter(u => u.id !== user.id);
}
}
roleSelectionChange(role: Role, isChecked: boolean): void {
if (isChecked) {
if (!this.selectedRoles.includes(role)) {
this.selectedRoles.push(role);
}
} else {
this.selectedRoles = this.selectedRoles.filter(r => r.id !== role.id);
}
}
// Utility methods
getStatusColor(isActive: boolean): string {
return isActive ? 'success' : 'danger';
}
getStatusText(isActive: boolean): string {
return isActive ? 'ใช้งาน' : 'ไม่ใช้งาน';
}
formatDate(date: Date | string | undefined): string {
if (!date) return '-';
return new Date(date).toLocaleDateString('th-TH');
}
getPermissionCategories(permissions: Permission[]): string[] {
return [...new Set(permissions.map(p => p.category))];
}
getPermissionsByCategory(permissions: Permission[], category: string): Permission[] {
return permissions.filter(p => p.category === category);
}
}
import { Directive, Input, TemplateRef, ViewContainerRef, OnInit, OnDestroy } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { MenuPermissionService } from '../../portal-manage/services/menu-permission.service';
@Directive({
selector: '[appMenuPermission]'
})
export class MenuPermissionDirective implements OnInit, OnDestroy {
@Input() appMenuPermission: string = '';
@Input() appMenuPermissionType: 'view' | 'create' | 'edit' | 'delete' | 'export' | 'import' = 'view';
private destroy$ = new Subject<void>();
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private menuPermissionService: MenuPermissionService
) {}
ngOnInit(): void {
if (this.appMenuPermission) {
this.menuPermissionService.canAccessMenu(this.appMenuPermission, this.appMenuPermissionType)
.pipe(takeUntil(this.destroy$))
.subscribe(hasPermission => {
if (hasPermission) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
});
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"rooms": [
{
"id": "1",
"name": "ห้องประชุมใหญ่",
"capacity": 20,
"location": "ชั้น 5",
"floor": "5",
"building": "อาคาร A",
"amenities": ["โปรเจคเตอร์", "ไวท์บอร์ด", "ระบบเสียง", "WiFi"],
"isActive": true,
"description": "ห้องประชุมขนาดใหญ่เหมาะสำหรับการประชุมสำคัญ"
},
{
"id": "2",
"name": "ห้องประชุมเล็ก",
"capacity": 8,
"location": "ชั้น 3",
"floor": "3",
"building": "อาคาร A",
"amenities": ["โปรเจคเตอร์", "ไวท์บอร์ด", "WiFi"],
"isActive": true,
"description": "ห้องประชุมขนาดเล็กเหมาะสำหรับการประชุมทีม"
},
{
"id": "3",
"name": "ห้องประชุม VIP",
"capacity": 12,
"location": "ชั้น 10",
"floor": "10",
"building": "อาคาร B",
"amenities": ["โปรเจคเตอร์", "ไวท์บอร์ด", "ระบบเสียง", "WiFi", "เครื่องปรับอากาศ", "โต๊ะประชุมไม้"],
"isActive": true,
"description": "ห้องประชุมระดับ VIP สำหรับการประชุมสำคัญ"
}
],
"bookings": [
{
"id": "1",
"roomId": "1",
"roomName": "ห้องประชุมใหญ่",
"title": "ประชุมทีมพัฒนา",
"description": "ประชุมรายสัปดาห์ของทีมพัฒนา",
"startDateTime": "2024-01-15T09:00:00.000Z",
"endDateTime": "2024-01-15T10:00:00.000Z",
"organizerId": "1",
"organizerName": "สมชาย ใจดี",
"attendees": [
{
"userId": "2",
"userName": "สมหญิง รักงาน",
"email": "somying@company.com",
"status": "accepted",
"responseDate": "2024-01-14T10:30:00.000Z"
},
{
"userId": "3",
"userName": "สมศักดิ์ ทำงาน",
"email": "somsak@company.com",
"status": "pending"
}
],
"status": "confirmed",
"createdAt": "2024-01-14T08:00:00.000Z",
"updatedAt": "2024-01-14T08:00:00.000Z",
"createdBy": "1",
"updatedBy": "1"
},
{
"id": "2",
"roomId": "2",
"roomName": "ห้องประชุมเล็ก",
"title": "ประชุมทีมขาย",
"description": "ประชุมรายงานผลการขายประจำเดือน",
"startDateTime": "2024-01-16T14:00:00.000Z",
"endDateTime": "2024-01-16T15:30:00.000Z",
"organizerId": "2",
"organizerName": "สมหญิง รักงาน",
"attendees": [
{
"userId": "4",
"userName": "สมพร ขายดี",
"email": "somporn@company.com",
"status": "accepted",
"responseDate": "2024-01-15T16:00:00.000Z"
}
],
"status": "pending",
"createdAt": "2024-01-15T09:00:00.000Z",
"updatedAt": "2024-01-15T09:00:00.000Z",
"createdBy": "2",
"updatedBy": "2"
}
]
}
[
{
"id": "dashboard",
"name": "แดชบอร์ด",
"path": "/portal-manage/dashboard",
"icon": "grid-alt",
"order": 1,
"isVisible": true,
"permissions": {
"view": true,
"create": true,
"edit": true,
"delete": true,
"export": true,
"import": false
},
"children": [
{
"id": "dashboard-management",
"name": "จัดการแดชบอร์ด",
"path": "/portal-manage/dashboard/management",
"order": 1,
"isVisible": true,
"permissions": {
"view": true,
"create": true,
"edit": true,
"delete": true,
"export": true,
"import": false
}
},
{
"id": "widget-warehouse",
"name": "คลังวิดเจ็ต",
"path": "/portal-manage/dashboard/widget-warehouse",
"order": 2,
"isVisible": true,
"permissions": {
"view": true,
"create": true,
"edit": true,
"delete": true,
"export": true,
"import": true
}
},
{
"id": "widget-linker",
"name": "เชื่อมโยงวิดเจ็ตกับชุดข้อมูล",
"path": "/portal-manage/dashboard/widget-linker",
"icon": "link",
"order": 3,
"isVisible": true,
"permissions": {
"view": true,
"create": true,
"edit": true,
"delete": true,
"export": false,
"import": false
}
}
]
},
{
"id": "company-management",
"name": "จัดการบริษัท",
"path": "/portal-manage/company-management",
"icon": "building",
"order": 2,
"isVisible": true,
"permissions": {
"view": true,
"create": true,
"edit": true,
"delete": true,
"export": true,
"import": false
}
},
{
"id": "permission-management",
"name": "จัดการสิทธิ์",
"path": "/portal-manage/permission-management",
"icon": "shield",
"order": 3,
"isVisible": true,
"permissions": {
"view": true,
"create": true,
"edit": true,
"delete": true,
"export": true,
"import": false
}
},
{
"id": "meeting-booking",
"name": "จองห้องประชุม",
"path": "/portal-manage/meeting-booking",
"icon": "calendar",
"order": 4,
"isVisible": true,
"permissions": {
"view": true,
"create": true,
"edit": true,
"delete": true,
"export": false,
"import": false
}
},
{
"id": "myhr-lite",
"name": "MyHR Lite",
"path": "/portal-manage/myhr-lite",
"icon": "user",
"order": 5,
"isVisible": true,
"permissions": {
"view": true,
"create": true,
"edit": true,
"delete": false,
"export": true,
"import": false
}
},
{
"id": "myhr-plus",
"name": "MyHR Plus",
"path": "/portal-manage/myhr-plus",
"icon": "user-check",
"order": 6,
"isVisible": true,
"permissions": {
"view": true,
"create": true,
"edit": true,
"delete": false,
"export": true,
"import": false
}
},
{
"id": "myjob",
"name": "MyJob",
"path": "/portal-manage/myjob",
"icon": "briefcase",
"order": 7,
"isVisible": true,
"permissions": {
"view": true,
"create": true,
"edit": true,
"delete": false,
"export": true,
"import": false
}
},
{
"id": "mylearn",
"name": "MyLearn",
"path": "/portal-manage/mylearn",
"icon": "book",
"order": 8,
"isVisible": true,
"permissions": {
"view": true,
"create": true,
"edit": true,
"delete": false,
"export": true,
"import": false
}
}
]
{
"users": [
{
"id": "1",
"username": "admin",
"email": "admin@company.com",
"firstName": "สมชาย",
"lastName": "ใจดี",
"fullName": "สมชาย ใจดี",
"phone": "081-234-5678",
"department": "IT",
"position": "System Administrator",
"isActive": true,
"lastLogin": "2024-01-15T08:30:00.000Z",
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-15T08:30:00.000Z",
"createdBy": "system",
"updatedBy": "system"
},
{
"id": "2",
"username": "manager",
"email": "manager@company.com",
"firstName": "สมหญิง",
"lastName": "รักงาน",
"fullName": "สมหญิง รักงาน",
"phone": "081-234-5679",
"department": "HR",
"position": "HR Manager",
"isActive": true,
"lastLogin": "2024-01-14T17:00:00.000Z",
"createdAt": "2024-01-02T00:00:00.000Z",
"updatedAt": "2024-01-14T17:00:00.000Z",
"createdBy": "1",
"updatedBy": "1"
},
{
"id": "3",
"username": "user1",
"email": "user1@company.com",
"firstName": "สมศักดิ์",
"lastName": "ทำงาน",
"fullName": "สมศักดิ์ ทำงาน",
"phone": "081-234-5680",
"department": "IT",
"position": "Developer",
"isActive": true,
"lastLogin": "2024-01-14T09:00:00.000Z",
"createdAt": "2024-01-03T00:00:00.000Z",
"updatedAt": "2024-01-14T09:00:00.000Z",
"createdBy": "1",
"updatedBy": "1"
}
],
"roles": [
{
"id": "admin",
"name": "admin",
"displayName": "ผู้ดูแลระบบ",
"description": "มีสิทธิ์เข้าถึงระบบทั้งหมด",
"isSystem": true,
"isActive": true,
"permissions": ["*"],
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z",
"createdBy": "system",
"updatedBy": "system"
},
{
"id": "manager",
"name": "manager",
"displayName": "ผู้จัดการ",
"description": "สามารถจัดการทีมและดูรายงาน",
"isSystem": false,
"isActive": true,
"permissions": ["user.view", "user.edit", "report.view", "meeting.manage"],
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z",
"createdBy": "system",
"updatedBy": "system"
},
{
"id": "user",
"name": "user",
"displayName": "ผู้ใช้ทั่วไป",
"description": "สิทธิ์พื้นฐานในการใช้งานระบบ",
"isSystem": false,
"isActive": true,
"permissions": ["profile.view", "profile.edit", "meeting.book"],
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z",
"createdBy": "system",
"updatedBy": "system"
}
],
"permissions": [
{
"id": "user.view",
"name": "user.view",
"displayName": "ดูข้อมูลผู้ใช้",
"description": "สามารถดูข้อมูลผู้ใช้ได้",
"category": "User Management",
"resource": "user",
"action": "view",
"isSystem": true
},
{
"id": "user.edit",
"name": "user.edit",
"displayName": "แก้ไขข้อมูลผู้ใช้",
"description": "สามารถแก้ไขข้อมูลผู้ใช้ได้",
"category": "User Management",
"resource": "user",
"action": "edit",
"isSystem": true
},
{
"id": "user.create",
"name": "user.create",
"displayName": "สร้างผู้ใช้ใหม่",
"description": "สามารถสร้างผู้ใช้ใหม่ได้",
"category": "User Management",
"resource": "user",
"action": "create",
"isSystem": true
},
{
"id": "user.delete",
"name": "user.delete",
"displayName": "ลบผู้ใช้",
"description": "สามารถลบผู้ใช้ได้",
"category": "User Management",
"resource": "user",
"action": "delete",
"isSystem": true
},
{
"id": "role.view",
"name": "role.view",
"displayName": "ดูข้อมูลบทบาท",
"description": "สามารถดูข้อมูลบทบาทได้",
"category": "Role Management",
"resource": "role",
"action": "view",
"isSystem": true
},
{
"id": "role.edit",
"name": "role.edit",
"displayName": "แก้ไขข้อมูลบทบาท",
"description": "สามารถแก้ไขข้อมูลบทบาทได้",
"category": "Role Management",
"resource": "role",
"action": "edit",
"isSystem": true
},
{
"id": "role.create",
"name": "role.create",
"displayName": "สร้างบทบาทใหม่",
"description": "สามารถสร้างบทบาทใหม่ได้",
"category": "Role Management",
"resource": "role",
"action": "create",
"isSystem": true
},
{
"id": "role.delete",
"name": "role.delete",
"displayName": "ลบบทบาท",
"description": "สามารถลบบทบาทได้",
"category": "Role Management",
"resource": "role",
"action": "delete",
"isSystem": true
},
{
"id": "meeting.manage",
"name": "meeting.manage",
"displayName": "จัดการการประชุม",
"description": "สามารถจัดการการประชุมได้",
"category": "Meeting Management",
"resource": "meeting",
"action": "manage",
"isSystem": true
},
{
"id": "meeting.book",
"name": "meeting.book",
"displayName": "จองห้องประชุม",
"description": "สามารถจองห้องประชุมได้",
"category": "Meeting Management",
"resource": "meeting",
"action": "book",
"isSystem": true
},
{
"id": "dashboard.view",
"name": "dashboard.view",
"displayName": "ดูแดชบอร์ด",
"description": "สามารถดูแดชบอร์ดได้",
"category": "Dashboard",
"resource": "dashboard",
"action": "view",
"isSystem": true
},
{
"id": "dashboard.manage",
"name": "dashboard.manage",
"displayName": "จัดการแดชบอร์ด",
"description": "สามารถจัดการแดชบอร์ดได้",
"category": "Dashboard",
"resource": "dashboard",
"action": "manage",
"isSystem": true
}
],
"departments": [
{
"id": "1",
"name": "IT",
"description": "แผนกเทคโนโลยีสารสนเทศ",
"isActive": true,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
},
{
"id": "2",
"name": "HR",
"description": "แผนกทรัพยากรบุคคล",
"isActive": true,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
},
{
"id": "3",
"name": "Finance",
"description": "แผนกการเงิน",
"isActive": true,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
},
{
"id": "4",
"name": "Marketing",
"description": "แผนกการตลาด",
"isActive": true,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}
],
"positions": [
{
"id": "1",
"name": "System Administrator",
"description": "ผู้ดูแลระบบ",
"level": 5,
"departmentId": "1",
"isActive": true,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
},
{
"id": "2",
"name": "HR Manager",
"description": "ผู้จัดการทรัพยากรบุคคล",
"level": 4,
"departmentId": "2",
"isActive": true,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
},
{
"id": "3",
"name": "Developer",
"description": "นักพัฒนา",
"level": 3,
"departmentId": "1",
"isActive": true,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
},
{
"id": "4",
"name": "HR Officer",
"description": "เจ้าหน้าที่ทรัพยากรบุคคล",
"level": 2,
"departmentId": "2",
"isActive": true,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}
]
}
......@@ -15,6 +15,8 @@
<link rel="stylesheet" href="assets/JS/pace/themes/silver/pace-theme-flash.css" />
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Thai:wght@100..900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Remix Icons -->
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.0.0/fonts/remixicon.css" rel="stylesheet">
<!-- <link
href="https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700;800&display=swap"
rel="stylesheet"
......
......@@ -26,6 +26,8 @@ module.exports = {
md: "0.5rem",
lg: "0.75rem",
xl: "1rem",
"2xl": "1rem",
"3xl": "1.5rem",
full: "9999px",
},
fontFamily: {
......@@ -137,6 +139,14 @@ module.exports = {
"gradient-1": "linear-gradient(102deg,transparent 41%,primary/50 0)",
"gradient-1": "linear-gradient(102deg,light 41%,transparent 0)",
},
backdropBlur: {
'sm': '4px',
'md': '8px',
'lg': '12px',
'xl': '16px',
'2xl': '24px',
'3xl': '40px',
},
},
animation: {
projects: "particles 2s linear infinite",
......
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