This commit is contained in:
2026-03-10 13:36:45 +03:30
parent 45d48f79dc
commit b379787002
208 changed files with 2328 additions and 2510 deletions
+30 -3
View File
@@ -6,13 +6,20 @@ import { IPaginatedQuery, IResponseMetadata } from '../models/service.model';
/**
* Base interface for all state objects
*/
export interface BaseState<T = any> {
export interface BaseState {
loading: boolean;
error: Maybe<string>;
isRefreshing?: boolean;
initialized: boolean;
}
/**
* Base interface for entity
*/
export interface EntityState<T = any> extends BaseState {
entity: Maybe<T>;
}
/**
* Base interface for paginated lists
*/
@@ -29,7 +36,7 @@ export interface PaginatedState<T = any> extends BaseState {
/**
* Base interface for entity states (CRUD operations)
*/
export interface EntityState<T = any> extends BaseState {
export interface EntitiesState<T = any> extends BaseState {
entities: Record<string | number, T>;
selectedId: Maybe<string | number>;
ids: (string | number)[];
@@ -158,11 +165,31 @@ export abstract class BaseStore<T extends BaseState> {
}
/**
* Entity store with CRUD operations
* Entity store
*/
export abstract class EntityStore<
T,
TState extends EntityState<T> = EntityState<T>,
> extends BaseStore<TState> {
// Computed selectors
readonly entity = computed(() => this._state().entity);
/**
* Set entity
*/
setEntity(entity: Maybe<T>): void {
this.patchState({
entity,
} as Partial<TState>);
}
}
/**
* Entity store with CRUD operations
*/
export abstract class EntitiesStore<
T,
TState extends EntitiesState<T> = EntitiesState<T>,
> extends BaseStore<TState> {
// Computed selectors
readonly entities = computed(() => this._state().entities);
+3 -2
View File
@@ -4,13 +4,14 @@ export * from './base-store';
// Global state management
export * from './global/global.store';
// State effects and utilities
export * from './state-effects.service';
// // State effects and utilities
// export * from './state-effects.service';
// State management interfaces
export type {
Action,
BaseState,
EntitiesState,
EntityState,
LoadingState,
PaginatedState,
-326
View File
@@ -1,326 +0,0 @@
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);
}
}