diff --git a/src/app/app.ts b/src/app/app.ts index c8d8d0c..fb1a49b 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -7,6 +7,7 @@ import { provideIcons } from '@ng-icons/core'; import { Title } from '@angular/platform-browser'; import { filter, map, mergeMap } from 'rxjs'; import { AuthService } from './core/services/auth.service'; +import { RoleSyncService } from './core/services/role-sync.service'; @Component({ selector: 'app-root', @@ -22,6 +23,7 @@ export class App implements OnInit { private router = inject(Router); private activatedRoute = inject(ActivatedRoute); private authService = inject(AuthService); + private roleSyncService = inject(RoleSyncService); private cdr = inject(ChangeDetectorRef); async ngOnInit(): Promise { diff --git a/src/app/core/models/dcb-bo-hub-transaction.model.ts b/src/app/core/models/dcb-bo-hub-transaction.model.ts new file mode 100644 index 0000000..507189c --- /dev/null +++ b/src/app/core/models/dcb-bo-hub-transaction.model.ts @@ -0,0 +1,149 @@ +// [file name]: transactions/models/transaction.ts +import { Currency } from '@core/models/dcb-bo-hub-subscription.model'; + +// Types de transaction basés sur les abonnements +export enum TransactionType { + SUBSCRIPTION_PAYMENT = 'SUBSCRIPTION_PAYMENT', + SUBSCRIPTION_RENEWAL = 'SUBSCRIPTION_RENEWAL', + ONE_TIME_PAYMENT = 'ONE_TIME_PAYMENT' +} + +export interface Transaction { + // Identifiants + id: string; + externalReference?: string; + subscriptionId?: number; + + // Informations financières + amount: number; + currency: Currency; + + // Statut et type + status: TransactionStatus; + type: TransactionType; + + // Informations produit/abonnement + productId: string; + productName: string; + periodicity?: string; + + // Dates + transactionDate: Date; + createdAt: Date; + updatedAt: Date; + nextPaymentDate?: Date; + + // Informations marchand + merchantPartnerId?: number; + + // Métadonnées + metadata?: any; +} + +export interface TransactionQuery { + page?: number; + limit?: number; + search?: string; + status?: TransactionStatus; + startDate?: Date; + endDate?: Date; + merchantPartnerId?: number; + periodicity?: string; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +} + +export interface TransactionStats { + total: number; + totalAmount: number; + successCount: number; + failedCount: number; + pendingCount: number; + averageAmount: number; + byPeriodicity?: { + DAILY: number; + WEEKLY: number; + MONTHLY: number; + YEARLY: number; + }; +} + +export interface PaginatedTransactions { + data: Transaction[]; + total: number; + page: number; + limit: number; + totalPages: number; + stats: TransactionStats; +} + +export type TransactionStatus = + | 'PENDING' + | 'SUCCESS' + | 'FAILED' + | 'CANCELLED' + | 'EXPIRED'; + +export interface RefundRequest { + transactionId: string; + subscriptionId?: number; + reason?: string; + amount?: number; +} + +export interface TransactionExportRequest { + format: 'csv' | 'excel' | 'pdf'; + query: TransactionQuery; + columns?: string[]; +} + +// Utilitaires pour les transactions +export class TransactionUtils { + static getStatusDisplayName(status: TransactionStatus): string { + const statusNames = { + 'PENDING': 'En attente', + 'SUCCESS': 'Réussi', + 'FAILED': 'Échoué', + 'CANCELLED': 'Annulé', + 'EXPIRED': 'Expiré' + }; + return statusNames[status] || status; + } + + static getTypeDisplayName(type: TransactionType): string { + const typeNames = { + [TransactionType.SUBSCRIPTION_PAYMENT]: 'Paiement d\'abonnement', + [TransactionType.SUBSCRIPTION_RENEWAL]: 'Renouvellement d\'abonnement', + [TransactionType.ONE_TIME_PAYMENT]: 'Paiement unique' + }; + return typeNames[type] || type; + } + + static formatAmount(amount: number, currency: Currency): string { + return new Intl.NumberFormat('fr-FR', { + style: 'currency', + currency: currency + }).format(amount); + } + + static getPeriodicityDisplayName(periodicity: string): string { + const periodicityNames: Record = { + 'Daily': 'Quotidien', + 'Weekly': 'Hebdomadaire', + 'Monthly': 'Mensuel', + 'Yearly': 'Annuel' + }; + return periodicityNames[periodicity] || periodicity; + } + + static mapSubscriptionStatus(status: string): TransactionStatus { + const statusMap: Record = { + 'ACTIVE': 'SUCCESS', + 'PENDING': 'PENDING', + 'SUSPENDED': 'FAILED', + 'CANCELLED': 'CANCELLED', + 'EXPIRED': 'EXPIRED' + }; + return statusMap[status] || 'PENDING'; + } +} \ No newline at end of file diff --git a/src/app/core/models/dcb-bo-hub-user.model.ts b/src/app/core/models/dcb-bo-hub-user.model.ts index f5fd487..6d54d92 100644 --- a/src/app/core/models/dcb-bo-hub-user.model.ts +++ b/src/app/core/models/dcb-bo-hub-user.model.ts @@ -46,24 +46,36 @@ export interface UsersStatistics { totalUsers: number; } -// dcb-bo-hub-user.model.ts - MIS À JOUR export interface User { - id: string; // UUID Keycloak + id: string; username: string; email: string; - firstName: string; - lastName: string; + firstName?: string; + lastName?: string; enabled: boolean; emailVerified: boolean; - userType: UserType; // HUB ou MERCHANT_PARTNER + userType: UserType; // HUB ou MERCHANT_PARTNER + role: UserRole; // Rôle(s) - role: UserRole; - + // Champ critique pour la logique + merchantPartnerId?: string; // ID INT dans Merchant Config pour CE merchant + + // Métadonnées createdBy?: string; createdByUsername?: string; - createdTimestamp: number; + createdTimestamp?: number; lastLogin?: number; - profileImage?: string | null +} + +export interface AssociateUserToMerchantDto { + userId: string; + merchantPartnerId: string; + role: UserRole; // Rôle à assigner dans le contexte du merchant +} + +export interface DissociateUserFromMerchantDto { + userId: string; + merchantPartnerId: string; } export interface SyncResult { @@ -84,6 +96,7 @@ export interface CreateUserDto { role: UserRole; enabled?: boolean; emailVerified?: boolean; + merchantPartnerId?: string; } export interface UpdateUserDto { @@ -142,6 +155,7 @@ export interface SearchUsersParams { role?: UserRole; enabled?: boolean; userType?: UserType; + merchantPartnerId?: string; page?: number; limit?: number; } diff --git a/src/app/core/models/merchant-config.model.ts b/src/app/core/models/merchant-config.model.ts index 9c1e0c2..a219560 100644 --- a/src/app/core/models/merchant-config.model.ts +++ b/src/app/core/models/merchant-config.model.ts @@ -63,9 +63,7 @@ export interface MerchantUser { username?: string; email?: string; firstName?: string; - lastName?: string; - merchantPartnerId?: number - merchantConfigId?: string; // Référence au merchant dans MerchantConfig + lastName?: string; // Référence au merchant dans MerchantConfig createdAt?: string; updatedAt?: string; } @@ -113,8 +111,6 @@ export interface ApiMerchantUser { email?: string; firstName?: string; lastName?: string; - merchantPartnerId?: number; - merchantConfigId?: string; createdAt?: string; updatedAt?: string; } diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts index 0ceb7d2..1d657d2 100644 --- a/src/app/core/services/auth.service.ts +++ b/src/app/core/services/auth.service.ts @@ -1,15 +1,18 @@ import { Injectable, inject } from '@angular/core'; -import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { Router } from '@angular/router'; import { environment } from '@environments/environment'; -import { BehaviorSubject, Observable, throwError, tap, catchError } from 'rxjs'; +import { BehaviorSubject, Observable, throwError, tap, catchError, finalize, of } from 'rxjs'; import { firstValueFrom } from 'rxjs'; +import { DashboardAccessService } from '@modules/dcb-dashboard/services/dashboard-access.service'; + import { User, UserType, UserRole, } from '@core/models/dcb-bo-hub-user.model'; +import { TransactionAccessService } from '@modules/transactions/services/transaction-access.service'; // === INTERFACES DTO AUTH === export interface LoginDto { @@ -58,6 +61,9 @@ export class AuthService { private readonly tokenKey = 'access_token'; private readonly refreshTokenKey = 'refresh_token'; + + private readonly dashboardAccessService = inject(DashboardAccessService); + private readonly transactionAccessService = inject(TransactionAccessService); private authState$ = new BehaviorSubject(this.isAuthenticated()); private userProfile$ = new BehaviorSubject(null); @@ -168,19 +174,113 @@ export class AuthService { /** * Déconnexion utilisateur */ +/** + * Déconnexion utilisateur avec nettoyage complet + */ logout(): Observable { + const token = this.getAccessToken(); + + // Si pas de token, nettoyer et retourner un observable complet + if (!token) { + this.clearAuthData(); + return of({ message: 'Already logged out' }); + } + + // Ajouter le token dans le header si nécessaire + const headers = new HttpHeaders({ + 'Authorization': `Bearer ${token}` + }); + return this.http.post( `${environment.iamApiUrl}/auth/logout`, - {} + {}, + { headers } ).pipe( - tap(() => this.clearAuthData()), - catchError(error => { + tap(() => { this.clearAuthData(); - return throwError(() => error); + this.dashboardAccessService.clearCache(); + this.transactionAccessService.clearCache(); + this.clearAllStorage(); // Nettoyer tout le storage + }), + catchError(error => { + // Même en cas d'erreur, nettoyer tout + this.clearAuthData(); + this.dashboardAccessService.clearCache(); + this.transactionAccessService.clearCache(); + this.clearAllStorage(); + console.warn('Logout API error, but local data cleared:', error); + // Retourner un succès simulé pour permettre la navigation + return of({ message: 'Local session cleared' }); + }), + finalize(() => { + // Garantir le nettoyage dans tous les cas + this.clearAuthData(); + this.dashboardAccessService.clearCache(); + this.transactionAccessService.clearCache(); }) ); } + /** + * Déconnexion forcée sans appel API + */ + forceLogout(): void { + this.clearAuthData(); + this.dashboardAccessService.clearCache(); + this.transactionAccessService.clearCache(); + this.clearAllStorage(); + } + + /** + * Nettoyer toutes les données d'authentification + */ + private clearAuthData(): void { + + this.dashboardAccessService.clearCache(); + this.transactionAccessService.clearCache(); + + // Supprimer tous les tokens et données utilisateur + localStorage.removeItem(this.tokenKey); + localStorage.removeItem(this.refreshTokenKey); + localStorage.removeItem('user_profile'); + localStorage.removeItem('auth_state'); + + // Nettoyer sessionStorage également + sessionStorage.clear(); + + // Réinitialiser les BehaviorSubjects + this.authState$.next(false); + this.userProfile$.next(null); + } + + /** + * Nettoyer tout le stockage local + */ + private clearAllStorage(): void { + // Supprimer toutes les clés liées à l'authentification + Object.keys(localStorage).forEach(key => { + if (key.includes('token') || key.includes('auth') || key.includes('user')) { + localStorage.removeItem(key); + } + }); + + // Nettoyer sessionStorage + sessionStorage.clear(); + + // Nettoyer les cookies + this.clearCookies(); + } + + /** + * Nettoyer les cookies + */ + private clearCookies(): void { + document.cookie.split(';').forEach(cookie => { + const [name] = cookie.split('='); + document.cookie = `${name.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + }); + } + /** * Chargement du profil utilisateur */ @@ -231,6 +331,7 @@ export class AuthService { lastName: apiUser.lastName || apiUser.lastname || apiUser.family_name || '', enabled: apiUser.enabled ?? apiUser.active ?? true, emailVerified: apiUser.emailVerified ?? apiUser.email_verified ?? false, + merchantPartnerId: apiUser.merchantPartnerId || apiUser.partnerId || apiUser.merchantId || null, userType: userType, role: apiUser.clientRoles || apiUser.clientRoles?.[0] || '', // Gérer rôle unique ou tableau createdBy: apiUser.createdBy || apiUser.creatorId || null, @@ -256,13 +357,6 @@ export class AuthService { } } - private clearAuthData(): void { - localStorage.removeItem(this.tokenKey); - localStorage.removeItem(this.refreshTokenKey); - this.authState$.next(false); - this.userProfile$.next(null); - } - // === VALIDATION DU TOKEN === validateToken(): Observable { @@ -448,6 +542,14 @@ export class AuthService { const profile = this.userProfile$.value; return profile?.id || null; } + + /** + * Récupère le merchantPartnerId de l'utilisateur courant (si marchand) + */ + getCurrentMerchantPartnerId(): string | null { + const profile = this.userProfile$.value; + return profile?.merchantPartnerId || null; + } /** * Vérifie si le profil fourni est celui de l'utilisateur courant diff --git a/src/app/core/services/hub-users-roles-management-old.service.ts b/src/app/core/services/hub-users-roles-management-old.service.ts new file mode 100644 index 0000000..9059a89 --- /dev/null +++ b/src/app/core/services/hub-users-roles-management-old.service.ts @@ -0,0 +1,445 @@ +import { Injectable, inject } from '@angular/core'; +import { HubUsersService } from '@modules/hub-users-management/hub-users.service'; +import { MerchantUsersService } from '@modules/hub-users-management/merchant-users.service'; +import { BehaviorSubject, Observable, map, tap, of, catchError } from 'rxjs'; +import { UserRole, UserType, AvailableRole } from '@core/models/dcb-bo-hub-user.model'; + +// Interfaces +export interface RolePermission { + canCreateUsers: boolean; + canEditUsers: boolean; + canDeleteUsers: boolean; + canManageRoles: boolean; + canViewStats: boolean; + canManageMerchants: boolean; + canAccessAdmin: boolean; + canAccessSupport: boolean; + canAccessPartner: boolean; + assignableRoles: UserRole[]; +} + +export interface AvailableRolesWithPermissions { + roles: (AvailableRole & { permissions: RolePermission })[]; +} + +interface RoleConfig { + label: string; + description: string; + badgeClass: string; + icon: string; + permissions: RolePermission; +} + +// Permissions par défaut +const DEFAULT_PERMISSIONS: RolePermission = { + canCreateUsers: false, + canEditUsers: false, + canDeleteUsers: false, + canManageRoles: false, + canViewStats: false, + canManageMerchants: false, + canAccessAdmin: false, + canAccessSupport: false, + canAccessPartner: false, + assignableRoles: [] +}; + +// Configuration des rôles +const ROLE_CONFIG: Record = { + [UserRole.DCB_ADMIN]: { + label: 'Administrateur DCB', + description: 'Administrateur système avec tous les accès', + badgeClass: 'bg-danger', + icon: 'lucideShield', + permissions: { + canCreateUsers: true, + canEditUsers: true, + canDeleteUsers: true, + canManageRoles: true, + canViewStats: true, + canManageMerchants: true, + canAccessAdmin: true, + canAccessSupport: true, + canAccessPartner: true, + assignableRoles: Object.values(UserRole) + } + }, + [UserRole.DCB_SUPPORT]: { + label: 'Support DCB', + description: 'Support technique avec accès étendus', + badgeClass: 'bg-info', + icon: 'lucideHeadphones', + permissions: { + canCreateUsers: true, + canEditUsers: true, + canDeleteUsers: false, + canManageRoles: true, + canViewStats: true, + canManageMerchants: true, + canAccessAdmin: false, + canAccessSupport: true, + canAccessPartner: true, + assignableRoles: [ + UserRole.DCB_SUPPORT, + UserRole.DCB_PARTNER_ADMIN, + UserRole.DCB_PARTNER_MANAGER, + UserRole.DCB_PARTNER_SUPPORT + ] + } + }, + [UserRole.DCB_PARTNER_ADMIN]: { + label: 'Admin Partenaire', + description: 'Administrateur de partenaire marchand', + badgeClass: 'bg-warning', + icon: 'lucideShieldCheck', + permissions: { + canCreateUsers: true, + canEditUsers: true, + canDeleteUsers: true, + canManageRoles: true, + canViewStats: true, + canManageMerchants: false, + canAccessAdmin: false, + canAccessSupport: false, + canAccessPartner: false, + assignableRoles: [UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT] + } + }, + [UserRole.DCB_PARTNER_MANAGER]: { + label: 'Manager Partenaire', + description: 'Manager opérationnel partenaire', + badgeClass: 'bg-success', + icon: 'lucideUserCog', + permissions: { + canCreateUsers: false, + canEditUsers: false, + canDeleteUsers: false, + canManageRoles: false, + canViewStats: true, + canManageMerchants: true, + canAccessAdmin: false, + canAccessSupport: false, + canAccessPartner: true, + assignableRoles: [] + } + }, + [UserRole.DCB_PARTNER_SUPPORT]: { + label: 'Support Partenaire', + description: 'Support technique partenaire', + badgeClass: 'bg-secondary', + icon: 'lucideHeadphones', + permissions: { + canCreateUsers: false, + canEditUsers: false, + canDeleteUsers: false, + canManageRoles: false, + canViewStats: true, + canManageMerchants: false, + canAccessAdmin: false, + canAccessSupport: false, + canAccessPartner: true, + assignableRoles: [] + } + }, + [UserRole.MERCHANT_CONFIG_ADMIN]: { + label: 'Admin Marchand', + description: 'Administrateur de configuration marchand', + badgeClass: 'bg-warning', + icon: 'lucideSettings', + permissions: DEFAULT_PERMISSIONS + }, + [UserRole.MERCHANT_CONFIG_MANAGER]: { + label: 'Manager Marchand', + description: 'Manager de configuration marchand', + badgeClass: 'bg-success', + icon: 'lucideUserCog', + permissions: DEFAULT_PERMISSIONS + }, + [UserRole.MERCHANT_CONFIG_TECHNICAL]: { + label: 'Technique Marchand', + description: 'Support technique configuration marchand', + badgeClass: 'bg-secondary', + icon: 'lucideWrench', + permissions: DEFAULT_PERMISSIONS + }, + [UserRole.MERCHANT_CONFIG_VIEWER]: { + label: 'Visualiseur Marchand', + description: 'Visualiseur de configuration marchand', + badgeClass: 'bg-light', + icon: 'lucideEye', + permissions: DEFAULT_PERMISSIONS + } +} as const; + +// Rôles Hub (pour les filtres) +const HUB_ROLES = [ + UserRole.DCB_ADMIN, + UserRole.DCB_SUPPORT, +] as const; + +// Rôles Marchands (pour les filtres) +const MERCHANT_ROLES = [ + UserRole.DCB_PARTNER_ADMIN, + UserRole.DCB_PARTNER_MANAGER, + UserRole.DCB_PARTNER_SUPPORT, + UserRole.MERCHANT_CONFIG_ADMIN, + UserRole.MERCHANT_CONFIG_MANAGER, + UserRole.MERCHANT_CONFIG_TECHNICAL, + UserRole.MERCHANT_CONFIG_VIEWER +] as const; + +@Injectable({ + providedIn: 'root' +}) +export class RoleManagementService { + private hubUsersService = inject(HubUsersService); + private merchantUsersService = inject(MerchantUsersService); + + private availableRoles$ = new BehaviorSubject(null); + private currentUserRole$ = new BehaviorSubject(null); + + /** + * Charge les rôles Hub disponibles + */ + loadAvailableHubRoles(): Observable { + return this.loadRoles( + () => this.hubUsersService.getAvailableHubRoles(), + 'hub' + ); + } + + /** + * Charge les rôles Marchands disponibles + */ + loadAvailableMerchantRoles(): Observable { + return this.loadRoles( + () => this.merchantUsersService.getAvailableMerchantRoles(), + 'merchant' + ); + } + + /** + * Méthode générique pour charger les rôles + */ + private loadRoles( + fetchFn: () => Observable<{ roles: AvailableRole[] }>, + type: 'hub' | 'merchant' + ): Observable { + return fetchFn().pipe( + map(apiResponse => ({ + roles: apiResponse.roles.map(role => ({ + ...role, + permissions: this.getPermissionsForRole(role.value) + })) + })), + tap(roles => this.availableRoles$.next(roles)), + catchError(error => { + console.error(`Error loading ${type} roles:`, error); + return of({ roles: [] } as AvailableRolesWithPermissions); + }) + ); + } + + /** + * Définit le rôle de l'utilisateur courant + */ + setCurrentUserRole(role: UserRole): void { + this.currentUserRole$.next(role); + } + + /** + * Récupère le rôle de l'utilisateur courant + */ + getCurrentUserRole(): Observable { + return this.currentUserRole$.asObservable(); + } + + /** + * Récupère la valeur actuelle du rôle utilisateur (synchrone) + */ + getCurrentUserRoleValue(): UserRole | null { + return this.currentUserRole$.value; + } + + /** + * Récupère les permissions détaillées selon le rôle + */ + getPermissionsForRole(role: UserRole | null): RolePermission { + if (!role) { + return DEFAULT_PERMISSIONS; + } + return ROLE_CONFIG[role]?.permissions || DEFAULT_PERMISSIONS; + } + + /** + * Vérifie si un rôle peut être attribué par l'utilisateur courant + */ + canAssignRole(currentUserRole: UserRole | null, targetRole: UserRole): boolean { + if (!currentUserRole) return false; + + const fullPermissionRoles = [ + UserRole.DCB_ADMIN, + UserRole.DCB_SUPPORT + ]; + + if (fullPermissionRoles.includes(currentUserRole)) { + return true; + } + + const permissions = this.getPermissionsForRole(currentUserRole); + return permissions.assignableRoles.includes(targetRole); + } + + // Méthodes d'utilité pour les permissions + canCreateUsers(currentUserRole: UserRole | null): boolean { + return this.getPermission(currentUserRole, 'canCreateUsers'); + } + + canEditUsers(currentUserRole: UserRole | null): boolean { + return this.getPermission(currentUserRole, 'canEditUsers'); + } + + canDeleteUsers(currentUserRole: UserRole | null): boolean { + return this.getPermission(currentUserRole, 'canDeleteUsers'); + } + + canManageRoles(currentUserRole: UserRole | null): boolean { + return this.getPermission(currentUserRole, 'canManageRoles'); + } + + canViewStats(currentUserRole: UserRole | null): boolean { + return this.getPermission(currentUserRole, 'canViewStats'); + } + + canManageMerchants(currentUserRole: UserRole | null): boolean { + return this.getPermission(currentUserRole, 'canManageMerchants'); + } + + canAccessAdmin(currentUserRole: UserRole | null): boolean { + return this.getPermission(currentUserRole, 'canAccessAdmin'); + } + + canAccessSupport(currentUserRole: UserRole | null): boolean { + return this.getPermission(currentUserRole, 'canAccessSupport'); + } + + canAccessPartner(currentUserRole: UserRole | null): boolean { + return this.getPermission(currentUserRole, 'canAccessPartner'); + } + + /** + * Méthode helper générique pour les permissions + */ + private getPermission( + role: UserRole | null, + permissionKey: keyof RolePermission + ): boolean { + if (!role) return false; + const permissions = this.getPermissionsForRole(role); + return Boolean(permissions[permissionKey]); + } + + /** + * Méthodes d'utilité pour les rôles + */ + getRoleLabel(role: string): string { + const userRole = role as UserRole; + return ROLE_CONFIG[userRole]?.label || role; + } + + getRoleDescription(role: string | UserRole): string { + const userRole = role as UserRole; + return ROLE_CONFIG[userRole]?.description || 'Description non disponible'; + } + + getRoleBadgeClass(role: string): string { + const userRole = role as UserRole; + return ROLE_CONFIG[userRole]?.badgeClass || 'bg-secondary'; + } + + getRoleIcon(role: string): string { + const userRole = role as UserRole; + return ROLE_CONFIG[userRole]?.icon || 'lucideUser'; + } + + /** + * Vérifications de type de rôle + */ + isAdminRole(role: UserRole): boolean { + return role === UserRole.DCB_ADMIN; + } + + isSupportRole(role: UserRole): boolean { + return role === UserRole.DCB_SUPPORT; + } + + isMerchantUserRole(role: UserRole): boolean { + return role === UserRole.DCB_PARTNER_ADMIN + || role === UserRole.DCB_PARTNER_MANAGER + || role === UserRole.DCB_PARTNER_SUPPORT + || role === UserRole.MERCHANT_CONFIG_ADMIN + || role === UserRole.MERCHANT_CONFIG_MANAGER + || role === UserRole.MERCHANT_CONFIG_TECHNICAL + || role === UserRole.MERCHANT_CONFIG_VIEWER; + } + + /** + * Gestion des listes de rôles + */ + getAllRoles(): UserRole[] { + return Object.values(UserRole); + } + + getHubRoles(): UserRole[] { + return [...HUB_ROLES]; + } + + getMerchantRoles(): UserRole[] { + return [...MERCHANT_ROLES]; + } + + getAssignableRoles(currentUserRole: UserRole | null): UserRole[] { + if (!currentUserRole) return []; + return this.getPermissionsForRole(currentUserRole).assignableRoles; + } + + getAssignableHubRoles(currentUserRole: UserRole | null): UserRole[] { + return this.filterAssignableRoles(currentUserRole, HUB_ROLES); + } + + getAssignableMerchantRoles(currentUserRole: UserRole | null): UserRole[] { + return this.filterAssignableRoles(currentUserRole, MERCHANT_ROLES); + } + + private filterAssignableRoles( + currentUserRole: UserRole | null, + roleList: readonly UserRole[] + ): UserRole[] { + if (!currentUserRole) return []; + const permissions = this.getPermissionsForRole(currentUserRole); + return roleList.filter(role => permissions.assignableRoles.includes(role)); + } + + /** + * Vérifications de rôles + */ + hasRole(userRole: UserRole | null, targetRole: UserRole): boolean { + return userRole === targetRole; + } + + hasAnyRole(userRole: UserRole | null, targetRoles: UserRole[]): boolean { + return userRole ? targetRoles.includes(userRole) : false; + } + + /** + * Gestion du cache + */ + clearCache(): void { + this.availableRoles$.next(null); + this.currentUserRole$.next(null); + } + + getAvailableRoles(): Observable { + return this.availableRoles$.asObservable(); + } +} \ No newline at end of file diff --git a/src/app/core/services/hub-users-roles-management.service.ts b/src/app/core/services/hub-users-roles-management.service.ts index 9059a89..84a45b4 100644 --- a/src/app/core/services/hub-users-roles-management.service.ts +++ b/src/app/core/services/hub-users-roles-management.service.ts @@ -1,445 +1,231 @@ -import { Injectable, inject } from '@angular/core'; -import { HubUsersService } from '@modules/hub-users-management/hub-users.service'; -import { MerchantUsersService } from '@modules/hub-users-management/merchant-users.service'; -import { BehaviorSubject, Observable, map, tap, of, catchError } from 'rxjs'; -import { UserRole, UserType, AvailableRole } from '@core/models/dcb-bo-hub-user.model'; +import { Injectable } from '@angular/core'; -// Interfaces -export interface RolePermission { - canCreateUsers: boolean; - canEditUsers: boolean; - canDeleteUsers: boolean; - canManageRoles: boolean; - canViewStats: boolean; - canManageMerchants: boolean; - canAccessAdmin: boolean; - canAccessSupport: boolean; - canAccessPartner: boolean; - assignableRoles: UserRole[]; -} - -export interface AvailableRolesWithPermissions { - roles: (AvailableRole & { permissions: RolePermission })[]; -} - -interface RoleConfig { - label: string; - description: string; - badgeClass: string; - icon: string; - permissions: RolePermission; -} - -// Permissions par défaut -const DEFAULT_PERMISSIONS: RolePermission = { - canCreateUsers: false, - canEditUsers: false, - canDeleteUsers: false, - canManageRoles: false, - canViewStats: false, - canManageMerchants: false, - canAccessAdmin: false, - canAccessSupport: false, - canAccessPartner: false, - assignableRoles: [] -}; - -// Configuration des rôles -const ROLE_CONFIG: Record = { - [UserRole.DCB_ADMIN]: { - label: 'Administrateur DCB', - description: 'Administrateur système avec tous les accès', - badgeClass: 'bg-danger', - icon: 'lucideShield', - permissions: { - canCreateUsers: true, - canEditUsers: true, - canDeleteUsers: true, - canManageRoles: true, - canViewStats: true, - canManageMerchants: true, - canAccessAdmin: true, - canAccessSupport: true, - canAccessPartner: true, - assignableRoles: Object.values(UserRole) - } - }, - [UserRole.DCB_SUPPORT]: { - label: 'Support DCB', - description: 'Support technique avec accès étendus', - badgeClass: 'bg-info', - icon: 'lucideHeadphones', - permissions: { - canCreateUsers: true, - canEditUsers: true, - canDeleteUsers: false, - canManageRoles: true, - canViewStats: true, - canManageMerchants: true, - canAccessAdmin: false, - canAccessSupport: true, - canAccessPartner: true, - assignableRoles: [ - UserRole.DCB_SUPPORT, - UserRole.DCB_PARTNER_ADMIN, - UserRole.DCB_PARTNER_MANAGER, - UserRole.DCB_PARTNER_SUPPORT - ] - } - }, - [UserRole.DCB_PARTNER_ADMIN]: { - label: 'Admin Partenaire', - description: 'Administrateur de partenaire marchand', - badgeClass: 'bg-warning', - icon: 'lucideShieldCheck', - permissions: { - canCreateUsers: true, - canEditUsers: true, - canDeleteUsers: true, - canManageRoles: true, - canViewStats: true, - canManageMerchants: false, - canAccessAdmin: false, - canAccessSupport: false, - canAccessPartner: false, - assignableRoles: [UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT] - } - }, - [UserRole.DCB_PARTNER_MANAGER]: { - label: 'Manager Partenaire', - description: 'Manager opérationnel partenaire', - badgeClass: 'bg-success', - icon: 'lucideUserCog', - permissions: { - canCreateUsers: false, - canEditUsers: false, - canDeleteUsers: false, - canManageRoles: false, - canViewStats: true, - canManageMerchants: true, - canAccessAdmin: false, - canAccessSupport: false, - canAccessPartner: true, - assignableRoles: [] - } - }, - [UserRole.DCB_PARTNER_SUPPORT]: { - label: 'Support Partenaire', - description: 'Support technique partenaire', - badgeClass: 'bg-secondary', - icon: 'lucideHeadphones', - permissions: { - canCreateUsers: false, - canEditUsers: false, - canDeleteUsers: false, - canManageRoles: false, - canViewStats: true, - canManageMerchants: false, - canAccessAdmin: false, - canAccessSupport: false, - canAccessPartner: true, - assignableRoles: [] - } - }, - [UserRole.MERCHANT_CONFIG_ADMIN]: { - label: 'Admin Marchand', - description: 'Administrateur de configuration marchand', - badgeClass: 'bg-warning', - icon: 'lucideSettings', - permissions: DEFAULT_PERMISSIONS - }, - [UserRole.MERCHANT_CONFIG_MANAGER]: { - label: 'Manager Marchand', - description: 'Manager de configuration marchand', - badgeClass: 'bg-success', - icon: 'lucideUserCog', - permissions: DEFAULT_PERMISSIONS - }, - [UserRole.MERCHANT_CONFIG_TECHNICAL]: { - label: 'Technique Marchand', - description: 'Support technique configuration marchand', - badgeClass: 'bg-secondary', - icon: 'lucideWrench', - permissions: DEFAULT_PERMISSIONS - }, - [UserRole.MERCHANT_CONFIG_VIEWER]: { - label: 'Visualiseur Marchand', - description: 'Visualiseur de configuration marchand', - badgeClass: 'bg-light', - icon: 'lucideEye', - permissions: DEFAULT_PERMISSIONS - } -} as const; - -// Rôles Hub (pour les filtres) -const HUB_ROLES = [ - UserRole.DCB_ADMIN, - UserRole.DCB_SUPPORT, -] as const; - -// Rôles Marchands (pour les filtres) -const MERCHANT_ROLES = [ - UserRole.DCB_PARTNER_ADMIN, - UserRole.DCB_PARTNER_MANAGER, - UserRole.DCB_PARTNER_SUPPORT, - UserRole.MERCHANT_CONFIG_ADMIN, - UserRole.MERCHANT_CONFIG_MANAGER, - UserRole.MERCHANT_CONFIG_TECHNICAL, - UserRole.MERCHANT_CONFIG_VIEWER -] as const; - -@Injectable({ - providedIn: 'root' -}) -export class RoleManagementService { - private hubUsersService = inject(HubUsersService); - private merchantUsersService = inject(MerchantUsersService); +export enum UserRole { + // Rôles Hub + DCB_ADMIN = 'dcb-admin', + DCB_SUPPORT = 'dcb-support', - private availableRoles$ = new BehaviorSubject(null); - private currentUserRole$ = new BehaviorSubject(null); + // Rôles Merchant User + // Rôles Partenaires (Business) + DCB_PARTNER_ADMIN = 'dcb-partner-admin', + DCB_PARTNER_MANAGER = 'dcb-partner-manager', + DCB_PARTNER_SUPPORT = 'dcb-partner-support', - /** - * Charge les rôles Hub disponibles - */ - loadAvailableHubRoles(): Observable { - return this.loadRoles( - () => this.hubUsersService.getAvailableHubRoles(), - 'hub' - ); + // Rôles Configuration Marchands (Technique) + MERCHANT_CONFIG_ADMIN = 'ADMIN', + MERCHANT_CONFIG_MANAGER = 'MANAGER', + MERCHANT_CONFIG_TECHNICAL = 'TECHNICAL', + MERCHANT_CONFIG_VIEWER = 'VIEWER', +} + +type RoleCategory = 'hub' | 'partner' | 'config'; + +@Injectable({ providedIn: 'root' }) +export class RoleManagementService { + private currentRole: UserRole | null = null; + + // Mapping des rôles équivalents + private readonly roleEquivalents = new Map([ + [UserRole.DCB_PARTNER_ADMIN, [UserRole.MERCHANT_CONFIG_ADMIN]], + [UserRole.DCB_PARTNER_MANAGER, [UserRole.MERCHANT_CONFIG_MANAGER]], + [UserRole.DCB_PARTNER_SUPPORT, [UserRole.MERCHANT_CONFIG_TECHNICAL, UserRole.MERCHANT_CONFIG_VIEWER]], + [UserRole.MERCHANT_CONFIG_ADMIN, [UserRole.DCB_PARTNER_ADMIN]], + [UserRole.MERCHANT_CONFIG_MANAGER, [UserRole.DCB_PARTNER_MANAGER]], + [UserRole.MERCHANT_CONFIG_TECHNICAL, [UserRole.DCB_PARTNER_SUPPORT]], + [UserRole.MERCHANT_CONFIG_VIEWER, [UserRole.DCB_PARTNER_SUPPORT]] + ]); + + // Catégories des rôles + private readonly roleCategories: Record = { + [UserRole.DCB_ADMIN]: 'hub', + [UserRole.DCB_SUPPORT]: 'hub', + [UserRole.DCB_PARTNER_ADMIN]: 'partner', + [UserRole.DCB_PARTNER_MANAGER]: 'partner', + [UserRole.DCB_PARTNER_SUPPORT]: 'partner', + [UserRole.MERCHANT_CONFIG_ADMIN]: 'config', + [UserRole.MERCHANT_CONFIG_MANAGER]: 'config', + [UserRole.MERCHANT_CONFIG_TECHNICAL]: 'config', + [UserRole.MERCHANT_CONFIG_VIEWER]: 'config' + }; + + // Labels des rôles + private readonly roleLabels: Record = { + [UserRole.DCB_ADMIN]: 'Administrateur DCB', + [UserRole.DCB_SUPPORT]: 'Support DCB', + [UserRole.DCB_PARTNER_ADMIN]: 'Admin Partenaire', + [UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire', + [UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire', + [UserRole.MERCHANT_CONFIG_ADMIN]: 'Admin Configuration', + [UserRole.MERCHANT_CONFIG_MANAGER]: 'Manager Configuration', + [UserRole.MERCHANT_CONFIG_TECHNICAL]: 'Technique Configuration', + [UserRole.MERCHANT_CONFIG_VIEWER]: 'Visualiseur Configuration' + }; + + // Icônes des rôles + private readonly roleIcons: Record = { + [UserRole.DCB_ADMIN]: 'lucideShield', + [UserRole.DCB_SUPPORT]: 'lucideHeadphones', + [UserRole.DCB_PARTNER_ADMIN]: 'lucideShieldCheck', + [UserRole.DCB_PARTNER_MANAGER]: 'lucideUserCog', + [UserRole.DCB_PARTNER_SUPPORT]: 'user-headset', + [UserRole.MERCHANT_CONFIG_ADMIN]: 'lucideSettings', + [UserRole.MERCHANT_CONFIG_MANAGER]: 'lucideSliders', + [UserRole.MERCHANT_CONFIG_TECHNICAL]: 'lucideWrench', + [UserRole.MERCHANT_CONFIG_VIEWER]: 'lucideEye' + }; + + // === GESTION DU RÔLE COURANT === + setCurrentRole(role: UserRole | null): void { + this.currentRole = role; } - /** - * Charge les rôles Marchands disponibles - */ - loadAvailableMerchantRoles(): Observable { - return this.loadRoles( - () => this.merchantUsersService.getAvailableMerchantRoles(), - 'merchant' - ); + getCurrentRole(): UserRole | null { + return this.currentRole; } - /** - * Méthode générique pour charger les rôles - */ - private loadRoles( - fetchFn: () => Observable<{ roles: AvailableRole[] }>, - type: 'hub' | 'merchant' - ): Observable { - return fetchFn().pipe( - map(apiResponse => ({ - roles: apiResponse.roles.map(role => ({ - ...role, - permissions: this.getPermissionsForRole(role.value) - })) - })), - tap(roles => this.availableRoles$.next(roles)), - catchError(error => { - console.error(`Error loading ${type} roles:`, error); - return of({ roles: [] } as AvailableRolesWithPermissions); - }) - ); + // === VÉRIFICATIONS DE RÔLES INDIVIDUELS === + + isAdmin(): boolean { + return this.currentRole === UserRole.DCB_ADMIN; } - /** - * Définit le rôle de l'utilisateur courant - */ - setCurrentUserRole(role: UserRole): void { - this.currentUserRole$.next(role); + isSupport(): boolean { + return this.currentRole === UserRole.DCB_SUPPORT; } - /** - * Récupère le rôle de l'utilisateur courant - */ - getCurrentUserRole(): Observable { - return this.currentUserRole$.asObservable(); + isPartnerAdmin(): boolean { + return this.currentRole === UserRole.DCB_PARTNER_ADMIN; } - /** - * Récupère la valeur actuelle du rôle utilisateur (synchrone) - */ - getCurrentUserRoleValue(): UserRole | null { - return this.currentUserRole$.value; + isPartnerManager(): boolean { + return this.currentRole === UserRole.DCB_PARTNER_MANAGER; } - /** - * Récupère les permissions détaillées selon le rôle - */ - getPermissionsForRole(role: UserRole | null): RolePermission { - if (!role) { - return DEFAULT_PERMISSIONS; - } - return ROLE_CONFIG[role]?.permissions || DEFAULT_PERMISSIONS; + isPartnerSupport(): boolean { + return this.currentRole === UserRole.DCB_PARTNER_SUPPORT; } - /** - * Vérifie si un rôle peut être attribué par l'utilisateur courant - */ - canAssignRole(currentUserRole: UserRole | null, targetRole: UserRole): boolean { - if (!currentUserRole) return false; + isConfigAdmin(): boolean { + return this.currentRole === UserRole.MERCHANT_CONFIG_ADMIN; + } - const fullPermissionRoles = [ - UserRole.DCB_ADMIN, - UserRole.DCB_SUPPORT - ]; + isConfigManager(): boolean { + return this.currentRole === UserRole.MERCHANT_CONFIG_MANAGER; + } - if (fullPermissionRoles.includes(currentUserRole)) { - return true; + isConfigTechnical(): boolean { + return this.currentRole === UserRole.MERCHANT_CONFIG_TECHNICAL; + } + + isConfigViewer(): boolean { + return this.currentRole === UserRole.MERCHANT_CONFIG_VIEWER; + } + + // === VÉRIFICATIONS AVEC MAPPING === + + isAnyAdmin(): boolean { + return this.isAdmin() || this.isPartnerAdmin() || this.isConfigAdmin(); + } + + isAnyManager(): boolean { + return this.isPartnerManager() || this.isConfigManager(); + } + + isAnySupport(): boolean { + return this.isSupport() || this.isPartnerSupport() || this.isConfigTechnical() || this.isConfigViewer(); + } + + // === VÉRIFICATIONS DE CATÉGORIES === + + isHubUser(): boolean { + return this.isAdmin() || this.isSupport(); + } + + isPartnerUser(): boolean { + return this.isPartnerAdmin() || this.isPartnerManager() || this.isPartnerSupport(); + } + + isConfigUser(): boolean { + return this.isConfigAdmin() || this.isConfigManager() || this.isConfigTechnical() || this.isConfigViewer(); + } + + getRoleCategory(): RoleCategory | null { + if (!this.currentRole) return null; + return this.roleCategories[this.currentRole]; + } + + // === MAPPING ET ÉQUIVALENTS === + + getEquivalentRoles(): UserRole[] { + if (!this.currentRole) return []; + + const equivalents = [this.currentRole]; + const mappedRoles = this.roleEquivalents.get(this.currentRole); + + if (mappedRoles) { + equivalents.push(...mappedRoles); } - const permissions = this.getPermissionsForRole(currentUserRole); - return permissions.assignableRoles.includes(targetRole); + return [...new Set(equivalents)]; } - // Méthodes d'utilité pour les permissions - canCreateUsers(currentUserRole: UserRole | null): boolean { - return this.getPermission(currentUserRole, 'canCreateUsers'); + hasEquivalentRole(targetRole: UserRole): boolean { + if (!this.currentRole) return false; + + if (this.currentRole === targetRole) return true; + + const equivalents = this.roleEquivalents.get(this.currentRole); + return equivalents ? equivalents.includes(targetRole) : false; } - canEditUsers(currentUserRole: UserRole | null): boolean { - return this.getPermission(currentUserRole, 'canEditUsers'); + getMappedRole(): UserRole | null { + if (!this.currentRole) return null; + + const equivalents = this.roleEquivalents.get(this.currentRole); + return equivalents && equivalents.length > 0 ? equivalents[0] : this.currentRole; } - canDeleteUsers(currentUserRole: UserRole | null): boolean { - return this.getPermission(currentUserRole, 'canDeleteUsers'); + // === VÉRIFICATIONS GÉNÉRIQUES === + + hasRole(role: UserRole): boolean { + return this.currentRole === role; } - canManageRoles(currentUserRole: UserRole | null): boolean { - return this.getPermission(currentUserRole, 'canManageRoles'); + hasAnyRole(...roles: UserRole[]): boolean { + if (!this.currentRole) return false; + return roles.includes(this.currentRole); } - canViewStats(currentUserRole: UserRole | null): boolean { - return this.getPermission(currentUserRole, 'canViewStats'); + // === UTILITAIRES === + + getRoleLabel(role?: UserRole): string { + const targetRole = role || this.currentRole; + return targetRole ? this.roleLabels[targetRole] || targetRole : ''; } - canManageMerchants(currentUserRole: UserRole | null): boolean { - return this.getPermission(currentUserRole, 'canManageMerchants'); + getRoleIcon(role?: UserRole): string { + const targetRole = role || this.currentRole; + return targetRole ? this.roleIcons[targetRole] || 'user' : 'user'; } - canAccessAdmin(currentUserRole: UserRole | null): boolean { - return this.getPermission(currentUserRole, 'canAccessAdmin'); - } - - canAccessSupport(currentUserRole: UserRole | null): boolean { - return this.getPermission(currentUserRole, 'canAccessSupport'); - } - - canAccessPartner(currentUserRole: UserRole | null): boolean { - return this.getPermission(currentUserRole, 'canAccessPartner'); - } - - /** - * Méthode helper générique pour les permissions - */ - private getPermission( - role: UserRole | null, - permissionKey: keyof RolePermission - ): boolean { - if (!role) return false; - const permissions = this.getPermissionsForRole(role); - return Boolean(permissions[permissionKey]); - } - - /** - * Méthodes d'utilité pour les rôles - */ - getRoleLabel(role: string): string { - const userRole = role as UserRole; - return ROLE_CONFIG[userRole]?.label || role; - } - - getRoleDescription(role: string | UserRole): string { - const userRole = role as UserRole; - return ROLE_CONFIG[userRole]?.description || 'Description non disponible'; - } - - getRoleBadgeClass(role: string): string { - const userRole = role as UserRole; - return ROLE_CONFIG[userRole]?.badgeClass || 'bg-secondary'; - } - - getRoleIcon(role: string): string { - const userRole = role as UserRole; - return ROLE_CONFIG[userRole]?.icon || 'lucideUser'; - } - - /** - * Vérifications de type de rôle - */ - isAdminRole(role: UserRole): boolean { - return role === UserRole.DCB_ADMIN; - } - - isSupportRole(role: UserRole): boolean { - return role === UserRole.DCB_SUPPORT; - } - - isMerchantUserRole(role: UserRole): boolean { - return role === UserRole.DCB_PARTNER_ADMIN - || role === UserRole.DCB_PARTNER_MANAGER - || role === UserRole.DCB_PARTNER_SUPPORT - || role === UserRole.MERCHANT_CONFIG_ADMIN - || role === UserRole.MERCHANT_CONFIG_MANAGER - || role === UserRole.MERCHANT_CONFIG_TECHNICAL - || role === UserRole.MERCHANT_CONFIG_VIEWER; - } - - /** - * Gestion des listes de rôles - */ getAllRoles(): UserRole[] { return Object.values(UserRole); } getHubRoles(): UserRole[] { - return [...HUB_ROLES]; + return [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT]; } - getMerchantRoles(): UserRole[] { - return [...MERCHANT_ROLES]; + getPartnerRoles(): UserRole[] { + return [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; } - getAssignableRoles(currentUserRole: UserRole | null): UserRole[] { - if (!currentUserRole) return []; - return this.getPermissionsForRole(currentUserRole).assignableRoles; - } - - getAssignableHubRoles(currentUserRole: UserRole | null): UserRole[] { - return this.filterAssignableRoles(currentUserRole, HUB_ROLES); - } - - getAssignableMerchantRoles(currentUserRole: UserRole | null): UserRole[] { - return this.filterAssignableRoles(currentUserRole, MERCHANT_ROLES); - } - - private filterAssignableRoles( - currentUserRole: UserRole | null, - roleList: readonly UserRole[] - ): UserRole[] { - if (!currentUserRole) return []; - const permissions = this.getPermissionsForRole(currentUserRole); - return roleList.filter(role => permissions.assignableRoles.includes(role)); - } - - /** - * Vérifications de rôles - */ - hasRole(userRole: UserRole | null, targetRole: UserRole): boolean { - return userRole === targetRole; - } - - hasAnyRole(userRole: UserRole | null, targetRoles: UserRole[]): boolean { - return userRole ? targetRoles.includes(userRole) : false; - } - - /** - * Gestion du cache - */ - clearCache(): void { - this.availableRoles$.next(null); - this.currentUserRole$.next(null); - } - - getAvailableRoles(): Observable { - return this.availableRoles$.asObservable(); + getConfigRoles(): UserRole[] { + return [ + UserRole.MERCHANT_CONFIG_ADMIN, + UserRole.MERCHANT_CONFIG_MANAGER, + UserRole.MERCHANT_CONFIG_TECHNICAL, + UserRole.MERCHANT_CONFIG_VIEWER + ]; } } \ No newline at end of file diff --git a/src/app/core/services/menu.service.ts b/src/app/core/services/menu.service.ts index 17154ee..56d50c7 100644 --- a/src/app/core/services/menu.service.ts +++ b/src/app/core/services/menu.service.ts @@ -2,6 +2,7 @@ import { Injectable, inject } from '@angular/core'; import { AuthService } from './auth.service'; import { PermissionsService } from './permissions.service'; import { MenuItemType, UserDropdownItemType } from '@/app/types/layout'; +import { UserRole } from './hub-users-roles-management.service'; @Injectable({ providedIn: 'root' }) export class MenuService { @@ -23,7 +24,7 @@ export class MenuService { return this.permissionsService.canAccessModule(modulePath, userRoles); } - private filterMenuItems(items: MenuItemType[], userRoles: string[]): MenuItemType[] { + private filterMenuItems(items: MenuItemType[], userRoles: UserRole[]): MenuItemType[] { return items .filter(item => this.shouldDisplayMenuItem(item, userRoles)) .map(item => ({ @@ -32,7 +33,7 @@ export class MenuService { })); } - private shouldDisplayMenuItem(item: MenuItemType, userRoles: string[]): boolean { + private shouldDisplayMenuItem(item: MenuItemType, userRoles: UserRole[]): boolean { if (item.isTitle) return true; if (item.url && item.url !== '#') { @@ -47,7 +48,7 @@ export class MenuService { return true; } - private filterUserDropdownItems(items: UserDropdownItemType[], userRoles: string[]): UserDropdownItemType[] { + private filterUserDropdownItems(items: UserDropdownItemType[], userRoles: UserRole[]): UserDropdownItemType[] { return items.filter(item => { if (item.isDivider || item.isHeader || !item.url || item.url === '#') { return true; @@ -132,7 +133,6 @@ export class MenuService { ]; } - // Mettez à jour votre méthode private getFullUserDropdown(): UserDropdownItemType[] { return [ { label: 'Welcome back!', isHeader: true }, diff --git a/src/app/core/services/role-sync.service.ts b/src/app/core/services/role-sync.service.ts new file mode 100644 index 0000000..b391694 --- /dev/null +++ b/src/app/core/services/role-sync.service.ts @@ -0,0 +1,49 @@ +import { Injectable, inject } from '@angular/core'; +import { AuthService } from './auth.service'; +import { RoleManagementService } from './hub-users-roles-management.service'; +import { filter } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class RoleSyncService { + private authService = inject(AuthService); + private roleService = inject(RoleManagementService); + + constructor() { + this.setupRoleSync(); + } + + private setupRoleSync(): void { + // Synchroniser le rôle lorsque l'état d'authentification change + this.authService.getAuthState().subscribe(isAuthenticated => { + if (isAuthenticated) { + this.syncRoleFromAuth(); + } else { + // Utilisateur déconnecté - réinitialiser le rôle + this.roleService.setCurrentRole(null); + } + }); + + // Synchroniser également lorsque le profil utilisateur change + this.authService.getUserProfile() + .pipe(filter(profile => !!profile)) + .subscribe(() => { + if (this.authService.isAuthenticated()) { + this.syncRoleFromAuth(); + } + }); + } + + private syncRoleFromAuth(): void { + try { + const userRole = this.authService.getCurrentUserRole(); + if (userRole) { + this.roleService.setCurrentRole(userRole); + console.log(`Rôle synchronisé: ${userRole}`); + } + } catch (error) { + console.error('Erreur lors de la synchronisation du rôle:', error); + } + } +} \ No newline at end of file diff --git a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.html b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.html index ba11d2b..d0faebe 100644 --- a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.html +++ b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.html @@ -1,18 +1,47 @@
- user-image -
-
- {{ getUserInitials() }} | {{ getDisplayName() }} -
-
- {{ getUserRole(user) }} -
-
+ + @if (isLoading) { +
+
+ Loading... +
+
+
Chargement...
+
Profil utilisateur
+
+
+ } + + + @if (!isLoading) { +
+ user-image +
+
+ {{ getDisplayName() || 'Utilisateur' }} +
+
+ + @if (!hasError) { + {{ getUserRole() }} + } + @if (hasError) { + + Erreur - + + + } +
+
+
+ }
\ No newline at end of file diff --git a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts index 83d4a02..01467b2 100644 --- a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts +++ b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts @@ -3,7 +3,7 @@ import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; import { userDropdownItems } from '@layouts/components/data'; import { AuthService } from '@/app/core/services/auth.service'; import { User, UserRole } from '@core/models/dcb-bo-hub-user.model'; -import { Subject, takeUntil } from 'rxjs'; +import { Subject, takeUntil, distinctUntilChanged, filter, startWith } from 'rxjs'; @Component({ selector: 'app-user-profile', @@ -19,43 +19,159 @@ export class UserProfileComponent implements OnInit, OnDestroy { user: User | null = null; isLoading = true; + hasError = false; + currentProfileLoaded = false; ngOnInit(): void { - this.loadUser(); + console.log('🔄 UserProfileComponent initializing...'); - // Subscribe to auth state changes + // S'abonner aux changements du profil utilisateur + this.setupProfileSubscription(); + + // S'abonner aux changements d'état d'authentification + this.setupAuthStateSubscription(); + + // Vérifier l'état initial + this.checkInitialState(); + } + + /** + * Vérifie l'état initial sans accéder à la propriété privée + */ + private checkInitialState(): void { + // Utiliser la méthode publique isAuthenticated() + if (this.authService.isAuthenticated()) { + console.log('🔍 User is authenticated, checking profile...'); + // Le profil sera chargé via la subscription + } else { + console.log('🔐 User not authenticated'); + this.user = null; + this.isLoading = false; + this.cdr.detectChanges(); + } + } + + /** + * S'abonne aux changements du profil utilisateur via la méthode publique + */ + private setupProfileSubscription(): void { + this.authService.getUserProfile() + .pipe( + takeUntil(this.destroy$), + startWith(null), // Émettre null au début + filter(profile => { + // Filtrer les valeurs null si on a déjà un profil + if (this.currentProfileLoaded && profile === null) { + return false; + } + return true; + }), + distinctUntilChanged((prev, curr) => { + // Comparer les profils par leur ID + return prev?.id === curr?.id; + }) + ) + .subscribe({ + next: (profile) => { + if (profile) { + console.log('📥 User profile updated:', profile.username); + this.user = profile; + this.currentProfileLoaded = true; + } else { + console.log('📭 User profile cleared'); + this.user = null; + this.currentProfileLoaded = false; + } + + this.isLoading = false; + this.hasError = false; + this.cdr.detectChanges(); + }, + error: (error) => { + console.error('❌ Error in profile subscription:', error); + this.hasError = true; + this.isLoading = false; + this.cdr.detectChanges(); + } + }); + } + + /** + * S'abonne aux changements d'état d'authentification + */ + private setupAuthStateSubscription(): void { this.authService.getAuthState() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (isAuthenticated) => { + console.log('🔐 Auth state changed:', isAuthenticated); + if (isAuthenticated) { - this.loadUser(); + // Si l'utilisateur vient de se connecter, forcer le chargement du profil + if (!this.currentProfileLoaded) { + console.log('🔄 User just logged in, loading profile...'); + this.loadUserProfile(); + } } else { + // Si l'utilisateur s'est déconnecté + console.log('👋 User logged out'); this.user = null; + this.currentProfileLoaded = false; this.isLoading = false; + this.cdr.detectChanges(); } } }); } - loadUser(): void { + /** + * Charge le profil utilisateur explicitement + */ + loadUserProfile(): void { + console.log('🚀 Loading user profile...'); this.isLoading = true; + this.hasError = false; + this.cdr.detectChanges(); - this.authService.getProfile() + this.authService.loadUserProfile() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (profile) => { - this.user = profile; + console.log('✅ Profile loaded successfully:', profile.username); + // Note: le profil sera automatiquement mis à jour via la subscription getUserProfile() this.isLoading = false; + this.cdr.detectChanges(); }, error: (error) => { - console.error('Failed to load user profile:', error); - this.user = null; + console.error('❌ Failed to load user profile:', error); + this.hasError = true; this.isLoading = false; + this.cdr.detectChanges(); + + // Essayer de rafraîchir le token si erreur 401 + if (error.status === 401) { + this.authService.refreshAccessToken().subscribe({ + next: () => { + // Recharger le profil après rafraîchissement + this.loadUserProfile(); + }, + error: (refreshError) => { + console.error('❌ Refresh token failed:', refreshError); + } + }); + } } }); } + /** + * Méthode pour réessayer le chargement en cas d'erreur + */ + retryLoadProfile(): void { + console.log('🔄 Retrying profile load...'); + this.loadUserProfile(); + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); @@ -64,7 +180,7 @@ export class UserProfileComponent implements OnInit, OnDestroy { // Helper methods for template getUserInitials(): string { if (!this.user?.firstName || !this.user?.lastName) { - return 'UU'; // User Unknown + return this.user?.username?.substring(0, 2).toUpperCase() || 'UU'; } return `${this.user.firstName.charAt(0)}${this.user.lastName.charAt(0)}`.toUpperCase(); } @@ -75,26 +191,70 @@ export class UserProfileComponent implements OnInit, OnDestroy { } // Get user role with proper mapping - getUserRole(user: User | null): string { - if (!user) return 'Utilisateur'; + getUserRole(): string { + if (!this.user) return 'Utilisateur'; - // Use role from profile or fallback to token roles - const role = user.role || this.authService.getCurrentUserRoles(); + // Utiliser le rôle du profil ou la méthode publique du service + const role = this.user.role || this.authService.getCurrentUserPrimaryRole(); + + if (!role) return 'Utilisateur'; // Map role to display name - const roleDisplayNames: { [key in UserRole]: string } = { - [UserRole.DCB_ADMIN]: 'Administrateur système avec tous les accès', - [UserRole.DCB_SUPPORT]: 'Support technique avec accès étendus', - [UserRole.DCB_PARTNER_ADMIN]: 'Administrateur de partenaire marchand', - [UserRole.DCB_PARTNER_MANAGER]: 'Manager opérationnel partenaire', - [UserRole.DCB_PARTNER_SUPPORT]: 'Support technique partenaire', - [UserRole.MERCHANT_CONFIG_ADMIN]: 'Administrateur de partenaire marchand', - [UserRole.MERCHANT_CONFIG_MANAGER]: 'Manager opérationnel partenaire', - [UserRole.MERCHANT_CONFIG_TECHNICAL]: 'Support technique partenaire', - [UserRole.MERCHANT_CONFIG_VIEWER]: 'Support technique partenaire' + const roleDisplayNames: { [key: string]: string } = { + [UserRole.DCB_ADMIN]: 'Administrateur système', + [UserRole.DCB_SUPPORT]: 'Support technique', + [UserRole.DCB_PARTNER_ADMIN]: 'Administrateur partenaire', + [UserRole.DCB_PARTNER_MANAGER]: 'Manager opérationnel', + [UserRole.DCB_PARTNER_SUPPORT]: 'Support technique', + [UserRole.MERCHANT_CONFIG_ADMIN]: 'Administrateur configuration', + [UserRole.MERCHANT_CONFIG_MANAGER]: 'Manager configuration', + [UserRole.MERCHANT_CONFIG_TECHNICAL]: 'Technicien', + [UserRole.MERCHANT_CONFIG_VIEWER]: 'Consultant' }; - const primaryRole = role; - return roleDisplayNames[primaryRole] || 'Utilisateur'; + return roleDisplayNames[role] || 'Utilisateur'; + } + + /** + * Méthode pour obtenir la classe CSS du badge de rôle + */ + getRoleBadgeClass(): string { + if (!this.user?.role) return 'badge bg-secondary'; + + const roleClassMap: { [key: string]: string } = { + [UserRole.DCB_ADMIN]: 'badge bg-danger', + [UserRole.DCB_SUPPORT]: 'badge bg-info', + [UserRole.DCB_PARTNER_ADMIN]: 'badge bg-primary', + [UserRole.DCB_PARTNER_MANAGER]: 'badge bg-success', + [UserRole.DCB_PARTNER_SUPPORT]: 'badge bg-warning', + [UserRole.MERCHANT_CONFIG_ADMIN]: 'badge bg-primary', + [UserRole.MERCHANT_CONFIG_MANAGER]: 'badge bg-success', + [UserRole.MERCHANT_CONFIG_TECHNICAL]: 'badge bg-warning', + [UserRole.MERCHANT_CONFIG_VIEWER]: 'badge bg-secondary' + }; + + return roleClassMap[this.user.role] || 'badge bg-secondary'; + } + + /** + * Obtient l'URL de l'avatar de l'utilisateur + */ + getUserAvatar(): string { + if (!this.user) { + return 'assets/images/users/user-default.jpg'; + } + + // Vous pouvez implémenter une logique pour générer un avatar personnalisé + // ou utiliser une image par défaut basée sur l'email/nom + return `assets/images/users/user-${(this.user.id?.charCodeAt(0) % 5) + 1}.jpg`; + } + + /** + * Gère les erreurs de chargement d'avatar + */ + onAvatarError(event: Event): void { + const img = event.target as HTMLImageElement; + img.src = 'assets/images/users/user-default.jpg'; + img.onerror = null; // Éviter les boucles infinies } } \ No newline at end of file diff --git a/src/app/modules/auth/logout.ts b/src/app/modules/auth/logout.ts index 24871e1..adb07d9 100644 --- a/src/app/modules/auth/logout.ts +++ b/src/app/modules/auth/logout.ts @@ -1,7 +1,7 @@ -// app/modules/auth/logout/logout.component.ts -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; -import { AuthService } from '@core/services/auth.service'; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; +import { AuthService } from "@core/services/auth.service"; +import { Subject, takeUntil, finalize } from "rxjs"; @Component({ template: ` @@ -22,7 +22,9 @@ import { AuthService } from '@core/services/auth.service'; } `] }) -export class Logout implements OnInit { +export class Logout implements OnInit, OnDestroy { + private destroy$ = new Subject(); + private isLoggingOut = false; // Flag pour éviter les doubles déconnexions constructor( private authService: AuthService, @@ -30,22 +32,59 @@ export class Logout implements OnInit { ) {} ngOnInit() { + // Vérifier si nous sommes déjà en train de déconnecter + if (this.isLoggingOut) { + return; + } + + this.isLoggingOut = true; this.logout(); } private logout(): void { - this.authService.logout().subscribe({ - next: () => { - // Redirection vers la page de login après déconnexion - this.router.navigate(['/auth/login'], { - queryParams: { logout: 'success' } - }); + + // Attendre un peu pour laisser le temps à l'UI de s'afficher + setTimeout(() => { + this.authService.logout().pipe( + takeUntil(this.destroy$), + finalize(() => { + this.navigateToLogin(); + }) + ).subscribe({ + next: () => { + // Succès - navigation gérée dans finalize + }, + error: (error) => { + console.warn('Erreur lors de la déconnexion:', error); + } + }); + }, 300); + } + + private navigateToLogin(): void { + // S'assurer que nous ne naviguons qu'une seule fois + if (this.router.url.includes('/auth/login')) { + return; + } + + // Naviguer UNE seule fois vers le login + this.router.navigate(['/auth/login'], { + queryParams: { + logout: 'success', + timestamp: Date.now() // Pour éviter le cache }, - error: (error) => { - console.error('Erreur lors de la déconnexion:', error); - // Rediriger même en cas d'erreur - this.router.navigate(['/auth/login']); - } + replaceUrl: true // Remplacer l'historique actuel + }).then(() => { + // Marquer comme terminé + this.isLoggingOut = false; + }).catch(() => { + this.isLoggingOut = false; }); } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + this.isLoggingOut = false; + } } \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/components/dcb-reporting-dashboard.html b/src/app/modules/dcb-dashboard/components/dcb-reporting-dashboard.html index 3889907..e36daf8 100644 --- a/src/app/modules/dcb-dashboard/components/dcb-reporting-dashboard.html +++ b/src/app/modules/dcb-dashboard/components/dcb-reporting-dashboard.html @@ -6,19 +6,36 @@

Dashboard FinTech Reporting + + + Merchant {{ merchantId }} + + + + Hub Admin +

-

Surveillance en temps réel des transactions et abonnements

+

+ + {{ currentRoleLabel }} - {{ getCurrentMerchantName() }} +

+ -
- -
+ +
+
- + +
-
- - - - - - -
- -
+ + +
@@ -85,31 +104,72 @@
+ +
+
+ + {{ currentRoleLabel }} +
+
+ + +
+
+ + {{ getCurrentMerchantName() }} +
+
+
Mis à jour: {{ lastUpdated | date:'HH:mm:ss' }}
-
+ + +
Services: {{ stats.onlineServices }}/{{ stats.totalServices }} en ligne
-
+ + +
- - Opérateur: Orange + + Merchant ID: {{ merchantId }} +
+
+ + +
+
+ + Merchant ID: {{ merchantId }}
- -
+ +
- + +
+ Permissions insuffisantes +
Vous n'avez pas les permissions nécessaires pour voir les données.
+
+
+
+ + +
+
+
{{ syncResponse.message }}
Synchronisée à {{ formatDate(syncResponse.timestamp) }}
@@ -119,7 +179,7 @@
-
+
@@ -171,9 +231,9 @@
- +
-
+
@@ -181,13 +241,13 @@

{{ formatNumber(getPaymentStats().monthly.transactions) }}

Mensuel
-
- +
+
{{ formatCurrency(getPaymentStats().monthly.revenue) }} - + {{ getPaymentStats().monthly.successRate | number:'1.0-0' }}% @@ -257,7 +317,7 @@ Global
- +
@@ -274,7 +334,7 @@
-
+
@@ -285,6 +345,14 @@
{{ getChartTitle(dataSelection.metric) }} + + + Merchant {{ merchantId }} + + + + Données globales +

Visualisation en temps réel

@@ -334,15 +402,16 @@

Chargement du graphique...

-

Aucune donnée disponible

-
-
@@ -353,8 +422,8 @@
- -
+ +
@@ -380,18 +449,19 @@
-
+
- Performance Globale + Performance
@@ -412,7 +482,7 @@
- {{ formatNumber(dailyTransactions?.items?.[0]?.successCount || 0) }} + {{ formatNumber(getCurrentTransactionData()?.items?.[0]?.successCount || 0) }}
Réussies
@@ -420,7 +490,7 @@
- {{ formatNumber(dailyTransactions?.items?.[0]?.failedCount || 0) }} + {{ formatNumber(getCurrentTransactionData()?.items?.[0]?.failedCount || 0) }}
Échouées
@@ -428,7 +498,7 @@
- {{ formatNumber(dailyTransactions?.items?.[0]?.pendingCount || 0) }} + {{ formatNumber(getCurrentTransactionData()?.items?.[0]?.pendingCount || 0) }}
En attente
@@ -443,7 +513,7 @@
-
+
@@ -462,7 +532,7 @@
@@ -522,7 +592,7 @@
-
+
@@ -533,14 +603,19 @@
Transactions récentes + + {{ getCurrentMerchantName() }} +

Dernières 24 heures

@@ -557,10 +632,10 @@ - +
{{ item.period }}
- {{ item.merchantPartnerId ? 'Merchant ' + item.merchantPartnerId : 'Tous' }} + {{ isViewingGlobal() ? 'Tous merchants' : 'Merchant ' + merchantId }}
{{ formatCurrency(item.totalAmount) }}
@@ -574,7 +649,8 @@ - + +

Aucune transaction disponible

@@ -588,7 +664,7 @@
- {{ (dailyTransactions?.items?.length || 0) }} périodes au total + {{ getCurrentTransactionData()?.items?.length || 0 }} périodes au total Voir tout
@@ -596,8 +672,8 @@
- -
+ +
@@ -627,7 +703,7 @@ name="lucideInfo" class="text-info fs-5">
- +

Aucune alerte active

Tous les systèmes fonctionnent normalement
@@ -673,11 +749,13 @@
- Dashboard FinTech v2.0 + Dashboard FinTech v2.0 • + + {{ currentRoleLabel }}
- - APIs: IAM, Config, Core, Reporting + + {{ getCurrentMerchantName() }}
diff --git a/src/app/modules/dcb-dashboard/components/dcb-reporting-dashboard.ts b/src/app/modules/dcb-dashboard/components/dcb-reporting-dashboard.ts index 822a47d..e76ecd1 100644 --- a/src/app/modules/dcb-dashboard/components/dcb-reporting-dashboard.ts +++ b/src/app/modules/dcb-dashboard/components/dcb-reporting-dashboard.ts @@ -6,13 +6,14 @@ import { ViewChild, ElementRef, AfterViewInit, - ChangeDetectorRef } from '@angular/core'; + ChangeDetectorRef +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { catchError, finalize, tap } from 'rxjs/operators'; import { of, Subscription, forkJoin, Observable } from 'rxjs'; import { Chart, ChartConfiguration as ChartJsConfiguration, registerables } from 'chart.js'; -import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; +import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { NgIconComponent, provideIcons } from '@ng-icons/core'; import { @@ -23,7 +24,7 @@ import { lucideLayoutDashboard, lucideStore, lucideFilter, lucideHeartPulse, lucideCpu, lucidePhone, lucideTrophy, lucidePlus, lucideListChecks, lucideDatabase, lucideInfo, lucideBell, lucideCode, - lucideBanknoteArrowUp + lucideBanknoteArrowUp, lucideShield, lucideGlobe } from '@ng-icons/lucide'; import { @@ -35,16 +36,14 @@ import { ReportPeriod } from '../models/dcb-reporting.models'; import { ReportService } from '../services/dcb-reporting.service'; +import { DashboardAccess, AllowedMerchant, DashboardAccessService } from '../services/dashboard-access.service'; +import { AuthService } from '@core/services/auth.service'; // ============ TYPES ET INTERFACES ============ -// Types de graphiques supportés export type ChartType = 'line' | 'bar' | 'pie' | 'doughnut'; - -// Alias pour ChartConfiguration pour éviter les conflits de noms type ChartConfiguration = ChartJsConfiguration; -// Configuration de graphique personnalisée export interface ChartConfig { type: ChartType; data: ChartDataNormalized; @@ -81,7 +80,6 @@ interface Alert { timestamp: Date; } -// État de sélection des données interface DataSelection { metric: 'revenue' | 'transactions' | 'successRate' | 'activeSubscriptions'; period: ReportPeriod; @@ -89,14 +87,12 @@ interface DataSelection { merchantPartnerId?: number; } -// Interface pour les métriques interface MetricOption { id: 'revenue' | 'transactions' | 'successRate' | 'activeSubscriptions'; label: string; icon: string; } -// Interface pour les statistiques de paiement interface PaymentStats { daily: { transactions: number; @@ -121,7 +117,6 @@ interface PaymentStats { overallSuccessRate: number; } -// Interface pour les statistiques d'abonnement interface SubscriptionStats { total: number; active: number; @@ -137,7 +132,7 @@ interface SubscriptionStats { templateUrl: './dcb-reporting-dashboard.html', styleUrls: ['./dcb-reporting-dashboard.css'], standalone: true, - imports: [CommonModule, FormsModule, NgIconComponent], + imports: [CommonModule, FormsModule, NgIconComponent, NgbDropdownModule], providers: [ provideIcons({ lucideActivity, lucideAlertCircle, lucideCheckCircle2, lucideRefreshCw, @@ -146,19 +141,19 @@ interface SubscriptionStats { lucideDollarSign, lucideUsers, lucideCreditCard, lucideSettings, lucideLayoutDashboard, lucideStore, lucideFilter, lucideHeartPulse, lucideCpu, lucidePhone, lucideTrophy, lucidePlus, lucideListChecks, - lucideDatabase, lucideInfo, lucideBell, lucideCode, lucideBanknoteArrowUp + lucideDatabase, lucideInfo, lucideBell, lucideCode, lucideBanknoteArrowUp, + lucideShield, lucideGlobe }) ] }) export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { private subscriptions: Subscription[] = []; private reportService = inject(ReportService); + private authService = inject(AuthService); - // Canvas pour les graphiques @ViewChild('mainChartCanvas') mainChartCanvas!: ElementRef; @ViewChild('comparisonChartCanvas') comparisonChartCanvas!: ElementRef; @ViewChild('successRateChartCanvas') successRateChartCanvas!: ElementRef; - @ViewChild('metricDropdown') metricDropdown?: NgbDropdown; @ViewChild('optionsDropdown') optionsDropdown?: NgbDropdown; @@ -168,7 +163,6 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { weeklyTransactions: TransactionReport | null = null; monthlyTransactions: TransactionReport | null = null; yearlyTransactions: TransactionReport | null = null; - transactionsWithDates: TransactionReport | null = null; dailySubscriptions: SubscriptionReport | null = null; merchantSubscriptions: SubscriptionReport | null = null; @@ -176,8 +170,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { syncResponse: SyncResponse | null = null; healthStatus: HealthCheckStatus[] = []; - // ============ CONFIGURATION DES GRAPHIQUES ============ - + // ============ CONFIGURATION ============ chartConfigs: ChartConfig[] = []; currentChartType: ChartType = 'line'; availableChartTypes: ChartType[] = ['line', 'bar', 'pie', 'doughnut']; @@ -190,10 +183,10 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { }; availableMetrics: MetricOption[] = [ - { id: 'revenue', label: 'Revenue', icon: 'lucideBanknoteArrowUp' }, - { id: 'transactions', label: 'Transactions', icon: 'lucideCreditCard' }, - { id: 'successRate', label: 'Taux de succès', icon: 'lucideActivity' }, - { id: 'activeSubscriptions', label: 'Abonnements actifs', icon: 'lucideUsers' } + { id: 'revenue', label: 'Revenue', icon: 'lucideBanknoteArrowUp' }, + { id: 'transactions', label: 'Transactions', icon: 'lucideCreditCard' }, + { id: 'successRate', label: 'Taux de succès', icon: 'lucideActivity' }, + { id: 'activeSubscriptions', label: 'Abonnements actifs', icon: 'lucideUsers' } ]; availablePeriods = [ @@ -204,7 +197,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { ]; // ============ PARAMÈTRES ============ - merchantId: number = 4; + merchantId: number | null = null; startDate: string = new Date().toISOString().split('T')[0]; endDate: string = new Date().toISOString().split('T')[0]; currentYear = new Date().getFullYear(); @@ -217,12 +210,13 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { weeklyTransactions: false, monthlyTransactions: false, yearlyTransactions: false, - transactionsWithDates: false, dailySubscriptions: false, merchantSubscriptions: false, sync: false, healthCheck: false, - chart: false + chart: false, + globalData: false, + merchantData: false }; errors: { [key: string]: string } = {}; @@ -230,7 +224,6 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { // ============ SANTÉ DU SYSTÈME ============ systemHealth: ServiceHealth[] = []; - alerts: Alert[] = []; stats = { @@ -249,7 +242,6 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { offlineServices: 0 }; - // Nouveau : Statut global du health check overallHealth: { status: 'healthy' | 'warning' | 'critical'; message: string; @@ -295,160 +287,437 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { ] }; + // ============ PROPRIÉTÉS RÔLES ============ + access!: DashboardAccess; + currentRoleLabel: string = ''; + currentRoleIcon: string = ''; + allowedMerchants: AllowedMerchant[] = []; + + // ============ ÉTAT D'AFFICHAGE ============ + isViewingGlobalData: boolean = true; + constructor( + private accessService: DashboardAccessService, private cdr: ChangeDetectorRef ) { Chart.register(...registerables); } - + ngOnInit(): void { - // Charger les données et vérifier la santé des APIs - this.loadAllData(); - this.checkSystemHealth(); + this.initializeAccess(); + this.loadAllowedMerchants(); + this.initializeDashboard(); - // Planifier des vérifications périodiques toutes les 5 minutes - setInterval(() => { - this.checkSystemHealth(); - }, 5 * 60 * 1000); + if (this.accessService.shouldShowSystemHealth()) { + setInterval(() => { + this.checkSystemHealth(); + }, 5 * 60 * 1000); + } } + private initializeAccess(): void { + this.access = this.accessService.getDashboardAccess(); + this.currentRoleLabel = this.access.roleLabel; + this.currentRoleIcon = this.access.roleIcon; + + const merchantPartnerId = this.access.merchantId; + + if (this.access.isMerchantUser) { + + if (merchantPartnerId) { + // Utiliser le merchantId du cache directement s'il existe + if (this.access.merchantId) { + this.merchantId = this.access.merchantId; + } else { + // Sinon, essayer de le récupérer + const idNum = Number(merchantPartnerId); + this.merchantId = isNaN(idNum) ? null : idNum; + } + + if (this.merchantId) { + this.accessService.setSelectedMerchantId(this.merchantId); + this.isViewingGlobalData = false; + } + } + } else if (this.access.isHubUser) { + const selectedMerchantId = this.accessService.getSelectedMerchantId(); + if (selectedMerchantId) { + this.merchantId = selectedMerchantId; + this.isViewingGlobalData = false; + } else { + this.isViewingGlobalData = true; + } + } + } + + // ============ INITIALISATION ============ + + private loadAllowedMerchants(): void { + this.subscriptions.push( + this.accessService.getAvailableMerchants().subscribe({ + next: (merchants) => { + this.allowedMerchants = merchants; + this.cdr.detectChanges(); + }, + error: (err) => { + console.error('Erreur lors du chargement des merchants:', err); + this.allowedMerchants = []; + } + }) + ); + } + + private initializeDashboard(): void { + console.log(`Dashboard initialisé pour: ${this.currentRoleLabel} (${this.access.isHubUser ? 'Hub User' : 'Merchant User'})`); + + if (this.access.isHubUser) { + if (this.isViewingGlobalData) { + this.loadGlobalData(); + } else { + this.loadMerchantData(this.merchantId); + } + } else { + this.loadMerchantData(this.merchantId); + } + + if (this.accessService.shouldShowSystemHealth()) { + this.checkSystemHealth(); + } + } + + // ============ CHARGEMENT DES DONNÉES ============ + private loadGlobalData(): void { + if (!this.access.isHubUser) return; + + console.log('Chargement des données globales'); + this.loading.globalData = true; + this.lastUpdated = new Date(); + + const requests = [ + this.loadDailyTransactions(), + this.loadWeeklyTransactions(), + this.loadMonthlyTransactions(), + this.loadYearlyTransactions(), + this.loadDailySubscriptions() + ]; + + this.subscriptions.push( + forkJoin(requests).subscribe({ + next: () => { + console.log('Données globales chargées avec succès'); + this.loading.globalData = false; + this.calculateStats(); + this.cdr.detectChanges(); + setTimeout(() => this.updateAllCharts(), 100); + }, + error: (err) => { + console.error('Erreur lors du chargement des données globales:', err); + this.loading.globalData = false; + this.addAlert('danger', 'Erreur de chargement', 'Impossible de charger les données globales', 'Maintenant'); + this.cdr.detectChanges(); + } + }) + ); + } + + private loadMerchantData(merchantId: number | null): void { + console.log('Chargement des données pour merchant:', merchantId); + + if (!merchantId) { + console.error('Merchant ID invalide:', merchantId); + this.addAlert('danger', 'Erreur', 'Merchant ID invalide', 'Maintenant'); + return; + } + + this.loading.merchantData = true; + this.lastUpdated = new Date(); + + const requests = [ + this.loadDailyTransactions(merchantId), + this.loadWeeklyTransactions(merchantId), + this.loadMonthlyTransactions(merchantId), + this.loadYearlyTransactions(merchantId), + this.loadDailySubscriptions(merchantId) + ]; + + this.subscriptions.push( + forkJoin(requests).subscribe({ + next: () => { + console.log(`Données du merchant ${merchantId} chargées avec succès`); + this.loading.merchantData = false; + this.calculateStats(); + this.cdr.detectChanges(); + setTimeout(() => this.updateAllCharts(), 100); + }, + error: (err) => { + console.error(`Erreur lors du chargement des données du merchant ${merchantId}:`, err); + this.loading.merchantData = false; + this.addAlert('danger', 'Erreur de chargement', `Impossible de charger les données du merchant ${merchantId}`, 'Maintenant'); + this.cdr.detectChanges(); + } + }) + ); + } + + // ============ MÉTHODES DE CHARGEMENT INDIVIDUELLES ============ + private loadDailyTransactions(merchantId?: number): Observable { + this.loading.dailyTransactions = true; + this.errors['dailyTransactions'] = ''; + + return this.reportService.getDailyTransactions(merchantId).pipe( + catchError(err => { + this.errors['dailyTransactions'] = err.message || 'Erreur de chargement'; + console.error('Error loading daily transactions:', err); + return of(null); + }), + tap(data => { + if (this.isValidTransactionReport(data)) { + if (merchantId) { + this.merchantTransactions = data; + } else { + this.dailyTransactions = data; + } + } else { + if (merchantId) { + this.merchantTransactions = null; + } else { + this.dailyTransactions = null; + } + } + }), + finalize(() => { + this.loading.dailyTransactions = false; + }) + ); + } + + private loadWeeklyTransactions(merchantId?: number): Observable { + this.loading.weeklyTransactions = true; + this.errors['weeklyTransactions'] = ''; + + return this.reportService.getWeeklyTransactions(merchantId).pipe( + catchError(err => { + this.errors['weeklyTransactions'] = err.message || 'Erreur de chargement'; + console.error('Error loading weekly transactions:', err); + return of(null); + }), + tap(data => { + if (this.isValidTransactionReport(data)) { + this.weeklyTransactions = data; + } else { + this.weeklyTransactions = null; + } + }), + finalize(() => { + this.loading.weeklyTransactions = false; + }) + ); + } + + private loadMonthlyTransactions(merchantId?: number): Observable { + this.loading.monthlyTransactions = true; + this.errors['monthlyTransactions'] = ''; + + return this.reportService.getMonthlyTransactions(merchantId).pipe( + catchError(err => { + this.errors['monthlyTransactions'] = err.message || 'Erreur de chargement'; + console.error('Error loading monthly transactions:', err); + return of(null); + }), + tap(data => { + if (this.isValidTransactionReport(data)) { + this.monthlyTransactions = data; + } else { + this.monthlyTransactions = null; + } + }), + finalize(() => { + this.loading.monthlyTransactions = false; + }) + ); + } + + private loadYearlyTransactions(merchantId?: number): Observable { + this.loading.yearlyTransactions = true; + this.errors['yearlyTransactions'] = ''; + + return this.reportService.getYearlyTransactions(this.currentYear, merchantId).pipe( + catchError(err => { + this.errors['yearlyTransactions'] = err.message || 'Erreur de chargement'; + console.error('Error loading yearly transactions:', err); + return of(null); + }), + tap(data => { + if (this.isValidTransactionReport(data)) { + this.yearlyTransactions = data; + } else { + this.yearlyTransactions = null; + } + }), + finalize(() => { + this.loading.yearlyTransactions = false; + }) + ); + } + + private loadDailySubscriptions(merchantId?: number): Observable { + this.loading.dailySubscriptions = true; + this.errors['dailySubscriptions'] = ''; + + return this.reportService.getDailySubscriptions(merchantId).pipe( + catchError(err => { + this.errors['dailySubscriptions'] = err.message || 'Erreur de chargement'; + console.error('Error loading daily subscriptions:', err); + return of(null); + }), + tap(data => { + if (this.isValidSubscriptionReport(data)) { + if (merchantId) { + this.merchantSubscriptions = data; + } else { + this.dailySubscriptions = data; + } + } else { + if (merchantId) { + this.merchantSubscriptions = null; + } else { + this.dailySubscriptions = null; + } + } + }), + finalize(() => { + this.loading.dailySubscriptions = false; + }) + ); + } + + // ============ GESTION DES MERCHANTS ============ + selectMerchant(merchantId: number): void { + if (!this.access.isHubUser) { + console.warn('Sélection de merchant réservée aux Hub Users'); + return; + } + + if (merchantId === 0) { + this.isViewingGlobalData = true; + this.merchantId = 0; + this.accessService.setSelectedMerchantId(0); + this.loadGlobalData(); + this.addAlert('info', 'Mode global activé', 'Affichage des données globales', 'Maintenant'); + } else { + this.isViewingGlobalData = false; + this.merchantId = merchantId; + this.accessService.setSelectedMerchantId(merchantId); + this.loadMerchantData(merchantId); + this.addAlert('info', 'Merchant changé', `Affichage des données du merchant ${merchantId}`, 'Maintenant'); + } + + this.dataSelection.merchantPartnerId = merchantId === 0 ? undefined : merchantId; + } + + // ============ GESTION DES GRAPHIQUES ============ ngAfterViewInit(): void { - // Initialiser les graphiques après le chargement initial setTimeout(() => { this.updateAllCharts(); }, 500); } - ngOnDestroy(): void { - this.subscriptions.forEach(sub => sub.unsubscribe()); - } - - // ============ MÉTHODES POUR LE TEMPLATE ============ - - getPaymentStats(): PaymentStats { - return { - daily: { - transactions: this.dailyTransactions?.totalCount || 0, - revenue: this.dailyTransactions?.totalAmount || 0, - successRate: this.getSuccessRate(this.dailyTransactions) - }, - weekly: { - transactions: this.weeklyTransactions?.totalCount || 0, - revenue: this.weeklyTransactions?.totalAmount || 0, - successRate: this.getSuccessRate(this.weeklyTransactions) - }, - monthly: { - transactions: this.monthlyTransactions?.totalCount || 0, - revenue: this.monthlyTransactions?.totalAmount || 0, - successRate: this.getSuccessRate(this.monthlyTransactions) - }, - yearly: { - transactions: this.stats.yearlyTransactions, - revenue: this.stats.yearlyRevenue, - successRate: this.stats.avgSuccessRate - }, - overallSuccessRate: this.stats.successRate - }; - } - - getSubscriptionStats(): SubscriptionStats { - const dailyItem = this.dailySubscriptions?.items?.[0]; - return { - total: this.dailySubscriptions?.totalCount || 0, - active: this.stats.activeSubscriptions, - newToday: dailyItem?.count || 0, - cancelled: dailyItem?.cancelledCount || 0, - activePercentage: this.dailySubscriptions?.totalCount ? - (this.stats.activeSubscriptions / this.dailySubscriptions.totalCount) * 100 : 0 - }; - } - - getPerformanceLabel(successRate: number): string { - if (successRate >= 95) return 'Excellent'; - if (successRate >= 90) return 'Bon'; - if (successRate >= 80) return 'Moyen'; - return 'À améliorer'; - } - - getAlertBadgeClass(): string { - if (this.alerts.length === 0) return 'bg-success'; - if (this.alerts.filter(a => a.type === 'danger').length > 0) return 'bg-danger'; - if (this.alerts.filter(a => a.type === 'warning').length > 0) return 'bg-warning'; - return 'bg-info'; - } - - // ============ GESTION DES GRAPHIQUES ============ - /** - * Met à jour tous les graphiques - */ updateAllCharts(): void { - if (!this.dailyTransactions) { - console.warn('Données quotidiennes non disponibles'); + const transactionData = this.getCurrentTransactionData(); + + if (!transactionData) { return; } try { - console.log('Mise à jour des graphiques...'); - if (this.mainChartCanvas?.nativeElement) { - console.log('Mise à jour du graphique principal'); this.updateMainChart(); - } else { - console.warn('Canvas principal non disponible'); } - if (this.comparisonChartCanvas?.nativeElement && this.weeklyTransactions && this.monthlyTransactions) { - console.log('Mise à jour du graphique de comparaison'); + if (this.comparisonChartCanvas?.nativeElement && this.weeklyTransactions && this.monthlyTransactions && this.isViewingGlobalData) { this.updateComparisonChart(); - } else { - console.warn('Canvas de comparaison ou données non disponibles'); } if (this.successRateChartCanvas?.nativeElement) { - console.log('Mise à jour du graphique de taux de succès'); this.updateSuccessRateChart(); - } else { - console.warn('Canvas de taux de succès non disponible'); } - console.log('Graphiques mis à jour avec succès'); - } catch (error) { console.error('Erreur lors de la mise à jour des graphiques:', error); } } - /** - * Met à jour le graphique de comparaison - */ - updateComparisonChart(): void { - if (!this.comparisonChartCanvas?.nativeElement || !this.weeklyTransactions || !this.monthlyTransactions) { - console.warn('Canvas ou données manquants pour le graphique de comparaison'); - return; - } + + private updateMainChart(): void { + if (!this.mainChartCanvas?.nativeElement) return; - const ctx = this.comparisonChartCanvas.nativeElement.getContext('2d'); - if (!ctx) { - console.error('Impossible d\'obtenir le contexte 2D du canvas'); - return; - } + const ctx = this.mainChartCanvas.nativeElement.getContext('2d'); + if (!ctx) return; try { - // Détruire l'ancien graphique si existe const existingChart = (ctx as any).chart; if (existingChart) { existingChart.destroy(); } - // Normaliser les données hebdomadaires et mensuelles - const weeklyData = this.reportService.normalizeForChart(this.weeklyTransactions, 'totalAmount'); - const monthlyData = this.reportService.normalizeForChart(this.monthlyTransactions, 'totalAmount'); + this.loading.chart = true; - if (!weeklyData || !monthlyData) { - console.warn('Données de normalisation manquantes'); + const transactionData = this.getCurrentTransactionData(); + const subscriptionData = this.getCurrentSubscriptionData(); + + if (!transactionData && !subscriptionData) { + this.loading.chart = false; return; } - // Prendre les 8 dernières périodes + const normalizedData = this.getNormalizedDataForMetric( + this.dataSelection.metric, + transactionData, + subscriptionData + ); + + if (!normalizedData || normalizedData.labels.length === 0) { + this.loading.chart = false; + return; + } + + const config = this.createChartConfig( + normalizedData, + this.dataSelection.chartType, + this.getChartTitle(this.dataSelection.metric) + ); + + const newChart = new Chart(ctx, config); + (ctx as any).chart = newChart; + + this.loading.chart = false; + this.cdr.detectChanges(); + + } catch (error) { + console.error('Erreur lors de la création du graphique principal:', error); + this.loading.chart = false; + this.cdr.detectChanges(); + } + } + + private updateComparisonChart(): void { + if (!this.comparisonChartCanvas?.nativeElement || !this.weeklyTransactions || !this.monthlyTransactions) { + return; + } + + const ctx = this.comparisonChartCanvas.nativeElement.getContext('2d'); + if (!ctx) return; + + try { + const existingChart = (ctx as any).chart; + if (existingChart) { + existingChart.destroy(); + } + + const weeklyData = this.reportService.normalizeForChart(this.weeklyTransactions, 'totalAmount'); + const monthlyData = this.reportService.normalizeForChart(this.monthlyTransactions, 'totalAmount'); + + if (!weeklyData || !monthlyData) return; + const labels = weeklyData.labels.slice(-8); const weeklyValues = weeklyData.dataset.slice(-8); const monthlyValues = monthlyData.dataset.slice(-8); @@ -478,17 +747,14 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { responsive: true, maintainAspectRatio: false, plugins: { - legend: { - position: 'top', - }, + legend: { position: 'top' }, tooltip: { callbacks: { label: (context: any) => { const value = context.raw; - if (this.dataSelection.metric === 'revenue') { - return `${this.formatCurrency(value)}`; - } - return `${this.formatNumber(value)}`; + return this.dataSelection.metric === 'revenue' + ? `${this.formatCurrency(value)}` + : `${this.formatNumber(value)}`; } } } @@ -511,7 +777,6 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { } }; - // Utilisez Chart directement, pas window.Chart const newChart = new Chart(ctx, config); (ctx as any).chart = newChart; @@ -520,90 +785,22 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { } } - /** - * Met à jour le graphique principal - */ - updateMainChart(): void { - if (!this.mainChartCanvas?.nativeElement) { - console.warn('Canvas non disponible pour le graphique principal'); - return; - } - - const ctx = this.mainChartCanvas.nativeElement.getContext('2d'); - if (!ctx) { - console.error('Impossible d\'obtenir le contexte 2D du canvas'); - return; - } - - try { - // Détruire l'ancien graphique si existe - const existingChart = (ctx as any).chart; - if (existingChart) { - existingChart.destroy(); - } - - this.loading.chart = true; - - // Obtenir les données normalisées - const normalizedData = this.getNormalizedDataForMetric( - this.dataSelection.metric, - this.dataSelection.period, - this.dataSelection.merchantPartnerId - ); - - if (!normalizedData || normalizedData.labels.length === 0) { - console.warn('Données insuffisantes pour le graphique'); - this.loading.chart = false; - return; - } - - // Créer la configuration du graphique - const config = this.createChartConfig( - normalizedData, - this.dataSelection.chartType, - this.getChartTitle(this.dataSelection.metric) - ); - - // Créer le nouveau graphique - const newChart = new Chart(ctx, config); - (ctx as any).chart = newChart; - - this.loading.chart = false; - - // Forcer la détection de changement - this.cdr.detectChanges(); - - } catch (error) { - console.error('Erreur lors de la création du graphique principal:', error); - this.loading.chart = false; - this.cdr.detectChanges(); - } - } - - /** - * Met à jour le graphique de taux de succès - */ - updateSuccessRateChart(): void { - if (!this.successRateChartCanvas?.nativeElement || !this.dailyTransactions) { - console.warn('Canvas ou données manquants pour le graphique de taux de succès'); - return; - } + private updateSuccessRateChart(): void { + if (!this.successRateChartCanvas?.nativeElement) return; const ctx = this.successRateChartCanvas.nativeElement.getContext('2d'); - if (!ctx) { - console.error('Impossible d\'obtenir le contexte 2D du canvas'); - return; - } + if (!ctx) return; try { - // Détruire l'ancien graphique si existe const existingChart = (ctx as any).chart; if (existingChart) { existingChart.destroy(); } - // Calculer le taux de succès global - const successRate = this.getSuccessRate(this.dailyTransactions); + const transactionData = this.getCurrentTransactionData(); + if (!transactionData) return; + + const successRate = this.getSuccessRate(transactionData); const remaining = 100 - successRate; const config: ChartJsConfiguration<'doughnut'> = { @@ -611,10 +808,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { data: { datasets: [{ data: [successRate, remaining], - backgroundColor: [ - this.getChartColor(successRate), - 'rgba(0, 0, 0, 0.1)' - ], + backgroundColor: [this.getChartColor(successRate), 'rgba(0, 0, 0, 0.1)'], borderWidth: 0 }] }, @@ -623,42 +817,48 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { maintainAspectRatio: false, cutout: '70%', plugins: { - legend: { - display: false - }, + legend: { display: false }, tooltip: { callbacks: { - label: (context: any) => { - return `${context.raw.toFixed(1)}%`; - } + label: (context: any) => `${context.raw.toFixed(1)}%` } } } } }; - // Utilisez Chart directement, pas window.Chart const newChart = new Chart(ctx, config); (ctx as any).chart = newChart; } catch (error) { console.error('Erreur lors de la création du graphique de taux de succès:', error); } - } - /** - * Obtient les données normalisées pour une métrique spécifique - */ + } + + // ============ UTILITAIRES DE DONNÉES ============ + getCurrentTransactionData(): TransactionReport | null { + return this.isViewingGlobalData ? + this.dailyTransactions : + (this.merchantTransactions || this.dailyTransactions); + } + + getCurrentSubscriptionData(): SubscriptionReport | null { + return this.isViewingGlobalData ? + this.dailySubscriptions : + (this.merchantSubscriptions || this.dailySubscriptions); + } + getNormalizedDataForMetric( metric: DataSelection['metric'], - period: ReportPeriod, - merchantPartnerId?: number + transactionData: TransactionReport | null, + subscriptionData: SubscriptionReport | null = null ): ChartDataNormalized | null { - const params = merchantPartnerId ? { merchantPartnerId } : undefined; + if (!transactionData && !subscriptionData) return null; switch (metric) { case 'revenue': - if (this.dailyTransactions) { - const normalized = this.reportService.normalizeForChart(this.dailyTransactions, 'totalAmount'); + if (transactionData) { + const normalized = this.reportService.normalizeForChart(transactionData, 'totalAmount'); normalized.datasetLabel = 'Revenue (XOF)'; normalized.backgroundColor = this.chartColors.primary.background; normalized.borderColor = this.chartColors.primary.border; @@ -667,8 +867,8 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { break; case 'transactions': - if (this.dailyTransactions) { - const normalized = this.reportService.normalizeForChart(this.dailyTransactions, 'count'); + if (transactionData) { + const normalized = this.reportService.normalizeForChart(transactionData, 'count'); normalized.datasetLabel = 'Nombre de transactions'; normalized.backgroundColor = this.chartColors.info.background; normalized.borderColor = this.chartColors.info.border; @@ -677,10 +877,9 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { break; case 'successRate': - if (this.dailyTransactions) { - // Calculer le taux de succès pour chaque période - const labels = this.dailyTransactions.items.map(item => item.period); - const dataset = this.dailyTransactions.items.map(item => { + if (transactionData) { + const labels = transactionData.items.map(item => item.period); + const dataset = transactionData.items.map(item => { const total = item.count || 0; const success = item.successCount || 0; return total > 0 ? (success / total) * 100 : 0; @@ -697,8 +896,9 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { break; case 'activeSubscriptions': - if (this.dailySubscriptions) { - const normalized = this.reportService.normalizeForChart(this.dailySubscriptions, 'activeCount'); + const subData = subscriptionData || this.dailySubscriptions; + if (subData) { + const normalized = this.reportService.normalizeForChart(subData, 'activeCount'); normalized.datasetLabel = 'Abonnements actifs'; normalized.backgroundColor = this.chartColors.warning.background; normalized.borderColor = this.chartColors.warning.border; @@ -709,18 +909,15 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { return null; } - - /** - * Crée une configuration de graphique - */ - createChartConfig( + + private createChartConfig( normalizedData: ChartDataNormalized, chartType: ChartType, title?: string ): ChartConfiguration { const isCircular = chartType === 'pie' || chartType === 'doughnut'; - const config: ChartConfiguration = { + return { type: chartType, data: { labels: normalizedData.labels, @@ -769,9 +966,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { scales: isCircular ? {} : { y: { beginAtZero: true, - grid: { - color: 'rgba(0, 0, 0, 0.05)' - }, + grid: { color: 'rgba(0, 0, 0, 0.05)' }, ticks: { callback: (value: any) => { if (typeof value === 'number') { @@ -784,94 +979,71 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { } }, x: { - grid: { - color: 'rgba(0, 0, 0, 0.05)' - }, - ticks: { - maxRotation: 45 - } + grid: { color: 'rgba(0, 0, 0, 0.05)' }, + ticks: { maxRotation: 45 } } } } }; - - return config; } - - /** - * Change la métrique affichée - */ - changeMetric(metric: 'revenue' | 'transactions' | 'successRate' | 'activeSubscriptions'): void { - this.dataSelection.metric = metric; - - // Fermer le dropdown - if (this.metricDropdown) { - this.metricDropdown.close(); + + // ============ GESTION DES ACTIONS ============ + refreshData(): void { + if (this.access.isHubUser) { + if (this.isViewingGlobalData) { + this.loadGlobalData(); + } else { + this.loadMerchantData(this.merchantId); + } + } else { + this.loadMerchantData(this.merchantId); } - - // Forcer la détection de changement - this.cdr.detectChanges(); - - // Mettre à jour le graphique après un court délai - setTimeout(() => { - this.updateMainChart(); - }, 50); } - /** - * Change la période affichée - */ - changePeriod(period: ReportPeriod): void { - this.dataSelection.period = period; - - // Forcer la détection de changement - this.cdr.detectChanges(); - - setTimeout(() => { - this.updateMainChart(); - }, 50); + triggerSync(): void { + if (!this.accessService.canTriggerSync()) { + this.addAlert('warning', 'Permission refusée', 'Vous n\'avez pas la permission de déclencher une synchronisation', 'Maintenant'); + return; + } + + const syncService = this.systemHealth.find(s => s.service === 'SYNC'); + if (syncService?.status === 'DOWN') { + this.addAlert('danger', 'Synchronisation impossible', 'Le service de synchronisation est actuellement hors ligne', 'Maintenant'); + return; + } + + this.loading.sync = true; + this.syncResponse = null; + + const sub = this.reportService.triggerManualSync().pipe( + catchError(err => { + this.errors['sync'] = err.message || 'Erreur de synchronisation'; + if (this.accessService.shouldShowAlerts()) { + this.addAlert('danger', 'Échec de synchronisation', `La synchronisation a échoué: ${err.message}`, 'Maintenant'); + } + return of(null); + }), + tap(data => { + if (data && typeof data.message === 'string' && typeof data.timestamp === 'string') { + this.syncResponse = data; + if (this.accessService.shouldShowAlerts()) { + this.addAlert('success', 'Synchronisation réussie', data.message, 'Maintenant'); + } + } + }), + finalize(() => { + this.loading.sync = false; + setTimeout(() => this.refreshData(), 2000); + }) + ).subscribe(); + + this.subscriptions.push(sub); } - /** - * Change le type de graphique principal - */ - changeChartType(type: ChartType): void { - this.dataSelection.chartType = type; - - // Forcer la détection de changement - this.cdr.detectChanges(); - - // Mettre à jour le graphique après un court délai - setTimeout(() => { - this.updateMainChart(); - }, 50); - } - - - /** - * Obtient le titre du graphique - */ - getChartTitle(metric: DataSelection['metric']): string { - const titles: Record = { - revenue: 'Évolution du Revenue', - transactions: 'Nombre de Transactions', - successRate: 'Taux de Succès', - activeSubscriptions: 'Abonnements Actifs' - }; - return titles[metric]; - } - - /** - * Obtient la couleur en fonction du taux de succès - */ - private getChartColor(rate: number): string { - if (rate >= 95) return '#28a745'; - if (rate >= 90) return '#ffc107'; - return '#dc3545'; - } - - // ============ VÉRIFICATION DE SANTÉ DES APIS ============ + // ============ HEALTH CHECK ============ checkSystemHealth(): void { + if (!this.accessService.shouldShowSystemHealth()) return; + this.loading.healthCheck = true; this.systemHealth = []; this.alerts = []; @@ -881,7 +1053,6 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { tap(response => { this.healthStatus = response.details; - // Convertir les résultats en format ServiceHealth this.systemHealth = response.details.map(api => ({ service: api.service, status: api.status, @@ -891,27 +1062,20 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { checkedAt: api.checkedAt, })); - // Mettre à jour les statistiques this.stats.totalServices = response.summary.total; this.stats.onlineServices = response.summary.up; this.stats.offlineServices = response.summary.down; - // Mettre à jour le statut global this.updateOverallHealth(); - - // Générer les alertes this.generateHealthAlerts(); this.loading.healthCheck = false; this.cdr.detectChanges(); }), catchError(err => { console.error('Erreur lors du health check:', err); + this.addAlert('danger', 'Erreur de vérification', 'Impossible de vérifier la santé des services', 'Maintenant'); this.loading.healthCheck = false; - this.cdr.detectChanges(); - - this.addAlert('danger', 'Erreur de vérification', - 'Impossible de vérifier la santé des services', 'Maintenant'); return of(null); }) ).subscribe() @@ -948,7 +1112,6 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { } private generateHealthAlerts(): void { - // Nettoyer les anciennes alertes de santé this.alerts = this.alerts.filter(alert => !alert.title.includes('hors ligne') && !alert.title.includes('opérationnels') @@ -968,444 +1131,34 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { } } - private addAlert(type: 'warning' | 'info' | 'success' | 'danger', - title: string, - description: string, - time: string): void { - if (this.alerts.length >= 5) { - this.alerts.shift(); - } - - this.alerts.push({ - type, - title, - description, - time, - timestamp: new Date() - }); - } - - // ============ CHARGEMENT DES DONNÉES ============ -// ============ CHARGEMENT DES DONNÉES ============ - loadAllData(): void { - this.loading.all = true; - this.lastUpdated = new Date(); - - const requests = [ - this.loadDailyTransactions(), - this.loadMerchantTransactions(), - this.loadWeeklyTransactions(), - this.loadMonthlyTransactions(), - this.loadYearlyTransactions(), - this.loadTransactionsWithDates(), - this.loadDailySubscriptions(), - this.loadMerchantSubscriptions() - ]; - - this.subscriptions.push( - forkJoin(requests).subscribe({ - next: () => { - this.loading.all = false; - this.calculateStats(); - - // Forcer la détection de changement - this.cdr.detectChanges(); - - // Maintenant initialiser les graphiques - setTimeout(() => { - this.initializeCharts(); - }, 100); - }, - error: (err) => { - console.error('Error loading all data:', err); - this.loading.all = false; - this.addAlert('danger', 'Erreur de chargement', - 'Impossible de charger toutes les données', 'Maintenant'); - - this.cdr.detectChanges(); - } - }) - ); - } - - /** - * Initialise les graphiques après le chargement des données - */ - private initializeCharts(): void { - if (this.dailyTransactions && this.mainChartCanvas?.nativeElement) { - this.updateAllCharts(); - } else { - // Réessayer après un court délai - setTimeout(() => { - if (this.dailyTransactions) { - this.updateAllCharts(); - } - }, 200); - } - - this.cdr.detectChanges(); - } - - // ============ MÉTHODES DE CHARGEMENT ============ - loadDailyTransactions(): Observable { - this.loading.dailyTransactions = true; - this.errors['dailyTransactions'] = ''; - - return this.reportService.getTransactionReport({}, 'daily').pipe( - catchError(err => { - this.errors['dailyTransactions'] = err.message || 'Erreur de chargement'; - console.error('Error loading daily transactions:', err); - return of(null); - }), - tap(data => { - if (this.isValidTransactionReport(data)) { - this.dailyTransactions = data; - } else { - this.dailyTransactions = null; - if (data !== null) { - this.errors['dailyTransactions'] = 'Format de données invalide'; - } - } - }), - finalize(() => { - this.loading.dailyTransactions = false; - }) - ); - } - - private loadMerchantTransactions(): Observable { - this.loading.merchantTransactions = true; - this.errors['merchantTransactions'] = ''; - - const params = { - merchantPartnerId: this.merchantId - }; - - return this.reportService.getTransactionReport(params, 'daily').pipe( - catchError(err => { - this.errors['merchantTransactions'] = err.message || 'Erreur de chargement'; - console.error('Error loading merchant transactions:', err); - return of(null); - }), - tap(data => { - if (this.isValidTransactionReport(data)) { - this.merchantTransactions = data; - } else { - this.merchantTransactions = null; - } - }), - finalize(() => { - this.loading.merchantTransactions = false; - }) - ); - } - - private loadWeeklyTransactions(): Observable { - this.loading.weeklyTransactions = true; - this.errors['weeklyTransactions'] = ''; - - return this.reportService.getTransactionReport({}, 'weekly').pipe( - catchError(err => { - this.errors['weeklyTransactions'] = err.message || 'Erreur de chargement'; - console.error('Error loading weekly transactions:', err); - return of(null); - }), - tap(data => { - if (this.isValidTransactionReport(data)) { - this.weeklyTransactions = data; - } else { - this.weeklyTransactions = null; - } - }), - finalize(() => { - this.loading.weeklyTransactions = false; - }) - ); - } - - private loadMonthlyTransactions(): Observable { - this.loading.monthlyTransactions = true; - this.errors['monthlyTransactions'] = ''; - - return this.reportService.getTransactionReport({}, 'monthly').pipe( - catchError(err => { - this.errors['monthlyTransactions'] = err.message || 'Erreur de chargement'; - console.error('Error loading monthly transactions:', err); - return of(null); - }), - tap(data => { - if (this.isValidTransactionReport(data)) { - this.monthlyTransactions = data; - } else { - this.monthlyTransactions = null; - } - }), - finalize(() => { - this.loading.monthlyTransactions = false; - }) - ); - } - - private loadYearlyTransactions(): Observable { - this.loading.yearlyTransactions = true; - this.errors['yearlyTransactions'] = ''; - - const startDate = `${this.currentYear}-01-01`; - const endDate = `${this.currentYear}-12-31`; - - return this.reportService.getTransactionsWithDates(startDate, endDate).pipe( - catchError(err => { - this.errors['yearlyTransactions'] = err.message || 'Erreur de chargement'; - console.error('Error loading yearly transactions:', err); - return of(null); - }), - tap(data => { - if (this.isValidTransactionReport(data)) { - this.yearlyTransactions = data; - } else { - this.yearlyTransactions = null; - } - }), - finalize(() => { - this.loading.yearlyTransactions = false; - }) - ); - } - - loadTransactionsWithDates(): Observable { - this.loading.transactionsWithDates = true; - this.errors['transactionsWithDates'] = ''; - - return this.reportService.getTransactionsWithDates(this.startDate, this.endDate).pipe( - catchError(err => { - this.errors['transactionsWithDates'] = err.message || 'Erreur de chargement'; - console.error('Error loading transactions with dates:', err); - return of(null); - }), - tap(data => { - if (this.isValidTransactionReport(data)) { - this.transactionsWithDates = data; - } else { - this.transactionsWithDates = null; - } - }), - finalize(() => { - this.loading.transactionsWithDates = false; - }) - ); - } - - loadDailySubscriptions(): Observable { - this.loading.dailySubscriptions = true; - this.errors['dailySubscriptions'] = ''; - - return this.reportService.getSubscriptionReport({}, 'daily').pipe( - catchError(err => { - this.errors['dailySubscriptions'] = err.message || 'Erreur de chargement'; - console.error('Error loading daily subscriptions:', err); - return of(null); - }), - tap(data => { - if (this.isValidSubscriptionReport(data)) { - this.dailySubscriptions = data; - } else { - this.dailySubscriptions = null; - } - }), - finalize(() => { - this.loading.dailySubscriptions = false; - }) - ); - } - - private loadMerchantSubscriptions(): Observable { - this.loading.merchantSubscriptions = true; - this.errors['merchantSubscriptions'] = ''; - - const params = { - merchantPartnerId: this.merchantId - }; - - return this.reportService.getSubscriptionReport(params, 'monthly').pipe( - catchError(err => { - this.errors['merchantSubscriptions'] = err.message || 'Erreur de chargement'; - console.error('Error loading merchant subscriptions:', err); - return of(null); - }), - tap(data => { - if (this.isValidSubscriptionReport(data)) { - this.merchantSubscriptions = data; - } else { - this.merchantSubscriptions = null; - } - }), - finalize(() => { - this.loading.merchantSubscriptions = false; - }) - ); - } - - // ============ MÉTHODES PUBLIQUES ============ - refreshChartData(): void { - this.cdr.detectChanges(); - setTimeout(() => { - this.updateAllCharts(); - }, 50); - } - - refreshMerchantData(): void { - this.subscriptions.push( - this.loadMerchantTransactions().subscribe(), - this.loadMerchantSubscriptions().subscribe(() => { - this.calculateStats(); - this.cdr.detectChanges(); - setTimeout(() => { - this.updateAllCharts(); - }, 100); - }) - ); - } - - refreshWithDates(): void { - this.subscriptions.push( - this.loadTransactionsWithDates().subscribe(() => { - this.calculateStats(); - this.updateAllCharts(); - }) - ); - } - - refreshHealthCheck(): void { - this.checkSystemHealth(); - } - - - triggerSync(): void { - const syncService = this.systemHealth.find(s => s.service === 'SYNC'); - - if (syncService?.status === 'DOWN') { - this.addAlert('danger', 'Synchronisation impossible', - 'Le service de synchronisation est actuellement hors ligne', 'Maintenant'); - return; - } - - this.loading.sync = true; - this.syncResponse = null; - - const sub = this.reportService.triggerManualSync().pipe( - catchError(err => { - this.errors['sync'] = err.message || 'Erreur de synchronisation'; - this.addAlert('danger', 'Échec de synchronisation', - `La synchronisation a échoué: ${err.message}`, 'Maintenant'); - return of(null); - }), - tap(data => { - if (data && typeof data.message === 'string' && typeof data.timestamp === 'string') { - this.syncResponse = data; - this.addAlert('success', 'Synchronisation réussie', - data.message, 'Maintenant'); - } - }), - finalize(() => { - this.loading.sync = false; - setTimeout(() => this.loadAllData(), 2000); - }) - ).subscribe(); - - this.subscriptions.push(sub); - } - - // ============ MÉTHODES DE RAFRAÎCHISSEMENT INDIVIDUEL ============ - refreshDailyTransactions(): void { - const sub = this.loadDailyTransactions().subscribe(() => { - this.calculateStats(); - this.updateAllCharts(); - }); - this.subscriptions.push(sub); - } - - refreshWeeklyTransactions(): void { - const sub = this.loadWeeklyTransactions().subscribe(() => { - this.calculateStats(); - this.updateAllCharts(); - }); - this.subscriptions.push(sub); - } - - refreshYearlyTransactions(): void { - const sub = this.loadYearlyTransactions().subscribe(() => { - this.calculateStats(); - this.updateAllCharts(); - }); - this.subscriptions.push(sub); - } - - refreshDailySubscriptions(): void { - const sub = this.loadDailySubscriptions().subscribe(() => { - this.calculateStats(); - this.updateAllCharts(); - }); - this.subscriptions.push(sub); - } - - // ============ UTILITAIRES ============ - private isValidTransactionReport(data: any): data is TransactionReport { - return data !== null && - data !== undefined && - typeof data === 'object' && - Array.isArray(data.items); - } - - private isValidSubscriptionReport(data: any): data is SubscriptionReport { - return data !== null && - data !== undefined && - typeof data === 'object' && - Array.isArray(data.items); - } - // ============ CALCUL DES STATISTIQUES ============ private calculateStats(): void { - this.stats.totalRevenue = [ - this.dailyTransactions?.totalAmount || 0, - this.weeklyTransactions?.totalAmount || 0, - this.monthlyTransactions?.totalAmount || 0 - ].reduce((a, b) => a + b, 0); + const transactionData = this.getCurrentTransactionData(); + const subscriptionData = this.getCurrentSubscriptionData(); - this.stats.totalTransactions = [ - this.dailyTransactions?.totalCount || 0, - this.weeklyTransactions?.totalCount || 0, - this.monthlyTransactions?.totalCount || 0 - ].reduce((a, b) => a + b, 0); + if (transactionData) { + this.stats.totalRevenue = transactionData.totalAmount || 0; + this.stats.totalTransactions = transactionData.totalCount || 0; + this.stats.successRate = this.getSuccessRate(transactionData); + + if (transactionData.items?.length) { + this.stats.maxRevenueDay = Math.max(...transactionData.items.map(item => item.totalAmount || 0)); + } + } this.stats.yearlyRevenue = this.yearlyTransactions?.totalAmount || 0; this.stats.yearlyTransactions = this.yearlyTransactions?.totalCount || 0; - this.stats.totalSubscriptions = [ - this.dailySubscriptions?.totalCount || 0, - this.merchantSubscriptions?.totalCount || 0 - ].reduce((a, b) => a + b, 0); - - const successRates = []; - if (this.dailyTransactions) { - const rate = this.getSuccessRate(this.dailyTransactions); - if (rate > 0) successRates.push(rate); + if (subscriptionData) { + this.stats.totalSubscriptions = subscriptionData.totalCount || 0; + this.stats.activeSubscriptions = subscriptionData.items?.reduce( + (sum, item) => sum + (item.activeCount || 0), 0 + ) || 0; } - - this.stats.successRate = successRates.length > 0 - ? successRates.reduce((a, b) => a + b, 0) / successRates.length - : this.stats.avgSuccessRate; - - this.stats.activeSubscriptions = this.dailySubscriptions?.items?.reduce( - (sum, item) => sum + (item.activeCount || 0), 0 - ) || 0; this.stats.avgTransaction = this.stats.totalTransactions > 0 ? this.stats.totalRevenue / this.stats.totalTransactions : 0; - - if (this.dailyTransactions?.items?.length) { - this.stats.maxRevenueDay = Math.max(...this.dailyTransactions.items.map(item => item.totalAmount || 0)); - } } getSuccessRate(transaction: TransactionReport | null): number { @@ -1416,6 +1169,105 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { return total > 0 ? (success / total) * 100 : 0; } + // ============ MÉTHODES UTILITAIRES POUR LE TEMPLATE ============ + getPaymentStats(): PaymentStats { + const transactionData = this.getCurrentTransactionData(); + + return { + daily: { + transactions: transactionData?.totalCount || 0, + revenue: transactionData?.totalAmount || 0, + successRate: this.getSuccessRate(transactionData) + }, + weekly: { + transactions: this.weeklyTransactions?.totalCount || 0, + revenue: this.weeklyTransactions?.totalAmount || 0, + successRate: this.getSuccessRate(this.weeklyTransactions) + }, + monthly: { + transactions: this.monthlyTransactions?.totalCount || 0, + revenue: this.monthlyTransactions?.totalAmount || 0, + successRate: this.getSuccessRate(this.monthlyTransactions) + }, + yearly: { + transactions: this.stats.yearlyTransactions, + revenue: this.stats.yearlyRevenue, + successRate: this.stats.avgSuccessRate + }, + overallSuccessRate: this.stats.successRate + }; + } + + getSubscriptionStats(): SubscriptionStats { + const subscriptionData = this.getCurrentSubscriptionData(); + const dailyItem = subscriptionData?.items?.[0]; + + return { + total: subscriptionData?.totalCount || 0, + active: this.stats.activeSubscriptions, + newToday: dailyItem?.count || 0, + cancelled: dailyItem?.cancelledCount || 0, + activePercentage: subscriptionData?.totalCount ? + (this.stats.activeSubscriptions / subscriptionData.totalCount) * 100 : 0 + }; + } + + getPerformanceLabel(successRate: number): string { + if (successRate >= 95) return 'Excellent'; + if (successRate >= 90) return 'Bon'; + if (successRate >= 80) return 'Moyen'; + return 'À améliorer'; + } + + getAlertBadgeClass(): string { + if (this.alerts.length === 0) return 'bg-success'; + if (this.alerts.filter(a => a.type === 'danger').length > 0) return 'bg-danger'; + if (this.alerts.filter(a => a.type === 'warning').length > 0) return 'bg-warning'; + return 'bg-info'; + } + + // ============ GESTION DES GRAPHIQUES (PUBLIC) ============ + changeMetric(metric: 'revenue' | 'transactions' | 'successRate' | 'activeSubscriptions'): void { + this.dataSelection.metric = metric; + + if (this.metricDropdown) { + this.metricDropdown.close(); + } + + this.cdr.detectChanges(); + + setTimeout(() => { + this.updateMainChart(); + }, 50); + } + + changePeriod(period: ReportPeriod): void { + this.dataSelection.period = period; + + this.cdr.detectChanges(); + + setTimeout(() => { + this.updateMainChart(); + }, 50); + } + + changeChartType(type: ChartType): void { + this.dataSelection.chartType = type; + + this.cdr.detectChanges(); + + setTimeout(() => { + this.updateMainChart(); + }, 50); + } + + refreshChartData(): void { + this.cdr.detectChanges(); + setTimeout(() => { + this.updateAllCharts(); + }, 50); + } + // ============ FORMATAGE ============ formatCurrency(amount: number): string { if (amount >= 1000000) { @@ -1494,4 +1346,236 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { const metric = this.availableMetrics.find(m => m.id === this.dataSelection.metric); return metric?.label || 'Revenue'; } + + getChartTitle(metric: DataSelection['metric']): string { + const titles: Record = { + revenue: 'Évolution du Revenue', + transactions: 'Nombre de Transactions', + successRate: 'Taux de Succès', + activeSubscriptions: 'Abonnements Actifs' + }; + return titles[metric]; + } + + private getChartColor(rate: number): string { + if (rate >= 95) return '#28a745'; + if (rate >= 90) return '#ffc107'; + return '#dc3545'; + } + + // ============ MÉTHODES UTILITAIRES POUR LES RÔLES ============ + getRoleStatusIcon(): string { + if (this.access.isHubUser) return 'lucideShield'; + return 'lucideStore'; + } + + getRoleStatusColor(): string { + if (this.access.isHubUser) return 'text-primary'; + return 'text-success'; + } + + getRoleBadgeClass(): string { + if (this.access.isHubUser) return 'bg-primary'; + return 'bg-success'; + } + + // ============ MÉTHODES UTILITAIRES POUR LES PERMISSIONS ============ + shouldShowSystemHealth(): boolean { + return this.accessService.shouldShowSystemHealth(); + } + + shouldShowAllTransactions(): boolean { + return this.access.isHubUser; + } + + canTriggerSync(): boolean { + return this.accessService.canTriggerSync(); + } + + canManageMerchants(): boolean { + return this.accessService.canManageMerchants(); + } + + shouldShowTransactions(): boolean { + return true; + } + + shouldShowCharts(): boolean { + return true; + } + + shouldShowKPIs(): boolean { + return true; + } + + shouldShowAlerts(): boolean { + return this.accessService.shouldShowAlerts(); + } + + canRefreshData(): boolean { + return true; + } + + canSelectMerchant(): boolean { + return this.access.isHubUser && this.allowedMerchants.length > 0; + } + + shouldShowMerchantId(): boolean { + return this.accessService.shouldShowMerchantId(); + } + + canEditMerchantFilter(): boolean { + return this.accessService.canEditMerchantFilter(); + } + + // ============ MÉTHODES SPÉCIFIQUES AU CONTEXTE ============ + isViewingGlobal(): boolean { + return this.isViewingGlobalData; + } + + isViewingMerchant(): boolean { + return !this.isViewingGlobalData && !this.merchantId ; + } + + getCurrentMerchantName(): string { + if (this.isViewingGlobalData) { + return 'Données globales'; + } + + const merchant = this.allowedMerchants.find(m => m.id === this.merchantId); + return merchant ? merchant.name : `Merchant ${this.merchantId}`; + } + + private getCurrentMerchantPartnerId(): string | null { + // Utiliser une valeur par défaut sécurisée + return this.access?.merchantId?.toString() || null; + } + + shouldShowMerchantSelector(): boolean { + return this.access.isHubUser && this.allowedMerchants.length > 1; + } + + // ============ MÉTHODES POUR LES KPIs ============ + getDisplayedKPIs(): any[] { + const kpis = []; + const paymentStats = this.getPaymentStats(); + const subscriptionStats = this.getSubscriptionStats(); + + kpis.push({ + title: 'Transactions', + value: this.formatNumber(paymentStats.daily.transactions), + subtext: 'Journalier', + metric: 'transactions', + color: 'primary', + icon: 'lucideStore', + period: 'daily' + }); + + if (this.access.isHubUser && this.isViewingGlobalData) { + kpis.push({ + title: 'Transactions', + value: this.formatNumber(paymentStats.weekly.transactions), + subtext: 'Hebdomadaire', + metric: 'transactions', + color: 'info', + icon: 'lucideCalendar', + period: 'weekly' + }); + } + + kpis.push({ + title: `Revenue ${this.currentYear}`, + value: this.formatCurrency(this.stats.yearlyRevenue), + subtext: 'Annuel', + metric: 'revenue', + color: 'purple', + icon: 'lucideTrophy', + period: 'yearly' + }); + + kpis.push({ + title: 'Abonnements', + value: this.formatNumber(subscriptionStats.active), + subtext: 'Actifs', + metric: 'subscriptions', + color: 'warning', + icon: 'lucideUsers', + period: 'current' + }); + + return kpis; + } + + shouldShowChart(chartType: 'main' | 'comparison' | 'successRate'): boolean { + switch (chartType) { + case 'main': + return true; + + case 'comparison': + return this.access.isHubUser && this.isViewingGlobalData; + + case 'successRate': + return this.getCurrentTransactionData() !== null; + + default: + return false; + } + } + + shouldShowSection(section: 'health' | 'alerts' | 'tables' | 'footerInfo'): boolean { + switch (section) { + case 'health': + return this.shouldShowSystemHealth(); + + case 'alerts': + return this.shouldShowAlerts(); + + case 'tables': + return true; + + case 'footerInfo': + return true; + + default: + return false; + } + } + + // ============ GESTION DES ALERTES ============ + private addAlert(type: 'warning' | 'info' | 'success' | 'danger', + title: string, + description: string, + time: string): void { + if (this.alerts.length >= 5) { + this.alerts.shift(); + } + + this.alerts.push({ + type, + title, + description, + time, + timestamp: new Date() + }); + } + + // ============ VALIDATION ============ + private isValidTransactionReport(data: any): data is TransactionReport { + return data !== null && + data !== undefined && + typeof data === 'object' && + Array.isArray(data.items); + } + + private isValidSubscriptionReport(data: any): data is SubscriptionReport { + return data !== null && + data !== undefined && + typeof data === 'object' && + Array.isArray(data.items); + } + + // ============ DESTRUCTION ============ + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } } \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/services/dashboard-access.service.ts b/src/app/modules/dcb-dashboard/services/dashboard-access.service.ts new file mode 100644 index 0000000..5e7127a --- /dev/null +++ b/src/app/modules/dcb-dashboard/services/dashboard-access.service.ts @@ -0,0 +1,233 @@ +import { inject, Injectable, Injector } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; +import { UserRole, RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service'; +import { AuthService } from '@core/services/auth.service'; + +// Interface minimaliste +export interface DashboardAccess { + // Type d'utilisateur - CORE + isHubUser: boolean; + isMerchantUser: boolean; + + // Info du rôle + roleLabel: string; + roleIcon: string; + userRole: UserRole; + + // Merchant info (seulement pour merchant users) + merchantId?: number; +} + +export interface AllowedMerchant { + id: number | undefined; + name: string; +} + +@Injectable({ providedIn: 'root' }) +export class DashboardAccessService { + private accessCache: DashboardAccess | null = null; + private merchantsCache: AllowedMerchant[] | null = null; + private currentMerchantId: number | null = null; + + private readonly injector = inject(Injector); + + constructor( + private roleService: RoleManagementService, + private merchantService: MerchantConfigService + ) {} + + /** + * Obtient l'accès simplifié + */ + getDashboardAccess(): DashboardAccess { + if (this.accessCache) { + return this.accessCache; + } + + const userRole = this.roleService.getCurrentRole(); + const isHubUser = this.roleService.isHubUser(); + + const access: DashboardAccess = { + isHubUser, + isMerchantUser: !isHubUser, + roleLabel: this.roleService.getRoleLabel(), + roleIcon: this.roleService.getRoleIcon(), + userRole: userRole || UserRole.DCB_SUPPORT, + }; + + // Pour les merchant users, définir leur merchant ID + if (!isHubUser) { + access.merchantId = this.getMerchantIdForUser(); + } + + this.accessCache = access; + return access; + } + + /** + * Obtient le merchant ID pour un merchant user + */ + private getMerchantIdForUser(): number | undefined { + // Récupérer le merchant ID de l'utilisateur courant + const authService = this.injector.get(AuthService); + + const merchantPartnerId = authService.getCurrentMerchantPartnerId(); + + if (!merchantPartnerId) return undefined; + + const merchantId = parseInt(merchantPartnerId, 10); + + return isNaN(merchantId) ? undefined : merchantId; + } + + /** + * Obtient les merchants disponibles + * - Hub users: tous les merchants + * - Merchant users: seulement leur merchant + */ + getAvailableMerchants(): Observable { + if (this.merchantsCache) { + return of(this.merchantsCache); + } + + const access = this.getDashboardAccess(); + + if (access.isHubUser) { + // Hub users voient tous les merchants + return this.merchantService.getAllMerchants().pipe( + map(merchants => { + const availableMerchants: any[] = merchants.map(m => ({ + id: m.id, + name: m.name + })); + this.merchantsCache = availableMerchants; + return availableMerchants; + }), + catchError(error => { + console.error('Erreur chargement merchants:', error); + return of([]); + }) + ); + } else { + // Merchant users: seulement leur merchant + const merchantId = access.merchantId || this.getMerchantIdForUser(); + return of([{ + id: merchantId, + name: `Merchant ${merchantId}` + }]); + } + } + + /** + * Définit le merchant sélectionné + */ + setSelectedMerchantId(merchantId: number): void { + this.currentMerchantId = merchantId; + } + + /** + * Obtient le merchant sélectionné + */ + getSelectedMerchantId(): number | null { + const access = this.getDashboardAccess(); + + if (access.isMerchantUser) { + // Merchant users: toujours leur merchant + return access.merchantId || null; + } + + // Hub users: le merchant sélectionné ou le premier + return this.currentMerchantId; + } + + /** + * Vérifie si un merchant est accessible + */ + canAccessMerchant(merchantId: number): Observable { + const access = this.getDashboardAccess(); + + // Hub users: tous les merchants sont accessibles + if (access.isHubUser) { + return of(true); + } + + // Merchant users: seulement leur merchant + return of(access.merchantId === merchantId); + } + + /** + * Nettoie le cache + */ + clearCache(): void { + this.accessCache = null; + this.merchantsCache = null; + this.currentMerchantId = null; + } + + /** + * Méthodes utilitaires pour le template + */ + + // Pour les Hub Users + shouldShowSystemHealth(): boolean { + return this.getDashboardAccess().isHubUser; + } + + shouldShowAllTransactions(): boolean { + return this.getDashboardAccess().isHubUser; + } + + canTriggerSync(): boolean { + const access = this.getDashboardAccess(); + return access.isHubUser && access.userRole === UserRole.DCB_ADMIN; + } + + canManageMerchants(): boolean { + const access = this.getDashboardAccess(); + return access.isHubUser && access.userRole === UserRole.DCB_ADMIN; + } + + // Pour tous les utilisateurs + shouldShowTransactions(): boolean { + return true; // Tous peuvent voir les transactions (mais scope différent) + } + + shouldShowCharts(): boolean { + return true; // Tous peuvent voir les transactions (mais scope différent) + } + + shouldShowKPIs(): boolean { + return true; // Tous peuvent voir les transactions (mais scope différent) + } + + shouldShowAlerts(): boolean { + return true; // Tous peuvent voir les transactions (mais scope différent) + } + + canRefreshData(): boolean { + return true; // Tous peuvent rafraîchir + } + + // Pour la sélection de merchant + canSelectMerchant(): boolean { + return this.getDashboardAccess().isHubUser; + } + + // Pour l'affichage du merchant ID + shouldShowMerchantId(): boolean { + const access = this.getDashboardAccess(); + return access.isMerchantUser || + (access.isHubUser && this.getSelectedMerchantId() !== null); + } + + // Pour l'édition du merchant filter + canEditMerchantFilter(): boolean { + const access = this.getDashboardAccess(); + if (access.isHubUser) { + return access.userRole === UserRole.DCB_ADMIN; + } + return access.userRole === UserRole.DCB_PARTNER_ADMIN; + } +} \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/services/dcb-reporting.service.ts b/src/app/modules/dcb-dashboard/services/dcb-reporting.service.ts index 55b853e..edd5ee0 100644 --- a/src/app/modules/dcb-dashboard/services/dcb-reporting.service.ts +++ b/src/app/modules/dcb-dashboard/services/dcb-reporting.service.ts @@ -14,10 +14,10 @@ import { environment } from '@environments/environment'; @Injectable({ providedIn: 'root' }) export class ReportService { - private baseUrl = `${environment.reportingApiUrl}/reporting`; - private readonly DEFAULT_TIMEOUT = 5000; // Timeout réduit pour les health checks + private baseUrl = `${environment.reportingApiUrl}/reporting`; // Mise à jour du chemin + private readonly DEFAULT_TIMEOUT = 5000; private readonly DEFAULT_RETRY = 1; - private readonly REPORTING_TIMEOUT = 10000; // Timeout normal pour les autres requêtes + private readonly REPORTING_TIMEOUT = 10000; // Configuration des APIs à scanner private apiEndpoints = { @@ -35,8 +35,15 @@ export class ReportService { private buildParams(params?: ReportParams): HttpParams { let httpParams = new HttpParams(); if (!params) return httpParams; + Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) httpParams = httpParams.set(key, value.toString()); + if (value !== undefined && value !== null) { + if (value instanceof Date) { + httpParams = httpParams.set(key, this.formatDate(value)); + } else { + httpParams = httpParams.set(key, value.toString()); + } + } }); return httpParams; } @@ -57,9 +64,227 @@ export class ReportService { } // --------------------- - // Health checks + // TRANSACTIONS - NOUVELLES MÉTHODES AVEC merchantPartnerId // --------------------- + /** + * Transactions journalières (global ou par merchant) + */ + getDailyTransactions(merchantPartnerId?: number): Observable { + const params = merchantPartnerId ? { merchantPartnerId } : undefined; + return this.http.get(`${this.baseUrl}/transactions/daily`, { + params: this.buildParams(params) + }).pipe( + timeout(this.REPORTING_TIMEOUT), + retry(this.DEFAULT_RETRY), + catchError(err => this.handleError(err)) + ); + } + + /** + * Transactions hebdomadaires (global ou par merchant) + */ + getWeeklyTransactions(merchantPartnerId?: number): Observable { + const params = merchantPartnerId ? { merchantPartnerId } : undefined; + return this.http.get(`${this.baseUrl}/transactions/weekly`, { + params: this.buildParams(params) + }).pipe( + timeout(this.REPORTING_TIMEOUT), + retry(this.DEFAULT_RETRY), + catchError(err => this.handleError(err)) + ); + } + + /** + * Transactions mensuelles (global ou par merchant) + */ + getMonthlyTransactions(merchantPartnerId?: number): Observable { + const params = merchantPartnerId ? { merchantPartnerId } : undefined; + return this.http.get(`${this.baseUrl}/transactions/monthly`, { + params: this.buildParams(params) + }).pipe( + timeout(this.REPORTING_TIMEOUT), + retry(this.DEFAULT_RETRY), + catchError(err => this.handleError(err)) + ); + } + + /** + * Transactions avec dates spécifiques (global ou par merchant) + */ + getTransactionsByDateRange( + startDate: string, + endDate: string, + merchantPartnerId?: number + ): Observable { + const params: any = { startDate, endDate }; + if (merchantPartnerId) { + params.merchantPartnerId = merchantPartnerId; + } + + return this.http.get(`${this.baseUrl}/transactions/daily`, { + params: this.buildParams(params) + }).pipe( + timeout(this.REPORTING_TIMEOUT), + retry(this.DEFAULT_RETRY), + catchError(err => this.handleError(err)) + ); + } + + /** + * Transactions annuelles (global ou par merchant) + */ + getYearlyTransactions(year: number, merchantPartnerId?: number): Observable { + const startDate = `${year}-01-01`; + const endDate = `${year}-12-31`; + return this.getTransactionsByDateRange(startDate, endDate, merchantPartnerId); + } + + // --------------------- + // SUBSCRIPTIONS - NOUVELLES MÉTHODES AVEC merchantPartnerId + // --------------------- + + /** + * Abonnements journaliers (global ou par merchant) + */ + getDailySubscriptions(merchantPartnerId?: number): Observable { + const params = merchantPartnerId ? { merchantPartnerId } : undefined; + return this.http.get(`${this.baseUrl}/subscriptions/daily`, { + params: this.buildParams(params) + }).pipe( + timeout(this.REPORTING_TIMEOUT), + retry(this.DEFAULT_RETRY), + catchError(err => this.handleError(err)) + ); + } + + /** + * Abonnements mensuels (global ou par merchant) + */ + getMonthlySubscriptions(merchantPartnerId?: number): Observable { + const params = merchantPartnerId ? { merchantPartnerId } : undefined; + return this.http.get(`${this.baseUrl}/subscriptions/monthly`, { + params: this.buildParams(params) + }).pipe( + timeout(this.REPORTING_TIMEOUT), + retry(this.DEFAULT_RETRY), + catchError(err => this.handleError(err)) + ); + } + + /** + * Abonnements hebdomadaires (global ou par merchant) + */ + getWeeklySubscriptions(merchantPartnerId?: number): Observable { + const params = merchantPartnerId ? { merchantPartnerId } : undefined; + return this.http.get(`${this.baseUrl}/subscriptions/weekly`, { + params: this.buildParams(params) + }).pipe( + timeout(this.REPORTING_TIMEOUT), + retry(this.DEFAULT_RETRY), + catchError(err => this.handleError(err)) + ); + } + + // --------------------- + // SYNCHRONISATION + // --------------------- + + triggerManualSync(): Observable { + return this.http.post(`${this.baseUrl}/sync/full`, {}).pipe( + timeout(this.REPORTING_TIMEOUT), + retry(this.DEFAULT_RETRY), + catchError(err => this.handleError(err)) + ); + } + + // --------------------- + // MÉTHODES DE SUPPORT POUR LA COMPATIBILITÉ + // --------------------- + + /** + * Compatibilité + */ + getTransactionReport( + params?: ReportParams, + period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily' + ): Observable { + switch (period) { + case 'daily': + return this.getDailyTransactions(params?.merchantPartnerId); + case 'weekly': + return this.getWeeklyTransactions(params?.merchantPartnerId); + case 'monthly': + return this.getMonthlyTransactions(params?.merchantPartnerId); + case 'yearly': + return this.getYearlyTransactions( + new Date().getFullYear(), + params?.merchantPartnerId + ); + default: + return this.getDailyTransactions(params?.merchantPartnerId); + } + } + + /** + * Compatibilité + */ + getSubscriptionReport( + params?: ReportParams, + period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily' + ): Observable { + switch (period) { + case 'daily': + return this.getDailySubscriptions(params?.merchantPartnerId); + case 'weekly': + return this.getWeeklySubscriptions(params?.merchantPartnerId); + case 'monthly': + return this.getMonthlySubscriptions(params?.merchantPartnerId); + default: + return this.getDailySubscriptions(params?.merchantPartnerId); + } + } + + /** + * Compatibilité + */ + getTransactionsWithDates( + startDate: string, + endDate: string, + merchantPartnerId?: number + ): Observable { + return this.getTransactionsByDateRange(startDate, endDate, merchantPartnerId); + } + + // --------------------- + // Multi-partner normalization for charts + // --------------------- + normalizeForChart(data: TransactionReport | SubscriptionReport, key: string): ChartDataNormalized { + const labels = data.items.map(i => i.period); + const dataset = data.items.map(i => (i as any)[key] ?? 0); + return { labels, dataset }; + } + + // --------------------- + // Multi-period & Multi-partner reporting + // --------------------- + getMultiPartnerReports( + partners: number[], + period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily', + type: 'transaction' | 'subscription' = 'transaction' + ): Observable<(TransactionReport | SubscriptionReport)[]> { + const requests = partners.map(id => { + return type === 'transaction' + ? this.getTransactionReport({ merchantPartnerId: id }, period) + : this.getSubscriptionReport({ merchantPartnerId: id }, period); + }); + return forkJoin(requests); + } + + // --------------------- + // Health checks (rest of the code remains the same) + // --------------------- + private checkApiAvailability( service: string, url: string @@ -240,57 +465,4 @@ export class ReportService { catchError(err => this.handleError(err)) ); } - - - triggerManualSync(): Observable { - return this.http.post(`${this.baseUrl}/sync/full`, {}).pipe( - timeout(this.REPORTING_TIMEOUT), - retry(this.DEFAULT_RETRY), - catchError(err => this.handleError(err)) - ); - } - - // --------------------- - // Transactions & Subscriptions Reports - // --------------------- - getTransactionReport(params?: ReportParams, period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily'): Observable { - return this.http.get(`${this.baseUrl}/transactions/${period}`, { params: this.buildParams(params) }).pipe( - timeout(this.REPORTING_TIMEOUT), - retry(this.DEFAULT_RETRY), - catchError(err => this.handleError(err)) - ); - } - - getSubscriptionReport(params?: ReportParams, period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily'): Observable { - return this.http.get(`${this.baseUrl}/subscriptions/${period}`, { params: this.buildParams(params) }).pipe( - timeout(this.REPORTING_TIMEOUT), - retry(this.DEFAULT_RETRY), - catchError(err => this.handleError(err)) - ); - } - - getTransactionsWithDates(startDate: string, endDate: string, merchantPartnerId?: number) { - return this.getTransactionReport({ startDate, endDate, merchantPartnerId }, 'daily'); - } - - // --------------------- - // Multi-partner normalization for charts - // --------------------- - normalizeForChart(data: TransactionReport | SubscriptionReport, key: string): ChartDataNormalized { - const labels = data.items.map(i => i.period); - const dataset = data.items.map(i => (i as any)[key] ?? 0); - return { labels, dataset }; - } - - // --------------------- - // Multi-period & Multi-partner reporting - // --------------------- - getMultiPartnerReports(partners: number[], period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily', type: 'transaction' | 'subscription' = 'transaction') { - const requests = partners.map(id => { - return type === 'transaction' - ? this.getTransactionReport({ merchantPartnerId: id }, period) - : this.getSubscriptionReport({ merchantPartnerId: id }, period); - }); - return forkJoin(requests); - } } \ No newline at end of file diff --git a/src/app/modules/hub-users-management/hub-users-list/hub-users-list.html b/src/app/modules/hub-users-management/hub-users-list/hub-users-list.html index 298e11f..5e128e2 100644 --- a/src/app/modules/hub-users-management/hub-users-list/hub-users-list.html +++ b/src/app/modules/hub-users-management/hub-users-list/hub-users-list.html @@ -229,6 +229,10 @@ @if (showUserTypeColumn()) { Type } + + @if (showMerchantPartnerColumn()) { + Merchant Partner + }
Utilisateur @@ -268,6 +272,25 @@ } + + @if (showMerchantPartnerColumn()) { + + @if (user.merchantPartnerId) { +
+
+ +
+
+ + {{ user.merchantPartnerId.substring(0, 8) }}... + +
+
+ } @else { + - + } + + }
diff --git a/src/app/modules/hub-users-management/hub-users-list/hub-users-list.ts b/src/app/modules/hub-users-management/hub-users-list/hub-users-list.ts index 0efaaa7..a18e5fe 100644 --- a/src/app/modules/hub-users-management/hub-users-list/hub-users-list.ts +++ b/src/app/modules/hub-users-management/hub-users-list/hub-users-list.ts @@ -15,7 +15,7 @@ import { } from '@core/models/dcb-bo-hub-user.model'; import { HubUsersService } from '../hub-users.service'; -import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service'; import { AuthService } from '@core/services/auth.service'; import { UiCard } from '@app/components/ui-card'; @@ -448,7 +448,7 @@ export class HubUsersList implements OnInit, OnDestroy { return roleInfo?.description || 'Description non disponible'; } - formatTimestamp(timestamp: number): string { + formatTimestamp(timestamp: number | undefined): string { if (!timestamp) return 'Non disponible'; return new Date(timestamp).toLocaleDateString('fr-FR', { year: 'numeric', diff --git a/src/app/modules/hub-users-management/hub-users-profile/hub-users-profile.ts b/src/app/modules/hub-users-management/hub-users-profile/hub-users-profile.ts index de3baf2..af4d044 100644 --- a/src/app/modules/hub-users-management/hub-users-profile/hub-users-profile.ts +++ b/src/app/modules/hub-users-management/hub-users-profile/hub-users-profile.ts @@ -14,7 +14,7 @@ import { } from '@core/models/dcb-bo-hub-user.model'; import { HubUsersService } from '../hub-users.service'; -import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service'; import { AuthService } from '@core/services/auth.service'; @Component({ diff --git a/src/app/modules/hub-users-management/hub-users.service.ts b/src/app/modules/hub-users-management/hub-users.service.ts index b869c07..39ff3a6 100644 --- a/src/app/modules/hub-users-management/hub-users.service.ts +++ b/src/app/modules/hub-users-management/hub-users.service.ts @@ -39,6 +39,7 @@ export interface UserProfileResponse { emailVerified: boolean; enabled: boolean; role: string[]; + merchantPartnerId?: string; createdBy?: string; createdByUsername?: string; } @@ -47,6 +48,10 @@ export interface MessageResponse { message: string; } +export interface MerchantPartnerIdResponse { + merchantPartnerId: string | null; +} + @Injectable({ providedIn: 'root' }) export class HubUsersService { private http = inject(HttpClient); @@ -292,6 +297,7 @@ export class HubUsersService { lastName: apiUser.lastName, enabled: apiUser.enabled, emailVerified: apiUser.emailVerified, + merchantPartnerId: apiUser.merchantPartnerId, userType: userType, role: apiUser.role, createdBy: apiUser.createdBy, @@ -328,6 +334,10 @@ export class HubUsersService { filteredUsers = filteredUsers.filter(user => user.enabled === filters.enabled); } + if (filters.merchantPartnerId) { + filteredUsers = filteredUsers.filter(user => user.merchantPartnerId === filters.merchantPartnerId); + } + if (filters.userType) { filteredUsers = filteredUsers.filter(user => user.userType === filters.userType); } diff --git a/src/app/modules/hub-users-management/hub-users.ts b/src/app/modules/hub-users-management/hub-users.ts index 3390a2f..1b8acf4 100644 --- a/src/app/modules/hub-users-management/hub-users.ts +++ b/src/app/modules/hub-users-management/hub-users.ts @@ -6,7 +6,7 @@ import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstr import { Subject, takeUntil } from 'rxjs'; import { HubUsersService } from './hub-users.service'; -import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service'; import { AuthService } from '@core/services/auth.service'; import { MerchantSyncService } from './merchant-sync-orchestrator.service'; import { PageTitle } from '@app/components/page-title/page-title'; diff --git a/src/app/modules/hub-users-management/merchant-manager.ts b/src/app/modules/hub-users-management/merchant-manager.ts index f7789ea..f9b672c 100644 --- a/src/app/modules/hub-users-management/merchant-manager.ts +++ b/src/app/modules/hub-users-management/merchant-manager.ts @@ -1,3 +1,4 @@ +import { UserRole, UserType } from '@core/models/dcb-bo-hub-user.model'; import { Observable, of, throwError, firstValueFrom, lastValueFrom } from 'rxjs'; // Mock des services @@ -36,12 +37,12 @@ const mockHubUsersService = { createHubUser: (data: any): Observable => of({ id: 'keycloak-123', username: data.username, - merchantConfigId: 123 + merchantPartnerId: 123 }), getHubUserById: (id: string): Observable => of({ id, username: 'owner', - merchantConfigId: 123 + merchantPartnerId: 123 }), updateHubUser: (id: string, data: any): Observable => of({ id, @@ -49,7 +50,7 @@ const mockHubUsersService = { }), deleteHubUser: (id: string): Observable => of(void 0), getAllDcbPartners: (): Observable<{users: any[]}> => of({ - users: [{ id: 'keycloak-123', merchantConfigId: 123 }] + users: [{ id: 'keycloak-123', merchantPartnerId: 123 }] }) }; @@ -78,7 +79,7 @@ const mockMerchantUsersService = { export class MerchantCrudTest { private testData = { currentMerchantId: 0, - currentDcbPartnerId: '', + currentMerchantPartnerId: '', currentUserId: '', currentMerchantConfigUserId: 0 }; @@ -132,9 +133,9 @@ export class MerchantCrudTest { // 2. Créer DCB_PARTNER dans Keycloak const dcbPartnerDto = { ...ownerData, - userType: 'HUB', - role: 'DCB_PARTNER', - merchantConfigId: merchantConfig.id + userType: UserType.HUB, + role: UserRole.DCB_PARTNER_ADMIN, + merchantPartnerId: merchantConfig.id }; const keycloakMerchant = await firstValueFrom( @@ -144,7 +145,7 @@ export class MerchantCrudTest { // Sauvegarder les IDs this.testData.currentMerchantId = merchantConfig.id; - this.testData.currentDcbPartnerId = keycloakMerchant.id; + this.testData.currentMerchantPartnerId = keycloakMerchant.id; const result = { merchantConfig, @@ -163,7 +164,7 @@ export class MerchantCrudTest { async testCreateMerchantUser(): Promise { console.log('🧪 TEST: CREATE Merchant User'); - if (!this.testData.currentMerchantId || !this.testData.currentDcbPartnerId) { + if (!this.testData.currentMerchantId || !this.testData.currentMerchantPartnerId) { console.log('⚠️ Créez d\'abord un merchant'); return; } @@ -182,8 +183,7 @@ export class MerchantCrudTest { const keycloakUserDto = { ...userData, userType: 'MERCHANT_PARTNER', - merchantPartnerId: this.testData.currentDcbPartnerId, - merchantConfigId: this.testData.currentMerchantId + merchantPartnerId: this.testData.currentMerchantId }; const keycloakUser = await firstValueFrom( @@ -226,7 +226,7 @@ export class MerchantCrudTest { async testReadMerchant(): Promise { console.log('🧪 TEST: READ Merchant'); - if (!this.testData.currentMerchantId || !this.testData.currentDcbPartnerId) { + if (!this.testData.currentMerchantId || !this.testData.currentMerchantPartnerId) { console.log('⚠️ Créez d\'abord un merchant'); return; } @@ -237,14 +237,14 @@ export class MerchantCrudTest { mockMerchantConfigService.getMerchantById(this.testData.currentMerchantId) ), firstValueFrom( - mockHubUsersService.getHubUserById(this.testData.currentDcbPartnerId) + mockHubUsersService.getHubUserById(this.testData.currentMerchantPartnerId) ) ]); console.log('🎯 RESULTAT READ:', { merchantConfig, keycloakMerchant, - coherence: keycloakMerchant.merchantConfigId === merchantConfig.id ? '✅ OK' : '❌ INCOHÉRENT' + coherence: keycloakMerchant.merchantPartnerId === merchantConfig.id ? '✅ OK' : '❌ INCOHÉRENT' }); } catch (error) { @@ -255,7 +255,7 @@ export class MerchantCrudTest { async testUpdateMerchant(): Promise { console.log('🧪 TEST: UPDATE Merchant'); - if (!this.testData.currentMerchantId || !this.testData.currentDcbPartnerId) { + if (!this.testData.currentMerchantId || !this.testData.currentMerchantPartnerId) { console.log('⚠️ Créez d\'abord un merchant'); return; } @@ -273,7 +273,7 @@ export class MerchantCrudTest { ), firstValueFrom( mockHubUsersService.updateHubUser( - this.testData.currentDcbPartnerId, + this.testData.currentMerchantPartnerId, { email: newEmail } ) ) @@ -363,7 +363,7 @@ export class MerchantCrudTest { async testDeleteMerchant(): Promise { console.log('🧪 TEST: DELETE Merchant'); - if (!this.testData.currentMerchantId || !this.testData.currentDcbPartnerId) { + if (!this.testData.currentMerchantId || !this.testData.currentMerchantPartnerId) { console.log('⚠️ Créez d\'abord un merchant'); return; } @@ -382,7 +382,7 @@ export class MerchantCrudTest { mockMerchantConfigService.deleteMerchant(this.testData.currentMerchantId) ), firstValueFrom( - mockHubUsersService.deleteHubUser(this.testData.currentDcbPartnerId) + mockHubUsersService.deleteHubUser(this.testData.currentMerchantPartnerId) ) ]); @@ -390,7 +390,7 @@ export class MerchantCrudTest { // Réinitialiser this.testData.currentMerchantId = 0; - this.testData.currentDcbPartnerId = ''; + this.testData.currentMerchantPartnerId = ''; } catch (error) { console.error('❌ ERREUR DELETE MERCHANT:', error); @@ -449,7 +449,7 @@ export class MerchantCrudTest { resetTestData() { this.testData = { currentMerchantId: 0, - currentDcbPartnerId: '', + currentMerchantPartnerId: '', currentUserId: '', currentMerchantConfigUserId: 0 }; diff --git a/src/app/modules/hub-users-management/merchant-users-list/merchant-users-list.html b/src/app/modules/hub-users-management/merchant-users-list/merchant-users-list.html index 7aca466..2d4e83a 100644 --- a/src/app/modules/hub-users-management/merchant-users-list/merchant-users-list.html +++ b/src/app/modules/hub-users-management/merchant-users-list/merchant-users-list.html @@ -153,6 +153,10 @@ + + @if (showMerchantPartnerColumn) { + + } @for (user of displayedUsers; track user.id) { + + @if (showMerchantPartnerColumn) { + + }
Merchant Partner
Utilisateur @@ -184,6 +188,21 @@
+
+
+ +
+
+ + {{ (user.merchantPartnerId || 'N/A').substring(0, 8) }}... + +
+
+
diff --git a/src/app/modules/hub-users-management/merchant-users-list/merchant-users-list.ts b/src/app/modules/hub-users-management/merchant-users-list/merchant-users-list.ts index 318c155..c9c0cac 100644 --- a/src/app/modules/hub-users-management/merchant-users-list/merchant-users-list.ts +++ b/src/app/modules/hub-users-management/merchant-users-list/merchant-users-list.ts @@ -15,7 +15,7 @@ import { } from '@core/models/dcb-bo-hub-user.model'; import { MerchantUsersService } from '../merchant-users.service'; -import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service'; import { AuthService } from '@core/services/auth.service'; import { UiCard } from '@app/components/ui-card'; @@ -118,12 +118,6 @@ export class MerchantUsersList implements OnInit, OnDestroy { this.currentUserRole = this.extractUserRole(user); this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole); - console.log('Merchant User Context Loaded:', { - role: this.currentUserRole, - merchantPartnerId: this.currentMerchantPartnerId, - canViewAllMerchants: this.canViewAllMerchants - }); - this.loadUsers(); }, error: (error) => { @@ -391,7 +385,7 @@ export class MerchantUsersList implements OnInit, OnDestroy { return roleInfo?.description || 'Description non disponible'; } - formatTimestamp(timestamp: number): string { + formatTimestamp(timestamp: number | undefined): string { if (!timestamp) return 'Non disponible'; return new Date(timestamp).toLocaleDateString('fr-FR', { year: 'numeric', diff --git a/src/app/modules/hub-users-management/merchant-users-profile/merchant-users-profile.ts b/src/app/modules/hub-users-management/merchant-users-profile/merchant-users-profile.ts index bf84972..45f360c 100644 --- a/src/app/modules/hub-users-management/merchant-users-profile/merchant-users-profile.ts +++ b/src/app/modules/hub-users-management/merchant-users-profile/merchant-users-profile.ts @@ -14,7 +14,7 @@ import { } from '@core/models/dcb-bo-hub-user.model'; import { MerchantUsersService } from '../merchant-users.service'; -import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service'; import { AuthService } from '@core/services/auth.service'; @Component({ diff --git a/src/app/modules/hub-users-management/merchant-users.service.ts b/src/app/modules/hub-users-management/merchant-users.service.ts index 44c2103..c1b230a 100644 --- a/src/app/modules/hub-users-management/merchant-users.service.ts +++ b/src/app/modules/hub-users-management/merchant-users.service.ts @@ -271,6 +271,7 @@ export class MerchantUsersService { lastName: apiUser.lastName, enabled: apiUser.enabled, emailVerified: apiUser.emailVerified, + merchantPartnerId: apiUser.merchantPartnerId, userType: userType, role: apiUser.role, createdBy: apiUser.createdBy, diff --git a/src/app/modules/hub-users-management/merchant-users.ts b/src/app/modules/hub-users-management/merchant-users.ts index a988f07..cc1cf5c 100644 --- a/src/app/modules/hub-users-management/merchant-users.ts +++ b/src/app/modules/hub-users-management/merchant-users.ts @@ -6,7 +6,7 @@ import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstr import { catchError, map, of, Subject, takeUntil } from 'rxjs'; import { MerchantUsersService } from './merchant-users.service'; -import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service'; import { AuthService } from '@core/services/auth.service'; import { PageTitle } from '@app/components/page-title/page-title'; import { MerchantUsersList } from './merchant-users-list/merchant-users-list'; @@ -157,8 +157,6 @@ export class MerchantUsersManagement implements OnInit, OnDestroy { this.currentUserType = this.extractUserType(user); - console.log(`MERCHANT User ROLE: ${this.currentUserRole}`); - if (this.currentUserRole) { this.roleService.setCurrentUserRole(this.currentUserRole); this.userPermissions = this.roleService.getPermissionsForRole(this.currentUserRole); @@ -167,7 +165,6 @@ export class MerchantUsersManagement implements OnInit, OnDestroy { this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole); this.assignableRoles = this.roleService.getAssignableRoles(this.currentUserRole); - console.log('Assignable roles:', this.assignableRoles); } }, diff --git a/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts b/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts index 8a61e99..780f515 100644 --- a/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts +++ b/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts @@ -15,7 +15,7 @@ import { } from '@core/models/merchant-config.model'; import { MerchantConfigService } from '../merchant-config.service'; -import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service'; import { AuthService } from '@core/services/auth.service'; import { UiCard } from '@app/components/ui-card'; @@ -158,10 +158,6 @@ export class MerchantConfigsList implements OnInit, OnDestroy { this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole); - console.log(`MERCHANT User ROLE: ${this.currentUserRole}`); - console.log(`Merchant Config ID: ${this.currentMerchantConfigId}`); - console.log(`canViewAllMerchants: ${this.canViewAllMerchants}`); - this.loadMerchants(); }, error: (error) => { diff --git a/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts b/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts index c0559c5..9b8c21b 100644 --- a/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts +++ b/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts @@ -19,7 +19,7 @@ import { import { MerchantConfigService } from '../merchant-config.service'; import { MerchantDataAdapter } from '../merchant-data-adapter.service'; -import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service'; import { AuthService } from '@core/services/auth.service'; import { UserRole } from '@core/models/dcb-bo-hub-user.model'; diff --git a/src/app/modules/merchant-config/merchant-config.service.ts b/src/app/modules/merchant-config/merchant-config.service.ts index 2a0090d..8195321 100644 --- a/src/app/modules/merchant-config/merchant-config.service.ts +++ b/src/app/modules/merchant-config/merchant-config.service.ts @@ -199,7 +199,6 @@ export class MerchantConfigService { timeout(this.REQUEST_TIMEOUT), map(apiUser => { console.log(`✅ User ${userId} role updated successfully`); - // ✅ UTILISATION DE L'ADAPTER return this.dataAdapter.convertApiUserToFrontend(apiUser); }), catchError(error => this.handleError('updateUserRole', error, { merchantId, userId })) diff --git a/src/app/modules/merchant-config/merchant-config.ts b/src/app/modules/merchant-config/merchant-config.ts index 25fd8b7..0e02c0c 100644 --- a/src/app/modules/merchant-config/merchant-config.ts +++ b/src/app/modules/merchant-config/merchant-config.ts @@ -6,7 +6,7 @@ import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstr import { catchError, finalize, map, of, Subject, takeUntil } from 'rxjs'; import { MerchantConfigService } from './merchant-config.service'; -import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service'; import { AuthService } from '@core/services/auth.service'; import { MerchantSyncService } from '../hub-users-management/merchant-sync-orchestrator.service'; import { PageTitle } from '@app/components/page-title/page-title'; diff --git a/src/app/modules/profile/profile.ts b/src/app/modules/profile/profile.ts index 2f6b5b8..2bd46ab 100644 --- a/src/app/modules/profile/profile.ts +++ b/src/app/modules/profile/profile.ts @@ -16,7 +16,7 @@ import { import { HubUsersService } from '@modules/hub-users-management/hub-users.service'; import { MerchantUsersService } from '@modules/hub-users-management/merchant-users.service'; -import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service'; import { AuthService } from '@core/services/auth.service'; @Component({ diff --git a/src/app/modules/subscriptions/subscriptions.ts b/src/app/modules/subscriptions/subscriptions.ts index 0c4c461..0702497 100644 --- a/src/app/modules/subscriptions/subscriptions.ts +++ b/src/app/modules/subscriptions/subscriptions.ts @@ -19,7 +19,7 @@ import { Currency } from '@core/models/dcb-bo-hub-subscription.model'; import { User, UserRole } from '@core/models/dcb-bo-hub-user.model'; -import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service'; @Component({ selector: 'app-subscriptions', diff --git a/src/app/modules/transactions/details/details.html b/src/app/modules/transactions/details/details.html index a3a21a7..b7b89d5 100644 --- a/src/app/modules/transactions/details/details.html +++ b/src/app/modules/transactions/details/details.html @@ -1,351 +1,204 @@
- - @if (loading && !transaction) { + +
+ + @if (accessDenied) {
-
- Chargement... -
-

Chargement des détails de la transaction...

-
- } - - - @if (error) { -
- -
{{ error }}
- -
- } - - @if (success) { -
- -
{{ success }}
- -
- } - - @if (transaction && !loading) { -
- -
- -
-
-
-
Transaction #{{ transaction.id }}
- - - {{ transaction.status }} - -
-
- - - -
-
-
- -
-
-
-
- -
-
-
Montant
-
- {{ formatCurrency(transaction.amount, transaction.currency) }} -
-
-
-
-
-
-
- -
-
-
Date de transaction
-
{{ formatDate(transaction.transactionDate) }}
- {{ formatRelativeTime(transaction.transactionDate) }} -
-
-
-
- - -
-
-
Informations de la transaction
-
- -
- -
- - {{ transaction.msisdn }} - -
-
- -
- -
- - {{ transaction.operator }} - {{ transaction.country }} -
-
- -
- -
- -
-
{{ transaction.productName }}
- ID: {{ transaction.productId }} -
-
-
- -
- -
- {{ transaction.productCategory }} -
-
- - @if (transaction.merchantName) { -
- -
- - {{ transaction.merchantName }} -
-
- } - - @if (transaction.externalId) { -
- -
- {{ transaction.externalId }} - -
-
- } -
- - -
-
-
Informations techniques
-
- -
- -
{{ formatDate(transaction.createdAt) }}
-
- -
- -
{{ formatDate(transaction.updatedAt) }}
-
- - @if (transaction.userAgent) { -
- -
{{ transaction.userAgent }}
-
- } - - @if (transaction.ipAddress) { -
- -
{{ transaction.ipAddress }}
-
- } -
- - - @if (showErrorDetails()) { -
-
-
- - Détails de l'erreur -
-
- - @if (transaction.errorCode) { -
- -
{{ transaction.errorCode }}
-
- } - - @if (transaction.errorMessage) { -
- -
{{ transaction.errorMessage }}
-
- } -
- } -
-
-
- - -
- -
-
-
Actions
-
-
-
- - @if (canRefund()) { - - } - - - @if (canRetry()) { - - } - - - @if (canCancel()) { - - } - - - -
-
-
- - -
-
-
Métadonnées
-
-
-
-
- ID Transaction: - {{ transaction.id }} -
- -
- Opérateur ID: - {{ transaction.operatorId }} -
- - @if (transaction.merchantId) { -
- Marchand ID: - {{ transaction.merchantId }} -
- } - -
- Devise: - {{ transaction.currency }} -
- -
- Statut: - - {{ transaction.status }} - -
-
-
-
- - - @if (getCustomDataKeys().length > 0) { -
-
-
Données personnalisées
-
-
-
- @for (key of getCustomDataKeys(); track key) { -
- {{ key }}: - {{ transaction.customData![key] }} -
- } -
-
-
- } -
-
- } - - - @if (!transaction && !loading) { -
- -
Transaction non trouvée
-

La transaction avec l'ID "{{ transactionId }}" n'existe pas ou a été supprimée.

+ +
Accès refusé
+

Vous n'avez pas la permission d'accéder à cette transaction.

+ } @else { + + @if (loading && !transaction) { +
+
+ Chargement... +
+

Chargement des détails de la transaction...

+
+ } + + + @if (error) { +
+ +
{{ error }}
+ +
+ } + + @if (success) { +
+ +
{{ success }}
+ +
+ } + + @if (transaction && !loading) { +
+ +
+ +
+
+
+
Transaction #{{ transaction.id }}
+ + + {{ transaction.status }} + +
+
+ + + +
+
+
+ +
+
+
+
+ +
+
+
Montant
+
+ {{ formatCurrency(transaction.amount, transaction.currency) }} +
+
+
+
+
+
+
+ +
+
+
Date de transaction
+
{{ formatDate(transaction.transactionDate) }}
+ {{ formatRelativeTime(transaction.transactionDate) }} +
+
+
+
+ + +
+
+
Informations de la transaction
+
+ +
+ +
+ +
+
{{ transaction.productName }}
+ ID: {{ transaction.productId }} +
+
+
+ + @if (transaction.externalReference) { +
+ +
+ {{ transaction.externalReference }} + +
+
+ } +
+ + +
+
+
Informations techniques
+
+ +
+ +
{{ formatDate(transaction.createdAt) }}
+
+ +
+ +
{{ formatDate(transaction.updatedAt) }}
+
+
+ +
+
+
+ + +
+ +
+
+
Métadonnées
+
+
+
+
+ ID Transaction: + {{ transaction.id }} +
+ @if (transaction.merchantPartnerId) { +
+ Marchand ID: + {{ transaction.merchantPartnerId }} +
+ } + +
+ Devise: + {{ transaction.currency }} +
+ +
+ Statut: + + {{ transaction.status }} + +
+
+
+
+
+
+ } + + + @if (!transaction && !loading) { +
+ +
Transaction non trouvée
+

La transaction avec l'ID "{{ transactionId }}" n'existe pas ou a été supprimée.

+ +
+ } }
\ No newline at end of file diff --git a/src/app/modules/transactions/details/details.ts b/src/app/modules/transactions/details/details.ts index b50a54c..311662e 100644 --- a/src/app/modules/transactions/details/details.ts +++ b/src/app/modules/transactions/details/details.ts @@ -1,3 +1,4 @@ +// [file name]: transactions/details/details.ts (mise à jour) import { Component, inject, OnInit, Input, ChangeDetectorRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; @@ -21,12 +22,16 @@ import { lucideUser, lucideGlobe, lucideAlertCircle, - lucideInfo + lucideInfo, + lucideShield, + lucideStore, + lucideLock } from '@ng-icons/lucide'; import { NgbAlertModule, NgbTooltipModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { TransactionsService } from '../services/transactions.service'; -import { Transaction, TransactionStatus, RefundRequest } from '../models/transaction'; +import { TransactionAccessService, TransactionAccess } from '../services/transaction-access.service'; +import { Transaction, TransactionStatus} from '@core/models/dcb-bo-hub-transaction.model'; @Component({ selector: 'app-transaction-details', @@ -48,11 +53,15 @@ import { Transaction, TransactionStatus, RefundRequest } from '../models/transac }) export class TransactionDetails implements OnInit { private transactionsService = inject(TransactionsService); + private accessService = inject(TransactionAccessService); private modalService = inject(NgbModal); private cdRef = inject(ChangeDetectorRef); @Input() transactionId!: string; + // Permissions + access!: TransactionAccess; + // Données transaction: Transaction | null = null; loading = false; @@ -62,101 +71,63 @@ export class TransactionDetails implements OnInit { // Actions refunding = false; retrying = false; + + // Accès + canViewSensitiveData = false; + accessDenied = false; ngOnInit() { + this.initializePermissions(); + if (this.transactionId) { this.loadTransactionDetails(); } } + + private initializePermissions() { + this.access = this.accessService.getTransactionAccess(); + this.canViewSensitiveData = this.access.canViewSensitiveData; + } loadTransactionDetails() { + if (!this.access.canViewDetails) { + this.error = 'Vous n\'avez pas la permission de voir les détails des transactions'; + this.accessDenied = true; + this.cdRef.detectChanges(); + return; + } + this.loading = true; this.error = ''; + this.accessDenied = false; this.transactionsService.getTransactionById(this.transactionId).subscribe({ next: (transaction) => { - this.transaction = transaction; - this.loading = false; - this.cdRef.detectChanges(); + // Vérifier si l'utilisateur a accès à cette transaction spécifique + this.accessService.canAccessTransaction(transaction.merchantPartnerId ? transaction.merchantPartnerId : undefined) + .subscribe(canAccess => { + if (!canAccess) { + this.error = 'Vous n\'avez pas accès à cette transaction'; + this.accessDenied = true; + this.loading = false; + this.cdRef.detectChanges(); + return; + } + + this.transaction = transaction; + this.loading = false; + this.cdRef.detectChanges(); + }); }, error: (error) => { this.error = 'Erreur lors du chargement des détails de la transaction'; this.loading = false; - - // Données mockées pour le développement - const mockTransactions = this.transactionsService.getMockTransactions(); - this.transaction = mockTransactions.find(tx => tx.id === this.transactionId) || mockTransactions[0]; - this.loading = false; - this.cdRef.detectChanges(); console.error('Error loading transaction details:', error); } }); } - // Actions sur la transaction - refundTransaction() { - if (!this.transaction) return; - - this.refunding = true; - const refundRequest: RefundRequest = { - transactionId: this.transaction.id, - reason: 'Remboursement manuel par l\'administrateur' - }; - - this.transactionsService.refundTransaction(refundRequest).subscribe({ - next: (response) => { - this.transaction = response.transaction; - this.refunding = false; - this.success = 'Transaction remboursée avec succès'; - this.cdRef.detectChanges(); - }, - error: (error) => { - this.refunding = false; - this.error = 'Erreur lors du remboursement de la transaction'; - this.cdRef.detectChanges(); - console.error('Error refunding transaction:', error); - } - }); - } - - retryTransaction() { - if (!this.transaction) return; - - this.retrying = true; - this.transactionsService.retryTransaction(this.transaction.id).subscribe({ - next: (response) => { - this.transaction = response.transaction; - this.retrying = false; - this.success = 'Nouvelle tentative lancée avec succès'; - this.cdRef.detectChanges(); - }, - error: (error) => { - this.retrying = false; - this.error = 'Erreur lors de la nouvelle tentative'; - this.cdRef.detectChanges(); - console.error('Error retrying transaction:', error); - } - }); - } - - cancelTransaction() { - if (!this.transaction) return; - - this.transactionsService.cancelTransaction(this.transaction.id).subscribe({ - next: () => { - this.success = 'Transaction annulée avec succès'; - this.loadTransactionDetails(); // Recharger les données - this.cdRef.detectChanges(); - }, - error: (error) => { - this.error = 'Erreur lors de l\'annulation de la transaction'; - this.cdRef.detectChanges(); - console.error('Error cancelling transaction:', error); - } - }); - } - // Utilitaires copyToClipboard(text: string) { navigator.clipboard.writeText(text).then(() => { @@ -170,21 +141,12 @@ export class TransactionDetails implements OnInit { window.print(); } - // Méthode pour obtenir les clés des données personnalisées - getCustomDataKeys(): string[] { - if (!this.transaction?.customData) { - return []; - } - return Object.keys(this.transaction.customData); - } - // Getters pour l'affichage getStatusBadgeClass(status: TransactionStatus): string { switch (status) { case 'SUCCESS': return 'badge bg-success'; case 'PENDING': return 'badge bg-warning'; case 'FAILED': return 'badge bg-danger'; - case 'REFUNDED': return 'badge bg-info'; case 'CANCELLED': return 'badge bg-secondary'; case 'EXPIRED': return 'badge bg-dark'; default: return 'badge bg-secondary'; @@ -196,7 +158,6 @@ export class TransactionDetails implements OnInit { case 'SUCCESS': return 'lucideCheckCircle'; case 'PENDING': return 'lucideClock'; case 'FAILED': return 'lucideXCircle'; - case 'REFUNDED': return 'lucideUndo2'; case 'CANCELLED': return 'lucideBan'; default: return 'lucideClock'; } @@ -241,18 +202,31 @@ export class TransactionDetails implements OnInit { } canRefund(): boolean { - return this.transaction?.status === 'SUCCESS'; + return this.access.canRefund && this.transaction?.status === 'SUCCESS'; } canRetry(): boolean { - return this.transaction?.status === 'FAILED'; + return this.access.canRetry && this.transaction?.status === 'FAILED'; } canCancel(): boolean { - return this.transaction?.status === 'PENDING'; + return this.access.canCancel && this.transaction?.status === 'PENDING'; } - - showErrorDetails(): boolean { - return !!this.transaction?.errorCode || !!this.transaction?.errorMessage; + + // Méthodes pour le template + getUserBadgeClass(): string { + return this.access.isHubUser ? 'bg-primary' : 'bg-success'; + } + + getUserBadgeIcon(): string { + return this.access.isHubUser ? 'lucideShield' : 'lucideStore'; + } + + getUserBadgeText(): string { + return this.access.isHubUser ? 'Hub User' : 'Merchant User'; + } + + showSensitiveData(): boolean { + return this.canViewSensitiveData && !this.accessDenied; } } \ No newline at end of file diff --git a/src/app/modules/transactions/list/list.html b/src/app/modules/transactions/list/list.html index a674cde..9fa956e 100644 --- a/src/app/modules/transactions/list/list.html +++ b/src/app/modules/transactions/list/list.html @@ -5,29 +5,26 @@

Gestion des Transactions

- +
+ + + + {{ getUserBadgeText() }} + + + + Merchant {{ currentMerchantId }} + +
- -
- -
- - - -
-
-
- - @if (paginatedData?.stats) { -
-
-
-
-
-
- Total -
{{ getTotal() }}
-
-
- Succès -
{{ getSuccessCount() }}
-
-
- Échecs -
{{ getFailedCount() }}
-
-
- En attente -
{{ getPendingCount() }}
-
-
- Taux de succès -
{{ getSuccessRate() }}%
-
-
- Montant total -
{{ formatCurrency(getTotalAmount()) }}
-
-
-
-
+ + @if (!access.canViewTransactions) { +
+
+ +
+ Accès refusé +

Vous n'avez pas les permissions nécessaires pour accéder à cette section.

+
+ } @else { + + + @if (paginatedData?.stats) { +
+
+
+
+
+
+ Total +
{{ getTotal() }}
+
+
+ Succès +
{{ getSuccessCount() }}
+
+
+ Échecs +
{{ getFailedCount() }}
+
+
+ En attente +
{{ getPendingCount() }}
+
+
+ Montant total +
{{ formatCurrency(getTotalAmount()) }}
+
+
+
+
+
+
+ } + + +
+
+
+ + + + +
+
+ +
+
+ + + + + + + + + +
+
+
+ + + @if (error) { +
+ + {{ error }} +
} - -
-
-
- - - - + + @if (loading) { +
+
+ Chargement... +
+

Chargement des transactions...

-
- -
-
- - + } - - - - - - -
-
-
- - - @if (error) { -
- - {{ error }} -
- } - - - @if (loading) { -
-
- Chargement... -
-

Chargement des transactions...

-
- } - - - @if (!loading) { -
-
-
- - - - - - - - - - - - - - - - @for (transaction of transactions; track transaction.id) { - - + + } + +
- - -
- ID - -
-
-
- MSISDN - -
-
Opérateur -
- Montant - -
-
ProduitStatut -
- Date - -
-
Actions
+ + @if (!loading) { +
+
+
+ + + + - - - - - - + + + + + + + + + + + @for (transaction of transactions; track transaction.id) { + + + + + + + + + + + + + } + @empty { + + - - } - @empty { - - - - } - -
- - {{ transaction.id }}{{ transaction.msisdn }} - {{ transaction.operator }} - - - {{ formatCurrency(transaction.amount, transaction.currency) }} - - -
- {{ transaction.productName }} + +
+
+ ID +
- -
- - - {{ transaction.status }} - - - {{ formatDate(transaction.transactionDate) }} - -
-
TypeMerchant +
+ Montant + +
+
PériodicitéStatut +
+ Date début + +
+
Prochain paiementActions
+ - + {{ transaction.id }} + + + Abonnement + + + + Merchant {{ transaction.merchantPartnerId }} + + + + {{ formatCurrency(transaction.amount, transaction.currency) }} + + + @if (transaction.periodicity) { + + {{ getPeriodicityDisplayName(transaction.periodicity) }} + + } + + + + {{ getStatusDisplayName(transaction.status) }} + + + {{ formatDate(transaction.transactionDate) }} + + @if (transaction.nextPaymentDate) { + {{ formatDate(transaction.nextPaymentDate) }} + } @else { + - + } + +
+ +
+
+ +

Aucune transaction trouvée

+ - - @if (transaction.status === 'SUCCESS') { - - } - - @if (transaction.status === 'FAILED') { - - } - -
- -

Aucune transaction trouvée

- -
+
+
-
- - @if (paginatedData && paginatedData.totalPages > 1) { -
-
- Affichage de {{ (filters.page! - 1) * filters.limit! + 1 }} à - {{ (filters.page! * filters.limit!) > (paginatedData.total || 0) ? (paginatedData.total || 0) : (filters.page! * filters.limit!) }} - sur {{ paginatedData.total || 0 }} transactions + + @if (paginatedData && paginatedData.totalPages >= 1) { +
+
+ Affichage de {{ (filters.page! - 1) * filters.limit! + 1 }} à + {{ (filters.page! * filters.limit!) > (paginatedData.total || 0) ? (paginatedData.total || 0) : (filters.page! * filters.limit!) }} + sur {{ paginatedData.total || 0 }} transactions +
+
- -
+ } } + }
\ No newline at end of file diff --git a/src/app/modules/transactions/list/list.ts b/src/app/modules/transactions/list/list.ts index b51c977..ec35092 100644 --- a/src/app/modules/transactions/list/list.ts +++ b/src/app/modules/transactions/list/list.ts @@ -1,4 +1,4 @@ -import { Component, inject, OnInit, ChangeDetectorRef, Output, EventEmitter } from '@angular/core'; +import { Component, inject, OnInit, ChangeDetectorRef, Output, EventEmitter, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { NgIcon, provideNgIconsConfig } from '@ng-icons/core'; @@ -16,13 +16,32 @@ import { lucideClock, lucideXCircle, lucideUndo2, - lucideBan + lucideBan, + lucideShield, + lucideStore, + lucideCalendar, + lucideRepeat, + lucideCreditCard } from '@ng-icons/lucide'; import { NgbPaginationModule, NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { TransactionsService } from '../services/transactions.service'; -import { Transaction, TransactionQuery, TransactionStatus, PaginatedTransactions } from '../models/transaction'; -import { environment } from '@environments/environment'; +import { TransactionAccessService, TransactionAccess } from '../services/transaction-access.service'; + +import { + Transaction, + TransactionQuery, + PaginatedTransactions, + TransactionType, + TransactionStatus, + TransactionUtils +} from '@core/models/dcb-bo-hub-transaction.model'; + +import { + Currency, + SubscriptionPeriodicity, + SubscriptionUtils +} from '@core/models/dcb-bo-hub-subscription.model'; @Component({ selector: 'app-transactions-list', @@ -42,13 +61,19 @@ import { environment } from '@environments/environment'; ], templateUrl: './list.html' }) -export class TransactionsList implements OnInit { +export class TransactionsList implements OnInit, OnDestroy { private transactionsService = inject(TransactionsService); + private accessService = inject(TransactionAccessService); private cdRef = inject(ChangeDetectorRef); @Output() transactionSelected = new EventEmitter(); @Output() openRefundModal = new EventEmitter(); + // Permissions + access!: TransactionAccess; + currentUserRole = ''; + currentMerchantId?: number; + // Données transactions: Transaction[] = []; paginatedData: PaginatedTransactions | null = null; @@ -63,19 +88,22 @@ export class TransactionsList implements OnInit { page: 1, limit: 20, status: undefined, - operator: '', - country: '', startDate: undefined, endDate: undefined, - msisdn: '', sortBy: 'transactionDate', sortOrder: 'desc' }; // Options de filtre - statusOptions: TransactionStatus[] = ['PENDING', 'SUCCESS', 'FAILED', 'REFUNDED', 'CANCELLED']; - operatorOptions: string[] = ['Orange', 'Free', 'SFR', 'Bouygues']; - countryOptions: string[] = ['FR', 'BE', 'CH', 'LU']; + statusOptions: TransactionStatus[] = ['PENDING', 'SUCCESS', 'FAILED', 'CANCELLED']; + typeOptions: TransactionType[] = [ + TransactionType.SUBSCRIPTION_PAYMENT, + TransactionType.SUBSCRIPTION_RENEWAL, + TransactionType.ONE_TIME_PAYMENT + ]; + periodicityOptions = Object.values(SubscriptionPeriodicity); + operatorOptions: string[] = ['Orange']; + countryOptions: string[] = ['SN']; // Tri sortField: string = 'transactionDate'; @@ -86,10 +114,36 @@ export class TransactionsList implements OnInit { selectAll = false; ngOnInit() { + this.initializePermissions(); this.loadTransactions(); } + + ngOnDestroy() { + // Nettoyage si nécessaire + } + + private initializePermissions() { + this.access = this.accessService.getTransactionAccess(); + + // Ajouter le merchant ID aux filtres si nécessaire + if (this.access.isMerchantUser && this.access.allowedMerchantIds.length > 0) { + this.filters.merchantPartnerId = this.access.allowedMerchantIds[0]; + } else if (this.access.isHubUser && this.access.merchantId) { + // Pour les hub users qui veulent voir un merchant spécifique + this.filters.merchantPartnerId = this.access.merchantId; + } + + // Définir le rôle pour l'affichage + this.currentUserRole = this.access.userRoleLabel; + this.currentMerchantId = this.access.merchantId; + } loadTransactions() { + if (!this.access.canViewTransactions) { + this.error = 'Vous n\'avez pas la permission de voir les transactions'; + return; + } + this.loading = true; this.error = ''; @@ -103,6 +157,11 @@ export class TransactionsList implements OnInit { // Appliquer le tri this.filters.sortBy = this.sortField; this.filters.sortOrder = this.sortDirection; + + // Appliquer les restrictions de merchant pour les non-admin hub users + if (!this.access.canManageAll && this.access.allowedMerchantIds.length > 0) { + this.filters.merchantPartnerId = this.access.allowedMerchantIds[0]; + } this.transactionsService.getTransactions(this.filters).subscribe({ next: (data) => { @@ -114,30 +173,6 @@ export class TransactionsList implements OnInit { error: (error) => { this.error = 'Erreur lors du chargement des transactions'; this.loading = false; - - // Fallback sur les données mockées en développement - if (environment.production === false) { - this.transactions = this.transactionsService.getMockTransactions(); - this.paginatedData = { - data: this.transactions, - total: this.transactions.length, - page: 1, - limit: 20, - totalPages: 1, - stats: { - total: this.transactions.length, - totalAmount: this.transactions.reduce((sum, tx) => sum + tx.amount, 0), - successCount: this.transactions.filter(tx => tx.status === 'SUCCESS').length, - failedCount: this.transactions.filter(tx => tx.status === 'FAILED').length, - pendingCount: this.transactions.filter(tx => tx.status === 'PENDING').length, - refundedCount: this.transactions.filter(tx => tx.status === 'REFUNDED').length, - successRate: 75, - averageAmount: 4.74 - } - }; - this.loading = false; - } - this.cdRef.detectChanges(); console.error('Error loading transactions:', error); } @@ -156,14 +191,17 @@ export class TransactionsList implements OnInit { page: 1, limit: 20, status: undefined, - operator: '', - country: '', startDate: undefined, endDate: undefined, - msisdn: '', sortBy: 'transactionDate', sortOrder: 'desc' }; + + // Réappliquer les restrictions de merchant + if (this.access.isMerchantUser && this.access.allowedMerchantIds.length > 0) { + this.filters.merchantPartnerId = this.access.allowedMerchantIds[0]; + } + this.loadTransactions(); } @@ -174,7 +212,6 @@ export class TransactionsList implements OnInit { } onOperatorFilterChange(operator: string) { - this.filters.operator = operator; this.filters.page = 1; this.loadTransactions(); } @@ -185,6 +222,15 @@ export class TransactionsList implements OnInit { this.filters.page = 1; this.loadTransactions(); } + + // Permissions pour les filtres + canUseMerchantFilter(): boolean { + return this.access.canFilterByMerchant && this.access.allowedMerchantIds.length > 1; + } + + canUseAllFilters(): boolean { + return this.access.canViewAllTransactions; + } // Tri sort(field: string) { @@ -210,21 +256,12 @@ export class TransactionsList implements OnInit { // Actions viewTransactionDetails(transactionId: string) { - this.transactionSelected.emit(transactionId); - } - - refundTransaction(transactionId: string) { - this.openRefundModal.emit(transactionId); - } - - retryTransaction(transactionId: string) { - this.transactionsService.retryTransaction(transactionId).subscribe({ - next: () => { - this.loadTransactions(); - }, - error: (error) => { - console.error('Error retrying transaction:', error); - this.error = 'Erreur lors de la nouvelle tentative'; + // Vérifier les permissions avant d'afficher + this.accessService.canAccessTransaction().subscribe(canAccess => { + if (canAccess) { + this.transactionSelected.emit(transactionId); + } else { + this.error = 'Vous n\'avez pas la permission de voir les détails de cette transaction'; this.cdRef.detectChanges(); } }); @@ -253,37 +290,12 @@ export class TransactionsList implements OnInit { this.selectedTransactions.size === this.transactions.length; } - // Export - exportTransactions(format: 'csv' | 'excel' | 'pdf') { - const exportRequest = { - format: format, - query: this.filters, - columns: ['id', 'msisdn', 'operator', 'amount', 'status', 'transactionDate', 'productName'] - }; - - this.transactionsService.exportTransactions(exportRequest).subscribe({ - next: (response) => { - // Télécharger le fichier - const link = document.createElement('a'); - link.href = response.url; - link.download = response.filename; - link.click(); - }, - error: (error) => { - console.error('Error exporting transactions:', error); - this.error = 'Erreur lors de l\'export'; - this.cdRef.detectChanges(); - } - }); - } - - // Utilitaires d'affichage + // Utilitaires d'affichage MIS À JOUR getStatusBadgeClass(status: TransactionStatus): string { switch (status) { case 'SUCCESS': return 'badge bg-success'; case 'PENDING': return 'badge bg-warning'; case 'FAILED': return 'badge bg-danger'; - case 'REFUNDED': return 'badge bg-info'; case 'CANCELLED': return 'badge bg-secondary'; case 'EXPIRED': return 'badge bg-dark'; default: return 'badge bg-secondary'; @@ -295,35 +307,145 @@ export class TransactionsList implements OnInit { case 'SUCCESS': return 'lucideCheckCircle'; case 'PENDING': return 'lucideClock'; case 'FAILED': return 'lucideXCircle'; - case 'REFUNDED': return 'lucideUndo2'; case 'CANCELLED': return 'lucideBan'; default: return 'lucideClock'; } } - formatCurrency(amount: number, currency: string = 'EUR'): string { - return new Intl.NumberFormat('fr-FR', { - style: 'currency', - currency: currency - }).format(amount); + getTypeIcon(type: TransactionType): string { + switch (type) { + case TransactionType.SUBSCRIPTION_PAYMENT: return 'lucideCreditCard'; + case TransactionType.SUBSCRIPTION_RENEWAL: return 'lucideRepeat'; + case TransactionType.ONE_TIME_PAYMENT: return 'lucideCreditCard'; + default: return 'lucideCreditCard'; + } } - formatDate(date: Date): string { - return new Intl.DateTimeFormat('fr-FR', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }).format(new Date(date)); + getPeriodicityBadgeClass(periodicity?: string): string { + if (!periodicity) return 'badge bg-secondary'; + + switch (periodicity.toLowerCase()) { + case 'daily': + return 'badge bg-primary'; + case 'weekly': + return 'badge bg-info'; + case 'monthly': + return 'badge bg-success'; + case 'yearly': + return 'badge bg-warning'; + default: + return 'badge bg-secondary'; + } + } + + formatCurrency(amount: number, currency: Currency = Currency.XOF): string { + return TransactionUtils.formatAmount(amount, currency); + } + + formatDate(date: Date | string | undefined | null): string { + // Si la date est null/undefined, retourner une chaîne vide + if (!date) { + return '-'; + } + + // Si c'est déjà une Date valide + if (date instanceof Date) { + // Vérifier si la Date est valide + if (isNaN(date.getTime())) { + return 'Date invalide'; + } + return new Intl.DateTimeFormat('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }).format(date); + } + + // Si c'est une chaîne, essayer de la convertir + if (typeof date === 'string') { + const dateObj = new Date(date); + + // Vérifier si la conversion a réussi + if (isNaN(dateObj.getTime())) { + // Essayer d'autres formats + const alternativeDate = this.parseDateString(date); + if (alternativeDate && !isNaN(alternativeDate.getTime())) { + return new Intl.DateTimeFormat('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }).format(alternativeDate); + } + return 'Date invalide'; + } + + return new Intl.DateTimeFormat('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }).format(dateObj); + } + + // Pour tout autre type, retourner '-' + return '-'; + } + + private parseDateString(dateString: string): Date | null { + try { + // Essayer différents formats de date + const formats = [ + dateString, // Format ISO original + dateString.replace(' ', 'T'), // Remplacer espace par T + dateString.split('.')[0], // Enlever les millisecondes + ]; + + for (const format of formats) { + const date = new Date(format); + if (!isNaN(date.getTime())) { + return date; + } + } + + return null; + } catch { + return null; + } } getAmountColor(amount: number): string { - if (amount >= 10) return 'text-danger fw-bold'; - if (amount >= 5) return 'text-warning fw-semibold'; + if (amount >= 10000) return 'text-danger fw-bold'; + if (amount >= 5000) return 'text-warning fw-semibold'; return 'text-success'; } + // utilitaires pour les abonnements + getStatusDisplayName(status: TransactionStatus): string { + return TransactionUtils.getStatusDisplayName(status); + } + + getTypeDisplayName(type: TransactionType): string { + return TransactionUtils.getTypeDisplayName(type); + } + + getPeriodicityDisplayName(periodicity?: string): string { + if (!periodicity) return ''; + + const periodicityNames: Record = { + 'daily': 'Quotidien', + 'weekly': 'Hebdomadaire', + 'monthly': 'Mensuel', + 'yearly': 'Annuel' + }; + + return periodicityNames[periodicity.toLowerCase()] || periodicity; + } + // Méthodes pour sécuriser l'accès aux stats getTotal(): number { return this.paginatedData?.stats?.total || 0; @@ -341,15 +463,27 @@ export class TransactionsList implements OnInit { return this.paginatedData?.stats?.pendingCount || 0; } - getSuccessRate(): number { - return this.paginatedData?.stats?.successRate || 0; - } - getTotalAmount(): number { return this.paginatedData?.stats?.totalAmount || 0; } - - getMinValue(a: number, b: number): number { - return Math.min(a, b); + + // Méthodes pour le template + getUserBadgeClass(): string { + return this.access.isHubUser ? 'bg-primary' : 'bg-success'; + } + + getUserBadgeIcon(): string { + return this.access.isHubUser ? 'lucideShield' : 'lucideStore'; + } + + getUserBadgeText(): string { + return this.access.isHubUser ? 'Hub User' : 'Merchant User'; + } + + getScopeText(): string { + if (this.access.isMerchantUser && this.currentMerchantId) { + return `Merchant ${this.currentMerchantId}`; + } + return this.access.canManageAll ? 'Tous les merchants' : 'Merchants autorisés'; } } \ No newline at end of file diff --git a/src/app/modules/transactions/models/transaction.ts b/src/app/modules/transactions/models/transaction.ts deleted file mode 100644 index ae12b9a..0000000 --- a/src/app/modules/transactions/models/transaction.ts +++ /dev/null @@ -1,80 +0,0 @@ -export interface Transaction { - id: string; - msisdn: string; - operator: string; - operatorId: string; - country: string; - amount: number; - currency: string; - status: TransactionStatus; - productId: string; - productName: string; - productCategory: string; - transactionDate: Date; - createdAt: Date; - updatedAt: Date; - externalId?: string; - merchantId?: string; - merchantName?: string; - errorCode?: string; - errorMessage?: string; - userAgent?: string; - ipAddress?: string; - customData?: { [key: string]: any }; -} - -export interface TransactionQuery { - page?: number; - limit?: number; - search?: string; - status?: TransactionStatus; - operator?: string; - country?: string; - startDate?: Date; - endDate?: Date; - msisdn?: string; - productId?: string; - merchantId?: string; - sortBy?: string; - sortOrder?: 'asc' | 'desc'; -} - -export interface TransactionStats { - total: number; - totalAmount: number; - successCount: number; - failedCount: number; - pendingCount: number; - refundedCount: number; - successRate: number; - averageAmount: number; -} - -export interface PaginatedTransactions { - data: Transaction[]; - total: number; - page: number; - limit: number; - totalPages: number; - stats: TransactionStats; -} - -export type TransactionStatus = - | 'PENDING' - | 'SUCCESS' - | 'FAILED' - | 'REFUNDED' - | 'CANCELLED' - | 'EXPIRED'; - -export interface RefundRequest { - transactionId: string; - reason?: string; - amount?: number; -} - -export interface TransactionExportRequest { - format: 'csv' | 'excel' | 'pdf'; - query: TransactionQuery; - columns?: string[]; -} \ No newline at end of file diff --git a/src/app/modules/transactions/services/transaction-access.service.ts b/src/app/modules/transactions/services/transaction-access.service.ts new file mode 100644 index 0000000..441c109 --- /dev/null +++ b/src/app/modules/transactions/services/transaction-access.service.ts @@ -0,0 +1,207 @@ +// [file name]: transactions/services/transaction-access.service.ts +import { Injectable, Injector, inject } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { RoleManagementService, UserRole } from '@core/services/hub-users-roles-management.service'; +import { AuthService } from '@core/services/auth.service'; + +export interface TransactionAccess { + // Permissions de visualisation + canViewTransactions: boolean; + canViewAllTransactions: boolean; // Toutes vs seulement les siennes + canViewDetails: boolean; + + // Permissions d'actions + canRefund: boolean; + canRetry: boolean; + canCancel: boolean; + canExport: boolean; + + // Permissions administratives + canManageAll: boolean; // Toutes les transactions + canFilterByMerchant: boolean; + canViewSensitiveData: boolean; // IP, User Agent, etc. + + // Scope + allowedMerchantIds: number[]; + isHubUser: boolean; + isMerchantUser: boolean; + + // Informations utilisateur + userRole: UserRole; + userRoleLabel: string; + merchantId?: number; +} + +@Injectable({ providedIn: 'root' }) +export class TransactionAccessService { + private accessCache: TransactionAccess | null = null; + private readonly injector = inject(Injector); + + constructor( + private roleService: RoleManagementService + ) {} + + + getTransactionAccess(): TransactionAccess { + if (this.accessCache) { + return this.accessCache; + } + + const userRole = this.roleService.getCurrentRole() || UserRole.DCB_SUPPORT; // Valeur par défaut + const isHubUser = this.roleService.isHubUser(); + const merchantId = this.getCurrentMerchantId(); + + const access: TransactionAccess = { + // Pour tous les utilisateurs (avec restrictions) + canViewTransactions: this.canViewTransactions(userRole, isHubUser), + canViewAllTransactions: this.canViewAllTransactions(userRole, isHubUser), + canViewDetails: this.canViewDetails(userRole, isHubUser), + + // Actions selon le rôle + canRefund: this.canPerformRefund(userRole, isHubUser), + canRetry: this.canPerformRetry(userRole, isHubUser), + canCancel: this.canPerformCancel(userRole, isHubUser), + canExport: this.canExport(userRole, isHubUser), + + // Permissions administratives + canManageAll: this.canManageAll(userRole, isHubUser), + canFilterByMerchant: this.canFilterByMerchant(userRole, isHubUser), + canViewSensitiveData: this.canViewSensitiveData(userRole, isHubUser), + + // Scope + allowedMerchantIds: this.getAllowedMerchantIds(isHubUser, merchantId), + isHubUser, + isMerchantUser: !isHubUser, + + // Informations utilisateur + userRole, + userRoleLabel: this.roleService.getRoleLabel() || 'Utilisateur', + merchantId + }; + + this.accessCache = access; + return access; + } + + // === MÉTHODES DE DÉTERMINATION DES PERMISSIONS === + + private canViewTransactions(userRole: UserRole, isHubUser: boolean): boolean { + // Tous les rôles peuvent voir les transactions + return true; + } + + private canViewAllTransactions(userRole: UserRole, isHubUser: boolean): boolean { + // Hub users et DCB_PARTNER_ADMIN peuvent voir toutes les transactions + return isHubUser || userRole === UserRole.DCB_PARTNER_ADMIN; + } + + private canViewDetails(userRole: UserRole, isHubUser: boolean): boolean { + // Tous peuvent voir les détails (avec restrictions de scope) + return true; + } + + private canPerformRefund(userRole: UserRole, isHubUser: boolean): boolean { + // DCB_ADMIN: peut rembourser toutes les transactions + // DCB_SUPPORT: peut rembourser les transactions de son périmètre + // DCB_PARTNER_ADMIN: peut rembourser les transactions de son merchant + return isHubUser + ? userRole === UserRole.DCB_ADMIN || userRole === UserRole.DCB_SUPPORT + : userRole === UserRole.DCB_PARTNER_ADMIN; + } + + private canPerformRetry(userRole: UserRole, isHubUser: boolean): boolean { + // Mêmes permissions que le remboursement + return this.canPerformRefund(userRole, isHubUser); + } + + private canPerformCancel(userRole: UserRole, isHubUser: boolean): boolean { + // Plus restrictif - seulement les admins peuvent annuler + return isHubUser + ? userRole === UserRole.DCB_ADMIN + : userRole === UserRole.DCB_PARTNER_ADMIN; + } + + private canExport(userRole: UserRole, isHubUser: boolean): boolean { + // Tous peuvent exporter leurs propres données + return true; + } + + private canManageAll(userRole: UserRole, isHubUser: boolean): boolean { + // Seulement DCB_ADMIN hub peut tout gérer + return isHubUser && userRole === UserRole.DCB_ADMIN; + } + + private canFilterByMerchant(userRole: UserRole, isHubUser: boolean): boolean { + // Seuls les hub users peuvent filtrer par merchant + return isHubUser; + } + + private canViewSensitiveData(userRole: UserRole, isHubUser: boolean): boolean { + // Hub users et DCB_PARTNER_ADMIN peuvent voir les données sensibles + return isHubUser || userRole === UserRole.DCB_PARTNER_ADMIN; + } + + // === GESTION DU SCOPE === + + private getCurrentMerchantId(): number | undefined { + // Récupérer le merchant ID de l'utilisateur courant + const authService = this.injector.get(AuthService); + + const merchantPartnerId = authService.getCurrentMerchantPartnerId(); + + if (!merchantPartnerId) return undefined; + + const merchantId = parseInt(merchantPartnerId, 10); + + return isNaN(merchantId) ? undefined : merchantId; + } + + private getAllowedMerchantIds(isHubUser: boolean, merchantId?: number): number[] { + if (isHubUser) { + // Hub users peuvent voir tous les merchants + return []; // Tableau vide = tous les merchants + } else { + // Merchant users: seulement leur merchant + return merchantId ? [merchantId] : []; + } + } + + // === MÉTHODES PUBLIQUES === + + // Vérifie si l'utilisateur peut accéder à une transaction spécifique + canAccessTransaction(transactionMerchantId?: number): Observable { + const access = this.getTransactionAccess(); + + // Hub users avec permission de tout voir + if (access.canManageAll) { + return of(true); + } + + // Si pas de merchant ID sur la transaction, accès limité + if (!transactionMerchantId) { + return of(access.isHubUser); + } + + // Merchant users: seulement leur merchant + if (access.isMerchantUser) { + return of(access.allowedMerchantIds.includes(transactionMerchantId)); + } + + // Hub users: vérifier si le merchant est dans leur liste autorisée + if (access.allowedMerchantIds.length === 0) { + return of(true); // Tous les merchants autorisés + } + + return of(access.allowedMerchantIds.includes(transactionMerchantId)); + } + + // Nettoyer le cache + clearCache(): void { + this.accessCache = null; + } + + // Rafraîchir les permissions (après changement de rôle/merchant) + refreshAccess(): void { + this.clearCache(); + } +} \ No newline at end of file diff --git a/src/app/modules/transactions/services/transactions.service.ts b/src/app/modules/transactions/services/transactions.service.ts index 74d297b..912ed8a 100644 --- a/src/app/modules/transactions/services/transactions.service.ts +++ b/src/app/modules/transactions/services/transactions.service.ts @@ -1,177 +1,536 @@ +// transactions.service.ts import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable, map, catchError, throwError, of, tap } from 'rxjs'; import { environment } from '@environments/environment'; -import { Observable, map, catchError, throwError } from 'rxjs'; import { Transaction, TransactionQuery, PaginatedTransactions, TransactionStats, - RefundRequest -} from '../models/transaction'; + TransactionType, + TransactionStatus +} from '@core/models/dcb-bo-hub-transaction.model'; + +import { + Currency +} from '@core/models/dcb-bo-hub-subscription.model'; + +interface ApiResponse { + data: any[]; + meta?: { + total: number; + page: number; + limit: number; + totalPages: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + }; +} + +interface CacheEntry { + key: string; + data: any[]; + timestamp: number; + query: TransactionQuery; +} @Injectable({ providedIn: 'root' }) export class TransactionsService { private http = inject(HttpClient); - private apiUrl = `${environment.localServiceTestApiUrl}/transactions`; + + private baseApiUrl = `${environment.apiCoreUrl}`; + private subscriptionsUrl = `${this.baseApiUrl}/subscriptions`; + private paymentsUrl = `${this.baseApiUrl}/payments`; + + // Cache pour éviter de recharger toutes les données + private cache: CacheEntry | null = null; + private readonly CACHE_TTL = 30000; // 30 secondes - // === CRUD OPERATIONS === + // === OPÉRATIONS PRINCIPALES === + getTransactions(query: TransactionQuery): Observable { - let params = new HttpParams(); + const cacheKey = this.generateCacheKey(query); - // Ajouter tous les paramètres de query - Object.keys(query).forEach(key => { - const value = query[key as keyof TransactionQuery]; - if (value !== undefined && value !== null) { - if (value instanceof Date) { - params = params.set(key, value.toISOString()); - } else { - params = params.set(key, value.toString()); - } + const canUseCache = this.cache && + this.cache.key === cacheKey && + Date.now() - this.cache.timestamp < this.CACHE_TTL; + + if (query.merchantPartnerId) { + if (canUseCache) { + console.log('Using cached data for merchant:', query.merchantPartnerId); + return of(this.createPaginatedResponseFromCache(query)); } - }); - - return this.http.get(`${this.apiUrl}`, { params }).pipe( - catchError(error => { - console.error('Error loading transactions:', error); - return throwError(() => error); - }) - ); + return this.getSubscriptionsByMerchant(query).pipe( + tap((response: PaginatedTransactions & { rawApiData?: any[] }) => { + this.cache = { + key: cacheKey, + data: response.rawApiData || [], + timestamp: Date.now(), + query: { ...query, page: 1, limit: 1000 } + }; + }) + ); + } else { + if (canUseCache) { + console.log('Using cached data for all subscriptions'); + return of(this.createPaginatedResponseFromCache(query)); + } + return this.getAllSubscriptions(query).pipe( + tap((response: PaginatedTransactions & { rawApiData?: any[] }) => { + // Stocker les données BRUTES de l'API + this.cache = { + key: cacheKey, + data: response.rawApiData || [], + timestamp: Date.now(), + query: { ...query, page: 1, limit: 1000 } + }; + }) + ); + } } getTransactionById(id: string): Observable { - return this.http.get(`${this.apiUrl}/${id}`).pipe( + const subscriptionId = parseInt(id, 10); + if (isNaN(subscriptionId)) { + return throwError(() => new Error(`ID de transaction invalide: ${id}`)); + } + + return this.http.get(`${this.subscriptionsUrl}/${subscriptionId}`).pipe( + map(subscription => this.mapSubscriptionToTransaction(subscription)), catchError(error => { - console.error('Error loading transaction:', error); + console.error(`Error loading transaction ${id}:`, error); return throwError(() => error); }) ); } - // === ACTIONS === - refundTransaction(refundRequest: RefundRequest): Observable<{ message: string; transaction: Transaction }> { - return this.http.post<{ message: string; transaction: Transaction }>( - `${this.apiUrl}/${refundRequest.transactionId}/refund`, - refundRequest - ); - } - - cancelTransaction(transactionId: string): Observable<{ message: string }> { - return this.http.post<{ message: string }>( - `${this.apiUrl}/${transactionId}/cancel`, - {} - ); - } - - retryTransaction(transactionId: string): Observable<{ message: string; transaction: Transaction }> { - return this.http.post<{ message: string; transaction: Transaction }>( - `${this.apiUrl}/${transactionId}/retry`, - {} + getTransactionPayments(merchantId: number, subscriptionId: number, page: number = 1, limit: number = 10): Observable { + let params = new HttpParams() + .set('page', page.toString()) + .set('limit', limit.toString()); + + return this.http.get(`${this.paymentsUrl}/merchant/${merchantId}/subscription/${subscriptionId}`, { params }).pipe( + map(response => { + const data = response?.data || []; + const meta = response?.meta || { + total: data.length, + page: page, + limit: limit, + totalPages: Math.ceil(data.length / limit), + hasNextPage: false, + hasPreviousPage: false + }; + + const transactions = data.map((payment: any) => this.mapPaymentToTransaction(payment)); + const stats = this.calculateStats(transactions); + + return { + data: transactions, + total: meta.total, + page: meta.page, + limit: meta.limit, + totalPages: meta.totalPages, + stats + }; + }), + catchError(error => { + console.error(`Error loading payments for subscription ${subscriptionId}:`, error); + return of({ + data: [], + total: 0, + page: 1, + limit: 10, + totalPages: 1, + stats: this.calculateStats([]) + }); + }) ); } // === STATISTIQUES === + getTransactionStats(query?: Partial): Observable { - let params = new HttpParams(); + const transactionQuery: TransactionQuery = { + page: 1, + limit: 1000, + ...query + }; - if (query) { - Object.keys(query).forEach(key => { - const value = query[key as keyof TransactionQuery]; - if (value !== undefined && value !== null) { - if (value instanceof Date) { - params = params.set(key, value.toISOString()); - } else { - params = params.set(key, value.toString()); - } - } - }); - } - - return this.http.get(`${this.apiUrl}/stats`, { params }); - } - - // === EXPORT === - exportTransactions(exportRequest: any): Observable<{ url: string; filename: string }> { - return this.http.post<{ url: string; filename: string }>( - `${this.apiUrl}/export`, - exportRequest + return this.getTransactions(transactionQuery).pipe( + map(response => response.stats) ); } - // === MOCK DATA POUR LE DÉVELOPPEMENT === - getMockTransactions(): Transaction[] { - return [ - { - id: 'tx_001', - msisdn: '+33612345678', - operator: 'Orange', - operatorId: 'orange_fr', - country: 'FR', - amount: 4.99, - currency: 'EUR', - status: 'SUCCESS', - productId: 'prod_premium', - productName: 'Contenu Premium', - productCategory: 'ENTERTAINMENT', - transactionDate: new Date('2024-01-15T14:30:00'), - createdAt: new Date('2024-01-15T14:30:00'), - updatedAt: new Date('2024-01-15T14:30:00'), - externalId: 'ext_123456', - merchantName: 'MediaCorp' - }, - { - id: 'tx_002', - msisdn: '+33798765432', - operator: 'Free', - operatorId: 'free_fr', - country: 'FR', - amount: 2.99, - currency: 'EUR', - status: 'PENDING', - productId: 'prod_basic', - productName: 'Abonnement Basique', - productCategory: 'SUBSCRIPTION', - transactionDate: new Date('2024-01-15T14:25:00'), - createdAt: new Date('2024-01-15T14:25:00'), - updatedAt: new Date('2024-01-15T14:25:00'), - externalId: 'ext_123457' - }, - { - id: 'tx_003', - msisdn: '+33687654321', - operator: 'SFR', - operatorId: 'sfr_fr', - country: 'FR', - amount: 9.99, - currency: 'EUR', - status: 'FAILED', - productId: 'prod_pro', - productName: 'Pack Professionnel', - productCategory: 'BUSINESS', - transactionDate: new Date('2024-01-15T14:20:00'), - createdAt: new Date('2024-01-15T14:20:00'), - updatedAt: new Date('2024-01-15T14:20:00'), - errorCode: 'INSUFFICIENT_FUNDS', - errorMessage: 'Solde insuffisant' - }, - { - id: 'tx_004', - msisdn: '+33611223344', - operator: 'Bouygues', - operatorId: 'bouygues_fr', - country: 'FR', - amount: 1.99, - currency: 'EUR', - status: 'REFUNDED', - productId: 'prod_mini', - productName: 'Pack Découverte', - productCategory: 'GAMING', - transactionDate: new Date('2024-01-15T14:15:00'), - createdAt: new Date('2024-01-15T14:15:00'), - updatedAt: new Date('2024-01-15T16:30:00'), - merchantName: 'GameStudio' + // === MÉTHODES PRIVÉES === + + private getAllSubscriptions(query: TransactionQuery): Observable { + let params = new HttpParams(); + + // Paramètres supportés par l'API subscription + if (query.status) { + const subscriptionStatus = this.mapToSubscriptionStatus(query.status); + if (subscriptionStatus) { + params = params.set('status', subscriptionStatus); } - ]; + } + + if (query.periodicity) { + params = params.set('periodicity', query.periodicity); + } + + if (query.startDate) { + params = params.set('startDate', query.startDate.toISOString()); + } + + if (query.endDate) { + params = params.set('endDate', query.endDate.toISOString()); + } + + return this.http.get(this.subscriptionsUrl, { params }).pipe( + map(apiResponse => this.createPaginatedResponse(apiResponse, query, true)), + catchError(error => { + console.error('Error loading all subscriptions:', error); + return throwError(() => error); + }) + ); + } + + private getSubscriptionsByMerchant(query: TransactionQuery): Observable { + if (!query.merchantPartnerId) { + return throwError(() => new Error('Merchant ID is required')); + } + + let params = new HttpParams(); + + // Paramètres supportés par l'API + if (query.status) { + const subscriptionStatus = this.mapToSubscriptionStatus(query.status); + if (subscriptionStatus) { + params = params.set('status', subscriptionStatus); + } + } + + if (query.periodicity) { + params = params.set('periodicity', query.periodicity); + } + + if (query.startDate) { + params = params.set('startDate', query.startDate.toISOString()); + } + + if (query.endDate) { + params = params.set('endDate', query.endDate.toISOString()); + } + + return this.http.get(`${this.subscriptionsUrl}/merchant/${query.merchantPartnerId}`, { params }).pipe( + map(apiResponse => this.createPaginatedResponse(apiResponse, query, true)), + catchError(error => { + console.error(`Error loading subscriptions for merchant ${query.merchantPartnerId}:`, error); + return throwError(() => error); + }) + ); + } + + private createPaginatedResponse( + apiResponse: ApiResponse, + query: TransactionQuery, + clientSidePagination: boolean = false + ): PaginatedTransactions & { rawApiData?: any[] } { // <-- Ajout du type de retour + const rawData = apiResponse?.data || []; + + // Convertir toutes les données en transactions + const allTransactions = rawData.map((item: any) => this.mapSubscriptionToTransaction(item)); + + // Appliquer la recherche locale si fournie + let filteredTransactions = [...allTransactions]; + + if (query.search) { + const searchLower = query.search.toLowerCase(); + filteredTransactions = filteredTransactions.filter(tx => + tx.id.toString().toLowerCase().includes(searchLower) || + (tx.externalReference && tx.externalReference.toLowerCase().includes(searchLower)) || + tx.productName.toLowerCase().includes(searchLower) + ); + } + + // Appliquer le tri côté client + if (query.sortBy) { + this.sortTransactions(filteredTransactions, query.sortBy, query.sortOrder); + } + + let data: Transaction[]; + let total: number; + let page: number; + let limit: number; + let totalPages: number; + + if (clientSidePagination) { + // Pagination côté client + page = query.page || 1; + limit = query.limit || 20; + total = filteredTransactions.length; + totalPages = Math.ceil(total / limit); + const startIndex = (page - 1) * limit; + data = filteredTransactions.slice(startIndex, startIndex + limit); + } else { + // Pagination côté serveur (si meta existe) + const meta = apiResponse?.meta || { + total: filteredTransactions.length, + page: query.page || 1, + limit: query.limit || 20, + totalPages: 1, + hasNextPage: false, + hasPreviousPage: false + }; + + data = filteredTransactions; + total = meta.total; + page = meta.page; + limit = meta.limit; + totalPages = meta.totalPages; + } + + const stats = this.calculateStats(filteredTransactions); + + return { + data, + total, + page, + limit, + totalPages, + stats, + rawApiData: rawData + }; + } + + private createPaginatedResponseFromCache(query: TransactionQuery): PaginatedTransactions { + if (!this.cache) { + throw new Error('No cache available'); + } + + const rawData = this.cache.data || []; + + // Convertir les données brutes en transactions + const allTransactions = rawData.map((item: any) => this.mapSubscriptionToTransaction(item)); + + // Appliquer la recherche + let filteredTransactions = [...allTransactions]; + if (query.search) { + const searchLower = query.search.toLowerCase(); + filteredTransactions = filteredTransactions.filter(tx => + tx.id.toString().toLowerCase().includes(searchLower) || + (tx.externalReference && tx.externalReference.toLowerCase().includes(searchLower)) + ); + } + + // Appliquer le tri + if (query.sortBy) { + this.sortTransactions(filteredTransactions, query.sortBy, query.sortOrder); + } + + // Pagination + const page = query.page || 1; + const limit = query.limit || 20; + const total = filteredTransactions.length; + const totalPages = Math.ceil(total / limit); + const startIndex = (page - 1) * limit; + const data = filteredTransactions.slice(startIndex, startIndex + limit); + + // Calculer les stats + const stats = this.calculateStats(filteredTransactions); + + return { + data, + total, + page, + limit, + totalPages, + stats + }; + } + + private generateCacheKey(query: TransactionQuery): string { + return [ + query.merchantPartnerId || 'all', + query.status || 'all', + query.periodicity || 'all', + query.startDate?.toISOString().split('T')[0] || 'all', + query.endDate?.toISOString().split('T')[0] || 'all', + query.search || 'all' + ].join('|'); + } + + private mapSubscriptionToTransaction(subscription: any): Transaction { + return { + // Identifiants + id: subscription.id.toString(), + externalReference: subscription.externalReference || undefined, + subscriptionId: subscription.id, + + // Informations financières + amount: subscription.amount, + currency: subscription.currency as Currency, + + // Statut et type + status: this.mapToTransactionStatus(subscription.status), + type: TransactionType.SUBSCRIPTION_PAYMENT, + + // Informations produit/abonnement + productId: subscription.planId?.toString() || '', + productName: `Abonnement ${subscription.periodicity}`, + periodicity: subscription.periodicity, + + // Dates + transactionDate: new Date(subscription.startDate), + createdAt: new Date(subscription.createdAt), + updatedAt: new Date(subscription.updatedAt), + nextPaymentDate: subscription.nextPaymentDate ? new Date(subscription.nextPaymentDate) : undefined, + + // Informations marchand + merchantPartnerId: subscription.merchantPartnerId, + + // Métadonnées + metadata: subscription.metadata || {} + }; + } + + private mapPaymentToTransaction(payment: any): Transaction { + return { + id: payment.id.toString(), + externalReference: payment.externalReference || undefined, + amount: payment.amount, + currency: payment.currency as Currency, + status: payment.status as TransactionStatus, + type: TransactionType.SUBSCRIPTION_PAYMENT, + transactionDate: new Date(payment.createdAt), + createdAt: new Date(payment.createdAt), + updatedAt: new Date(payment.updatedAt), + merchantPartnerId: payment.merchantPartnerId, + subscriptionId: payment.subscriptionId, + productId: payment.subscriptionId?.toString() || '', + productName: 'Paiement abonnement', + metadata: payment.metadata || {} + }; + } + + private mapToTransactionStatus(subscriptionStatus: string): TransactionStatus { + const statusMap: Record = { + 'ACTIVE': 'SUCCESS', + 'PENDING': 'PENDING', + 'SUSPENDED': 'FAILED', + 'CANCELLED': 'CANCELLED', + 'EXPIRED': 'EXPIRED' + }; + return statusMap[subscriptionStatus] || 'PENDING'; + } + + private mapToSubscriptionStatus(transactionStatus: TransactionStatus): string | null { + const statusMap: Record = { + 'SUCCESS': 'ACTIVE', + 'PENDING': 'PENDING', + 'FAILED': 'SUSPENDED', + 'CANCELLED': 'CANCELLED', + 'EXPIRED': 'EXPIRED' + }; + return statusMap[transactionStatus] || null; + } + + private sortTransactions(transactions: Transaction[], field: string, order: 'asc' | 'desc' = 'desc'): void { + transactions.sort((a, b) => { + let aValue: any; + let bValue: any; + + switch (field) { + case 'id': + aValue = parseInt(a.id); + bValue = parseInt(b.id); + break; + case 'amount': + aValue = a.amount; + bValue = b.amount; + break; + case 'transactionDate': + aValue = a.transactionDate?.getTime(); + bValue = b.transactionDate?.getTime(); + break; + case 'createdAt': + aValue = a.createdAt?.getTime(); + bValue = b.createdAt?.getTime(); + break; + case 'status': + aValue = a.status; + bValue = b.status; + break; + default: + aValue = a[field as keyof Transaction]; + bValue = b[field as keyof Transaction]; + } + + // Gérer les valeurs undefined/null + if (aValue === undefined || aValue === null) aValue = order === 'asc' ? Infinity : -Infinity; + if (bValue === undefined || bValue === null) bValue = order === 'asc' ? Infinity : -Infinity; + + // Comparer + if (aValue < bValue) return order === 'asc' ? -1 : 1; + if (aValue > bValue) return order === 'asc' ? 1 : -1; + return 0; + }); + } + + private calculateStats(transactions: Transaction[]): TransactionStats { + const total = transactions.length; + const totalAmount = transactions.reduce((sum, tx) => sum + tx.amount, 0); + const successCount = transactions.filter(tx => tx.status === 'SUCCESS').length; + const failedCount = transactions.filter(tx => tx.status === 'FAILED').length; + const pendingCount = transactions.filter(tx => tx.status === 'PENDING').length; + const cancelledCount = transactions.filter(tx => tx.status === 'CANCELLED').length; + + const byPeriodicity = { + DAILY: transactions.filter(tx => tx.periodicity === 'Daily').length, + WEEKLY: transactions.filter(tx => tx.periodicity === 'Weekly').length, + MONTHLY: transactions.filter(tx => tx.periodicity === 'Monthly').length, + YEARLY: transactions.filter(tx => tx.periodicity === 'Yearly').length + }; + + return { + total, + totalAmount, + successCount, + failedCount, + pendingCount, + averageAmount: total > 0 ? totalAmount / total : 0, + byPeriodicity + }; + } + + // === MÉTHODES PUBLIQUES UTILITAIRES === + + clearCache(): void { + this.cache = null; + } + + refreshTransactions(query: TransactionQuery): Observable { + this.clearCache(); + return this.getTransactions(query); + } + + // === MÉTHODES NON IMPLÉMENTÉES (APIs non disponibles) === + + refundTransaction(): Observable<{ message: string; transaction: Transaction }> { + return throwError(() => new Error('Remboursement non implémenté - API non disponible')); + } + + cancelTransaction(): Observable<{ message: string }> { + return throwError(() => new Error('Annulation non implémentée - API non disponible')); + } + + retryTransaction(): Observable<{ message: string; transaction: Transaction }> { + return throwError(() => new Error('Nouvelle tentative non implémentée - API non disponible')); + } + + exportTransactions(): Observable<{ url: string; filename: string }> { + return throwError(() => new Error('Export non implémenté - API non disponible')); } } \ No newline at end of file diff --git a/src/app/modules/transactions/transactions.ts b/src/app/modules/transactions/transactions.ts index b275365..2ab0bc2 100644 --- a/src/app/modules/transactions/transactions.ts +++ b/src/app/modules/transactions/transactions.ts @@ -1,10 +1,11 @@ -import { Component, inject, TemplateRef, ViewChild } from '@angular/core'; +import { Component, inject, TemplateRef, ViewChild, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; import { PageTitle } from '@app/components/page-title/page-title'; import { TransactionsList } from './list/list'; import { TransactionDetails } from './details/details'; import { NgIcon } from '@ng-icons/core'; +import { TransactionAccessService } from './services/transaction-access.service'; @Component({ selector: 'app-transactions', @@ -19,22 +20,37 @@ import { NgIcon } from '@ng-icons/core'; ], templateUrl: './transactions.html', }) -export class Transactions { +export class Transactions implements OnInit { private modalService = inject(NgbModal); - + private accessService = inject(TransactionAccessService); + activeView: 'list' | 'details' = 'list'; selectedTransactionId: string | null = null; - + + // Permissions + canAccessModule = true; // Par défaut true, pourrait être configuré par rôle + accessDenied = false; + + ngOnInit() { + this.checkAccess(); + } + + private checkAccess() { + const access = this.accessService.getTransactionAccess(); + this.canAccessModule = access.canViewTransactions; + this.accessDenied = !access.canViewTransactions; + } + showListView() { this.activeView = 'list'; this.selectedTransactionId = null; } - + showDetailsView(transactionId: string) { this.activeView = 'details'; this.selectedTransactionId = transactionId; } - + // Gestion des modals openModal(content: TemplateRef, size: 'sm' | 'lg' | 'xl' = 'lg') { this.modalService.open(content, { @@ -43,6 +59,6 @@ export class Transactions { scrollable: true }); } - + @ViewChild('refundModal') refundModal!: TemplateRef; } \ No newline at end of file