07fec02ea1
- Added logo image to assets. - Implemented RTL support styles in rtlSupport.scss for various components including breadcrumb, datatable, and tree node toggle button. chore: configure environment files for production, staging, and development - Created environment.prod.ts for production settings. - Created environment.staging.ts for staging settings. - Created environment.ts for local development settings. feat: define custom theme preset - Added presets.ts to define a custom theme preset using PrimeUIX with specific component styles and semantic colors. chore: set up proxy configuration for local development
327 lines
9.0 KiB
TypeScript
327 lines
9.0 KiB
TypeScript
import { Injectable, inject } from '@angular/core';
|
|
import { combineLatest, map, shareReplay, startWith } from 'rxjs';
|
|
import { LOCAL_STORAGE_KEYS } from 'src/assets/constants';
|
|
import { AuthStore } from '../../modules/auth/state/auth.store';
|
|
import { GlobalStore } from './global/global.store';
|
|
|
|
/**
|
|
* State effects service for handling cross-store interactions
|
|
*/
|
|
@Injectable({
|
|
providedIn: 'root',
|
|
})
|
|
export class StateEffects {
|
|
private readonly authStore = inject(AuthStore);
|
|
private readonly globalStore = inject(GlobalStore);
|
|
|
|
constructor() {
|
|
this.initializeEffects();
|
|
}
|
|
|
|
/**
|
|
* Initialize all effects
|
|
*/
|
|
private initializeEffects(): void {
|
|
this.setupAuthEffects();
|
|
this.setupGlobalEffects();
|
|
}
|
|
|
|
/**
|
|
* Setup authentication-related effects
|
|
*/
|
|
private setupAuthEffects(): void {
|
|
// Update global notifications when auth state changes
|
|
this.authStore.state$.subscribe((authState) => {
|
|
// if (authState.isAuthenticated && authState.user) {
|
|
// this.globalStore.addNotification({
|
|
// type: 'success',
|
|
// title: 'خوش آمدید',
|
|
// message: `${authState.user.firstName} ${authState.user.lastName} عزیز، خوش آمدید`,
|
|
// });
|
|
// }
|
|
});
|
|
|
|
// Handle session expiry warnings
|
|
this.authStore.state$.subscribe((authState) => {
|
|
if (authState.sessionExpiry) {
|
|
const timeUntilExpiry = authState.sessionExpiry - Date.now();
|
|
const minutesUntilExpiry = Math.floor(timeUntilExpiry / 60000);
|
|
|
|
// Show warning when 5 minutes remain
|
|
if (minutesUntilExpiry === 5) {
|
|
this.globalStore.addNotification({
|
|
type: 'warning',
|
|
title: 'هشدار انقضای جلسه',
|
|
message: 'جلسه شما در 5 دقیقه منقضی میشود',
|
|
actions: [
|
|
{
|
|
label: 'تمدید جلسه',
|
|
action: () => this.authStore.refreshAuthToken().subscribe(),
|
|
type: 'primary',
|
|
},
|
|
],
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup global effects
|
|
*/
|
|
private setupGlobalEffects(): void {
|
|
// Auto-save preferences when they change
|
|
this.globalStore.state$.subscribe((globalState) => {
|
|
localStorage.setItem('user_preferences', JSON.stringify(globalState.preferences));
|
|
});
|
|
|
|
// Handle theme changes
|
|
this.globalStore.state$.subscribe((globalState) => {
|
|
document.documentElement.setAttribute('data-theme', globalState.theme);
|
|
});
|
|
|
|
// Handle language changes
|
|
this.globalStore.state$.subscribe((globalState) => {
|
|
document.documentElement.setAttribute('lang', globalState.language);
|
|
document.documentElement.setAttribute('dir', globalState.language === 'fa' ? 'rtl' : 'ltr');
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* State selectors for complex cross-store queries
|
|
*/
|
|
@Injectable({
|
|
providedIn: 'root',
|
|
})
|
|
export class StateSelectors {
|
|
private readonly authStore = inject(AuthStore);
|
|
private readonly globalStore = inject(GlobalStore);
|
|
|
|
/**
|
|
* Combined application state for UI components
|
|
*/
|
|
readonly appState$ = combineLatest([this.authStore.state$, this.globalStore.state$]).pipe(
|
|
map(([authState, globalState]) => ({
|
|
isAuthenticated: authState.isAuthenticated,
|
|
user: authState.user,
|
|
userRole: authState.user?.role,
|
|
loading: authState.loading || globalState.loading,
|
|
sidebarCollapsed: globalState.sidebarCollapsed,
|
|
theme: globalState.theme,
|
|
language: globalState.language,
|
|
notifications: globalState.notifications,
|
|
unreadCount: globalState.notifications.filter((n) => !n.read).length,
|
|
breadcrumbs: globalState.breadcrumbs,
|
|
isOnline: globalState.isOnline,
|
|
})),
|
|
shareReplay(1),
|
|
);
|
|
|
|
/**
|
|
* User permissions combined with role-based access
|
|
*/
|
|
readonly userAccess$ = this.authStore.state$.pipe(
|
|
map((authState) => ({
|
|
isAuthenticated: authState.isAuthenticated,
|
|
role: authState.user?.role,
|
|
permissions: authState.permissions,
|
|
canAccessAdmin: authState.user?.role === 'ADMIN' || authState.user?.role === 'SUPERADMIN',
|
|
canAccessStudent: authState.user?.role === 'STUDENTS',
|
|
canAccessGrader: authState.user?.role === 'GRADER',
|
|
canAccessSchools: authState.user?.role === 'SCHOOL',
|
|
canAccessSuperAdmin: authState.user?.role === 'SUPERADMIN',
|
|
})),
|
|
shareReplay(1),
|
|
);
|
|
|
|
/**
|
|
* Navigation state for header/sidebar components
|
|
*/
|
|
readonly navigationState$ = combineLatest([this.authStore.state$, this.globalStore.state$]).pipe(
|
|
map(([authState, globalState]) => ({
|
|
user: authState.user,
|
|
sidebarCollapsed: globalState.sidebarCollapsed,
|
|
breadcrumbs: globalState.breadcrumbs,
|
|
// userInitials: authState.user
|
|
// ? `${authState.user.firstName[0]}${authState.user.lastName[0]}`
|
|
// : '',
|
|
// userFullName: authState.user ? `${authState.user.firstName} ${authState.user.lastName}` : '',
|
|
})),
|
|
shareReplay(1),
|
|
);
|
|
|
|
/**
|
|
* Notification state for notification components
|
|
*/
|
|
readonly notificationState$ = this.globalStore.state$.pipe(
|
|
map((globalState) => ({
|
|
notifications: globalState.notifications,
|
|
unreadCount: globalState.notifications.filter((n) => !n.read).length,
|
|
hasUnread: globalState.notifications.some((n) => !n.read),
|
|
recentNotifications: globalState.notifications
|
|
.sort((a, b) => b.timestamp - a.timestamp)
|
|
.slice(0, 5),
|
|
})),
|
|
shareReplay(1),
|
|
);
|
|
|
|
/**
|
|
* Loading state across all stores
|
|
*/
|
|
readonly loadingState$ = combineLatest([this.authStore.state$, this.globalStore.state$]).pipe(
|
|
map(([authState, globalState]) => ({
|
|
isLoading: authState.loading || globalState.loading,
|
|
authLoading: authState.loading,
|
|
globalLoading: globalState.loading,
|
|
})),
|
|
startWith({ isLoading: false, authLoading: false, globalLoading: false }),
|
|
shareReplay(1),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* State utilities for common operations
|
|
*/
|
|
@Injectable({
|
|
providedIn: 'root',
|
|
})
|
|
export class StateUtils {
|
|
private readonly authStore = inject(AuthStore);
|
|
private readonly globalStore = inject(GlobalStore);
|
|
|
|
/**
|
|
* Check if user can access route
|
|
*/
|
|
canAccessRoute(requiredRoles?: string[], requiredPermissions?: string[]): boolean {
|
|
const authState = this.authStore.getCurrentState();
|
|
|
|
if (!authState.isAuthenticated) {
|
|
return false;
|
|
}
|
|
|
|
if (requiredRoles && requiredRoles.length > 0) {
|
|
const userRole = authState.user?.role;
|
|
if (!userRole || !requiredRoles.includes(userRole)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (requiredPermissions && requiredPermissions.length > 0) {
|
|
const hasAllPermissions = requiredPermissions.every((permission) =>
|
|
authState.permissions.includes(permission),
|
|
);
|
|
if (!hasAllPermissions) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Show success message
|
|
*/
|
|
showSuccess(title: string, message: string): void {
|
|
this.globalStore.addNotification({
|
|
type: 'success',
|
|
title,
|
|
message,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show error message
|
|
*/
|
|
showError(title: string, message: string): void {
|
|
this.globalStore.addNotification({
|
|
type: 'error',
|
|
title,
|
|
message,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show warning message
|
|
*/
|
|
showWarning(title: string, message: string): void {
|
|
this.globalStore.addNotification({
|
|
type: 'warning',
|
|
title,
|
|
message,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show info message
|
|
*/
|
|
showInfo(title: string, message: string): void {
|
|
this.globalStore.addNotification({
|
|
type: 'info',
|
|
title,
|
|
message,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update page breadcrumbs
|
|
*/
|
|
setBreadcrumbs(breadcrumbs: Array<{ label: string; url?: string; icon?: string }>): void {
|
|
this.globalStore.setBreadcrumbs(breadcrumbs);
|
|
}
|
|
|
|
/**
|
|
* Get user display name
|
|
*/
|
|
getUserDisplayName(): string {
|
|
const user = this.authStore.getCurrentState().user;
|
|
return '';
|
|
// return user ? `${user.firstName} ${user.lastName}` : '';
|
|
}
|
|
|
|
/**
|
|
* Get user initials
|
|
*/
|
|
getUserInitials(): string {
|
|
const user = this.authStore.getCurrentState().user;
|
|
return '';
|
|
// return user ? `${user.firstName[0]}${user.lastName[0]}` : '';
|
|
}
|
|
|
|
/**
|
|
* Check if user is online
|
|
*/
|
|
isOnline(): boolean {
|
|
return this.globalStore.getCurrentState().isOnline;
|
|
}
|
|
|
|
/**
|
|
* Get current theme
|
|
*/
|
|
getCurrentTheme(): 'light' | 'dark' {
|
|
return this.globalStore.getCurrentState().theme;
|
|
}
|
|
|
|
/**
|
|
* Get current language
|
|
*/
|
|
getCurrentLanguage(): 'fa' | 'en' {
|
|
return this.globalStore.getCurrentState().language;
|
|
}
|
|
|
|
/**
|
|
* Clear all application state (useful for logout)
|
|
*/
|
|
clearAllState(): void {
|
|
this.authStore.reset();
|
|
this.globalStore.reset();
|
|
|
|
// Clear localStorage
|
|
localStorage.removeItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN);
|
|
localStorage.removeItem(LOCAL_STORAGE_KEYS.REFRESH_TOKEN);
|
|
localStorage.removeItem(LOCAL_STORAGE_KEYS.USER_DATA);
|
|
localStorage.removeItem(LOCAL_STORAGE_KEYS.LAST_LOGIN_TIME);
|
|
localStorage.removeItem(LOCAL_STORAGE_KEYS.LOGIN_ATTEMPTS);
|
|
}
|
|
}
|