From 4c8e3f229dcc81d4882a26ec5fa0cf21f4dac29f Mon Sep 17 00:00:00 2001 From: diallolatoile Date: Mon, 10 Nov 2025 01:28:04 +0000 Subject: [PATCH] feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature --- package-lock.json | 9 +- package.json | 3 +- src/app/app.ts | 50 +- src/app/core/models/dcb-bo-hub-user.model.ts | 160 +++- src/app/core/services/auth.service.ts | 140 ++- ... => hub-users-roles-management.service.ts} | 384 +++++--- src/app/core/services/menu.service.ts | 17 +- src/app/core/services/permissions.service.ts | 20 +- src/app/layouts/components/data.ts | 19 +- .../user-profile/user-profile.component.html | 12 +- .../user-profile/user-profile.component.ts | 98 +- src/app/modules/auth/unauthorized.ts | 2 +- .../hub-users-list/hub-users-list.html} | 255 +++-- .../hub-users-list/hub-users-list.ts | 597 ++++++++++++ .../hub-users-profile/hub-users-profile.html} | 124 ++- .../hub-users-profile/hub-users-profile.ts | 545 +++++++++++ .../hub-users.html} | 290 +++--- .../hub-users-management/hub-users.service.ts | 390 ++++++++ .../hub-users.ts | 714 ++++++++------ .../merchant-users-list.html} | 134 ++- .../merchant-users-list.ts | 517 ++++++++++ .../merchant-users-profile.html} | 393 +++----- .../merchant-users-profile.ts} | 296 +++--- .../hub-users-management/merchant-users.html | 882 ++++++++++++++++++ .../merchant-users.service.ts | 351 +++++++ .../hub-users-management/merchant-users.ts | 806 ++++++++++++++++ src/app/modules/hub-users/hub-users.html | 557 ----------- src/app/modules/hub-users/hub-users.routes.ts | 16 - src/app/modules/hub-users/hub-users.spec.ts | 2 - src/app/modules/hub-users/list/list.spec.ts | 2 - src/app/modules/hub-users/list/list.ts | 372 -------- .../hub-users/models/hub-user.model.ts | 115 --- .../hub-users/models/hub-userl-v1.mode.ts | 54 -- .../modules/hub-users/models/user.model.ts | 311 ------ .../modules/hub-users/profile/profile.spec.ts | 2 - src/app/modules/hub-users/profile/profile.ts | 378 -------- .../modules/hub-users/services/api.service.ts | 78 -- .../hub-users/services/hub-users.service.ts | 361 ------- src/app/modules/hub-users/structure.txt | 15 - .../modules/merchant-users/config/config.html | 436 --------- .../merchant-users/config/config.spec.ts | 2 - .../modules/merchant-users/config/config.ts | 317 ------- .../modules/merchant-users/list/list.spec.ts | 2 - src/app/modules/merchant-users/list/list.ts | 695 -------------- .../merchant-users/merchant-users.spec.ts | 2 - .../modules/merchant-users/merchant-users.ts | 744 --------------- .../models/merchant-user.model.ts | 115 --- .../models/partners-config.model.ts | 531 ----------- .../merchant-users/profile/profile.spec.ts | 2 - .../services/merchant-users.service.ts | 331 ------- .../services/partner-config.service.ts | 233 ----- .../modules/merchant-users/stats/stats.html | 249 ----- .../merchant-users/stats/stats.spec.ts | 2 - src/app/modules/merchant-users/stats/stats.ts | 15 - src/app/modules/merchant-users/types.ts | 6 - src/app/modules/modules.routes.ts | 52 +- src/app/modules/profile/profile.html | 569 +++++++---- src/app/modules/profile/profile.ts | 567 ++++++++--- src/main.ts | 13 +- 59 files changed, 6759 insertions(+), 7595 deletions(-) rename src/app/core/services/{role-management.service.ts => hub-users-roles-management.service.ts} (53%) rename src/app/modules/{hub-users/list/list.html => hub-users-management/hub-users-list/hub-users-list.html} (55%) create mode 100644 src/app/modules/hub-users-management/hub-users-list/hub-users-list.ts rename src/app/modules/{hub-users/profile/profile.html => hub-users-management/hub-users-profile/hub-users-profile.html} (80%) create mode 100644 src/app/modules/hub-users-management/hub-users-profile/hub-users-profile.ts rename src/app/modules/{merchant-users/merchant-users.html => hub-users-management/hub-users.html} (71%) create mode 100644 src/app/modules/hub-users-management/hub-users.service.ts rename src/app/modules/{hub-users => hub-users-management}/hub-users.ts (54%) rename src/app/modules/{merchant-users/list/list.html => hub-users-management/merchant-users-list/merchant-users-list.html} (77%) create mode 100644 src/app/modules/hub-users-management/merchant-users-list/merchant-users-list.ts rename src/app/modules/{merchant-users/profile/profile.html => hub-users-management/merchant-users-profile/merchant-users-profile.html} (56%) rename src/app/modules/{merchant-users/profile/profile.ts => hub-users-management/merchant-users-profile/merchant-users-profile.ts} (63%) create mode 100644 src/app/modules/hub-users-management/merchant-users.html create mode 100644 src/app/modules/hub-users-management/merchant-users.service.ts create mode 100644 src/app/modules/hub-users-management/merchant-users.ts delete mode 100644 src/app/modules/hub-users/hub-users.html delete mode 100644 src/app/modules/hub-users/hub-users.routes.ts delete mode 100644 src/app/modules/hub-users/hub-users.spec.ts delete mode 100644 src/app/modules/hub-users/list/list.spec.ts delete mode 100644 src/app/modules/hub-users/list/list.ts delete mode 100644 src/app/modules/hub-users/models/hub-user.model.ts delete mode 100644 src/app/modules/hub-users/models/hub-userl-v1.mode.ts delete mode 100644 src/app/modules/hub-users/models/user.model.ts delete mode 100644 src/app/modules/hub-users/profile/profile.spec.ts delete mode 100644 src/app/modules/hub-users/profile/profile.ts delete mode 100644 src/app/modules/hub-users/services/api.service.ts delete mode 100644 src/app/modules/hub-users/services/hub-users.service.ts delete mode 100644 src/app/modules/hub-users/structure.txt delete mode 100644 src/app/modules/merchant-users/config/config.html delete mode 100644 src/app/modules/merchant-users/config/config.spec.ts delete mode 100644 src/app/modules/merchant-users/config/config.ts delete mode 100644 src/app/modules/merchant-users/list/list.spec.ts delete mode 100644 src/app/modules/merchant-users/list/list.ts delete mode 100644 src/app/modules/merchant-users/merchant-users.spec.ts delete mode 100644 src/app/modules/merchant-users/merchant-users.ts delete mode 100644 src/app/modules/merchant-users/models/merchant-user.model.ts delete mode 100644 src/app/modules/merchant-users/models/partners-config.model.ts delete mode 100644 src/app/modules/merchant-users/profile/profile.spec.ts delete mode 100644 src/app/modules/merchant-users/services/merchant-users.service.ts delete mode 100644 src/app/modules/merchant-users/services/partner-config.service.ts delete mode 100644 src/app/modules/merchant-users/stats/stats.html delete mode 100644 src/app/modules/merchant-users/stats/stats.spec.ts delete mode 100644 src/app/modules/merchant-users/stats/stats.ts delete mode 100644 src/app/modules/merchant-users/types.ts diff --git a/package-lock.json b/package-lock.json index 2e7b434..4b51b4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,8 +63,7 @@ "quill": "^2.0.3", "rxjs": "~7.8.2", "simplebar-angular": "^3.3.2", - "tslib": "^2.8.1", - "zone.js": "^0.15.1" + "tslib": "^2.8.1" }, "devDependencies": { "@angular/build": "^20.3.6", @@ -11060,12 +11059,6 @@ "peerDependencies": { "zod": "^3.24.1" } - }, - "node_modules/zone.js": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", - "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT" } } } diff --git a/package.json b/package.json index 5d13b45..0aa92f0 100644 --- a/package.json +++ b/package.json @@ -66,8 +66,7 @@ "quill": "^2.0.3", "rxjs": "~7.8.2", "simplebar-angular": "^3.3.2", - "tslib": "^2.8.1", - "zone.js": "^0.15.1" + "tslib": "^2.8.1" }, "devDependencies": { "@angular/build": "^20.3.6", diff --git a/src/app/app.ts b/src/app/app.ts index c7df4f5..c2d2b33 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,4 +1,4 @@ -import { Component, inject, OnInit } from '@angular/core'; +import { Component, inject, OnInit, ChangeDetectorRef } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router'; import * as tablerIcons from '@ng-icons/tabler-icons'; import * as tablerIconsFill from '@ng-icons/tabler-icons/fill'; @@ -22,33 +22,55 @@ export class App implements OnInit { private router = inject(Router); private activatedRoute = inject(ActivatedRoute); private authService = inject(AuthService); + private cdr = inject(ChangeDetectorRef); async ngOnInit(): Promise { + this.setupTitleListener(); + + setTimeout(async () => { + await this.initializeAuth(); + }); + } + + private async initializeAuth(): Promise { try { const isAuthenticated = await this.authService.initialize(); - - if (!isAuthenticated && this.router.url === '/') { - this.router.navigate(['/auth/login']); - } else if (isAuthenticated && this.router.url === '/') { - this.router.navigate(['/dcb-dashboard']); - } + + setTimeout(() => { + this.handleInitialNavigation(isAuthenticated); + }); } catch (error) { console.error('Error during authentication initialization:', error); - this.router.navigate(['/auth/login']); + setTimeout(() => { + this.router.navigate(['/auth/login']); + }); } - - this.setupTitleListener(); } - private checkPublicRouteRedirection(): void { + private handleInitialNavigation(isAuthenticated: boolean): void { const currentUrl = this.router.url; - const publicRoutes = ['/auth/login', '/auth/reset-password', '/auth/forgot-password']; - // Si l'utilisateur est authentifié et sur une route publique, rediriger vers la page d'accueil - if (publicRoutes.includes(currentUrl)) { + if (!isAuthenticated && this.shouldRedirectToLogin(currentUrl)) { + this.router.navigate(['/auth/login']); + } else if (isAuthenticated && this.shouldRedirectToDashboard(currentUrl)) { this.router.navigate(['/dcb-dashboard']); } + + this.cdr.detectChanges(); + } + + private shouldRedirectToLogin(url: string): boolean { + return url === '/' || !this.isPublicRoute(url); + } + + private shouldRedirectToDashboard(url: string): boolean { + return url === '/' || this.isPublicRoute(url); + } + + private isPublicRoute(url: string): boolean { + const publicRoutes = ['/auth/login', '/auth/reset-password', '/auth/forgot-password']; + return publicRoutes.some(route => url.startsWith(route)); } private setupTitleListener(): void { 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 514749e..2d52bc7 100644 --- a/src/app/core/models/dcb-bo-hub-user.model.ts +++ b/src/app/core/models/dcb-bo-hub-user.model.ts @@ -1,46 +1,61 @@ +// === ENUMS COHÉRENTS === export enum UserType { HUB = 'HUB', - MERCHANT = 'MERCHANT', - MERCHANT_USER = 'MERCHANT_USER' + MERCHANT_PARTNER = 'MERCHANT' } export enum UserRole { - // HUB roles - DCB_ADMIN = 'DCB_ADMIN', - DCB_SUPPORT = 'DCB_SUPPORT', - DCB_PARTNER = 'DCB_PARTNER', - - // MERCHANT roles - DCB_PARTNER_ADMIN = 'DCB_PARTNER_ADMIN', - DCB_PARTNER_MANAGER = 'DCB_PARTNER_MANAGER', - DCB_PARTNER_SUPPORT = 'DCB_PARTNER_SUPPORT' + // Rôles Hub (sans merchantPartnerId) + DCB_ADMIN = 'dcb-admin', + DCB_SUPPORT = 'dcb-support', + DCB_PARTNER = 'dcb-partner', + + // Rôles Merchant Partner (avec merchantPartnerId obligatoire) + DCB_PARTNER_ADMIN = 'dcb-partner-admin', + DCB_PARTNER_MANAGER = 'dcb-partner-manager', + DCB_PARTNER_SUPPORT = 'dcb-partner-support' } -// === BASE USER MODEL === -export interface BaseUserDto { +// Enum pour le contexte Angular (identique à l'ancien) +export enum UserContext { + HUB = 'HUB', + MERCHANT = 'MERCHANT' +} + +// Ajoutez cette interface dans vos modèles +export interface GlobalUsersOverview { + hubUsers: User[]; + merchantUsers: User[]; + statistics: { + totalHubUsers: number; + totalMerchantUsers: number; + totalUsers: number; + }; +} + +export interface UsersStatistics { + totalHubUsers: number; + totalMerchantUsers: number; + totalUsers: number; +} + +// === MODÈLE USER PRINCIPAL === +export interface User { id: string; username: string; email: string; firstName: string; lastName: string; - role: UserRole; enabled: boolean; emailVerified: boolean; - createdBy: string; - createdByUsername: string; + userType: UserType; // HUB ou MERCHANT + merchantPartnerId?: string; + role: UserRole; + createdBy?: string; + createdByUsername?: string; createdTimestamp: number; lastLogin?: number; - userType: UserType; -} - -// === EXTENSIONS === -export interface HubUserDto extends BaseUserDto { - userType: UserType.HUB; -} - -export interface MerchantUserDto extends BaseUserDto { - userType: UserType.MERCHANT; - merchantPartnerId: string; + profileImage?: string | null } // === DTOs CRUD === @@ -50,10 +65,11 @@ export interface CreateUserDto { firstName: string; lastName: string; password: string; + userType: UserType; role: UserRole; enabled?: boolean; emailVerified?: boolean; - merchantPartnerId?: string; // obligatoire si MERCHANT + merchantPartnerId?: string; } export interface UpdateUserDto { @@ -61,6 +77,7 @@ export interface UpdateUserDto { lastName?: string; email?: string; enabled?: boolean; + role?: UserRole; } export interface ResetPasswordDto { @@ -71,7 +88,7 @@ export interface ResetPasswordDto { // === PAGINATION / STATS === export interface PaginatedUserResponse { - users: BaseUserDto[]; + users: User[]; total: number; page: number; limit: number; @@ -93,6 +110,7 @@ export interface AvailableRole { label: string; description: string; allowedForCreation: boolean; + userType: UserType; } export interface AvailableRolesResponse { @@ -110,4 +128,88 @@ export interface SearchUsersParams { role?: UserRole; enabled?: boolean; userType?: UserType; + merchantPartnerId?: string; + page?: number; + limit?: number; } + +// === UTILITAIRES === +export class UserUtils { + static isHubUser(user: User): boolean { + return user.userType === UserType.HUB; + } + + static isMerchantPartnerUser(user: User): boolean { + return user.userType === UserType.MERCHANT_PARTNER; + } + + static hasRole(user: User, role: UserRole): boolean { + if (!user.role) return false; + return user.role.includes(role); + } + + static getRoleDisplayName(role: UserRole): string { + const roleNames = { + [UserRole.DCB_ADMIN]: 'DCB Admin', + [UserRole.DCB_SUPPORT]: 'DCB Support', + [UserRole.DCB_PARTNER]: 'DCB Partner', + [UserRole.DCB_PARTNER_ADMIN]: 'Partner Admin', + [UserRole.DCB_PARTNER_MANAGER]: 'Partner Manager', + [UserRole.DCB_PARTNER_SUPPORT]: 'Partner Support' + }; + return roleNames[role] || role; + } + + static getUserTypeDisplayName(userType: UserType): string { + const typeNames = { + [UserType.HUB]: 'Hub', + [UserType.MERCHANT_PARTNER]: 'Merchant Partner' + }; + return typeNames[userType] || userType; + } + + // Méthode pour convertir UserContext (Angular) en UserType (API) + static contextToUserType(context: UserContext): UserType { + return context === UserContext.HUB ? UserType.HUB : UserType.MERCHANT_PARTNER; + } + + // Méthode pour convertir UserType (API) en UserContext (Angular) + static userTypeToContext(userType: UserType): UserContext { + return userType === UserType.HUB ? UserContext.HUB : UserContext.MERCHANT; + } + + static validateUserCreation(user: CreateUserDto): string[] { + const errors: string[] = []; + + // Validation merchantPartnerId + if (user.userType === UserType.MERCHANT_PARTNER && !user.merchantPartnerId) { + errors.push('merchantPartnerId est obligatoire pour les utilisateurs Merchant Partner'); + } + + if (user.userType === UserType.HUB && user.merchantPartnerId) { + errors.push('merchantPartnerId ne doit pas être défini pour les utilisateurs Hub'); + } + + if (!user.role) { + errors.push('Un rôle doit être assigné'); + } + + // Validation cohérence rôle/type + const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]; + const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; + + if (user.userType === UserType.HUB && user.role) { + if (!hubRoles.includes(user.role)) { + errors.push(`Rôle invalide pour un utilisateur Hub: ${user.role}`); + } + } + + if (user.userType === UserType.MERCHANT_PARTNER && user.role) { + if (!merchantRoles.includes(user.role)) { + errors.push(`Rôle invalide pour un utilisateur Merchant Partner: ${user.role}`); + } + } + + return errors; + } +} \ No newline at end of file diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts index d22b6ea..62da742 100644 --- a/src/app/core/services/auth.service.ts +++ b/src/app/core/services/auth.service.ts @@ -6,11 +6,9 @@ import { BehaviorSubject, Observable, throwError, tap, catchError } from 'rxjs'; import { firstValueFrom } from 'rxjs'; import { + User, UserType, UserRole, - BaseUserDto, - HubUserDto, - MerchantUserDto } from '@core/models/dcb-bo-hub-user.model'; // === INTERFACES DTO AUTH === @@ -57,13 +55,12 @@ export interface TokenValidationResponseDto { }) export class AuthService { private readonly http = inject(HttpClient); - private readonly router = inject(Router); private readonly tokenKey = 'access_token'; private readonly refreshTokenKey = 'refresh_token'; private authState$ = new BehaviorSubject(this.isAuthenticated()); - private userProfile$ = new BehaviorSubject(null); + private userProfile$ = new BehaviorSubject(null); private initialized$ = new BehaviorSubject(false); // === INITIALISATION DE L'APPLICATION === @@ -72,31 +69,42 @@ export class AuthService { * Initialise l'authentification au démarrage de l'application */ async initialize(): Promise { + + await new Promise(resolve => setTimeout(resolve, 0)); + try { const token = this.getAccessToken(); if (!token) { - this.initialized$.next(true); + setTimeout(() => { + this.initialized$.next(true); + }); return false; } if (this.isTokenExpired(token)) { const refreshSuccess = await this.tryRefreshToken(); - this.initialized$.next(true); + setTimeout(() => { + this.initialized$.next(true); + }); return refreshSuccess; } // Token valide : charger le profil utilisateur await firstValueFrom(this.loadUserProfile()); - this.authState$.next(true); - this.initialized$.next(true); + setTimeout(() => { + this.authState$.next(true); + this.initialized$.next(true); + }); return true; } catch (error) { this.clearAuthData(); - this.initialized$.next(true); + setTimeout(() => { + this.initialized$.next(true); + }); return false; } } @@ -112,7 +120,7 @@ export class AuthService { } try { - const response = await firstValueFrom(this.refreshAccessToken()); + await firstValueFrom(this.refreshAccessToken()); await firstValueFrom(this.loadUserProfile()); this.authState$.next(true); return true; @@ -188,15 +196,66 @@ export class AuthService { /** * Chargement du profil utilisateur */ - loadUserProfile(): Observable { - return this.http.get( + loadUserProfile(): Observable { + return this.http.get( `${environment.iamApiUrl}/auth/profile` ).pipe( - tap(profile => this.userProfile$.next(profile)), - catchError(error => throwError(() => error)) + tap(apiResponse => { + // Déterminer le type d'utilisateur + const userType = this.determineUserType(apiResponse); + // Mapper vers le modèle User + const userProfile = this.mapToUserModel(apiResponse, userType); + + this.userProfile$.next(userProfile); + }), + catchError(error => { + console.error('❌ Erreur chargement profil:', error); + return throwError(() => error); + }) ); } + /** + * Détermine le type d'utilisateur basé sur la réponse API + */ + private determineUserType(apiUser: any): UserType { + + const hubRoles = [UserRole.DCB_ADMIN || UserRole.DCB_SUPPORT]; + const merchantRoles = [UserRole.DCB_PARTNER || UserRole.DCB_PARTNER_ADMIN || UserRole.DCB_PARTNER_MANAGER || UserRole.DCB_PARTNER_SUPPORT]; + + // Logique pour déterminer le type d'utilisateur + if (apiUser.clientRoles?.[0].includes(merchantRoles)) { + return UserType.MERCHANT_PARTNER; + } else if (apiUser.clientRoles?.[0].includes(hubRoles)) { + return UserType.HUB; + } else { + console.warn('Type d\'utilisateur non reconnu, rôle:', apiUser.clientRoles?.[0]); + return UserType.HUB; // Fallback + } + } + + private mapToUserModel(apiUser: any, userType: UserType): User { + const mappedUser: User = { + id: apiUser.id || apiUser.userId || '', + username: apiUser.username || apiUser.userName || '', + email: apiUser.email || '', + firstName: apiUser.firstName || apiUser.firstname || apiUser.given_name || '', + lastName: apiUser.lastName || apiUser.lastname || apiUser.family_name || '', + enabled: apiUser.enabled ?? apiUser.active ?? true, + emailVerified: apiUser.emailVerified ?? apiUser.email_verified ?? false, + userType: userType, + merchantPartnerId: apiUser.merchantPartnerId || apiUser.partnerId || apiUser.merchantId || null, + role: apiUser.clientRoles || apiUser.clientRoles?.[0] || '', // Gérer rôle unique ou tableau + createdBy: apiUser.createdBy || apiUser.creatorId || null, + createdByUsername: apiUser.createdByUsername || apiUser.creatorUsername || null, + createdTimestamp: apiUser.createdTimestamp || apiUser.createdAt || apiUser.creationDate || Date.now(), + lastLogin: apiUser.lastLogin || apiUser.lastLoginAt || apiUser.lastConnection || null + }; + + console.log('✅ Utilisateur mappé:', mappedUser); + return mappedUser; + } + // === GESTION DE SESSION === private handleLoginSuccess(response: LoginResponseDto): void { @@ -232,7 +291,7 @@ export class AuthService { return this.authState$.asObservable(); } - getUserProfile(): Observable { + getUserProfile(): Observable { return this.userProfile$.asObservable(); } @@ -322,55 +381,72 @@ export class AuthService { if (hubRoles.includes(role)) { return UserType.HUB; } else if (merchantRoles.includes(role)) { - return UserType.MERCHANT; + return UserType.MERCHANT_PARTNER; } return null; } + /** + * Récupère les clientRoles du profil utilisateur + */ + getCurrentUserClientRoles(): UserRole | null { + const profile = this.userProfile$.value; + return profile?.role || null; + } + + /** + * Récupère le rôle principal du profil utilisateur + */ + getCurrentUserPrimaryRole(): UserRole | null { + const clientRoles = this.getCurrentUserClientRoles(); + return clientRoles || null; + } + /** * Vérifie si l'utilisateur courant est un utilisateur Hub */ isHubUser(): boolean { - return this.getCurrentUserType() === UserType.HUB; + const profile = this.userProfile$.value; + return profile?.userType === UserType.HUB; } /** * Vérifie si l'utilisateur courant est un utilisateur Marchand */ isMerchantUser(): boolean { - return this.getCurrentUserType() === UserType.MERCHANT; + const profile = this.userProfile$.value; + return profile?.userType === UserType.MERCHANT_PARTNER; } /** * Vérifie si l'utilisateur courant a un rôle spécifique */ hasRole(role: UserRole): boolean { - return this.getCurrentUserRoles().includes(role); + const clientRoles = this.getCurrentUserClientRoles(); + return clientRoles === role; } /** * Vérifie si l'utilisateur courant a au moins un des rôles spécifiés */ - hasAnyRole(roles: UserRole[]): boolean { - const userRoles = this.getCurrentUserRoles(); - return roles.some(role => userRoles.includes(role)); + hasAnyRole(role: UserRole): boolean { + const userRoles = this.getCurrentUserClientRoles(); + return userRoles === role; } /** * Vérifie si l'utilisateur courant peut gérer les utilisateurs Hub */ canManageHubUsers(): boolean { - const hubAdminRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT]; - return this.hasAnyRole(hubAdminRoles); + return this.hasAnyRole(UserRole.DCB_ADMIN) || this.hasAnyRole(UserRole.DCB_SUPPORT); } /** * Vérifie si l'utilisateur courant peut gérer les utilisateurs Marchands */ canManageMerchantUsers(): boolean { - const allowedRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]; - return this.hasAnyRole(allowedRoles); + return this.hasAnyRole(UserRole.DCB_ADMIN) || this.hasAnyRole(UserRole.DCB_PARTNER); } // === MÉTHODES UTILITAIRES === @@ -379,7 +455,7 @@ export class AuthService { return this.authState$.asObservable(); } - getProfile(): Observable { + getProfile(): Observable { return this.getUserProfile(); } @@ -396,10 +472,7 @@ export class AuthService { */ getCurrentMerchantPartnerId(): string | null { const profile = this.userProfile$.value; - if (profile && 'merchantPartnerId' in profile) { - return (profile as MerchantUserDto).merchantPartnerId || null; - } - return null; + return profile?.merchantPartnerId || null; } /** @@ -414,8 +487,7 @@ export class AuthService { * Vérifie si l'utilisateur peut visualiser tous les marchands */ canViewAllMerchants(): boolean { - const hubAdminRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT]; - return this.hasAnyRole(hubAdminRoles); + return this.hasAnyRole(UserRole.DCB_ADMIN) || this.hasAnyRole(UserRole.DCB_PARTNER); } // === TOKENS === diff --git a/src/app/core/services/role-management.service.ts b/src/app/core/services/hub-users-roles-management.service.ts similarity index 53% rename from src/app/core/services/role-management.service.ts rename to src/app/core/services/hub-users-roles-management.service.ts index 45d5032..51c5ad9 100644 --- a/src/app/core/services/role-management.service.ts +++ b/src/app/core/services/hub-users-roles-management.service.ts @@ -1,7 +1,8 @@ import { Injectable, inject } from '@angular/core'; -import { HubUsersService } from '../../modules/hub-users/services/hub-users.service'; +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 } from '@core/models/dcb-bo-hub-user.model'; +import { UserRole, UserType, AvailableRole } from '@core/models/dcb-bo-hub-user.model'; export interface RolePermission { canCreateUsers: boolean; @@ -13,28 +14,11 @@ export interface RolePermission { canAccessAdmin: boolean; canAccessSupport: boolean; canAccessPartner: boolean; - assignableRoles: UserRole[]; // Ajout de cette propriété -} - -// Interface simplifiée pour la réponse API -export interface AvailableRoleResponse { - value: UserRole; - label: string; - description: string; - allowedForCreation?: boolean; -} - -export interface AvailableRolesResponse { - roles: AvailableRoleResponse[]; -} - -// Interface étendue pour l'usage interne avec les permissions -export interface AvailableRole extends AvailableRoleResponse { - permissions: RolePermission; + assignableRoles: UserRole[]; } export interface AvailableRolesWithPermissions { - roles: AvailableRole[]; + roles: (AvailableRole & { permissions: RolePermission })[]; } @Injectable({ @@ -42,102 +26,87 @@ export interface AvailableRolesWithPermissions { }) 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 disponibles depuis l'API et les enrichit avec les permissions + * Charge les rôles Hub disponibles */ - loadAvailableRoles(): Observable { + loadAvailableHubRoles(): Observable { return this.hubUsersService.getAvailableHubRoles().pipe( - map(apiResponse => { - // Enrichir les rôles de l'API avec les permissions - const rolesWithPermissions: AvailableRole[] = apiResponse.roles.map(role => ({ + map(apiResponse => ({ + roles: apiResponse.roles.map(role => ({ ...role, permissions: this.getPermissionsForRole(role.value) - })); - - const result: AvailableRolesWithPermissions = { - roles: rolesWithPermissions - }; - - this.availableRoles$.next(result); - return result; - }), - catchError(error => { - console.error('Error loading available roles:', error); - // Fallback avec les rôles par défaut - const defaultRoles: AvailableRolesWithPermissions = { - roles: [ - { - value: UserRole.DCB_ADMIN, - label: 'DCB Admin', - description: 'Full administrative access to the entire system', - permissions: this.getPermissionsForRole(UserRole.DCB_ADMIN) - }, - { - value: UserRole.DCB_SUPPORT, - label: 'DCB Support', - description: 'Support access with limited administrative capabilities', - permissions: this.getPermissionsForRole(UserRole.DCB_SUPPORT) - }, - { - value: UserRole.DCB_PARTNER, - label: 'DCB Partner', - description: 'Merchant partner with access to their own merchant ecosystem', - permissions: this.getPermissionsForRole(UserRole.DCB_PARTNER) - }, - { - value: UserRole.DCB_PARTNER_ADMIN, - label: 'Partner Admin', - description: 'Administrateur partenaire marchand', - permissions: this.getPermissionsForRole(UserRole.DCB_PARTNER_ADMIN) - }, - { - value: UserRole.DCB_PARTNER_MANAGER, - label: 'Partner Manager', - description: 'Manager partenaire marchand', - permissions: this.getPermissionsForRole(UserRole.DCB_PARTNER_MANAGER) - }, - { - value: UserRole.DCB_PARTNER_SUPPORT, - label: 'Partner Support', - description: 'Support partenaire marchand', - permissions: this.getPermissionsForRole(UserRole.DCB_PARTNER_SUPPORT) - } - ] - }; - - this.availableRoles$.next(defaultRoles); - return of(defaultRoles); - }) + })) + })), + tap(roles => this.availableRoles$.next(roles)), + catchError(error => this.getDefaultHubRoles(error)) ); } /** - * Récupère les rôles disponibles depuis le cache ou l'API + * Charge les rôles Marchands disponibles */ - getAvailableRoles(): Observable { - const cached = this.availableRoles$.value; - if (cached) { - return of(cached); - } - return this.loadAvailableRoles(); + loadAvailableMerchantRoles(): Observable { + return this.merchantUsersService.getAvailableMerchantRoles().pipe( + map(apiResponse => ({ + roles: apiResponse.roles.map(role => ({ + ...role, + permissions: this.getPermissionsForRole(role.value) + })) + })), + tap(roles => this.availableRoles$.next(roles)), + catchError(error => this.getDefaultMerchantRoles(error)) + ); } /** - * Récupère les rôles disponibles sous forme simplifiée (pour les selects) + * Rôles Hub par défaut en cas d'erreur */ - getAvailableRolesSimple(): Observable { - return this.getAvailableRoles().pipe( - map(response => response.roles.map(role => ({ - value: role.value, - label: role.label, - description: role.description, - allowedForCreation: role.allowedForCreation - }))) - ); + private getDefaultHubRoles(error?: any): Observable { + console.error('Error loading hub roles:', error); + const defaultRoles: AvailableRolesWithPermissions = { + roles: [ + UserRole.DCB_ADMIN, + UserRole.DCB_SUPPORT, + UserRole.DCB_PARTNER + ].map(role => ({ + value: role, + label: this.getRoleLabel(role), + description: this.getRoleDescription(role), + allowedForCreation: true, + userType: UserType.HUB, + permissions: this.getPermissionsForRole(role) + })) + }; + this.availableRoles$.next(defaultRoles); + return of(defaultRoles); + } + + /** + * Rôles Marchands par défaut en cas d'erreur + */ + private getDefaultMerchantRoles(error?: any): Observable { + console.error('Error loading merchant roles:', error); + const defaultRoles: AvailableRolesWithPermissions = { + roles: [ + UserRole.DCB_PARTNER_ADMIN, + UserRole.DCB_PARTNER_MANAGER, + UserRole.DCB_PARTNER_SUPPORT + ].map(role => ({ + value: role, + label: this.getRoleLabel(role), + description: this.getRoleDescription(role), + allowedForCreation: true, + userType: UserType.MERCHANT_PARTNER, + permissions: this.getPermissionsForRole(role) + })) + }; + this.availableRoles$.next(defaultRoles); + return of(defaultRoles); } /** @@ -154,11 +123,22 @@ export class RoleManagementService { 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): RolePermission { - const allRoles = Object.values(UserRole); + getPermissionsForRole(role: UserRole | null): RolePermission { + if (!role) { + return this.getDefaultPermissions(); + } + + const allRoles = this.getAllRoles(); const hubRoles = [UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER, UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; @@ -188,7 +168,7 @@ export class RoleManagementService { canAccessAdmin: false, canAccessSupport: true, canAccessPartner: true, - assignableRoles: hubRoles, + assignableRoles: hubRoles, }; case UserRole.DCB_PARTNER: @@ -198,10 +178,10 @@ export class RoleManagementService { canDeleteUsers: true, canManageRoles: true, canViewStats: true, - canManageMerchants: true, - canAccessAdmin: true, - canAccessSupport: true, - canAccessPartner: true, + canManageMerchants: false, + canAccessAdmin: false, + canAccessSupport: false, + canAccessPartner: false, assignableRoles: merchantRoles }; @@ -209,20 +189,20 @@ export class RoleManagementService { return { canCreateUsers: true, canEditUsers: true, - canDeleteUsers: false, + canDeleteUsers: true, canManageRoles: true, canViewStats: true, canManageMerchants: true, canAccessAdmin: false, canAccessSupport: false, canAccessPartner: true, - assignableRoles: [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT] + assignableRoles: [UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT] }; case UserRole.DCB_PARTNER_MANAGER: return { - canCreateUsers: true, - canEditUsers: true, + canCreateUsers: false, + canEditUsers: false, canDeleteUsers: false, canManageRoles: false, canViewStats: true, @@ -243,34 +223,47 @@ export class RoleManagementService { canManageMerchants: false, canAccessAdmin: false, canAccessSupport: false, - canAccessPartner: false, + canAccessPartner: true, assignableRoles: [] }; default: - return { - canCreateUsers: false, - canEditUsers: false, - canDeleteUsers: false, - canManageRoles: false, - canViewStats: true, - canManageMerchants: true, - canAccessAdmin: false, - canAccessSupport: false, - canAccessPartner: false, - assignableRoles: [] - }; + return this.getDefaultPermissions(); } } + /** + * Permissions par défaut (rôle inconnu ou non défini) + */ + private getDefaultPermissions(): RolePermission { + return { + canCreateUsers: false, + canEditUsers: false, + canDeleteUsers: false, + canManageRoles: false, + canViewStats: false, + canManageMerchants: false, + canAccessAdmin: false, + canAccessSupport: false, + canAccessPartner: false, + assignableRoles: [] + }; + } + /** * Vérifie si un rôle peut être attribué par l'utilisateur courant */ canAssignRole(currentUserRole: UserRole | null, targetRole: UserRole): boolean { if (!currentUserRole) return false; - // SEUL DCB_PARTNER peut attribuer tous les rôles - if (currentUserRole === UserRole.DCB_PARTNER, currentUserRole === UserRole.DCB_ADMIN, currentUserRole === UserRole.DCB_SUPPORT) { + // Rôles qui peuvent attribuer tous les rôles + const fullPermissionRoles = [ + UserRole.DCB_ADMIN, + UserRole.DCB_SUPPORT, + UserRole.DCB_PARTNER + ]; + + if (fullPermissionRoles.includes(currentUserRole)) { return true; } @@ -345,35 +338,60 @@ export class RoleManagementService { /** * Récupère le libellé d'un rôle */ - getRoleLabel(role: UserRole): string { - const roleLabels: { [key in UserRole]: string } = { - [UserRole.DCB_ADMIN]: 'Administrateur DCB', - [UserRole.DCB_SUPPORT]: 'Support DCB', - [UserRole.DCB_PARTNER]: 'Partenaire DCB', - [UserRole.DCB_PARTNER_ADMIN]: 'Admin Partenaire', - [UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire', - [UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire' + getRoleLabel(role: string): string { + const userRole = role as UserRole; + switch (userRole) { + case UserRole.DCB_ADMIN: + return 'Administrateur DCB'; + case UserRole.DCB_SUPPORT: + return 'Support DCB'; + case UserRole.DCB_PARTNER: + return 'Partenaire DCB'; + case UserRole.DCB_PARTNER_ADMIN: + return 'Admin Partenaire'; + case UserRole.DCB_PARTNER_MANAGER: + return 'Manager Partenaire'; + case UserRole.DCB_PARTNER_SUPPORT: + return 'Support Partenaire'; + default: + return role; + } + } + + /** + * Récupère la description d'un rôle + */ + getRoleDescription(role: string | UserRole): string { + const userRole = role as UserRole; + const roleDescriptions: { [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]: 'Partenaire commercial principal', + [UserRole.DCB_PARTNER_ADMIN]: 'Administrateur de partenaire marchand', + [UserRole.DCB_PARTNER_MANAGER]: 'Manager opérationnel partenaire', + [UserRole.DCB_PARTNER_SUPPORT]: 'Support technique partenaire' }; - return roleLabels[role] || 'Rôle inconnu'; + return roleDescriptions[userRole] || 'Description non disponible'; } /** * Récupère la classe CSS pour un badge de rôle */ - getRoleBadgeClass(role: UserRole): string { - switch (role) { + getRoleBadgeClass(role: string): string { + const userRole = role as UserRole; + switch (userRole) { case UserRole.DCB_ADMIN: return 'bg-danger'; case UserRole.DCB_SUPPORT: return 'bg-info'; case UserRole.DCB_PARTNER: + return 'bg-primary'; + case UserRole.DCB_PARTNER_ADMIN: + return 'bg-warning'; + case UserRole.DCB_PARTNER_MANAGER: return 'bg-success'; - case UserRole.DCB_PARTNER_ADMIN: - return 'bg-danger'; - case UserRole.DCB_PARTNER_MANAGER: - return 'bg-warning text-dark'; - case UserRole.DCB_PARTNER_SUPPORT: - return 'bg-info text-white'; + case UserRole.DCB_PARTNER_SUPPORT: + return 'bg-secondary'; default: return 'bg-secondary'; } @@ -382,8 +400,9 @@ export class RoleManagementService { /** * Récupère l'icône pour un rôle */ - getRoleIcon(role: UserRole): string { - switch (role) { + getRoleIcon(role: string): string { + const userRole = role as UserRole; + switch (userRole) { case UserRole.DCB_ADMIN: return 'lucideShield'; case UserRole.DCB_SUPPORT: @@ -391,7 +410,7 @@ export class RoleManagementService { case UserRole.DCB_PARTNER: return 'lucideBuilding'; case UserRole.DCB_PARTNER_ADMIN: - return 'lucideShield'; + return 'lucideShieldCheck'; case UserRole.DCB_PARTNER_MANAGER: return 'lucideUserCog'; case UserRole.DCB_PARTNER_SUPPORT: @@ -401,6 +420,7 @@ export class RoleManagementService { } } + /** * Vérifie si un rôle est un rôle administrateur */ @@ -426,7 +446,11 @@ export class RoleManagementService { * Vérifie si un rôle est un rôle marchand */ isMerchantRole(role: UserRole): boolean { - const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; + const merchantRoles = [ + UserRole.DCB_PARTNER_ADMIN, + UserRole.DCB_PARTNER_MANAGER, + UserRole.DCB_PARTNER_SUPPORT + ]; return merchantRoles.includes(role); } @@ -442,23 +466,71 @@ export class RoleManagementService { */ getAssignableRoles(currentUserRole: UserRole | null): UserRole[] { if (!currentUserRole) return []; - - const permissions = this.getPermissionsForRole(currentUserRole); - return permissions.assignableRoles; + return this.getPermissionsForRole(currentUserRole).assignableRoles; } /** - * Récupère uniquement les rôles Hub (DCB_ADMIN, DCB_SUPPORT, DCB_PARTNER) + * Récupère les rôles Hub assignables (pour le composant unifié) + */ + getAssignableHubRoles(currentUserRole: UserRole | null): UserRole[] { + if (!currentUserRole) return []; + + const allHubRoles = this.getHubRoles(); + const permissions = this.getPermissionsForRole(currentUserRole); + + return allHubRoles.filter(role => + permissions.assignableRoles.includes(role) + ); + } + + /** + * Récupère les rôles Marchands assignables (pour le composant unifié) + */ + getAssignableMerchantRoles(currentUserRole: UserRole | null): UserRole[] { + if (!currentUserRole) return []; + + const allMerchantRoles = this.getMerchantRoles(); + const permissions = this.getPermissionsForRole(currentUserRole); + + return allMerchantRoles.filter(role => + permissions.assignableRoles.includes(role) + ); + } + + /** + * Récupère uniquement les rôles Hub */ getHubRoles(): UserRole[] { - return [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]; + return [ + UserRole.DCB_ADMIN, + UserRole.DCB_SUPPORT, + UserRole.DCB_PARTNER + ]; } /** * Récupère uniquement les rôles Marchands */ getMerchantRoles(): UserRole[] { - return [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; + return [ + UserRole.DCB_PARTNER_ADMIN, + UserRole.DCB_PARTNER_MANAGER, + UserRole.DCB_PARTNER_SUPPORT + ]; + } + + /** + * Vérifie si l'utilisateur a un rôle spécifique + */ + hasRole(userRole: UserRole | null, targetRole: UserRole): boolean { + return userRole === targetRole; + } + + /** + * Vérifie si l'utilisateur a au moins un des rôles spécifiés + */ + hasAnyRole(userRole: UserRole | null, targetRoles: UserRole[]): boolean { + return userRole ? targetRoles.includes(userRole) : false; } /** @@ -466,5 +538,13 @@ export class RoleManagementService { */ clearCache(): void { this.availableRoles$.next(null); + this.currentUserRole$.next(null); + } + + /** + * Récupère les rôles disponibles (observable) + */ + getAvailableRoles(): Observable { + return this.availableRoles$.asObservable(); } } \ 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 745285b..5b2ba5d 100644 --- a/src/app/core/services/menu.service.ts +++ b/src/app/core/services/menu.service.ts @@ -76,11 +76,6 @@ export class MenuService { icon: 'lucideCreditCard', url: '/transactions', }, - { - label: 'Gestions Merchants/Partenaires', - icon: 'lucideStore', - url: '/merchant-partners' - }, { label: 'Opérateurs', icon: 'lucideServer', @@ -104,12 +99,18 @@ export class MenuService { { label: 'Utilisateurs & Sécurité', isTitle: true }, { - label: 'Gestion des Utilisateurs', + label: 'Utilisateurs Hub', icon: 'lucideUsers', - url: '/users', + url: '/hub-users-management', + }, + { + label: 'Utilisateurs Merchants/Partenaires', + icon: 'lucideStore', + url: '/merchant-users-management' }, - { label: 'Configuration', isTitle: true }, + { label: 'Configurations', isTitle: true }, + { label: 'Merchant Configs', icon: 'lucideStore', url: '/merchant-configs' }, { label: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' }, { label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' }, diff --git a/src/app/core/services/permissions.service.ts b/src/app/core/services/permissions.service.ts index 99e5e2b..3d41aca 100644 --- a/src/app/core/services/permissions.service.ts +++ b/src/app/core/services/permissions.service.ts @@ -23,8 +23,14 @@ export class PermissionsService { module: 'transactions', roles: this.allRoles, }, + // Users Admin et Support { - module: 'merchant-partners', + module: 'hub-users-management', + roles: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT], + }, + // Merchant Users + { + module: 'merchant-users-management', roles: this.allRoles, }, // Operators - Admin seulement @@ -46,16 +52,18 @@ export class PermissionsService { 'retry': [UserRole.DCB_ADMIN] } }, - // Users - Admin et Support - { - module: 'users', - roles: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT] - }, // Settings - Tout le monde { module: 'settings', roles: this.allRoles }, + + // Settings - Tout le monde + { + module: 'merchant-configs', + roles: this.allRoles + }, + // Integrations - Admin seulement { module: 'integrations', diff --git a/src/app/layouts/components/data.ts b/src/app/layouts/components/data.ts index 183e322..73cd7cd 100644 --- a/src/app/layouts/components/data.ts +++ b/src/app/layouts/components/data.ts @@ -59,11 +59,6 @@ export const menuItems: MenuItemType[] = [ icon: 'lucideCreditCard', url: '/transactions', }, - { - label: 'Gestions Merchants/Partners', - icon: 'lucideStore', - url: '/merchant-partners' - }, { label: 'Opérateurs', icon: 'lucideServer', @@ -104,18 +99,26 @@ export const menuItems: MenuItemType[] = [ // --------------------------- { label: 'Utilisateurs & Sécurité', isTitle: true }, { - label: 'Gestion des Utilisateurs', + label: 'Utilisateurs Hub', icon: 'lucideUsers', isCollapsed: true, children: [ - { label: 'Liste des Utilisateurs', url: '/users' }, + { label: 'Liste des Utilisateurs', url: '/hub-users-management' }, ], }, + { + label: 'Utilisateurs Merchants/Partners', + icon: 'lucideStore', + url: '/merchant-users-management' + }, + + // --------------------------- // Paramètres & Intégrations // --------------------------- - { label: 'Configuration', isTitle: true }, + { label: 'Configurations', isTitle: true }, + { label: 'Merchant Configs', icon: 'lucideStore', url: '/merchant-configs' }, { label: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' }, { label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' }, 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 109656c..ba11d2b 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 @@ -3,10 +3,16 @@ src="assets/images/users/user-2.jpg" class="rounded-circle me-2" width="36" + height="36" alt="user-image" + onerror="this.src='assets/images/users/user-default.jpg'" />
-
{{ user?.firstName }} - {{ user?.lastName }}
-
Administrateur
+
+ {{ getUserInitials() }} | {{ getDisplayName() }} +
+
+ {{ getUserRole(user) }} +
- + \ 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 cd9db9e..a20c8c9 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 @@ -1,7 +1,9 @@ -import { Component, inject, ChangeDetectorRef } from '@angular/core'; +import { Component, inject, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; 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'; @Component({ selector: 'app-user-profile', @@ -9,27 +11,87 @@ import { AuthService } from '@/app/core/services/auth.service'; imports: [NgbCollapseModule], templateUrl: './user-profile.component.html', }) -export class UserProfileComponent { +export class UserProfileComponent implements OnInit, OnDestroy { private authService = inject(AuthService); private cdr = inject(ChangeDetectorRef); + + private destroy$ = new Subject(); + + user: User | null = null; + isLoading = true; - user: any = null; - - constructor() { + ngOnInit(): void { this.loadUser(); - this.authService.onAuthState().subscribe(() => this.loadUser()); + + // Subscribe to auth state changes + this.authService.getAuthState() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (isAuthenticated) => { + if (isAuthenticated) { + this.loadUser(); + } else { + this.user = null; + this.isLoading = false; + } + } + }); } - loadUser() { - this.authService.getProfile().subscribe({ - next: profile => { - this.user = profile; - this.cdr.detectChanges(); - }, - error: () => { - this.user = null; - this.cdr.detectChanges(); - } - }); + loadUser(): void { + this.isLoading = true; + + this.authService.getProfile() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (profile) => { + this.user = profile; + this.isLoading = false; + }, + error: (error) => { + console.error('Failed to load user profile:', error); + this.user = null; + this.isLoading = false; + } + }); } -} + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + // Helper methods for template + getUserInitials(): string { + if (!this.user?.firstName || !this.user?.lastName) { + return 'UU'; // User Unknown + } + return `${this.user.firstName.charAt(0)}${this.user.lastName.charAt(0)}`.toUpperCase(); + } + + getDisplayName(): string { + if (!this.user) return 'Utilisateur'; + return `${this.user.firstName} ${this.user.lastName}`.trim() || this.user.username || 'Utilisateur'; + } + + // Get user role with proper mapping + getUserRole(user: User | null): string { + if (!user) return 'Utilisateur'; + + // Use role from profile or fallback to token roles + const role = user.role || this.authService.getCurrentUserRoles(); + + // Map role to display name + const roleDisplayNames: { [key in UserRole]: string } = { + [UserRole.DCB_ADMIN]: 'Administrateur', + [UserRole.DCB_SUPPORT]: 'Support Technique', + [UserRole.DCB_PARTNER]: 'Partenaire', + [UserRole.DCB_PARTNER_ADMIN]: 'Admin Partenaire', + [UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire', + [UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire', + }; + + const primaryRole = role; + return roleDisplayNames[primaryRole] || 'Utilisateur'; + } +} \ No newline at end of file diff --git a/src/app/modules/auth/unauthorized.ts b/src/app/modules/auth/unauthorized.ts index 957bd5b..44c4286 100644 --- a/src/app/modules/auth/unauthorized.ts +++ b/src/app/modules/auth/unauthorized.ts @@ -51,7 +51,7 @@ import { credits, currentYear } from '@/app/constants'
- Rôles requis : Administrateur, Gestionnaire ou Support + Rôles requis : Administrateur, Partenaire ou Support
diff --git a/src/app/modules/hub-users/list/list.html b/src/app/modules/hub-users-management/hub-users-list/hub-users-list.html similarity index 55% rename from src/app/modules/hub-users/list/list.html rename to src/app/modules/hub-users-management/hub-users-list/hub-users-list.html index 60ee7ef..b7b7e4b 100644 --- a/src/app/modules/hub-users/list/list.html +++ b/src/app/modules/hub-users-management/hub-users-list/hub-users-list.html @@ -1,12 +1,38 @@ - + Gérez les accès utilisateurs de votre plateforme DCB + > + + Gérez les accès utilisateurs de votre plateforme DCB
+ + @if (isAdminView) { +
+
+
+
+ +
+ Vue Administrative Globale : Vous visualisez tous les utilisateurs Hub et Merchant de la plateforme + @if (showStatistics) { +
+ + {{ getHubUsersCount() }} utilisateurs Hub • + {{ getMerchantUsersCount() }} utilisateurs Merchant • + {{ getTotalUsersCountAdmin() }} total + + } +
+
+
+
+
+ } +
@@ -19,53 +45,127 @@ [class.active]="roleFilter === 'all'" (click)="filterByRole('all')" > - Tous ({{ allUsers.length }}) - - - - + + + @if (isAdminView) { + + + + + + + } @else { + + + + + }
+ + + @if (isAdminView) { +
+ +
+ }
+
- @if (canCreateUsers) { + @if (showCreateButton) { } +
- +
-
+
@@ -73,44 +173,44 @@
+
- - - + +
+
-
+
- + @for (role of availableRoles; track role.value) { }
-
-
- - -
+ +
+
@@ -130,6 +230,7 @@
{{ error }}
+
} @@ -140,6 +241,14 @@ + + @if (showUserTypeColumn()) { + + } + + @if (showMerchantPartnerColumn()) { + + } - + @for (user of displayedUsers; track user.id) { + + @if (showUserTypeColumn()) { + + } + + @if (showMerchantPartnerColumn()) { + + } -
TypeMerchant Partner
Utilisateur @@ -152,12 +261,7 @@
-
- Rôle - -
-
Rôle Principal
Statut @@ -176,6 +280,33 @@
+ + {{ user.userType === UserType.HUB ? 'Hub' : 'Merchant' }} + + + @if (user.merchantPartnerId) { +
+
+ +
+
+ + {{ user.merchantPartnerId.substring(0, 8) }}... + +
+
+ } @else { + - + } +
@@ -229,7 +360,7 @@ } - @if (canDeleteUsers) { + @if (showDeleteButton) {
+
Aucun utilisateur trouvé

Aucun utilisateur ne correspond à vos critères de recherche.

- @if (canCreateUsers) { - 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 new file mode 100644 index 0000000..398f864 --- /dev/null +++ b/src/app/modules/hub-users-management/hub-users-list/hub-users-list.ts @@ -0,0 +1,597 @@ +import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, Input, OnDestroy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgIcon } from '@ng-icons/core'; +import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; +import { Observable, Subject, map, of } from 'rxjs'; +import { catchError, takeUntil } from 'rxjs/operators'; + +import { + PaginatedUserResponse, + User, + UserRole, + UserType, + UserUtils +} 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 { AuthService } from '@core/services/auth.service'; +import { UiCard } from '@app/components/ui-card'; + +@Component({ + selector: 'app-hub-users-list', + standalone: true, + imports: [ + CommonModule, + FormsModule, + NgIcon, + UiCard, + NgbPaginationModule + ], + templateUrl: './hub-users-list.html', +}) +export class HubUsersList implements OnInit, OnDestroy { + private authService = inject(AuthService); + private hubUsersService = inject(HubUsersService); + protected roleService = inject(RoleManagementService); + private cdRef = inject(ChangeDetectorRef); + private destroy$ = new Subject(); + + // Configuration + readonly UserRole = UserRole; + readonly UserType = UserType; + readonly UserUtils = UserUtils; + + // Inputs + @Input() canCreateUsers: boolean = false; + @Input() canDeleteUsers: boolean = false; + + // Outputs + + @Output() userSelected = new EventEmitter(); + @Output() openCreateUserModal = new EventEmitter(); + @Output() openResetPasswordModal = new EventEmitter(); + @Output() openDeleteUserModal = new EventEmitter(); + + // Données + allUsers: User[] = []; + filteredUsers: User[] = []; + displayedUsers: User[] = []; + + // États + loading = false; + error = ''; + + // Recherche et filtres + searchTerm = ''; + statusFilter: 'all' | 'enabled' | 'disabled' = 'all'; + emailVerifiedFilter: 'all' | 'verified' | 'not-verified' = 'all'; + roleFilter: UserRole | 'all' = 'all'; + contextFilter: 'all' | 'hub' | 'merchant' = 'all'; + + // Pagination + currentPage = 1; + itemsPerPage = 10; + totalItems = 0; + totalPages = 0; + + // Tri + sortField: keyof User = 'username'; + sortDirection: 'asc' | 'desc' = 'asc'; + + // Rôles disponibles pour le filtre + availableRoles: { value: UserRole | 'all'; label: string, description: string }[] = []; + + // Permissions + currentUserRole: UserRole | null = null; + canViewAllUsers = false; + + // Statistiques + statistics: any = null; + + // Getters pour la logique conditionnelle + get showCreateButton(): boolean { + return this.canCreateUsers; + } + + get showDeleteButton(): boolean { + return this.canDeleteUsers; + } + + ngOnInit() { + this.loadCurrentUserPermissions(); + this.initializeAvailableRoles(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + private loadCurrentUserPermissions() { + this.authService.getUserProfile() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (user) => { + this.currentUserRole = this.extractUserRole(user); + this.canViewAllUsers = this.canViewAllUsersCheck(this.currentUserRole); + + console.log('Hub User Context Loaded:', { + role: this.currentUserRole, + canViewAllUsers: this.canViewAllUsers + }); + + this.loadUsers(); + }, + error: (error) => { + console.error('Error loading current user permissions:', error); + this.fallbackPermissions(); + this.loadUsers(); + } + }); + } + + private extractUserRole(user: any): UserRole | null { + const userRoles = this.authService.getCurrentUserRoles(); + if (userRoles && userRoles.length > 0) { + return userRoles[0]; + } + return null; + } + + private canViewAllUsersCheck(role: UserRole | null): boolean { + if (!role) return false; + + const canViewAllRoles = [ + UserRole.DCB_ADMIN, + UserRole.DCB_SUPPORT + ]; + + return canViewAllRoles.includes(role); + } + + private fallbackPermissions(): void { + this.currentUserRole = this.authService.getCurrentUserRole(); + this.canViewAllUsers = this.canViewAllUsersCheck(this.currentUserRole); + } + + private initializeAvailableRoles() { + this.availableRoles = [ + { value: 'all', label: 'Tous les rôles', description: 'Tous les Roles' }, + { value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' }, + { value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' }, + { value: UserRole.DCB_PARTNER, label: 'DCB Partner', description: 'Partenaire commercial' }, + ]; + } + + loadUsers() { + this.loading = true; + this.error = ''; + + if (this.canViewAllUsers) { + // Vue admin : tous les utilisateurs (Hub + Merchant) + this.loadAllUsers(); + } else { + // Vue normale : utilisateurs Hub seulement + this.loadHubUsers(); + } + } + + private loadAllUsers() { + this.hubUsersService.getAllUsers() + .pipe( + takeUntil(this.destroy$), + catchError(error => { + console.error('Error loading all users:', error); + this.error = 'Erreur lors du chargement de tous les utilisateurs'; + return of(null); + }) + ) + .subscribe({ + next: (overview) => { + if (overview) { + // Combiner Hub + Merchant users + this.allUsers = [...overview.hubUsers, ...overview.merchantUsers]; + this.statistics = overview.statistics; + + console.log(`✅ Admin view: ${overview.hubUsers.length} hub + ${overview.merchantUsers.length} merchant users`); + this.applyFiltersAndPagination(); + } + this.loading = false; + this.cdRef.detectChanges(); + }, + error: () => { + this.loading = false; + this.allUsers = []; + this.filteredUsers = []; + this.displayedUsers = []; + this.cdRef.detectChanges(); + } + }); + } + + private loadHubUsers() { + this.hubUsersService.getHubUsers() + .pipe( + map((response: PaginatedUserResponse) => response.users), + takeUntil(this.destroy$), + catchError(error => { + console.error('Error loading hub users:', error); + this.error = 'Erreur lors du chargement des utilisateurs Hub'; + return of([] as User[]); + }) + ) + .subscribe({ + next: (users) => { + this.allUsers = users || []; + console.log(`✅ Loaded ${this.allUsers.length} hub users`); + this.applyFiltersAndPagination(); + this.loading = false; + this.cdRef.detectChanges(); + }, + error: () => { + this.error = 'Erreur lors du chargement des utilisateurs Hub'; + this.loading = false; + this.allUsers = []; + this.filteredUsers = []; + this.displayedUsers = []; + this.cdRef.detectChanges(); + } + }); + } + + get isAdminView(): boolean { + return this.canViewAllUsers; + } + + get showStatistics(): boolean { + return this.isAdminView && this.statistics !== null; + } + + get viewDescription(): string { + if (this.isAdminView) { + return 'Vue administrative - Tous les utilisateurs (Hub + Merchant)'; + } else { + return 'Utilisateurs Hub DCB'; + } + } + + // Recherche et filtres + onSearch() { + this.currentPage = 1; + this.applyFiltersAndPagination(); + } + + onClearFilters() { + this.searchTerm = ''; + this.statusFilter = 'all'; + this.emailVerifiedFilter = 'all'; + this.roleFilter = 'all'; + this.contextFilter = 'all'; + this.currentPage = 1; + this.applyFiltersAndPagination(); + } + + applyFiltersAndPagination() { + if (!this.allUsers) { + this.allUsers = []; + } + + // Appliquer les filtres + this.filteredUsers = this.allUsers.filter(user => { + const matchesSearch = !this.searchTerm || + user.username.toLowerCase().includes(this.searchTerm.toLowerCase()) || + user.email.toLowerCase().includes(this.searchTerm.toLowerCase()) || + (user.firstName && user.firstName.toLowerCase().includes(this.searchTerm.toLowerCase())) || + (user.lastName && user.lastName.toLowerCase().includes(this.searchTerm.toLowerCase())); + + const matchesStatus = this.statusFilter === 'all' || + (this.statusFilter === 'enabled' && user.enabled) || + (this.statusFilter === 'disabled' && !user.enabled); + + const matchesEmailVerified = this.emailVerifiedFilter === 'all' || + (this.emailVerifiedFilter === 'verified' && user.emailVerified) || + (this.emailVerifiedFilter === 'not-verified' && !user.emailVerified); + + const matchesRole = this.roleFilter === 'all' || + (user.role && user.role.includes(this.roleFilter)); + + // Filtre par contexte (seulement pour la vue admin) + const matchesContext = !this.isAdminView || + (this.contextFilter === 'all' || + (this.contextFilter === 'hub' && user.userType === UserType.HUB) || + (this.contextFilter === 'merchant' && user.userType === UserType.MERCHANT_PARTNER)); + + return matchesSearch && matchesStatus && matchesEmailVerified && matchesRole && matchesContext; + }); + + // Appliquer le tri + this.filteredUsers.sort((a, b) => { + const aValue = a[this.sortField]; + const bValue = b[this.sortField]; + + if (aValue === bValue) return 0; + + let comparison = 0; + if (typeof aValue === 'string' && typeof bValue === 'string') { + comparison = aValue.localeCompare(bValue); + } else if (typeof aValue === 'number' && typeof bValue === 'number') { + comparison = aValue - bValue; + } else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') { + comparison = (aValue === bValue) ? 0 : aValue ? -1 : 1; + } + + return this.sortDirection === 'asc' ? comparison : -comparison; + }); + + // Calculer la pagination + this.totalItems = this.filteredUsers.length; + this.totalPages = Math.ceil(this.totalItems / this.itemsPerPage); + + // Appliquer la pagination + const startIndex = (this.currentPage - 1) * this.itemsPerPage; + const endIndex = startIndex + this.itemsPerPage; + this.displayedUsers = this.filteredUsers.slice(startIndex, endIndex); + } + + // Tri + sort(field: keyof User) { + if (this.sortField === field) { + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + this.sortField = field; + this.sortDirection = 'asc'; + } + this.applyFiltersAndPagination(); + } + + getSortIcon(field: string): string { + if (this.sortField !== field) return 'lucideArrowUpDown'; + return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown'; + } + + // Pagination + onPageChange(page: number) { + this.currentPage = page; + this.applyFiltersAndPagination(); + } + + getStartIndex(): number { + return (this.currentPage - 1) * this.itemsPerPage + 1; + } + + getEndIndex(): number { + return Math.min(this.currentPage * this.itemsPerPage, this.totalItems); + } + + // Actions + viewUserProfile(userId: string) { + this.userSelected.emit(userId); + } + + resetPasswordRequested(user: User) { + this.openResetPasswordModal.emit(user.id); + } + + deleteUserRequested(user: User) { + this.openDeleteUserModal.emit(user.id); + } + + enableUser(user: User) { + this.hubUsersService.enableHubUser(user.id) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (updatedUser) => { + const index = this.allUsers.findIndex(u => u.id === user.id); + if (index !== -1) { + this.allUsers[index] = updatedUser; + } + this.applyFiltersAndPagination(); + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('Error enabling hub user:', error); + this.error = 'Erreur lors de l\'activation de l\'utilisateur'; + this.cdRef.detectChanges(); + } + }); + } + + disableUser(user: User) { + this.hubUsersService.disableHubUser(user.id) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (updatedUser) => { + const index = this.allUsers.findIndex(u => u.id === user.id); + if (index !== -1) { + this.allUsers[index] = updatedUser; + } + this.applyFiltersAndPagination(); + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('Error disabling hub user:', error); + this.error = 'Erreur lors de la désactivation de l\'utilisateur'; + this.cdRef.detectChanges(); + } + }); + } + + // Utilitaires d'affichage + getStatusBadgeClass(user: User): string { + if (!user.enabled) return 'badge bg-danger'; + if (!user.emailVerified) return 'badge bg-warning'; + return 'badge bg-success'; + } + + getStatusText(user: User): string { + if (!user.enabled) return 'Désactivé'; + if (!user.emailVerified) return 'Email non vérifié'; + return 'Actif'; + } + + getRoleBadgeClass(role: string | UserRole): string { + return this.roleService.getRoleBadgeClass(role); + } + + getRoleLabel(role: string | UserRole): string { + return this.roleService.getRoleLabel(role); + } + + getRoleIcon(role: string | UserRole): string { + return this.roleService.getRoleIcon(role); + } + + getRoleDescription(role: string | UserRole): string { + const roleInfo = this.availableRoles.find(r => r.value === role); + return roleInfo?.description || 'Description non disponible'; + } + + formatTimestamp(timestamp: number): string { + if (!timestamp) return 'Non disponible'; + return new Date(timestamp).toLocaleDateString('fr-FR', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } + + getUserInitials(user: User): string { + return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U'; + } + + getUserDisplayName(user: User): string { + if (user.firstName && user.lastName) { + return `${user.firstName} ${user.lastName}`; + } + return user.username; + } + + getEnabledUsersCount(): number { + return this.allUsers.filter(user => user.enabled).length; + } + + getDisabledUsersCount(): number { + return this.allUsers.filter(user => !user.enabled).length; + } + + userHasRole(user: User, role: UserRole): boolean { + return UserUtils.hasRole(user, role); + } + + // Recherche rapide par rôle + filterByRole(role: UserRole | 'all') { + this.roleFilter = role; + this.currentPage = 1; + this.applyFiltersAndPagination(); + } + + // Recharger les données + refreshData() { + this.loadUsers(); + } + + // Méthodes pour le template + getCardTitle(): string { + return 'Liste des Utilisateurs Hub'; + } + + getHelperText(): string { + return 'Gérez les accès utilisateurs de votre plateforme DCB'; + } + + getHelperIcon(): string { + return 'lucideUsers'; + } + + getContextAlertClass(): string { + return this.isAdminView ? 'alert-warning' : 'alert-secondary'; + } + + getContextIcon(): string { + return this.isAdminView ? 'lucideShield' : 'lucideUsers'; + } + + getContextTitle(): string { + return this.isAdminView ? 'Vue Administrative Globale :' : 'Utilisateurs Hub DCB :'; + } + + getContextDescription(): string { + return this.isAdminView + ? 'Vous visualisez tous les utilisateurs Hub et Merchant de la plateforme' + : 'Gérez les accès utilisateurs de votre plateforme DCB'; + } + + showContextAlert(): boolean { + return true; + } + + // Méthode pour compter les utilisateurs par rôle + getUsersCountByRole(role: UserRole): number { + if (!this.allUsers || this.allUsers.length === 0) return 0; + + return this.allUsers.filter(user => + user.role && user.role.includes(role) + ).length; + } + + getLoadingText(): string { + return 'Chargement des utilisateurs...'; + } + + getEmptyStateTitle(): string { + return 'Aucun utilisateur trouvé'; + } + + getEmptyStateDescription(): string { + return 'Aucun utilisateur ne correspond à vos critères de recherche.'; + } + + getEmptyStateButtonText(): string { + return 'Créer le premier utilisateur'; + } + + getColumnCount(): number { + return this.isAdminView ? 7 : 6; + } + + showUserTypeColumn(): boolean { + return this.isAdminView; + } + + showMerchantPartnerColumn(): boolean { + return this.isAdminView; + } + + // Statistiques + getTotalUsersCount(): number { + return this.allUsers.length; + } + + getActiveUsersCount(): number { + return this.allUsers.filter(user => user.enabled).length; + } + + getVerifiedUsersCount(): number { + return this.allUsers.filter(user => user.emailVerified).length; + } + + // Méthodes spécifiques pour la vue admin + getHubUsersCount(): number { + if (!this.isAdminView || !this.statistics) return 0; + return this.statistics.totalHubUsers || 0; + } + + getMerchantUsersCount(): number { + if (!this.isAdminView || !this.statistics) return 0; + return this.statistics.totalMerchantUsers || 0; + } + + getTotalUsersCountAdmin(): number { + if (!this.isAdminView || !this.statistics) return 0; + return this.statistics.totalUsers || 0; + } +} \ No newline at end of file diff --git a/src/app/modules/hub-users/profile/profile.html b/src/app/modules/hub-users-management/hub-users-profile/hub-users-profile.html similarity index 80% rename from src/app/modules/hub-users/profile/profile.html rename to src/app/modules/hub-users-management/hub-users-profile/hub-users-profile.html index 09c1cd1..d362905 100644 --- a/src/app/modules/hub-users/profile/profile.html +++ b/src/app/modules/hub-users-management/hub-users-profile/hub-users-profile.html @@ -8,14 +8,14 @@ @if (user) { {{ getUserDisplayName() }} } @else { - Profil Utilisateur + Profil Utilisateur Hub }
} @@ -104,6 +105,7 @@
{{ success }}
+
} @@ -115,7 +117,7 @@
Chargement...
-

Chargement du profil...

+

Chargement du profil utilisateur Hub...

} @@ -139,8 +141,13 @@
{{ getUserDisplayName() }}

@{{ user.username }}

+ + + Utilisateur Hub + + - + {{ getStatusText() }} @@ -155,12 +162,12 @@
- Créé le {{ formatTimestamp(user.createdTimestamp) }} + Créé le {{ getCreationDate() }}
@if (user.lastLogin) {
- Dernière connexion : {{ formatTimestamp(user.lastLogin) }} + Dernière connexion : {{ getLastLoginDate() }}
} @@ -170,60 +177,68 @@
-
Rôle Utilisateur
- @if (canManageRoles && !isEditing) { +
+ Rôle Utilisateur +
+ @if (showRoleManagement()) { Modifiable }
- +
- - - {{ getRoleLabel(user.role) }} - - - {{ getRoleDescription(user.role) }} - + @if (getUserRole()) { +
+ + + {{ getRoleLabel(user.role) }} + +
+ + + + {{ getRoleDescription(user.role) }} + + } @else { + Aucun rôle + }
- @if (canManageRoles && !isEditing) { + @if (showRoleManagement()) {
- +
- @if (updatingRoles) { + @if (updatingRole) {
Mise à jour...
Mise à jour en cours... } @else { - Sélectionnez un nouveau rôle pour cet utilisateur + Sélectionnez un nouveau rôle principal pour cet utilisateur }
- } @else if (!canManageRoles) { + } @else if (!canManageRoles()) {
@@ -243,16 +258,16 @@
Créé par : -
{{ user.createdByUsername || 'Système' }}
+
{{ getCreatorName() }}
Date de création : -
{{ formatTimestamp(user.createdTimestamp) }}
+
{{ getCreationDate() }}
Type d'utilisateur :
- {{ user.userType }} + Utilisateur Hub
@@ -289,7 +304,7 @@ type="button" class="btn btn-success btn-sm" (click)="saveProfile()" - [disabled]="saving" + [disabled]="saving || !isFormValid()" > @if (saving) {
@@ -403,6 +418,24 @@
} + +
+ +
+
+ + + {{ getRoleLabel(user.role) }} + +
+
+
+ {{ getUserRoleDisplay() }} +
+
+ @if (!isEditing) {
@@ -421,19 +454,19 @@
- {{ formatTimestamp(user.createdTimestamp) }} + {{ getCreationDate() }}
- {{ user.createdByUsername || 'Système' }} + {{ getCreatorName() }}
- {{ user.userType }} + Utilisateur Hub
@@ -444,7 +477,7 @@
- @if (!isEditing && canEditUsers) { + @if (!isEditing && canEditUser()) {
Actions de Gestion
@@ -455,13 +488,14 @@
- @if (user.enabled) { + @if (user.enabled && canToggleStatus()) { - } @else { + } @else if (!user.enabled && canToggleStatus()) { + } +
+
+
+
+ } + +
-
+
- +
Le mot de passe doit contenir au moins 8 caractères.
@@ -229,56 +268,56 @@ } - +
- @if (canManageAllRoles) { - - - Vous pouvez attribuer tous les rôles - + @if (canManageRoles) { + Sélectionnez le rôle principal à assigner à cet utilisateur } @else { - - - Permissions de rôle limitées - + Vous ne pouvez pas modifier les rôles disponibles }
- @if (roleSelect.invalid && roleSelect.touched) { -
- Le rôle est requis -
- } +
+ + +
+ +
+ + Utilisateur Hub + +
+
+ Tous les utilisateurs créés ici sont des utilisateurs Hub +
- @if (!canManageAllRoles) { + @if (!canManageRoles) {
@@ -295,23 +334,22 @@
} - - @if (newMerchantUser.role) { + @if (newUser.role) {
Rôle sélectionné : - - {{ getRoleDisplayName(newMerchantUser.role) }} + + {{ getRoleLabel(newUser.role) }}
- {{ getRoleDescription(newMerchantUser.role) }} + {{ getRoleDescription(newUser.role) }}
@@ -326,7 +364,7 @@ class="form-check-input" type="checkbox" id="enabledSwitch" - [(ngModel)]="newMerchantUser.enabled" + [(ngModel)]="newUser.enabled" name="enabled" [disabled]="creatingUser" checked @@ -344,7 +382,7 @@ class="form-check-input" type="checkbox" id="emailVerifiedSwitch" - [(ngModel)]="newMerchantUser.emailVerified" + [(ngModel)]="newUser.emailVerified" name="emailVerified" [disabled]="creatingUser" > @@ -360,8 +398,7 @@
Informations système :
- • Merchant Partner ID : {{ currentMerchantPartnerId || 'Chargement...' }}
- • Type d'utilisateur : MERCHANT
+ • Type d'utilisateur : HUB
• Créé par : Utilisateur courant
• Votre rôle : {{ currentUserRole || 'Non défini' }}
@@ -382,7 +419,7 @@
@@ -447,9 +484,9 @@
- {{ getRoleDisplayName(selectedUserForReset.role) }} + {{ getRoleLabel(selectedUserForReset.role) }} - • Merchant Partner: {{ selectedUserForReset.merchantPartnerId }} + • Type: Hub
@@ -460,17 +497,27 @@ - +
+ + +
Le mot de passe doit contenir au moins 8 caractères.
@@ -573,9 +620,13 @@
-
Êtes-vous sûr de vouloir supprimer cet utilisateur ?
+
Êtes-vous sûr de vouloir supprimer cet utilisateur Hub ?

- Cette action est irréversible. Toutes les données de cet utilisateur marchand seront définitivement perdues. + Cette action est irréversible. Toutes les données de + @if (selectedUserForDelete) { + {{ selectedUserForDelete.username }} + } + seront définitivement perdues.

@@ -592,12 +643,17 @@
Email : {{ selectedUserForDelete.email }}
- Rôle : + Rôle Principal : - {{ getRoleDisplayName(selectedUserForDelete.role) }} + {{ getRoleLabel(selectedUserForDelete.role) }} + @if (selectedUserForDelete.role && selectedUserForDelete.role.length > 1) { +
+ Rôles supplémentaires : + {{ selectedUserForDelete.role.length - 1 }} + }
- Merchant Partner : {{ selectedUserForDelete.merchantPartnerId }} + Type : Utilisateur Hub
@@ -631,7 +687,7 @@ type="button" class="btn btn-danger" (click)="confirmDeleteUser()" - [disabled]="deletingUser || !selectedUserForDelete" + [disabled]="deletingUser || !selectedUserForDelete || !canDeleteUsers" > @if (deletingUser) {
diff --git a/src/app/modules/hub-users-management/hub-users.service.ts b/src/app/modules/hub-users-management/hub-users.service.ts new file mode 100644 index 0000000..2f8f0f4 --- /dev/null +++ b/src/app/modules/hub-users-management/hub-users.service.ts @@ -0,0 +1,390 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { environment } from '@environments/environment'; +import { Observable, map, catchError, throwError, of } from 'rxjs'; + +import { + User, + CreateUserDto, + UpdateUserDto, + ResetPasswordDto, + PaginatedUserResponse, + AvailableRolesResponse, + SearchUsersParams, + UserRole, + UserType, + UserUtils, + GlobalUsersOverview, + UsersStatistics +} from '@core/models/dcb-bo-hub-user.model'; + +// Interfaces pour les nouvelles réponses +export interface TokenResponse { + access_token: string; + expires_in: number; + refresh_token: string; + refresh_expires_in: number; + token_type: string; + 'not-before-policy': number; + session_state: string; + scope: string; +} + +export interface UserProfileResponse { + id: string; + username: string; + email: string; + firstName: string; + lastName: string; + emailVerified: boolean; + enabled: boolean; + role: string[]; + merchantPartnerId?: string; + createdBy?: string; + createdByUsername?: string; +} + +export interface MessageResponse { + message: string; +} + +export interface MerchantPartnerIdResponse { + merchantPartnerId: string | null; +} + +@Injectable({ providedIn: 'root' }) +export class HubUsersService { + private http = inject(HttpClient); + private baseApiUrl = `${environment.iamApiUrl}/hub-users`; + + // === MÉTHODES SPÉCIFIQUES HUB === + + getAllUsers(): Observable { + return this.http.get(`${this.baseApiUrl}/all-users`).pipe( + map(response => { + // Validation de la réponse + if (!response || !Array.isArray(response.hubUsers) || !Array.isArray(response.merchantUsers)) { + throw new Error('Invalid response format from API'); + } + + return { + ...response, + hubUsers: response.hubUsers.map(user => this.mapToUserModel(user, UserType.HUB)), + merchantUsers: response.merchantUsers.map(user => this.mapToUserModel(user, UserType.MERCHANT_PARTNER)) + }; + }), + catchError(error => { + console.error('Error loading global users overview:'); + console.error('Status:', error.status); + console.error('Message:', error.message); + + if (error.status === 403) { + console.error('Access forbidden - user may not have admin rights'); + } + + return throwError(() => error); + }) + ); + } + + // Méthode pour les statistiques seulement + getUsersStatistics(): Observable { + return this.http.get(`${this.baseApiUrl}`).pipe( + map(response => response.statistics), + catchError(error => { + console.error('Error loading users statistics:', error); + return throwError(() => error); + }) + ); + } + + + createHubUser(createUserDto: CreateUserDto): Observable { + // Utiliser la validation centralisée + const errors = UserUtils.validateUserCreation(createUserDto); + if (errors.length > 0) { + return throwError(() => errors.join(', ')); + } + + if (!createUserDto.username?.trim()) { + return throwError(() => 'Username is required and cannot be empty'); + } + + if (!createUserDto.email?.trim()) { + return throwError(() => 'Email is required and cannot be empty'); + } + + if (!createUserDto.password || createUserDto.password.length < 8) { + return throwError(() => 'Password must be at least 8 characters'); + } + + // Avant de créer le payload, valider les données + if (createUserDto.userType === UserType.MERCHANT_PARTNER && !createUserDto.merchantPartnerId) { + return throwError(() => 'merchantPartnerId is required for merchant users'); + } + + const payload = { + username: createUserDto.username.trim(), + email: createUserDto.email.trim().toLowerCase(), // Normaliser l'email + firstName: createUserDto.firstName?.trim() || '', + lastName: createUserDto.lastName?.trim() || '', + password: createUserDto.password, + role: createUserDto.role, + enabled: createUserDto.enabled ?? true, + emailVerified: createUserDto.emailVerified ?? true, + merchantPartnerId: createUserDto.merchantPartnerId, + userType: createUserDto.userType.trim() + }; + + // Validation supplémentaire + if (payload.userType === UserType.HUB && payload.merchantPartnerId) { + return throwError(() => 'merchantPartnerId should not be provided for hub users'); + } + + console.log(payload) + + return this.http.post(`${this.baseApiUrl}`, payload).pipe( + map(user => this.mapToUserModel(user, UserType.HUB)), + catchError(error => { + console.error('Error creating hub user:', error); + return throwError(() => error); + }) + ); + } + + getHubUsers(page: number = 1, limit: number = 10, filters?: SearchUsersParams): Observable { + return this.http.get(`${this.baseApiUrl}`).pipe( + map(users => { + const mappedUsers = users.map(user => this.mapToUserModel(user, UserType.HUB)); + return this.filterAndPaginateUsers(mappedUsers, page, limit, filters); + }), + catchError(error => { + console.error('Error loading hub users:', error); + return throwError(() => error); + }) + ); + } + + getAllDcbPartners(page: number = 1, limit: number = 10, filters?: SearchUsersParams): Observable { + return this.http.get(`${this.baseApiUrl}/partners/dcb-partners`).pipe( + map(users => { + const mappedUsers = users.map(user => this.mapToUserModel(user, UserType.HUB)); + return this.filterAndPaginateUsers(mappedUsers, page, limit, filters); + }), + catchError(error => { + console.error('Error loading merchant hub users:', error); + return throwError(() => error); + }) + ); + } + + getHubUserById(id: string): Observable { + return this.http.get(`${this.baseApiUrl}/${id}`).pipe( + map(user => this.mapToUserModel(user, UserType.HUB)), + catchError(error => { + console.error(`Error loading hub user ${id}:`, error); + return throwError(() => error); + }) + ); + } + + updateHubUser(id: string, updateUserDto: UpdateUserDto): Observable { + const payload: any = { + firstName: updateUserDto.firstName, + lastName: updateUserDto.lastName, + email: updateUserDto.email, + enabled: updateUserDto.enabled + }; + + return this.http.put(`${this.baseApiUrl}/${id}`, payload).pipe( + map(user => this.mapToUserModel(user, UserType.HUB)), + catchError(error => { + console.error(`Error updating hub user ${id}:`, error); + return throwError(() => error); + }) + ); + } + + updateHubUserRole(id: string, role: UserRole): Observable { + const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]; + if (!hubRoles.includes(role)) { + return throwError(() => 'Invalid role for Hub user'); + } + + return this.http.put(`${this.baseApiUrl}/${id}/role`, { role }).pipe( + map(user => this.mapToUserModel(user, UserType.HUB)), + catchError(error => { + console.error(`Error updating role for hub user ${id}:`, error); + return throwError(() => error); + }) + ); + } + + deleteHubUser(id: string): Observable { + return this.http.delete(`${this.baseApiUrl}/${id}`).pipe( + catchError(error => { + console.error(`Error deleting hub user ${id}:`, error); + return throwError(() => error); + }) + ); + } + + resetHubUserPassword(id: string, resetPasswordDto: ResetPasswordDto): Observable { + const payload = { + newPassword: resetPasswordDto.newPassword, + temporary: resetPasswordDto.temporary !== undefined ? resetPasswordDto.temporary : true + }; + + return this.http.post( + `${this.baseApiUrl}/${id}/reset-password`, + payload + ).pipe( + catchError(error => { + console.error(`Error resetting password for hub user ${id}:`, error); + return throwError(() => error); + }) + ); + } + + enableHubUser(id: string): Observable { + return this.updateHubUser(id, { enabled: true }); + } + + disableHubUser(id: string): Observable { + return this.updateHubUser(id, { enabled: false }); + } + + getAvailableHubRoles(): Observable { + return of({ + roles: [ + { + value: UserRole.DCB_ADMIN, + label: 'DCB Admin', + description: 'Full administrative access to the entire system', + allowedForCreation: true, + userType: UserType.HUB + }, + { + value: UserRole.DCB_SUPPORT, + label: 'DCB Support', + description: 'Support access with limited administrative capabilities', + allowedForCreation: true, + userType: UserType.HUB + }, + { + value: UserRole.DCB_PARTNER, + label: 'DCB Partner', + description: 'Partner access to merchant management', + allowedForCreation: true, + userType: UserType.HUB + } + ] + } as AvailableRolesResponse); + } + + getHubUsersByRole(role: UserRole): Observable { + return this.http.get(`${this.baseApiUrl}/role/${role}`).pipe( + map(users => users.map(user => this.mapToUserModel(user, UserType.HUB))), + catchError(error => { + console.error(`Error loading hub users with role ${role}:`, error); + return throwError(() => error); + }) + ); + } + + searchHubUsers(params: SearchUsersParams): Observable { + return this.getHubUsers(1, 1000, params).pipe( + map(response => response.users) + ); + } + + hubUserExists(username: string): Observable<{ exists: boolean }> { + return this.searchHubUsers({ query: username }).pipe( + map(users => ({ + exists: users.some(user => user.username === username) + })), + catchError(error => { + console.error('Error checking if hub user exists:', error); + return of({ exists: false }); + }) + ); + } + + // === MÉTHODES UTILITAIRES === + + isValidRoleForHub(role: UserRole): boolean { + const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_PARTNER, UserRole.DCB_SUPPORT]; + return hubRoles.includes(role); + } + + // === MAPPING ET FILTRAGE === + + private mapToUserModel(apiUser: any, userType: UserType): User { + return { + id: apiUser.id, + username: apiUser.username, + email: apiUser.email, + firstName: apiUser.firstName, + lastName: apiUser.lastName, + enabled: apiUser.enabled, + emailVerified: apiUser.emailVerified, + userType: userType, + merchantPartnerId: apiUser.merchantPartnerId, + role: apiUser.role, + createdBy: apiUser.createdBy, + createdByUsername: apiUser.createdByUsername, + createdTimestamp: apiUser.createdTimestamp, + lastLogin: apiUser.lastLogin + }; + } + + private filterAndPaginateUsers( + users: User[], + page: number, + limit: number, + filters?: SearchUsersParams + ): PaginatedUserResponse { + let filteredUsers = users; + + if (filters) { + if (filters.query) { + const query = filters.query.toLowerCase(); + filteredUsers = filteredUsers.filter(user => + user.username.toLowerCase().includes(query) || + user.email.toLowerCase().includes(query) || + user.firstName?.toLowerCase().includes(query) || + user.lastName?.toLowerCase().includes(query) + ); + } + + if (filters.role) { + filteredUsers = filteredUsers.filter(user => user.role.includes(filters.role!)); + } + + if (filters.enabled !== undefined) { + filteredUsers = filteredUsers.filter(user => user.enabled === filters.enabled); + } + + if (filters.userType) { + filteredUsers = filteredUsers.filter(user => user.userType === filters.userType); + } + + if (filters.merchantPartnerId) { + filteredUsers = filteredUsers.filter(user => user.merchantPartnerId === filters.merchantPartnerId); + } + } + + // Pagination côté client + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + const paginatedUsers = filteredUsers.slice(startIndex, endIndex); + + return { + users: paginatedUsers, + total: filteredUsers.length, + page, + limit, + totalPages: Math.ceil(filteredUsers.length / limit) + }; + } +} \ No newline at end of file diff --git a/src/app/modules/hub-users/hub-users.ts b/src/app/modules/hub-users-management/hub-users.ts similarity index 54% rename from src/app/modules/hub-users/hub-users.ts rename to src/app/modules/hub-users-management/hub-users.ts index f8e9004..e49ccd0 100644 --- a/src/app/modules/hub-users/hub-users.ts +++ b/src/app/modules/hub-users-management/hub-users.ts @@ -3,21 +3,19 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { NgIcon } from '@ng-icons/core'; import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { catchError, map, of, Subject, takeUntil } from 'rxjs'; -import { PageTitle } from '@app/components/page-title/page-title'; -import { HubUsersList } from './list/list'; -import { HubUserProfile } from './profile/profile'; -import { HubUsersService } from './services/hub-users.service'; -import { RoleManagementService } from '@core/services/role-management.service'; +import { Subject, takeUntil } from 'rxjs'; + +import { HubUsersService } from './hub-users.service'; +import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; import { AuthService } from '@core/services/auth.service'; +import { PageTitle } from '@app/components/page-title/page-title'; +import { HubUsersList } from './hub-users-list/hub-users-list'; +import { HubUserProfile } from './hub-users-profile/hub-users-profile'; import { - HubUserDto, - CreateUserDto, ResetPasswordDto, UserRole, - PaginatedUserResponse, - MerchantUserDto, + UserType } from '@core/models/dcb-bo-hub-user.model'; @Component({ @@ -29,21 +27,29 @@ import { NgIcon, NgbNavModule, NgbModalModule, - PageTitle, + PageTitle, HubUsersList, - HubUserProfile, + HubUserProfile ], templateUrl: './hub-users.html', }) -export class HubUsers implements OnInit, OnDestroy { +export class HubUsersManagement implements OnInit, OnDestroy { private modalService = inject(NgbModal); - private usersService = inject(HubUsersService); private authService = inject(AuthService); + private hubUsersService = inject(HubUsersService); + protected roleService = inject(RoleManagementService); private cdRef = inject(ChangeDetectorRef); private destroy$ = new Subject(); - - protected roleService = inject(RoleManagementService); + // Configuration + readonly UserRole = UserRole; + + // Propriétés de configuration + pageTitle: string = 'Gestion des Utilisateurs Hub'; + pageSubtitle: string = 'Administrez les utilisateurs de la plateforme DCB'; + badge: any = { icon: 'lucideUsers', text: 'Hub Users' }; + + // État de l'interface activeTab: 'list' | 'profile' = 'list'; selectedUserId: string | null = null; @@ -54,45 +60,56 @@ export class HubUsers implements OnInit, OnDestroy { canDeleteUsers = false; canManageRoles = false; - // Données pour la création d'utilisateur - newUser: CreateUserDto = { - username: '', - email: '', - firstName: '', - lastName: '', - password: '', - role: UserRole.DCB_SUPPORT, - enabled: true, - emailVerified: false - }; - - // Liste des partenaires marchands (à récupérer depuis votre API) - merchantPartners: any[] = []; - selectedMerchantPartnerId: string = ''; - - availableRoles: { value: UserRole; label: string; description: string }[] = []; - assignableRoles: UserRole[] = []; + // Formulaire de création + newUser: { + username: string; + email: string; + firstName: string; + lastName: string; + password: string; + role: UserRole; + enabled: boolean; + emailVerified: boolean; + userType: UserType + } = this.getDefaultUserForm(); + // États des opérations creatingUser = false; createUserError = ''; - - // Données pour la réinitialisation de mot de passe - selectedUserForReset: HubUserDto | null = null; - newPassword = ''; - temporaryPassword = false; resettingPassword = false; resetPasswordError = ''; resetPasswordSuccess = ''; - selectedUserForDelete: HubUserDto | null = null; + newPassword = ''; + temporaryPassword = false; + deletingUser = false; deleteUserError = ''; + selectedUserForReset: any = null; + selectedUserForDelete: any = null; + + // UX améliorations + showPassword = false; + showNewPassword = false; + + // Références aux templates de modals + @ViewChild('createUserModal') createUserModal!: TemplateRef; + @ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef; + @ViewChild('deleteUserModal') deleteUserModal!: TemplateRef; + + // Références aux composants enfants + @ViewChild(HubUsersList) hubUsersList!: HubUsersList; + + // Rôles disponibles + availableRoles: { value: UserRole; label: string; description: string }[] = []; + assignableRoles: UserRole[] = []; + ngOnInit() { this.activeTab = 'list'; - this.initializeUserPermissions(); + this.loadCurrentUserPermissions(); this.loadAvailableRoles(); - this.loadMerchantPartners(); + this.newUser.role = UserRole.DCB_SUPPORT; } ngOnDestroy(): void { @@ -103,13 +120,15 @@ export class HubUsers implements OnInit, OnDestroy { /** * Initialise les permissions de l'utilisateur courant */ - private initializeUserPermissions(): void { + private loadCurrentUserPermissions(): void { this.authService.getUserProfile() .pipe(takeUntil(this.destroy$)) .subscribe({ - next: (profile) => { - this.currentUserRole = profile?.role?.[0] as UserRole || null; - + next: (user) => { + this.currentUserRole = this.extractUserRole(user); + + console.log(`HUB User ROLE: ${this.currentUserRole}`); + if (this.currentUserRole) { this.roleService.setCurrentUserRole(this.currentUserRole); this.userPermissions = this.roleService.getPermissionsForRole(this.currentUserRole); @@ -118,19 +137,45 @@ export class HubUsers implements OnInit, OnDestroy { this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole); this.assignableRoles = this.roleService.getAssignableRoles(this.currentUserRole); + console.log('Assignable roles:', this.assignableRoles); } }, error: (error) => { console.error('Error loading user profile:', error); + this.fallbackPermissions(); } }); } + /** + * Méthode robuste pour extraire le rôle de l'utilisateur + */ + private extractUserRole(user: any): UserRole | null { + const userRoles = this.authService.getCurrentUserRoles(); + if (userRoles && userRoles.length > 0) { + return userRoles[0]; + } + return null; + } + + /** + * Fallback en cas d'erreur de chargement du profil + */ + private fallbackPermissions(): void { + this.currentUserRole = this.authService.getCurrentUserRole(); + + if (this.currentUserRole) { + this.canCreateUsers = this.roleService.canCreateUsers(this.currentUserRole); + this.canDeleteUsers = this.roleService.canDeleteUsers(this.currentUserRole); + this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole); + } + } + /** * Charge les rôles disponibles */ private loadAvailableRoles(): void { - this.usersService.getAvailableHubRoles() + this.hubUsersService.getAvailableHubRoles() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (response) => { @@ -139,47 +184,243 @@ export class HubUsers implements OnInit, OnDestroy { label: role.label, description: role.description })); + console.log('Available hub roles loaded:', this.availableRoles); }, error: (error) => { - console.error('Error loading available hub roles:', error); - this.availableRoles = [ - { value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' }, - { value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' }, - { value: UserRole.DCB_PARTNER, label: 'DCB Partner', description: 'Partenaire commercial' }, - { value: UserRole.DCB_PARTNER_ADMIN, label: 'Partner Admin', description: 'Administrateur partenaire' }, - { value: UserRole.DCB_PARTNER_MANAGER, label: 'Partner Manager', description: 'Manager partenaire' }, - { value: UserRole.DCB_PARTNER_SUPPORT, label: 'Partner Support', description: 'Support partenaire' } - ]; + console.error('Error loading available roles:', error); + this.availableRoles = this.getFallbackRoles(); } }); } /** - * Charge la liste des partenaires marchands + * Rôles par défaut en cas d'erreur */ - private loadMerchantPartners(): void { - this.usersService.findAllMerchantUsers().pipe( - map((response: PaginatedUserResponse) => { - return response.users as MerchantUserDto[]; - }), - catchError(error => { - console.error('❌ Error loading all merchant users:', error); - return of([]); - }) - ); - + private getFallbackRoles(): any[] { + return [ + { value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' }, + { value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' }, + { value: UserRole.DCB_PARTNER, label: 'DCB Partner', description: 'Partenaire commercial' } + ]; } - /** - * Vérifie si un rôle est un rôle marchand - */ - isMerchantRole(role: UserRole): boolean { - const merchantRoles = [ - UserRole.DCB_PARTNER_ADMIN, - UserRole.DCB_PARTNER_MANAGER, - UserRole.DCB_PARTNER_SUPPORT - ]; - return merchantRoles.includes(role); + private getDefaultUserForm() { + return { + username: '', + email: '', + firstName: '', + lastName: '', + password: '', + role: UserRole.DCB_SUPPORT, + enabled: true, + emailVerified: false, + userType: UserType.HUB + }; + } + + // ==================== MÉTHODES D'INTERFACE ==================== + + userProfiles: { [userId: string]: any } = {}; // Stocker les profils par userId + users: any[] = []; // Liste des utilisateurs + loadingProfiles: { [userId: string]: boolean } = {}; // État de chargement par user + + // Méthode pour changer d'onglet + showTab(tab: 'list' | 'profile', userId?: string) { + console.log(`Switching to tab: ${tab}`, userId ? `for user ${userId}` : ''); + this.activeTab = tab; + + if (userId) { + this.selectedUserId = userId; + // Charger le profil si pas déjà chargé + if (!this.userProfiles[userId]) { + this.loadUserProfile(userId); + } + } else { + this.selectedUserId = null; + } + } + + // Charger un profil spécifique + loadUserProfile(userId: string) { + if (this.loadingProfiles[userId]) return; // Éviter les doublons + + this.loadingProfiles[userId] = true; + + this.hubUsersService.getHubUserById(userId).subscribe({ + next: (profile) => { + this.userProfiles[userId] = profile; + this.loadingProfiles[userId] = false; + console.log(`Profile loaded for user ${userId}:`, profile); + }, + error: (error) => { + console.error(`Error loading profile for user ${userId}:`, error); + this.loadingProfiles[userId] = false; + } + }); + } + + // Getter pour le profil actuel + get currentProfile() { + return this.selectedUserId ? this.userProfiles[this.selectedUserId] : null; + } + + // Getter pour l'état de chargement + get isLoadingProfile() { + return this.selectedUserId ? this.loadingProfiles[this.selectedUserId] : false; + } + + backToList() { + console.log('🔙 Returning to list view'); + this.activeTab = 'list'; + this.selectedUserId = null; + } + + // Méthodes de gestion des événements du composant enfant + onUserSelected(userId: string) { + this.showTab('profile', userId); + } + + onResetPasswordRequested(event: any) { + const userId = typeof event === 'string' ? event : event.detail || event; + this.openResetPasswordModal(userId); + } + + onDeleteUserRequested(event: any) { + const userId = typeof event === 'string' ? event : event.detail || event; + this.openDeleteUserModal(userId); + } + + // ==================== GESTION DES MODALS ==================== + + openModal(content: TemplateRef, size: 'sm' | 'lg' | 'xl' = 'lg') { + this.modalService.open(content, { + size: size, + centered: true, + scrollable: true + }); + } + + // Méthode pour ouvrir le modal de création d'utilisateur + openCreateUserModal() { + if (!this.canCreateUsers) { + console.warn('User does not have permission to create users'); + return; + } + + this.resetUserForm(); + this.createUserError = ''; + this.openModal(this.createUserModal); + } + + private resetUserForm() { + this.newUser = { + username: '', + email: '', + firstName: '', + lastName: '', + password: '', + role: UserRole.DCB_SUPPORT, + enabled: true, + emailVerified: false, + userType: UserType.HUB + }; + console.log('🔄 Hub user form reset'); + } + + // Méthode pour ouvrir le modal de réinitialisation de mot de passe + openResetPasswordModal(userId: string) { + this.hubUsersService.getHubUserById(userId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (user) => { + this.selectedUserForReset = user; + this.newPassword = ''; + this.temporaryPassword = false; + this.resetPasswordError = ''; + this.resetPasswordSuccess = ''; + this.openModal(this.resetPasswordModal); + console.log('✅ Hub user loaded for password reset:', user.username); + }, + error: (error) => { + console.error('❌ Error loading hub user for password reset:', error); + this.resetPasswordError = 'Erreur lors du chargement de l\'utilisateur'; + this.cdRef.detectChanges(); + } + }); + } + + // Méthode pour ouvrir le modal de suppression + openDeleteUserModal(userId: string) { + if (!this.canDeleteUsers) { + console.warn('User does not have permission to delete users'); + return; + } + + console.log(`🗑️ Opening delete modal for hub user: ${userId}`); + this.hubUsersService.getHubUserById(userId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (user) => { + this.selectedUserForDelete = user; + this.deleteUserError = ''; + this.openModal(this.deleteUserModal); + console.log('✅ Hub user loaded for deletion:', user.username); + }, + error: (error) => { + console.error('❌ Error loading hub user for deletion:', error); + this.deleteUserError = 'Erreur lors du chargement de l\'utilisateur'; + this.cdRef.detectChanges(); + } + }); + } + + onRoleSelectionChange(selectedRole: UserRole) { + this.newUser.role = selectedRole; + } + + // ==================== OPÉRATIONS CRUD ==================== + + createUser() { + if (!this.canCreateUsers) { + this.createUserError = 'Vous n\'avez pas la permission de créer des utilisateurs'; + return; + } + + const validation = this.validateUserForm(); + if (!validation.isValid) { + this.createUserError = validation.error!; + console.error('❌ Form validation failed:', validation.error); + return; + } + + // Vérifier la permission pour attribuer le rôle sélectionné + if (!this.canAssignRole(this.newUser.role)) { + this.createUserError = `Vous n'avez pas la permission d'attribuer le rôle: ${this.getRoleLabel(this.newUser.role)}`; + return; + } + + this.creatingUser = true; + this.createUserError = ''; + + console.log('📤 Creating hub user with data:', this.newUser); + + this.hubUsersService.createHubUser(this.newUser) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (createdUser) => { + console.log('✅ Hub user created successfully:', createdUser); + this.creatingUser = false; + this.modalService.dismissAll(); + this.refreshUsersList(); + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('❌ Error creating hub user:', error); + this.creatingUser = false; + this.createUserError = this.getErrorMessage(error); + this.cdRef.detectChanges(); + } + }); } /** @@ -189,6 +430,92 @@ export class HubUsers implements OnInit, OnDestroy { return this.roleService.canAssignRole(this.currentUserRole, targetRole); } + // Réinitialiser le mot de passe + confirmResetPassword() { + if (!this.selectedUserForReset || !this.newPassword || this.newPassword.length < 8) { + this.resetPasswordError = 'Veuillez saisir un mot de passe valide (au moins 8 caractères).'; + console.error('❌ Password reset validation failed'); + return; + } + + console.log('🔑 Confirming password reset for hub user:', this.selectedUserForReset.username); + + this.resettingPassword = true; + this.resetPasswordError = ''; + this.resetPasswordSuccess = ''; + + const resetPasswordDto: ResetPasswordDto = { + newPassword: this.newPassword, + temporary: this.temporaryPassword + }; + + this.hubUsersService.resetHubUserPassword( + this.selectedUserForReset.id, + resetPasswordDto + ).pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (response) => { + console.log('✅ Hub user password reset successfully'); + this.resettingPassword = false; + this.resetPasswordSuccess = 'Mot de passe réinitialisé avec succès !'; + this.cdRef.detectChanges(); + + // Fermer le modal après 2 secondes + setTimeout(() => { + this.modalService.dismissAll(); + }, 2000); + }, + error: (error) => { + console.error('❌ Error resetting hub user password:', error); + this.resettingPassword = false; + this.resetPasswordError = this.getResetPasswordErrorMessage(error); + this.cdRef.detectChanges(); + } + }); + } + + confirmDeleteUser() { + if (!this.selectedUserForDelete || !this.canDeleteUsers) { + console.error('❌ No hub user selected for deletion or no permission'); + return; + } + + console.log('🗑️ Confirming hub user deletion:', this.selectedUserForDelete.username); + + this.deletingUser = true; + this.deleteUserError = ''; + + this.hubUsersService.deleteHubUser(this.selectedUserForDelete.id) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => { + console.log('✅ Hub user deleted successfully'); + this.deletingUser = false; + this.modalService.dismissAll(); + this.refreshUsersList(); + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('❌ Error deleting hub user:', error); + this.deletingUser = false; + this.deleteUserError = this.getDeleteErrorMessage(error); + this.cdRef.detectChanges(); + } + }); + } + + // ==================== MÉTHODES UTILITAIRES ==================== + + private refreshUsersList(): void { + if (this.hubUsersList && typeof this.hubUsersList.refreshData === 'function') { + console.log('🔄 Refreshing hub users list...'); + this.hubUsersList.refreshData(); + } else { + console.warn('❌ HubUsersList component not available for refresh'); + this.showTab('list'); + } + } + // Méthodes proxy pour le template getRoleBadgeClass(role: UserRole): string { return this.roleService.getRoleBadgeClass(role); @@ -207,215 +534,12 @@ export class HubUsers implements OnInit, OnDestroy { return roleInfo?.description || 'Description non disponible'; } - showTab(tab: 'list' | 'profile', userId?: string) { - this.activeTab = tab; - - if (userId) { - this.selectedUserId = userId; - } + getUserInitials(user: any): string { + return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U'; } - backToList() { - this.activeTab = 'list'; - this.selectedUserId = null; - } + // ==================== GESTION DES ERREURS ==================== - // Méthodes pour les modals - openModal(content: TemplateRef, size: 'sm' | 'lg' | 'xl' = 'lg') { - this.modalService.open(content, { - size: size, - centered: true, - scrollable: true - }); - } - - // Méthode pour ouvrir le modal de création d'utilisateur - openCreateUserModal() { - if (!this.canCreateUsers) { - console.warn('User does not have permission to create users'); - return; - } - - this.newUser = { - username: '', - email: '', - firstName: '', - lastName: '', - password: '', - role: this.assignableRoles[0] || UserRole.DCB_SUPPORT, - enabled: true, - emailVerified: false - }; - this.selectedMerchantPartnerId = ''; - this.createUserError = ''; - this.openModal(this.createUserModal); - } - - // Méthode pour ouvrir le modal de réinitialisation de mot de passe - openResetPasswordModal(userId: string) { - this.usersService.getHubUserById(userId) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (user) => { - this.selectedUserForReset = user; - this.newPassword = ''; - this.temporaryPassword = false; - this.resetPasswordError = ''; - this.resetPasswordSuccess = ''; - this.openModal(this.resetPasswordModal); - }, - error: (error) => { - console.error('Error loading user for password reset:', error); - this.resetPasswordError = 'Erreur lors du chargement de l\'utilisateur'; - } - }); - } - - // Création d'utilisateur - createUser() { - if (!this.canCreateUsers) { - this.createUserError = 'Vous n\'avez pas la permission de créer des utilisateurs'; - return; - } - - const validation = this.validateUserForm(); - if (!validation.isValid) { - this.createUserError = validation.error!; - return; - } - - // Vérifier que l'utilisateur peut attribuer ce rôle - if (!this.canAssignRole(this.newUser.role)) { - this.createUserError = 'Vous n\'avez pas la permission d\'attribuer ce rôle'; - return; - } - - // Vérifier merchantPartnerId pour les rôles marchands - if (this.isMerchantRole(this.newUser.role) && !this.selectedMerchantPartnerId) { - this.createUserError = 'Le partenaire marchand est requis pour les utilisateurs marchands'; - return; - } - - this.creatingUser = true; - this.createUserError = ''; - - // Préparer les données pour l'API - const userData: CreateUserDto = { - ...this.newUser - }; - - // Ajouter merchantPartnerId si c'est un rôle marchand - if (this.isMerchantRole(this.newUser.role)) { - userData.merchantPartnerId = this.selectedMerchantPartnerId; - } - - this.usersService.createHubUser(userData) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (createdUser) => { - this.creatingUser = false; - this.modalService.dismissAll(); - - if (this.usersListComponent) { - this.usersListComponent.refreshData(); - } - - this.showTab('list'); - this.cdRef.detectChanges(); - }, - error: (error) => { - this.creatingUser = false; - this.createUserError = this.getErrorMessage(error); - this.cdRef.detectChanges(); - } - }); - } - - // Réinitialiser le mot de passe - confirmResetPassword() { - if (!this.selectedUserForReset || !this.newPassword || this.newPassword.length < 8) { - this.resetPasswordError = 'Veuillez saisir un mot de passe valide (au moins 8 caractères).'; - return; - } - - this.resettingPassword = true; - this.resetPasswordError = ''; - this.resetPasswordSuccess = ''; - - const resetPasswordDto: ResetPasswordDto = { - newPassword: this.newPassword, - temporary: this.temporaryPassword - }; - - this.usersService.resetHubUserPassword( - this.selectedUserForReset.id, - resetPasswordDto - ) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: () => { - this.resettingPassword = false; - this.resetPasswordSuccess = 'Mot de passe réinitialisé avec succès !'; - this.cdRef.detectChanges(); - }, - error: (error) => { - this.resettingPassword = false; - this.resetPasswordError = this.getResetPasswordErrorMessage(error); - this.cdRef.detectChanges(); - } - }); - } - - // Méthode pour ouvrir le modal de suppression - openDeleteUserModal(userId: string) { - if (!this.canDeleteUsers) { - console.warn('User does not have permission to delete users'); - return; - } - - this.usersService.getHubUserById(userId) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (user) => { - this.selectedUserForDelete = user; - this.deleteUserError = ''; - this.openModal(this.deleteUserModal); - }, - error: (error) => { - console.error('Error loading user for deletion:', error); - this.deleteUserError = 'Erreur lors du chargement de l\'utilisateur'; - } - }); - } - - confirmDeleteUser() { - if (!this.selectedUserForDelete || !this.canDeleteUsers) return; - - this.deletingUser = true; - this.deleteUserError = ''; - - this.usersService.deleteHubUser(this.selectedUserForDelete.id) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: () => { - this.deletingUser = false; - this.modalService.dismissAll(); - - if (this.usersListComponent) { - this.usersListComponent.refreshData(); - } - - this.cdRef.detectChanges(); - }, - error: (error) => { - this.deletingUser = false; - this.deleteUserError = this.getDeleteErrorMessage(error); - this.cdRef.detectChanges(); - } - }); - } - - // Gestion des erreurs private getErrorMessage(error: any): string { if (error.error?.message) { return error.error.message; @@ -424,7 +548,7 @@ export class HubUsers implements OnInit, OnDestroy { return 'Données invalides. Vérifiez les champs du formulaire.'; } if (error.status === 409) { - return 'Un utilisateur avec ce nom ou email existe déjà.'; + return 'Un utilisateur avec ce nom d\'utilisateur ou email existe déjà.'; } if (error.status === 403) { return 'Vous n\'avez pas les permissions nécessaires pour cette action.'; @@ -458,10 +582,14 @@ export class HubUsers implements OnInit, OnDestroy { if (error.status === 403) { return 'Vous n\'avez pas les permissions pour supprimer cet utilisateur.'; } + if (error.status === 409) { + return 'Impossible de supprimer cet utilisateur car il est associé à des données.'; + } return 'Erreur lors de la suppression de l\'utilisateur. Veuillez réessayer.'; } - // Validation du formulaire + // ==================== VALIDATION DU FORMULAIRE ==================== + private validateUserForm(): { isValid: boolean; error?: string } { const requiredFields = [ { field: this.newUser.username?.trim(), name: 'Nom d\'utilisateur' }, @@ -477,8 +605,13 @@ export class HubUsers implements OnInit, OnDestroy { } // Validation email + const email = this.newUser.email?.trim(); + if (!email) { + return { isValid: false, error: 'Email est requis' }; + } + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(this.newUser.email)) { + if (!emailRegex.test(email)) { return { isValid: false, error: 'Format d\'email invalide' }; } @@ -492,11 +625,4 @@ export class HubUsers implements OnInit, OnDestroy { return { isValid: true }; } - - @ViewChild(HubUsersList) usersListComponent!: HubUsersList; - - // Références aux templates de modals - @ViewChild('createUserModal') createUserModal!: TemplateRef; - @ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef; - @ViewChild('deleteUserModal') deleteUserModal!: TemplateRef; } \ No newline at end of file diff --git a/src/app/modules/merchant-users/list/list.html b/src/app/modules/hub-users-management/merchant-users-list/merchant-users-list.html similarity index 77% rename from src/app/modules/merchant-users/list/list.html rename to src/app/modules/hub-users-management/merchant-users-list/merchant-users-list.html index 8020403..746f589 100644 --- a/src/app/modules/merchant-users/list/list.html +++ b/src/app/modules/hub-users-management/merchant-users-list/merchant-users-list.html @@ -1,45 +1,15 @@ - - - + - @if (canViewAllMerchants) { - - Vue administrative - Tous les utilisateurs marchands - } @else if (isDcbPartner) { - - Votre équipe marchande - } @else { - - Utilisateurs de votre partenaire marchand - } + + Gérez les accès Marchands de votre plateforme DCB
- - @if (canViewAllMerchants) { -
-
- -
- Vue administrative DCB : Vous visualisez tous les utilisateurs marchands de la plateforme -
-
-
- } @else if (isDcbPartner) { -
-
- -
- Vue partenaire marchand : Vous gérez les utilisateurs de votre propre équipe - Merchant Partner ID: {{ currentMerchantPartnerId }} -
-
-
- } - +
@@ -52,7 +22,7 @@ [class.active]="roleFilter === 'all'" (click)="filterByRole('all')" > - Tous ({{ allUsers.length }}) + Tous ({{ getTotalUsersCount() }})
+
- @if (!canViewAllMerchants) { + @if (showCreateButton && canCreateUsers) { } +
- +
-
+
@@ -106,44 +85,44 @@
+
- - - + +
+
-
+
- + @for (role of availableRoles; track role.value) { }
-
-
- - -
+ +
+
@@ -163,6 +142,7 @@
{{ error }}
+
} @@ -173,14 +153,9 @@ - - @if (canViewAllMerchants) { - + + @if (showMerchantPartnerColumn) { + } - + @for (user of displayedUsers; track user.id) { - - @if (canViewAllMerchants) { + + @if (showMerchantPartnerColumn) { -
-
- Merchant Partner - -
-
Merchant Partner
@@ -194,12 +169,7 @@
-
- Rôle - -
-
Rôle Principal
Statut @@ -218,8 +188,8 @@
@@ -262,7 +232,7 @@
- {{ getRoleDisplayName(user.role) }} + {{ getRoleLabel(user.role) }} @@ -286,7 +256,7 @@ } - @if (!canViewAllMerchants) { + @if (showDeleteButton) {
+
Aucun utilisateur marchand trouvé

Aucun utilisateur ne correspond à vos critères de recherche.

- @if (!canViewAllMerchants) { - 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 new file mode 100644 index 0000000..ba38553 --- /dev/null +++ b/src/app/modules/hub-users-management/merchant-users-list/merchant-users-list.ts @@ -0,0 +1,517 @@ +import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, Input, OnDestroy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgIcon } from '@ng-icons/core'; +import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; +import { Observable, Subject, map, of } from 'rxjs'; +import { catchError, takeUntil } from 'rxjs/operators'; + +import { + User, + PaginatedUserResponse, + UserRole, + UserType, + UserUtils +} 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 { AuthService } from '@core/services/auth.service'; +import { UiCard } from '@app/components/ui-card'; + +@Component({ + selector: 'app-merchant-users-list', + standalone: true, + imports: [ + CommonModule, + FormsModule, + NgIcon, + UiCard, + NgbPaginationModule + ], + templateUrl: './merchant-users-list.html', +}) +export class MerchantUsersList implements OnInit, OnDestroy { + private authService = inject(AuthService); + private merchantUsersService = inject(MerchantUsersService); + protected roleService = inject(RoleManagementService); + private cdRef = inject(ChangeDetectorRef); + private destroy$ = new Subject(); + + // Configuration + readonly UserRole = UserRole; + readonly UserType = UserType; + readonly UserUtils = UserUtils; + + // Inputs + @Input() canCreateUsers: boolean = false; + @Input() canDeleteUsers: boolean = false; + + // Outputs + + @Output() userSelected = new EventEmitter(); + @Output() openCreateUserModal = new EventEmitter(); + @Output() openResetPasswordModal = new EventEmitter(); + @Output() openDeleteUserModal = new EventEmitter(); + + // Données + allUsers: User[] = []; + filteredUsers: User[] = []; + displayedUsers: User[] = []; + + // États + loading = false; + error = ''; + + // Recherche et filtres + searchTerm = ''; + statusFilter: 'all' | 'enabled' | 'disabled' = 'all'; + emailVerifiedFilter: 'all' | 'verified' | 'not-verified' = 'all'; + roleFilter: UserRole | 'all' = 'all'; + + // Pagination + currentPage = 1; + itemsPerPage = 10; + totalItems = 0; + totalPages = 0; + + // Tri + sortField: keyof User = 'username'; + sortDirection: 'asc' | 'desc' = 'asc'; + + // Rôles disponibles pour le filtre + availableRoles: { value: UserRole | 'all'; label: string, description: string }[] = []; + + // ID du merchant partner courant et permissions + currentMerchantPartnerId: string = ''; + currentUserRole: UserRole | null = null; + canViewAllMerchants = false; + + // Getters pour la logique conditionnelle + get showMerchantPartnerColumn(): boolean { + return this.canViewAllMerchants; + } + + get showCreateButton(): boolean { + return this.canCreateUsers; + } + + get showDeleteButton(): boolean { + return this.canDeleteUsers; + } + + ngOnInit() { + this.loadCurrentUserPermissions(); + this.initializeAvailableRoles(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + private loadCurrentUserPermissions() { + this.authService.getUserProfile() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (user) => { + this.currentUserRole = this.extractUserRole(user); + this.currentMerchantPartnerId = this.extractMerchantPartnerId(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) => { + console.error('Error loading current user permissions:', error); + this.fallbackPermissions(); + this.loadUsers(); + } + }); + } + + private extractUserRole(user: any): UserRole | null { + const userRoles = this.authService.getCurrentUserRoles(); + if (userRoles && userRoles.length > 0) { + return userRoles[0]; + } + return null; + } + + private extractMerchantPartnerId(user: any): string { + if (user?.merchantPartnerId) { + return user.merchantPartnerId; + } + return this.authService.getCurrentMerchantPartnerId() || ''; + } + + private canViewAllMerchantsCheck(role: UserRole | null): boolean { + if (!role) return false; + + const canViewAllRoles = [ + UserRole.DCB_ADMIN, + UserRole.DCB_SUPPORT, + UserRole.DCB_PARTNER_ADMIN + ]; + + return canViewAllRoles.includes(role); + } + + private fallbackPermissions(): void { + this.currentUserRole = this.authService.getCurrentUserRole(); + this.currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId() || ''; + this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole); + } + + private initializeAvailableRoles() { + this.availableRoles = [ + { value: 'all', label: 'Tous les rôles', description: 'Tous les Roles' }, + { value: UserRole.DCB_PARTNER_ADMIN, label: 'DCB Partner Admin', description: 'Admin Partenaire commercial' }, + { value: UserRole.DCB_PARTNER_MANAGER, label: 'DCB Partner Manager', description: 'Manager Partenaire commercial' }, + { value: UserRole.DCB_PARTNER_SUPPORT, label: 'DCB Partner Support', description: 'Support Partenaire commercial' } + ]; + } + + loadUsers() { + this.loading = true; + this.error = ''; + + let usersObservable: Observable; + + if (this.canViewAllMerchants) { + // Admin/Support accédant au contexte Merchant + usersObservable = this.merchantUsersService.getMerchantUsers(this.currentPage, this.itemsPerPage).pipe( + map((response: PaginatedUserResponse) => response.users) + ); + } else if (this.currentMerchantPartnerId) { + // Merchant régulier voyant son équipe + usersObservable = this.merchantUsersService.getMerchantUsersByPartner(this.currentMerchantPartnerId); + } else { + // Fallback + usersObservable = this.merchantUsersService.getMyMerchantUsers(); + } + + usersObservable + .pipe( + takeUntil(this.destroy$), + catchError(error => { + console.error('Error loading merchant users:', error); + this.error = 'Erreur lors du chargement des utilisateurs marchands'; + return of([] as User[]); + }) + ) + .subscribe({ + next: (users) => { + this.allUsers = users || []; + console.log(`✅ Loaded ${this.allUsers.length} merchant users`); + this.applyFiltersAndPagination(); + this.loading = false; + this.cdRef.detectChanges(); + }, + error: () => { + this.error = 'Erreur lors du chargement des utilisateurs marchands'; + this.loading = false; + this.allUsers = []; + this.filteredUsers = []; + this.displayedUsers = []; + this.cdRef.detectChanges(); + } + }); + } + + // Recherche et filtres + onSearch() { + this.currentPage = 1; + this.applyFiltersAndPagination(); + } + + onClearFilters() { + this.searchTerm = ''; + this.statusFilter = 'all'; + this.emailVerifiedFilter = 'all'; + this.roleFilter = 'all'; + this.currentPage = 1; + this.applyFiltersAndPagination(); + } + + applyFiltersAndPagination() { + if (!this.allUsers) { + this.allUsers = []; + } + + // Appliquer les filtres + this.filteredUsers = this.allUsers.filter(user => { + const matchesSearch = !this.searchTerm || + user.username.toLowerCase().includes(this.searchTerm.toLowerCase()) || + user.email.toLowerCase().includes(this.searchTerm.toLowerCase()) || + (user.firstName && user.firstName.toLowerCase().includes(this.searchTerm.toLowerCase())) || + (user.lastName && user.lastName.toLowerCase().includes(this.searchTerm.toLowerCase())); + + const matchesStatus = this.statusFilter === 'all' || + (this.statusFilter === 'enabled' && user.enabled) || + (this.statusFilter === 'disabled' && !user.enabled); + + const matchesEmailVerified = this.emailVerifiedFilter === 'all' || + (this.emailVerifiedFilter === 'verified' && user.emailVerified) || + (this.emailVerifiedFilter === 'not-verified' && !user.emailVerified); + + const matchesRole = this.roleFilter === 'all' || + (user.role && user.role.includes(this.roleFilter)); + + return matchesSearch && matchesStatus && matchesEmailVerified && matchesRole; + }); + + // Appliquer le tri + this.filteredUsers.sort((a, b) => { + const aValue = a[this.sortField]; + const bValue = b[this.sortField]; + + if (aValue === bValue) return 0; + + let comparison = 0; + if (typeof aValue === 'string' && typeof bValue === 'string') { + comparison = aValue.localeCompare(bValue); + } else if (typeof aValue === 'number' && typeof bValue === 'number') { + comparison = aValue - bValue; + } else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') { + comparison = (aValue === bValue) ? 0 : aValue ? -1 : 1; + } + + return this.sortDirection === 'asc' ? comparison : -comparison; + }); + + // Calculer la pagination + this.totalItems = this.filteredUsers.length; + this.totalPages = Math.ceil(this.totalItems / this.itemsPerPage); + + // Appliquer la pagination + const startIndex = (this.currentPage - 1) * this.itemsPerPage; + const endIndex = startIndex + this.itemsPerPage; + this.displayedUsers = this.filteredUsers.slice(startIndex, endIndex); + } + + // Tri + sort(field: keyof User) { + if (this.sortField === field) { + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + this.sortField = field; + this.sortDirection = 'asc'; + } + this.applyFiltersAndPagination(); + } + + getSortIcon(field: string): string { + if (this.sortField !== field) return 'lucideArrowUpDown'; + return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown'; + } + + // Pagination + onPageChange(page: number) { + this.currentPage = page; + this.applyFiltersAndPagination(); + } + + getStartIndex(): number { + return (this.currentPage - 1) * this.itemsPerPage + 1; + } + + getEndIndex(): number { + return Math.min(this.currentPage * this.itemsPerPage, this.totalItems); + } + + // Actions + viewUserProfile(userId: string) { + this.userSelected.emit(userId); + } + + resetPasswordRequested(user: User) { + this.openResetPasswordModal.emit(user.id); + } + + deleteUserRequested(user: User) { + this.openDeleteUserModal.emit(user.id); + } + + enableUser(user: User) { + this.merchantUsersService.enableMerchantUser(user.id) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (updatedUser) => { + const index = this.allUsers.findIndex(u => u.id === user.id); + if (index !== -1) { + this.allUsers[index] = updatedUser; + } + this.applyFiltersAndPagination(); + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('Error enabling merchant user:', error); + this.error = 'Erreur lors de l\'activation de l\'utilisateur'; + this.cdRef.detectChanges(); + } + }); + } + + disableUser(user: User) { + this.merchantUsersService.disableMerchantUser(user.id) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (updatedUser) => { + const index = this.allUsers.findIndex(u => u.id === user.id); + if (index !== -1) { + this.allUsers[index] = updatedUser; + } + this.applyFiltersAndPagination(); + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('Error disabling merchant user:', error); + this.error = 'Erreur lors de la désactivation de l\'utilisateur'; + this.cdRef.detectChanges(); + } + }); + } + + // Utilitaires d'affichage + getStatusBadgeClass(user: User): string { + if (!user.enabled) return 'badge bg-danger'; + if (!user.emailVerified) return 'badge bg-warning'; + return 'badge bg-success'; + } + + getStatusText(user: User): string { + if (!user.enabled) return 'Désactivé'; + if (!user.emailVerified) return 'Email non vérifié'; + return 'Actif'; + } + + getRoleBadgeClass(role: string | UserRole): string { + return this.roleService.getRoleBadgeClass(role); + } + + getRoleLabel(role: string | UserRole): string { + return this.roleService.getRoleLabel(role); + } + + getRoleIcon(role: string | UserRole): string { + return this.roleService.getRoleIcon(role); + } + + getRoleDescription(role: string | UserRole): string { + const roleInfo = this.availableRoles.find(r => r.value === role); + return roleInfo?.description || 'Description non disponible'; + } + + formatTimestamp(timestamp: number): string { + if (!timestamp) return 'Non disponible'; + return new Date(timestamp).toLocaleDateString('fr-FR', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } + + getUserInitials(user: User): string { + return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U'; + } + + getUserDisplayName(user: User): string { + if (user.firstName && user.lastName) { + return `${user.firstName} ${user.lastName}`; + } + return user.username; + } + + getEnabledUsersCount(): number { + return this.allUsers.filter(user => user.enabled).length; + } + + getDisabledUsersCount(): number { + return this.allUsers.filter(user => !user.enabled).length; + } + + userHasRole(user: User, role: UserRole): boolean { + return UserUtils.hasRole(user, role); + } + + // Recherche rapide par rôle + filterByRole(role: UserRole | 'all') { + this.roleFilter = role; + this.currentPage = 1; + this.applyFiltersAndPagination(); + } + + // Recharger les données + refreshData() { + this.loadUsers(); + } + + // Méthodes pour le template + getCardTitle(): string { + return 'Équipe Marchande'; + } + + getHelperText(): string { + return this.canViewAllMerchants + ? 'Vue administrative - Tous les utilisateurs marchands' + : 'Votre équipe marchande'; + } + + getHelperIcon(): string { + return this.canViewAllMerchants ? 'lucideShield' : 'lucideUsers'; + } + + // Méthode pour compter les utilisateurs par rôle + getUsersCountByRole(role: UserRole): number { + if (!this.allUsers || this.allUsers.length === 0) return 0; + + return this.allUsers.filter(user => + user.role && user.role.includes(role) + ).length; + } + + getLoadingText(): string { + return 'Chargement des utilisateurs marchands...'; + } + + getEmptyStateTitle(): string { + return 'Aucun utilisateur marchand trouvé'; + } + + getEmptyStateDescription(): string { + return 'Aucun utilisateur ne correspond à vos critères de recherche.'; + } + + getEmptyStateButtonText(): string { + return 'Créer le premier utilisateur'; + } + + getColumnCount(): number { + return this.showMerchantPartnerColumn ? 7 : 6; + } + + showMerchantPartnerId(): boolean { + return !this.canViewAllMerchants; + } + + // Statistiques + getTotalUsersCount(): number { + return this.allUsers.length; + } + + getActiveUsersCount(): number { + return this.allUsers.filter(user => user.enabled).length; + } + + getVerifiedUsersCount(): number { + return this.allUsers.filter(user => user.emailVerified).length; + } +} \ No newline at end of file diff --git a/src/app/modules/merchant-users/profile/profile.html b/src/app/modules/hub-users-management/merchant-users-profile/merchant-users-profile.html similarity index 56% rename from src/app/modules/merchant-users/profile/profile.html rename to src/app/modules/hub-users-management/merchant-users-profile/merchant-users-profile.html index b50d9fd..6319eb8 100644 --- a/src/app/modules/merchant-users/profile/profile.html +++ b/src/app/modules/hub-users-management/merchant-users-profile/merchant-users-profile.html @@ -14,8 +14,8 @@
- } - - - @if (user && !loading) { -
-
-
-
- -
- - Statut : {{ getPermissionStatus() }} - @if (currentUserRole === UserRole.DCB_PARTNER) { - Accès complet - } @else if (currentUserRole === UserRole.DCB_PARTNER_ADMIN) { - Accès limité au partenaire - } @else { - Permissions restreintes - } - -
-
-
+
} @@ -128,7 +117,7 @@
Chargement...
-

Chargement du profil...

+

Chargement du profil utilisateur Marchand...

} @@ -152,15 +141,13 @@
{{ getUserDisplayName() }}

@{{ user.username }}

- - - - {{ getRoleDisplayName(user.role) }} + + + Utilisateur Marchand - + {{ getStatusText() }} @@ -173,12 +160,6 @@ } -
- - - {{ user.merchantPartnerId }} - -
Créé le {{ getCreationDate() }} @@ -195,35 +176,73 @@
-
-
Rôle Utilisateur
+
+
+ Rôle Utilisateur +
+ @if (showRoleManagement()) { + Modifiable + }
- +
- - - {{ getRoleDisplayName(user.role) }} - - - {{ getRoleDescription(user.role) }} - + @if (getUserRole()) { +
+ + + {{ getRoleLabel(user.role) }} + +
+ + + + {{ getRoleDescription(user.role) }} + + } @else { + Aucun rôle + }
- - @if (canManageRoles()) { -
+ + @if (showRoleManagement()) { +
+ + +
+ @if (updatingRole) { +
+ Mise à jour... +
+ Mise à jour en cours... + } @else { + Sélectionnez un nouveau rôle principal pour cet utilisateur + } +
+
+ } @else if (!canManageRoles()) { +
- DCB Partner : Vous pouvez modifier le rôle de cet utilisateur. - -
- } @else { -
- - - Information : Seul un DCB Partner peut modifier les rôles des utilisateurs marchands. + Vous n'avez pas la permission de modifier les rôles
} @@ -248,13 +267,7 @@
Type d'utilisateur :
- {{ user.userType }} -
-
-
- Merchant Partner : -
- {{ user.merchantPartnerId }} + Utilisateur Marchand
@@ -309,7 +322,7 @@
- + @if (isEditing) { } @else {
@@ -328,7 +340,7 @@
- + @if (isEditing) { } @else {
@@ -358,7 +369,7 @@
- + @if (isEditing) { - @if (editedUser.email && !isValidEmail(editedUser.email)) { -
- Format d'email invalide -
- } } @else {
{{ user.email }} @@ -383,88 +388,8 @@ }
- -
- -
- - - {{ getRoleDisplayName(user.role) }} - -
-
- @if (canManageRoles()) { - - - Vous pouvez modifier ce rôle (DCB Partner) - - } @else { - - - Seul un DCB Partner peut modifier les rôles - - } -
-
- - - @if (isEditing && canManageRoles()) { -
-
-
-
- - Modification du Rôle (DCB Partner) -
-
-
- - -
- En tant que DCB Partner, vous pouvez modifier le rôle de cet utilisateur. -
- -
-
-
- } - - -
- -
- {{ user.merchantPartnerId }} -
-
- Identifiant du partenaire marchand -
-
- - @if (isEditing && canEnableDisableUser()) { + @if (isEditing) {
- } @else if (!isEditing) { + } @else {
@@ -493,6 +418,24 @@
} + +
+ +
+
+ + + {{ getRoleLabel(user.role) }} + +
+
+
+ {{ getUserRoleDisplay() }} +
+
+ @if (!isEditing) {
@@ -523,17 +466,9 @@
- {{ user.userType }} + Utilisateur Marchand
- @if (user.lastLogin) { -
- -
- {{ getLastLoginDate() }} -
-
- }
} @@ -542,77 +477,51 @@
- @if (!isEditing) { + @if (!isEditing && canEditUser()) {
Actions de Gestion
- - @if (canResetPassword()) { -
+
+ +
+
+ @if (user.enabled && canToggleStatus()) { -
- } - - - @if (canEnableDisableUser()) { -
- @if (user.enabled) { - - } @else { - - } -
- } - - - @if (canEditUser()) { -
+ } @else if (!user.enabled && canToggleStatus()) { -
- } -
- - -
- - @if (currentUserRole === UserRole.DCB_PARTNER) { - - DCB Partner : Vous avez un accès complet à toutes les fonctionnalités de gestion. - } @else if (currentUserRole === UserRole.DCB_PARTNER_ADMIN) { - - Admin Partenaire : Vous pouvez gérer les utilisateurs de votre partenaire marchand, mais pas modifier les rôles. - } @else { - - Permissions limitées : Contactez un DCB Partner pour les actions de gestion avancées. } - +
+
+ +
diff --git a/src/app/modules/merchant-users/profile/profile.ts b/src/app/modules/hub-users-management/merchant-users-profile/merchant-users-profile.ts similarity index 63% rename from src/app/modules/merchant-users/profile/profile.ts rename to src/app/modules/hub-users-management/merchant-users-profile/merchant-users-profile.ts index 4d73be3..45b3f85 100644 --- a/src/app/modules/merchant-users/profile/profile.ts +++ b/src/app/modules/hub-users-management/merchant-users-profile/merchant-users-profile.ts @@ -1,25 +1,27 @@ -// src/app/modules/merchant-users/profile/profile.ts import { Component, inject, OnInit, Input, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { NgIcon } from '@ng-icons/core'; import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'; import { Subject, takeUntil } from 'rxjs'; -import { MerchantUsersService } from '../services/merchant-users.service'; -import { AuthService } from '@core/services/auth.service'; -import { RoleManagementService } from '@core/services/role-management.service'; import { - MerchantUserDto, + User, UpdateUserDto, - UserRole + UserRole, + UserType, + UserUtils } 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 { AuthService } from '@core/services/auth.service'; + @Component({ selector: 'app-merchant-user-profile', standalone: true, imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule], - templateUrl: './profile.html', + templateUrl: './merchant-users-profile.html', styles: [` .avatar-lg { width: 80px; @@ -32,17 +34,20 @@ import { }) export class MerchantUserProfile implements OnInit, OnDestroy { private merchantUsersService = inject(MerchantUsersService); - private authService = inject(AuthService); private roleService = inject(RoleManagementService); + private authService = inject(AuthService); private cdRef = inject(ChangeDetectorRef); private destroy$ = new Subject(); + readonly UserRole = UserRole; + readonly UserType = UserType; + readonly UserUtils = UserUtils; @Input() userId!: string; @Output() back = new EventEmitter(); - @Output() openResetPasswordModal = new EventEmitter(); + @Output() resetPasswordRequested = new EventEmitter(); - user: MerchantUserDto | null = null; + user: User | null = null; loading = false; saving = false; error = ''; @@ -56,16 +61,22 @@ export class MerchantUserProfile implements OnInit, OnDestroy { editedUser: UpdateUserDto = {}; // Gestion des rôles - availableRoles: UserRole[] = [ - UserRole.DCB_PARTNER_ADMIN, - UserRole.DCB_PARTNER_MANAGER, - UserRole.DCB_PARTNER_SUPPORT - ]; + availableRoles: { value: UserRole; label: string; description: string }[] = []; updatingRole = false; + // Getters pour la logique conditionnelle + get isMerchantPartnerUser(): boolean { + return UserUtils.isMerchantPartnerUser(this.user!); + } + + userHasRole(user: User, role: UserRole): boolean { + return UserUtils.hasRole(user, role); + } + ngOnInit() { if (this.userId) { this.loadCurrentUserPermissions(); + this.loadAvailableRoles(); this.loadUserProfile(); } } @@ -84,6 +95,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { .subscribe({ next: (profile) => { this.currentUserRole = this.authService.getCurrentUserRole(); + this.cdRef.detectChanges(); }, error: (error) => { console.error('Error loading user permissions:', error); @@ -91,6 +103,30 @@ export class MerchantUserProfile implements OnInit, OnDestroy { }); } + /** + * Charge les rôles disponibles pour les utilisateurs Merchant + */ + private loadAvailableRoles(): void { + this.merchantUsersService.getAvailableMerchantRoles() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (response) => { + this.availableRoles = response.roles.map(role => ({ + value: role.value, + label: role.label, + description: role.description + })); + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('Error loading available roles:', error); + // Fallback pour les rôles Merchant + this.availableRoles = []; + this.cdRef.detectChanges(); + } + }); + } + loadUserProfile() { this.loading = true; this.error = ''; @@ -104,7 +140,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { this.cdRef.detectChanges(); }, error: (error) => { - this.error = 'Erreur lors du chargement du profil utilisateur marchand'; + this.error = 'Erreur lors du chargement du profil utilisateur Merchant'; this.loading = false; this.cdRef.detectChanges(); console.error('Error loading merchant user profile:', error); @@ -163,16 +199,26 @@ export class MerchantUserProfile implements OnInit, OnDestroy { }); } - // Gestion des rôles updateUserRole(newRole: UserRole) { if (!this.user || !this.canManageRoles()) { this.error = 'Vous n\'avez pas la permission de modifier les rôles'; return; } - // Vérifier que le nouveau rôle est différent - if (newRole === this.user.role) { - this.error = 'L\'utilisateur a déjà ce rôle'; + if (newRole === this.currentUserRole) { + this.error = 'L\'utilisateur a déjà ce rôle comme rôle principal'; + return; + } + + // Vérifier que l'utilisateur peut attribuer ce rôle + if (!this.roleService.canAssignRole(this.currentUserRole, newRole)) { + this.error = 'Vous n\'avez pas la permission d\'attribuer ce rôle'; + return; + } + + // Vérifier que le rôle est valide pour les utilisateurs Merchant + if (!this.isValidMerchantRole(newRole)) { + this.error = 'Rôle invalide pour un utilisateur Merchant'; return; } @@ -180,14 +226,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { this.error = ''; this.success = ''; - // Note: La modification de rôle nécessite une méthode spécifique - // Pour l'instant, on utilise la mise à jour standard - // Vous devrez peut-être implémenter une méthode updateUserRole dans le service - const updateData: UpdateUserDto = { - ...this.editedUser - }; - - this.merchantUsersService.updateMerchantUser(this.user.id, updateData) + this.merchantUsersService.updateMerchantUserRole(this.user.id, newRole) .pipe(takeUntil(this.destroy$)) .subscribe({ next: (updatedUser) => { @@ -206,26 +245,19 @@ export class MerchantUserProfile implements OnInit, OnDestroy { // Gestion du statut enableUser() { - if (!this.user || !this.canEnableDisableUser()) { - this.error = 'Vous n\'avez pas la permission d\'activer cet utilisateur'; - return; - } - - this.error = ''; - this.success = ''; + if (!this.user || !this.canEnableDisableUser()) return; this.merchantUsersService.enableMerchantUser(this.user.id) .pipe(takeUntil(this.destroy$)) .subscribe({ next: (updatedUser) => { this.user = updatedUser; - this.success = 'Utilisateur marchand activé avec succès'; - this.cdRef.detectChanges(); + this.success = 'Utilisateur Merchant activé avec succès'; + this.cdRef.detectChanges(); }, error: (error) => { this.error = this.getErrorMessage(error); - this.cdRef.detectChanges(); - console.error('Error enabling merchant user:', error); + this.cdRef.detectChanges(); } }); } @@ -244,7 +276,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { .subscribe({ next: (updatedUser) => { this.user = updatedUser; - this.success = 'Utilisateur marchand désactivé avec succès'; + this.success = 'Utilisateur Merchant désactivé avec succès'; this.cdRef.detectChanges(); }, error: (error) => { @@ -258,7 +290,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { // Réinitialisation du mot de passe resetPassword() { if (this.user && this.canResetPassword()) { - this.openResetPasswordModal.emit(this.user.id); + this.resetPasswordRequested.emit(this.user.id); } } @@ -268,26 +300,21 @@ export class MerchantUserProfile implements OnInit, OnDestroy { * Vérifie si l'utilisateur peut éditer cet utilisateur */ canEditUser(): boolean { - // Seul DCB_PARTNER peut éditer tous les utilisateurs marchands - if (this.currentUserRole === UserRole.DCB_PARTNER) { + // Toujours permettre d'éditer son propre profil + if (this.isCurrentUserProfile()) { return true; } - - // Les administrateurs marchands peuvent éditer les utilisateurs de leur partenaire - if (this.currentUserRole === UserRole.DCB_PARTNER_ADMIN) { - const currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId(); - return this.user?.merchantPartnerId === currentMerchantPartnerId; - } - - return false; + + // Pour les utilisateurs Merchant, utiliser les permissions du service de rôle + return this.roleService.canEditUsers(this.currentUserRole); } /** * Vérifie si l'utilisateur peut gérer les rôles */ canManageRoles(): boolean { - // SEUL DCB_PARTNER peut modifier les rôles des utilisateurs marchands - return this.currentUserRole === UserRole.DCB_PARTNER; + // Pour les Merchant, utiliser les permissions du service de rôle + return this.roleService.canManageRoles(this.currentUserRole); } /** @@ -298,42 +325,22 @@ export class MerchantUserProfile implements OnInit, OnDestroy { if (this.isCurrentUserProfile()) { return false; } - - // Seul DCB_PARTNER peut activer/désactiver les utilisateurs marchands - if (this.currentUserRole === UserRole.DCB_PARTNER) { - return true; - } - - // Les administrateurs marchands peuvent gérer les utilisateurs de leur partenaire - if (this.currentUserRole === UserRole.DCB_PARTNER_ADMIN) { - const currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId(); - return this.user?.merchantPartnerId === currentMerchantPartnerId; - } - - return false; + + // Pour les Merchant, utiliser les permissions du service de rôle + return this.roleService.canEditUsers(this.currentUserRole); } /** * Vérifie si l'utilisateur peut réinitialiser le mot de passe */ canResetPassword(): boolean { - // DCB_PARTNER peut réinitialiser tous les mots de passe - if (this.currentUserRole === UserRole.DCB_PARTNER) { - return true; - } - - // Les administrateurs marchands peuvent réinitialiser les mots de passe de leur partenaire - if (this.currentUserRole === UserRole.DCB_PARTNER_ADMIN) { - const currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId(); - return this.user?.merchantPartnerId === currentMerchantPartnerId; - } - // Les utilisateurs peuvent réinitialiser leur propre mot de passe if (this.isCurrentUserProfile()) { return true; } - return false; + // Pour les Merchant, utiliser les permissions générales + return this.roleService.canEditUsers(this.currentUserRole); } /** @@ -345,8 +352,8 @@ export class MerchantUserProfile implements OnInit, OnDestroy { return false; } - // Seul DCB_PARTNER peut supprimer les utilisateurs marchands - return this.currentUserRole === UserRole.DCB_PARTNER; + // Pour les Merchant, utiliser les permissions du service de rôle + return this.roleService.canDeleteUsers(this.currentUserRole); } // ==================== UTILITAIRES D'AFFICHAGE ==================== @@ -382,42 +389,46 @@ export class MerchantUserProfile implements OnInit, OnDestroy { } getUserDisplayName(): string { - if (!this.user) return 'Utilisateur Marchand'; + if (!this.user) return 'Utilisateur Merchant'; if (this.user.firstName && this.user.lastName) { return `${this.user.firstName} ${this.user.lastName}`; } return this.user.username; } - getRoleBadgeClass(role: UserRole): string { + getRoleBadgeClass(role: string | UserRole): string { return this.roleService.getRoleBadgeClass(role); } - getRoleDisplayName(role: UserRole): string { + getRoleLabel(role: string | UserRole): string { return this.roleService.getRoleLabel(role); } - getRoleIcon(role: UserRole): string { + getRoleIcon(role: string | UserRole): string { return this.roleService.getRoleIcon(role); } - getRoleDescription(role: UserRole): string { - const descriptions: { [key in UserRole]?: string } = { - [UserRole.DCB_PARTNER_ADMIN]: 'Accès administratif complet au sein du partenaire marchand', - [UserRole.DCB_PARTNER_MANAGER]: 'Accès de gestion avec capacités administratives limitées', - [UserRole.DCB_PARTNER_SUPPORT]: 'Rôle support avec accès en lecture seule et opérations de base' - }; - return descriptions[role] || 'Description non disponible'; + getRoleDescription(role: string | UserRole): string { + const roleInfo = this.availableRoles.find(r => r.value === role); + return roleInfo?.description || 'Description non disponible'; } - getUserType(): string { - if (!this.user) return 'Utilisateur Marchand'; - return this.roleService.getRoleLabel(this.user.role); + // Obtenir le rôle (peut être string ou UserRole) + getUserRole(): string | UserRole | undefined { + return this.user?.role; } - getUserTypeBadgeClass(): string { - if (!this.user) return 'bg-secondary'; - return this.roleService.getRoleBadgeClass(this.user.role); + // Pour le template, retourner un tableau pour la boucle + getUserRoles(): (string | UserRole)[] { + const role = this.user?.role; + if (!role) return []; + return Array.isArray(role) ? role : [role]; + } + + // Afficher le rôle + getUserRoleDisplay(): string { + if (!this.user) return 'Aucun rôle'; + return this.getRoleLabel(this.user.role); } // ==================== GESTION DES ERREURS ==================== @@ -433,7 +444,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { return 'Vous n\'avez pas les permissions pour effectuer cette action.'; } if (error.status === 404) { - return 'Utilisateur marchand non trouvé.'; + return 'Utilisateur non trouvé.'; } if (error.status === 409) { return 'Conflit de données. Cet utilisateur existe peut-être déjà.'; @@ -459,23 +470,36 @@ export class MerchantUserProfile implements OnInit, OnDestroy { return true; } - protected isValidEmail(email: string): boolean { + private isValidEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } + private isValidMerchantRole(role: UserRole): boolean { + const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; + return merchantRoles.includes(role); + } + // ==================== MÉTHODES UTILITAIRES ==================== - isAdmin(): boolean { - return this.user?.role === UserRole.DCB_PARTNER_ADMIN; + isCurrentUserProfile(): boolean { + if (!this.user?.id) return false; + return this.authService.isCurrentUserProfile(this.user.id); } - isManager(): boolean { - return this.user?.role === UserRole.DCB_PARTNER_MANAGER; + getCreationDate(): string { + if (!this.user?.createdTimestamp) return 'Non disponible'; + return this.formatTimestamp(this.user.createdTimestamp); } - isSupport(): boolean { - return this.user?.role === UserRole.DCB_PARTNER_SUPPORT; + getLastLoginDate(): string { + if (!this.user?.lastLogin) return 'Jamais connecté'; + return this.formatTimestamp(this.user.lastLogin); + } + + getCreatorName(): string { + if (!this.user?.createdByUsername) return 'Non disponible'; + return this.user.createdByUsername; } refresh() { @@ -488,62 +512,30 @@ export class MerchantUserProfile implements OnInit, OnDestroy { this.cdRef.detectChanges(); } - // Vérifie si c'est le profil de l'utilisateur courant - isCurrentUserProfile(): boolean { - if (!this.user?.id) return false; - return this.authService.isCurrentUserProfile(this.user.id); + // Méthodes pour le template + getProfileTitle(): string { + return 'Profil Utilisateur Merchant'; } - // Méthode pour obtenir la date de création formatée - getCreationDate(): string { - if (!this.user?.createdTimestamp) return 'Non disponible'; - return this.formatTimestamp(this.user.createdTimestamp); + getContextDescription(): string { + return 'Gestion des utilisateurs de la plateforme DCB'; } - // Méthode pour obtenir la date de dernière connexion formatée - getLastLoginDate(): string { - if (!this.user?.lastLogin) return 'Jamais connecté'; - return this.formatTimestamp(this.user.lastLogin); + getAssignableRoles(): UserRole[] { + const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; + return merchantRoles.filter(role => this.roleService.canAssignRole(this.currentUserRole, role)); } - // Méthode pour obtenir le nom du créateur - getCreatorName(): string { - if (!this.user?.createdByUsername) return 'Non disponible'; - return this.user.createdByUsername; + // Méthodes pour les actions spécifiques + canChangeRole(): boolean { + return this.canManageRoles() && !this.isCurrentUserProfile(); } - // Méthode pour obtenir l'ID du partenaire marchand - getMerchantPartnerId(): string { - return this.user?.merchantPartnerId || 'Non disponible'; + canToggleStatus(): boolean { + return this.canEnableDisableUser() && !this.isCurrentUserProfile(); } - // Vérifie si l'utilisateur a accès à ce profil marchand - canAccessMerchantProfile(): boolean { - if (!this.user) return false; - - // DCB_PARTNER peut accéder à tous les profils marchands - if (this.currentUserRole === UserRole.DCB_PARTNER) { - return true; - } - - const currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId(); - - // Les utilisateurs marchands ne peuvent voir que les utilisateurs de leur partenaire - if (this.authService.isMerchantUser()) { - return this.user.merchantPartnerId === currentMerchantPartnerId; - } - - return false; - } - - // Affiche le statut des permissions - getPermissionStatus(): string { - if (this.currentUserRole === UserRole.DCB_PARTNER) { - return 'DCB Partner - Accès complet'; - } else if (this.currentUserRole === UserRole.DCB_PARTNER_ADMIN) { - return 'Admin Partenaire - Accès limité à votre partenaire'; - } else { - return 'Permissions limitées'; - } + showRoleManagement(): boolean { + return this.canManageRoles() && !this.isCurrentUserProfile(); } } \ No newline at end of file diff --git a/src/app/modules/hub-users-management/merchant-users.html b/src/app/modules/hub-users-management/merchant-users.html new file mode 100644 index 0000000..91666e5 --- /dev/null +++ b/src/app/modules/hub-users-management/merchant-users.html @@ -0,0 +1,882 @@ +
+ + + + @if (currentUserRole) { +
+
+
+
+ +
+ + Rôle actuel : + + {{ getRoleLabel(currentUserRole) }} + + @if (!canCreateUsers) { + + + Permissions limitées + + } + +
+ @if (canCreateUsers) { + + } +
+
+
+
+ } + + +
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/modules/hub-users-management/merchant-users.service.ts b/src/app/modules/hub-users-management/merchant-users.service.ts new file mode 100644 index 0000000..e8a7f4a --- /dev/null +++ b/src/app/modules/hub-users-management/merchant-users.service.ts @@ -0,0 +1,351 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '@environments/environment'; +import { Observable, map, catchError, throwError, of } from 'rxjs'; + +import { + User, + CreateUserDto, + UpdateUserDto, + ResetPasswordDto, + PaginatedUserResponse, + AvailableRolesResponse, + SearchUsersParams, + UserRole, + UserType, + UserUtils +} from '@core/models/dcb-bo-hub-user.model'; + +// Interfaces pour les nouvelles réponses +export interface TokenResponse { + access_token: string; + expires_in: number; + refresh_token: string; + refresh_expires_in: number; + token_type: string; + 'not-before-policy': number; + session_state: string; + scope: string; +} + +export interface UserProfileResponse { + id: string; + username: string; + email: string; + firstName: string; + lastName: string; + emailVerified: boolean; + enabled: boolean; + role: string[]; + merchantPartnerId?: string; + createdBy?: string; + createdByUsername?: string; +} + +export interface MessageResponse { + message: string; +} + +export interface MerchantPartnerIdResponse { + merchantPartnerId: string | null; +} + +// ===== SERVICE UTILISATEURS MERCHANT ===== + +@Injectable({ providedIn: 'root' }) +export class MerchantUsersService { + private http = inject(HttpClient); + private baseApiUrl = `${environment.iamApiUrl}/merchant-users`; + + getUserMerchantPartnerId(userId: string): Observable { + return this.http.get(`${this.baseApiUrl}/merchant-partner/${userId}`).pipe( + map(response => response.merchantPartnerId), + catchError(error => { + console.error(`Error loading merchant partner ID for user ${userId}:`, error); + return throwError(() => error); + }) + ); + } + +// === MÉTHODES SPÉCIFIQUES MERCHANT === + + createMerchantUser(createUserDto: CreateUserDto): Observable { + // Utiliser la validation centralisée + const errors = UserUtils.validateUserCreation(createUserDto); + if (errors.length > 0) { + return throwError(() => errors.join(', ')); + } + + if (!createUserDto.username?.trim()) { + return throwError(() => 'Username is required and cannot be empty'); + } + + if (!createUserDto.email?.trim()) { + return throwError(() => 'Email is required and cannot be empty'); + } + + if (!createUserDto.password || createUserDto.password.length < 8) { + return throwError(() => 'Password must be at least 8 characters'); + } + + // Adapter le payload pour le nouveau contrôleur + const payload = { + username: createUserDto.username.trim(), + email: createUserDto.email.trim(), + firstName: (createUserDto.firstName || '').trim(), + lastName: (createUserDto.lastName || '').trim(), + password: createUserDto.password, + role: createUserDto.role, + enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true, + emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : true, + merchantPartnerId: createUserDto.merchantPartnerId?.trim() || null, + userType: createUserDto.userType.trim() + }; + + return this.http.post(`${this.baseApiUrl}`, payload).pipe( + map(user => this.mapToUserModel(user, UserType.MERCHANT_PARTNER)), + catchError(error => { + console.error('Error creating merchant user:', error); + return throwError(() => error); + }) + ); + } + + getMyMerchantUsers(): Observable { + return this.http.get(`${this.baseApiUrl}`).pipe( + map(users => users.map(user => this.mapToUserModel(user, UserType.MERCHANT_PARTNER))), + catchError(error => { + console.error('Error loading my merchant users:', error); + return throwError(() => error); + }) + ); + } + + getMerchantUsersByPartner(partnerId: string): Observable { + return this.getMyMerchantUsers().pipe( + map(users => users.filter(user => user.merchantPartnerId === partnerId)) + ); + } + + getMerchantUserById(id: string | undefined): Observable { + return this.http.get(`${this.baseApiUrl}/${id}`).pipe( + map(user => this.mapToUserModel(user, UserType.MERCHANT_PARTNER)), + catchError(error => { + console.error(`Error loading merchant user ${id}:`, error); + return throwError(() => error); + }) + ); + } + + getMerchantUsers(page: number = 1, limit: number = 10, filters?: SearchUsersParams): Observable { + return this.getMyMerchantUsers().pipe( + map(users => this.filterAndPaginateUsers(users, page, limit, filters)), + catchError(error => { + console.error('Error loading merchant users:', error); + return throwError(() => error); + }) + ); + } + + updateMerchantUser(id: string, updateUserDto: UpdateUserDto): Observable { + const payload: any = { + firstName: updateUserDto.firstName, + lastName: updateUserDto.lastName, + email: updateUserDto.email, + enabled: updateUserDto.enabled + }; + + return this.http.put(`${this.baseApiUrl}/${id}`, payload).pipe( + map(user => this.mapToUserModel(user, UserType.MERCHANT_PARTNER)), + catchError(error => { + console.error(`Error updating merchant user ${id}:`, error); + return throwError(() => error); + }) + ); + } + + deleteMerchantUser(id: string): Observable { + return this.http.delete(`${this.baseApiUrl}/${id}`).pipe( + catchError(error => { + console.error(`Error deleting merchant user ${id}:`, error); + return throwError(() => error); + }) + ); + } + + resetMerchantUserPassword(id: string, resetPasswordDto: ResetPasswordDto): Observable { + const payload = { + newPassword: resetPasswordDto.newPassword, + temporary: resetPasswordDto.temporary !== undefined ? resetPasswordDto.temporary : true + }; + + return this.http.post( + `${this.baseApiUrl}/${id}/reset-password`, + payload + ).pipe( + catchError(error => { + console.error(`Error resetting password for merchant user ${id}:`, error); + return throwError(() => error); + }) + ); + } + + updateMerchantUserRole(id: string, role: UserRole): Observable { + const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_SUPPORT, UserRole.DCB_PARTNER_MANAGER]; + if (!merchantRoles.includes(role)) { + return throwError(() => 'Invalid role for Merchant user'); + } + + return this.http.put(`${this.baseApiUrl}/${id}/role`, { role }).pipe( + map(user => this.mapToUserModel(user, UserType.MERCHANT_PARTNER)), + catchError(error => { + console.error(`Error updating role for merchant user ${id}:`, error); + return throwError(() => error); + }) + ); + } + + enableMerchantUser(id: string): Observable { + return this.updateMerchantUser(id, { enabled: true }); + } + + disableMerchantUser(id: string): Observable { + return this.updateMerchantUser(id, { enabled: false }); + } + + getAvailableMerchantRoles(): Observable { + return of({ + roles: [ + { + value: UserRole.DCB_PARTNER_ADMIN, + label: 'Partner Admin', + description: 'Full administrative access within the merchant partner', + allowedForCreation: true, + userType: UserType.MERCHANT_PARTNER + }, + { + value: UserRole.DCB_PARTNER_MANAGER, + label: 'Partner Manager', + description: 'Manager access with limited administrative capabilities', + allowedForCreation: true, + userType: UserType.MERCHANT_PARTNER + }, + { + value: UserRole.DCB_PARTNER_SUPPORT, + label: 'Partner Support', + description: 'Support role with read-only and basic operational access', + allowedForCreation: true, + userType: UserType.MERCHANT_PARTNER + } + ] + } as AvailableRolesResponse); + } + + searchMerchantUsers(params: SearchUsersParams): Observable { + return this.getMerchantUsers(1, 1000, params).pipe( + map(response => response.users) + ); + } + + merchantUserExists(username: string): Observable<{ exists: boolean }> { + return this.searchMerchantUsers({ query: username }).pipe( + map(users => ({ + exists: users.some(user => user.username === username) + })), + catchError(error => { + console.error('Error checking if merchant user exists:', error); + return of({ exists: false }); + }) + ); + } + + getMerchantUsersByRole(role: UserRole): Observable { + return this.searchMerchantUsers({ role }); + } + + getActiveMerchantUsers(): Observable { + return this.searchMerchantUsers({ enabled: true }); + } + + getInactiveMerchantUsers(): Observable { + return this.searchMerchantUsers({ enabled: false }); + } + + // === MÉTHODES UTILITAIRES === + + isValidRoleForMerchant(role: UserRole): boolean { + const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; + return merchantRoles.includes(role); + } + + // === MAPPING ET FILTRAGE === + + private mapToUserModel(apiUser: any, userType: UserType): User { + return { + id: apiUser.id, + username: apiUser.username, + email: apiUser.email, + firstName: apiUser.firstName, + lastName: apiUser.lastName, + enabled: apiUser.enabled, + emailVerified: apiUser.emailVerified, + userType: userType, + merchantPartnerId: apiUser.merchantPartnerId, + role: apiUser.role, // Convertir le rôle unique en tableau + createdBy: apiUser.createdBy, + createdByUsername: apiUser.createdByUsername, + createdTimestamp: apiUser.createdTimestamp, + lastLogin: apiUser.lastLogin + }; + } + + private filterAndPaginateUsers( + users: User[], + page: number, + limit: number, + filters?: SearchUsersParams + ): PaginatedUserResponse { + let filteredUsers = users; + + if (filters) { + if (filters.query) { + const query = filters.query.toLowerCase(); + filteredUsers = filteredUsers.filter(user => + user.username.toLowerCase().includes(query) || + user.email.toLowerCase().includes(query) || + user.firstName?.toLowerCase().includes(query) || + user.lastName?.toLowerCase().includes(query) + ); + } + + if (filters.role) { + filteredUsers = filteredUsers.filter(user => user.role.includes(filters.role!)); + } + + if (filters.enabled !== undefined) { + filteredUsers = filteredUsers.filter(user => user.enabled === filters.enabled); + } + + if (filters.userType) { + filteredUsers = filteredUsers.filter(user => user.userType === filters.userType); + } + + if (filters.merchantPartnerId) { + filteredUsers = filteredUsers.filter(user => user.merchantPartnerId === filters.merchantPartnerId); + } + } + + // Pagination côté client + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + const paginatedUsers = filteredUsers.slice(startIndex, endIndex); + + return { + users: paginatedUsers, + total: filteredUsers.length, + page, + limit, + totalPages: Math.ceil(filteredUsers.length / limit) + }; + } +} diff --git a/src/app/modules/hub-users-management/merchant-users.ts b/src/app/modules/hub-users-management/merchant-users.ts new file mode 100644 index 0000000..91f8e8e --- /dev/null +++ b/src/app/modules/hub-users-management/merchant-users.ts @@ -0,0 +1,806 @@ +import { Component, inject, OnInit, TemplateRef, ViewChild, ChangeDetectorRef, OnDestroy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgIcon } from '@ng-icons/core'; +import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; +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 { AuthService } from '@core/services/auth.service'; +import { PageTitle } from '@app/components/page-title/page-title'; +import { MerchantUsersList } from './merchant-users-list/merchant-users-list'; +import { MerchantUserProfile } from './merchant-users-profile/merchant-users-profile'; + +import { + PaginatedUserResponse, + ResetPasswordDto, + User, + UserRole, + UserType +} from '@core/models/dcb-bo-hub-user.model'; +import { HubUsersService } from './hub-users.service'; + +@Component({ + selector: 'app-merchant-users', + standalone: true, + imports: [ + CommonModule, + FormsModule, + NgIcon, + NgbNavModule, + NgbModalModule, + PageTitle, + MerchantUsersList, + MerchantUserProfile + ], + templateUrl: './merchant-users.html', +}) +export class MerchantUsersManagement implements OnInit, OnDestroy { + private modalService = inject(NgbModal); + private authService = inject(AuthService); + private merchantUsersService = inject(MerchantUsersService); + private hubUsersService = inject(HubUsersService); + protected roleService = inject(RoleManagementService); + private cdRef = inject(ChangeDetectorRef); + private destroy$ = new Subject(); + + // Configuration + readonly UserRole = UserRole; + + // Propriétés de configuration + pageTitle: string = 'Gestion des Utilisateurs Marchands'; + pageSubtitle: string = 'Administrez les utilisateurs de votre équipe marchande'; + badge: any = { icon: 'lucideBuilding', text: 'Merchant Users' }; + + // État de l'interface + activeTab: 'list' | 'profile' = 'list'; + selectedUserId: string | null = null; + + // Gestion des permissions + currentUserRole: UserRole | null = null; + currentUserType: UserType | null = null; + currentMerchantPartnerId: string = ''; + userPermissions: any = null; + canCreateUsers = false; + canDeleteUsers = false; + canManageRoles = false; + + // Formulaire de création + newUser: { + username: string; + email: string; + firstName: string; + lastName: string; + password: string; + role: UserRole; + enabled: boolean; + emailVerified: boolean; + merchantPartnerId: string; + userType: UserType; + } = this.getDefaultUserForm(); + + // États des opérations + creatingUser = false; + createUserError = ''; + resettingPassword = false; + resetPasswordError = ''; + resetPasswordSuccess = ''; + + newPassword = ''; + temporaryPassword = false; + + deletingUser = false; + deleteUserError = ''; + + selectedUserForReset: any = null; + selectedUserForDelete: any = null; + + // UX améliorations + showPassword = false; + showNewPassword = false; + + // Références aux templates de modals + @ViewChild('createUserModal') createUserModal!: TemplateRef; + @ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef; + @ViewChild('deleteUserModal') deleteUserModal!: TemplateRef; + + // Références aux composants enfants + @ViewChild(MerchantUsersList) merchantUsersList!: MerchantUsersList; + + // Rôles disponibles + availableRoles: { value: UserRole; label: string; description: string }[] = []; + assignableRoles: UserRole[] = []; + + merchantPartners: User[] = []; + loadingMerchantPartners = false; + merchantPartnersError = ''; + selectedMerchantPartnerId: string = ''; + + ngOnInit() { + this.activeTab = 'list'; + this.loadCurrentUserPermissions(); + this.loadAvailableRoles(); + this.newUser.role = UserRole.DCB_PARTNER_SUPPORT; + + // Charger la liste des partenaires marchands (pour les admins Hub) + if (this.currentUserRole === UserRole.DCB_ADMIN || + this.currentUserRole === UserRole.DCB_SUPPORT) { + + this.loadMerchantPartners(); + } + + // Initialiser le formulaire selon le contexte + this.initializeMerchantPartner(); + } + + // Initialiser le merchant partner selon le contexte + initializeMerchantPartner() { + if (this.currentUserRole === UserRole.DCB_PARTNER && this.currentMerchantPartnerId) { + // Auto-sélection pour DCB_PARTNER + this.selectedMerchantPartnerId = this.currentMerchantPartnerId; + }else if (this.currentUserRole === UserRole.DCB_PARTNER_ADMIN && this.currentMerchantPartnerId) { + // Auto-sélection pour DCB_PARTNER_ADMIN + this.selectedMerchantPartnerId = this.currentMerchantPartnerId; + } else if ((this.currentUserRole === UserRole.DCB_ADMIN || + this.currentUserRole === UserRole.DCB_SUPPORT) && + this.isMerchantRole(this.newUser.role)) { + // Forcer la sélection pour les admins Hub créant des users marchands + this.selectedMerchantPartnerId = ''; + } + } + + onRoleSelectionChange(selectedRole: UserRole) { + this.newUser.role = selectedRole; + } + + onPartnerSelectionChange(selectedPartner: User) { + this.newUser.merchantPartnerId = this.selectedMerchantPartnerId || ''; + + this.currentMerchantPartnerId = this.selectedMerchantPartnerId || '' + } + + /** + * Recharge les partenaires marchands (pour retry) + */ + reloadMerchantPartners(): void { + console.log('🔄 Rechargement des partenaires marchands...'); + this.loadMerchantPartners(); + } + + + /** + * Charge la liste des partenaires marchands + */ + loadMerchantPartners(): void { + this.loadingMerchantPartners = true; + this.merchantPartnersError = ''; + + console.log('🔄 Chargement des partenaires marchands...'); + + this.hubUsersService.getAllDcbPartners() + .pipe( + map((response: PaginatedUserResponse) => response.users), + takeUntil(this.destroy$), + catchError(error => { + console.error('Error loading hub users:', error); + this.loadingMerchantPartners = false; + this.merchantPartnersError = 'Impossible de charger la liste des partenaires marchands'; + return of([] as User[]); + }) + ) + .subscribe( + {next: (partners) => { + this.merchantPartners = partners; + this.loadingMerchantPartners = false; + + console.log(`✅ ${partners.length} partenaires marchands chargés`, partners); + + }, + error: (error) => { + console.error('❌ Erreur lors du chargement des partenaires marchands:', error); + this.loadingMerchantPartners = false; + this.merchantPartnersError = 'Impossible de charger la liste des partenaires marchands'; + } + }); + } + + /** + * Vérifie si l'utilisateur connecté est un DCB_PARTNER ou DCB_PARTNER_ADMIN + */ + get isPartnerUser(): boolean { + return this.currentUserRole === UserRole.DCB_PARTNER || + this.currentUserRole === UserRole.DCB_PARTNER_ADMIN; + } + + /** + * Vérifie si on doit afficher le champ Merchant Partner ID + */ + get showMerchantPartnerIdField(): boolean { + return this.isPartnerUser && this.isMerchantRole(this.newUser.role); + } + + get showMerchantPartnerField(): boolean { + if (this.isMerchantRole(this.newUser.role)) { + return true; + } + return false; + } + + get requireMerchantPartnerSelection(): boolean { + // Si l'utilisateur connecté est un admin/support Hub ET qu'il crée un utilisateur marchand + const isHubAdminCreatingMerchant = + (this.currentUserRole === UserRole.DCB_ADMIN || + this.currentUserRole === UserRole.DCB_SUPPORT) && + this.isMerchantRole(this.newUser.role); + return isHubAdminCreatingMerchant; + } + + get merchantSelectionHelpText(): string { + if (this.currentUserRole === UserRole.DCB_ADMIN || + this.currentUserRole === UserRole.DCB_SUPPORT) { + return 'En tant qu\'administrateur Hub, vous devez sélectionner un partenaire marchand pour cet utilisateur'; + } + + return 'Sélectionnez le partenaire marchand auquel cet utilisateur sera associé'; + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + /** + * Initialise les permissions de l'utilisateur courant + */ + private loadCurrentUserPermissions(): void { + this.authService.getUserProfile() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (user) => { + this.currentUserRole = this.extractUserRole(user); + this.currentMerchantPartnerId = this.extractMerchantPartnerId(user); + this.currentUserType = this.extractUserType(user); + + console.log(`MERCHANT User ROLE: ${this.currentUserRole}`); + console.log(`Merchant Partner ID: ${this.currentMerchantPartnerId}`); + + if (this.currentUserRole) { + this.roleService.setCurrentUserRole(this.currentUserRole); + this.userPermissions = this.roleService.getPermissionsForRole(this.currentUserRole); + this.canCreateUsers = this.roleService.canCreateUsers(this.currentUserRole); + this.canDeleteUsers = this.roleService.canDeleteUsers(this.currentUserRole); + this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole); + + this.assignableRoles = this.roleService.getAssignableRoles(this.currentUserRole); + console.log('Assignable roles:', this.assignableRoles); + } + + // Initialiser le merchantPartnerId + this.newUser.merchantPartnerId = this.currentMerchantPartnerId; + }, + error: (error) => { + console.error('Error loading user profile:', error); + this.fallbackPermissions(); + } + }); + } + + /** + * Extraire le rôle de l'utilisateur + */ + private extractUserRole(user: any): UserRole | null { + const userRoles = this.authService.getCurrentUserRoles(); + if (userRoles && userRoles.length > 0) { + return userRoles[0]; + } + return null; + } + + /** + * Extraire le type de l'utilisateur + */ + private extractUserType(user: any): UserType | null { + const userType = this.authService.getCurrentUserType(); + return userType || null; + } + + /** + * Extraire le merchantPartnerId + */ + private extractMerchantPartnerId(user: any): string { + if (user?.merchantPartnerId) { + return user.merchantPartnerId; + } + return this.authService.getCurrentMerchantPartnerId() || ''; + } + + /** + * Fallback en cas d'erreur de chargement du profil + */ + private fallbackPermissions(): void { + this.currentUserRole = this.authService.getCurrentUserRole(); + this.currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId() || ''; + + if (this.currentUserRole) { + this.canCreateUsers = this.roleService.canCreateUsers(this.currentUserRole); + this.canDeleteUsers = this.roleService.canDeleteUsers(this.currentUserRole); + this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole); + } + } + + /** + * Charge les rôles disponibles + */ + private loadAvailableRoles(): void { + this.merchantUsersService.getAvailableMerchantRoles() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (response) => { + this.availableRoles = response.roles.map(role => ({ + value: role.value, + label: role.label, + description: role.description + })); + console.log('Available merchant roles loaded:', this.availableRoles); + }, + error: (error) => { + console.error('Error loading available roles:', error); + this.availableRoles = this.getFallbackRoles(); + } + }); + } + + /** + * Rôles par défaut en cas d'erreur + */ + private getFallbackRoles(): any[] { + return [ + { value: UserRole.DCB_PARTNER_ADMIN, label: 'Partner Admin', description: 'Administrateur partenaire' }, + { value: UserRole.DCB_PARTNER_MANAGER, label: 'Partner Manager', description: 'Manager partenaire' }, + { value: UserRole.DCB_PARTNER_SUPPORT, label: 'Partner Support', description: 'Support partenaire' } + ]; + } + + private getDefaultUserForm() { + return { + username: '', + email: '', + firstName: '', + lastName: '', + password: '', + role: UserRole.DCB_PARTNER_SUPPORT, + enabled: true, + emailVerified: false, + merchantPartnerId: '', + userType: UserType.MERCHANT_PARTNER + }; + } + + // ==================== MÉTHODES D'INTERFACE ==================== + + userProfiles: { [userId: string]: any } = {}; // Stocker les profils par userId + users: any[] = []; // Liste des utilisateurs + loadingProfiles: { [userId: string]: boolean } = {}; // État de chargement par user + + // Méthode pour changer d'onglet + showTab(tab: 'list' | 'profile', userId?: string) { + console.log(`Switching to tab: ${tab}`, userId ? `for user ${userId}` : ''); + this.activeTab = tab; + + if (userId) { + this.selectedUserId = userId; + // Charger le profil si pas déjà chargé + if (!this.userProfiles[userId]) { + this.loadUserProfile(userId); + } + } else { + this.selectedUserId = null; + } + } + + // Charger un profil spécifique + loadUserProfile(userId: string) { + if (this.loadingProfiles[userId]) return; // Éviter les doublons + + this.loadingProfiles[userId] = true; + + this.merchantUsersService.getMerchantUserById(userId).subscribe({ + next: (profile) => { + this.userProfiles[userId] = profile; + this.loadingProfiles[userId] = false; + console.log(`Profile loaded for user ${userId}:`, profile); + }, + error: (error) => { + console.error(`Error loading profile for user ${userId}:`, error); + this.loadingProfiles[userId] = false; + } + }); + } + + // Getter pour le profil actuel + get currentProfile() { + return this.selectedUserId ? this.userProfiles[this.selectedUserId] : null; + } + + // Getter pour l'état de chargement + get isLoadingProfile() { + return this.selectedUserId ? this.loadingProfiles[this.selectedUserId] : false; + } + + + backToList() { + console.log('🔙 Returning to list view'); + this.activeTab = 'list'; + this.selectedUserId = null; + } + + // Méthodes de gestion des événements du composant enfant + onUserSelected(userId: string) { + this.showTab('profile', userId); + } + + onResetPasswordRequested(event: any) { + const userId = typeof event === 'string' ? event : event.detail || event; + this.openResetPasswordModal(userId); + } + + onDeleteUserRequested(event: any) { + const userId = typeof event === 'string' ? event : event.detail || event; + this.openDeleteUserModal(userId); + } + + // ==================== GESTION DES MODALS ==================== + + openModal(content: TemplateRef, size: 'sm' | 'lg' | 'xl' = 'lg') { + this.modalService.open(content, { + size: size, + centered: true, + scrollable: true + }); + } + + // Méthode pour ouvrir le modal de création d'utilisateur + openCreateUserModal() { + if (!this.canCreateUsers) { + console.warn('User does not have permission to create users'); + return; + } + + this.resetUserForm(); + this.createUserError = ''; + this.openModal(this.createUserModal); + } + + private resetUserForm() { + this.newUser = { + username: '', + email: '', + firstName: '', + lastName: '', + password: '', + role: UserRole.DCB_PARTNER_SUPPORT, + enabled: true, + emailVerified: false, + merchantPartnerId: this.selectedMerchantPartnerId, + userType: UserType.MERCHANT_PARTNER, + }; + console.log('🔄 Merchant user form reset'); + } + + // Méthode pour ouvrir le modal de réinitialisation de mot de passe + openResetPasswordModal(userId: string) { + this.merchantUsersService.getMerchantUserById(userId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (user) => { + this.selectedUserForReset = user; + this.newPassword = ''; + this.temporaryPassword = false; + this.resetPasswordError = ''; + this.resetPasswordSuccess = ''; + this.openModal(this.resetPasswordModal); + console.log('✅ Merchant user loaded for password reset:', user.username); + }, + error: (error) => { + console.error('❌ Error loading merchant user for password reset:', error); + this.resetPasswordError = 'Erreur lors du chargement de l\'utilisateur'; + this.cdRef.detectChanges(); + } + }); + } + + // Méthode pour ouvrir le modal de suppression + openDeleteUserModal(userId: string) { + if (!this.canDeleteUsers) { + console.warn('User does not have permission to delete users'); + return; + } + + console.log(`🗑️ Opening delete modal for merchant user: ${userId}`); + this.merchantUsersService.getMerchantUserById(userId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (user) => { + this.selectedUserForDelete = user; + this.deleteUserError = ''; + this.openModal(this.deleteUserModal); + console.log('✅ Merchant user loaded for deletion:', user.username); + }, + error: (error) => { + console.error('❌ Error loading merchant user for deletion:', error); + this.deleteUserError = 'Erreur lors du chargement de l\'utilisateur'; + this.cdRef.detectChanges(); + } + }); + } + + + // ==================== OPÉRATIONS CRUD ==================== + + createUser() { + if (!this.canCreateUsers) { + this.createUserError = 'Vous n\'avez pas la permission de créer des utilisateurs'; + return; + } + + const validation = this.validateUserForm(); + if (!validation.isValid) { + this.createUserError = validation.error!; + console.error('❌ Form validation failed:', validation.error); + return; + } + + // Vérifier la permission pour attribuer le rôle sélectionné + if (!this.canAssignRole(this.newUser.role)) { + this.createUserError = `Vous n'avez pas la permission d'attribuer le rôle: ${this.getRoleLabel(this.newUser.role)}`; + return; + } + + // Validation spécifique au contexte marchand + if (!this.newUser.merchantPartnerId) { + this.createUserError = 'Merchant Partner ID est requis pour les utilisateurs marchands'; + return; + } + + this.creatingUser = true; + this.createUserError = ''; + + console.log('📤 Creating merchant user with data:', this.newUser); + + this.merchantUsersService.createMerchantUser(this.newUser) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (createdUser) => { + console.log('✅ Merchant user created successfully:', createdUser); + this.creatingUser = false; + this.modalService.dismissAll(); + this.refreshUsersList(); + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('❌ Error creating merchant user:', error); + this.creatingUser = false; + this.createUserError = this.getErrorMessage(error); + this.cdRef.detectChanges(); + } + }); + } + + /** + * Vérifie si un rôle est un rôle marchand + */ + isMerchantRole(role: UserRole): boolean { + const merchantRoles = [ + UserRole.DCB_PARTNER_ADMIN, + UserRole.DCB_PARTNER_MANAGER, + UserRole.DCB_PARTNER_SUPPORT + ]; + + return merchantRoles.includes(role); + } + + /** + * Vérifie si l'utilisateur peut attribuer un rôle spécifique + */ + canAssignRole(targetRole: UserRole): boolean { + return this.roleService.canAssignRole(this.currentUserRole, targetRole); + } + + // Réinitialiser le mot de passe + confirmResetPassword() { + if (!this.selectedUserForReset || !this.newPassword || this.newPassword.length < 8) { + this.resetPasswordError = 'Veuillez saisir un mot de passe valide (au moins 8 caractères).'; + console.error('❌ Password reset validation failed'); + return; + } + + console.log('🔑 Confirming password reset for merchant user:', this.selectedUserForReset.username); + + this.resettingPassword = true; + this.resetPasswordError = ''; + this.resetPasswordSuccess = ''; + + const resetPasswordDto: ResetPasswordDto = { + newPassword: this.newPassword, + temporary: this.temporaryPassword + }; + + this.merchantUsersService.resetMerchantUserPassword( + this.selectedUserForReset.id, + resetPasswordDto + ).pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (response) => { + console.log('✅ Merchant user password reset successfully'); + this.resettingPassword = false; + this.resetPasswordSuccess = 'Mot de passe réinitialisé avec succès !'; + this.cdRef.detectChanges(); + + // Fermer le modal après 2 secondes + setTimeout(() => { + this.modalService.dismissAll(); + }, 2000); + }, + error: (error) => { + console.error('❌ Error resetting merchant user password:', error); + this.resettingPassword = false; + this.resetPasswordError = this.getResetPasswordErrorMessage(error); + this.cdRef.detectChanges(); + } + }); + } + + confirmDeleteUser() { + if (!this.selectedUserForDelete || !this.canDeleteUsers) { + console.error('❌ No merchant user selected for deletion or no permission'); + return; + } + + console.log('🗑️ Confirming merchant user deletion:', this.selectedUserForDelete.username); + + this.deletingUser = true; + this.deleteUserError = ''; + + this.merchantUsersService.deleteMerchantUser(this.selectedUserForDelete.id) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => { + console.log('✅ Merchant user deleted successfully'); + this.deletingUser = false; + this.modalService.dismissAll(); + this.refreshUsersList(); + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('❌ Error deleting merchant user:', error); + this.deletingUser = false; + this.deleteUserError = this.getDeleteErrorMessage(error); + this.cdRef.detectChanges(); + } + }); + } + + // ==================== MÉTHODES UTILITAIRES ==================== + + private refreshUsersList(): void { + if (this.merchantUsersList && typeof this.merchantUsersList.refreshData === 'function') { + console.log('🔄 Refreshing merchant users list...'); + this.merchantUsersList.refreshData(); + } else { + console.warn('❌ MerchantUsersList component not available for refresh'); + this.showTab('list'); + } + } + + // Méthodes proxy pour le template + getRoleBadgeClass(role: UserRole): string { + return this.roleService.getRoleBadgeClass(role); + } + + getRoleLabel(role: UserRole): string { + return this.roleService.getRoleLabel(role); + } + + getRoleIcon(role: UserRole): string { + return this.roleService.getRoleIcon(role); + } + + getRoleDescription(role: UserRole): string { + const roleInfo = this.availableRoles.find(r => r.value === role); + return roleInfo?.description || 'Description non disponible'; + } + + getUserInitials(user: any): string { + return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U'; + } + + // ==================== GESTION DES ERREURS ==================== + + private getErrorMessage(error: any): string { + if (error.error?.message) { + return error.error.message; + } + if (error.status === 400) { + return 'Données invalides. Vérifiez les champs du formulaire.'; + } + if (error.status === 409) { + return 'Un utilisateur avec ce nom d\'utilisateur ou email existe déjà.'; + } + if (error.status === 403) { + return 'Vous n\'avez pas les permissions nécessaires pour cette action.'; + } + return 'Erreur lors de la création de l\'utilisateur. Veuillez réessayer.'; + } + + private getResetPasswordErrorMessage(error: any): string { + if (error.error?.message) { + return error.error.message; + } + if (error.status === 404) { + return 'Utilisateur non trouvé.'; + } + if (error.status === 400) { + return 'Le mot de passe ne respecte pas les critères de sécurité.'; + } + if (error.status === 403) { + return 'Vous n\'avez pas les permissions pour réinitialiser ce mot de passe.'; + } + return 'Erreur lors de la réinitialisation du mot de passe. Veuillez réessayer.'; + } + + private getDeleteErrorMessage(error: any): string { + if (error.error?.message) { + return error.error.message; + } + if (error.status === 404) { + return 'Utilisateur non trouvé.'; + } + if (error.status === 403) { + return 'Vous n\'avez pas les permissions pour supprimer cet utilisateur.'; + } + if (error.status === 409) { + return 'Impossible de supprimer cet utilisateur car il est associé à des données.'; + } + return 'Erreur lors de la suppression de l\'utilisateur. Veuillez réessayer.'; + } + + // ==================== VALIDATION DU FORMULAIRE ==================== + + private validateUserForm(): { isValid: boolean; error?: string } { + const requiredFields = [ + { field: this.newUser.username?.trim(), name: 'Nom d\'utilisateur' }, + { field: this.newUser.email?.trim(), name: 'Email' }, + { field: this.newUser.firstName?.trim(), name: 'Prénom' }, + { field: this.newUser.lastName?.trim(), name: 'Nom' }, + { field: this.selectedMerchantPartnerId?.trim(), name: 'Merchant Partner ID' } + ]; + + for (const { field, name } of requiredFields) { + if (!field) { + return { isValid: false, error: `${name} est requis` }; + } + } + + // Validation email + const email = this.newUser.email?.trim(); + if (!email) { + return { isValid: false, error: 'Email est requis' }; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return { isValid: false, error: 'Format d\'email invalide' }; + } + + if (!this.newUser.password || this.newUser.password.length < 8) { + return { isValid: false, error: 'Le mot de passe doit contenir au moins 8 caractères' }; + } + + if (!this.newUser.role) { + return { isValid: false, error: 'Le rôle est requis' }; + } + + return { isValid: true }; + } +} \ No newline at end of file diff --git a/src/app/modules/hub-users/hub-users.html b/src/app/modules/hub-users/hub-users.html deleted file mode 100644 index d365f0b..0000000 --- a/src/app/modules/hub-users/hub-users.html +++ /dev/null @@ -1,557 +0,0 @@ -
- - - - @if (currentUserRole) { -
-
-
-
- -
- - Rôle actuel : - - {{ roleService.getRoleLabel(currentUserRole) }} - - @if (!canCreateUsers) { - - - Permissions limitées - - } - -
- @if (canCreateUsers) { - - } -
-
-
-
- } - - -
-
- - -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/app/modules/hub-users/hub-users.routes.ts b/src/app/modules/hub-users/hub-users.routes.ts deleted file mode 100644 index 453b37e..0000000 --- a/src/app/modules/hub-users/hub-users.routes.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Routes } from '@angular/router'; -import { HubUsers } from './hub-users'; -import { authGuard } from '../../core/guards/auth.guard'; -import { roleGuard } from '../../core/guards/role.guard'; - -export const USERS_ROUTES: Routes = [ - { - path: 'users', - canActivate: [authGuard, roleGuard], - component: HubUsers, - data: { - title: 'Gestion des Utilisateurs', - requiredRoles: ['admin'] // pour information - } - } -]; \ No newline at end of file diff --git a/src/app/modules/hub-users/hub-users.spec.ts b/src/app/modules/hub-users/hub-users.spec.ts deleted file mode 100644 index 3fcfa19..0000000 --- a/src/app/modules/hub-users/hub-users.spec.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { HubUsers } from './hub-users'; -describe('Users', () => {}); \ No newline at end of file diff --git a/src/app/modules/hub-users/list/list.spec.ts b/src/app/modules/hub-users/list/list.spec.ts deleted file mode 100644 index dcf60b3..0000000 --- a/src/app/modules/hub-users/list/list.spec.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { UsersList } from './list'; -describe('UsersList', () => {}); \ No newline at end of file diff --git a/src/app/modules/hub-users/list/list.ts b/src/app/modules/hub-users/list/list.ts deleted file mode 100644 index fc66301..0000000 --- a/src/app/modules/hub-users/list/list.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, Input, OnDestroy } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { NgIcon } from '@ng-icons/core'; -import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; -import { Subject, takeUntil } from 'rxjs'; -import { HubUsersService } from '../services/hub-users.service'; -import { RoleManagementService } from '@core/services/role-management.service'; -import { UiCard } from '@app/components/ui-card'; - -import { - HubUserDto, - PaginatedUserResponse, - UserRole, - UserType -} from '@core/models/dcb-bo-hub-user.model'; - -@Component({ - selector: 'app-hub-users-list', - standalone: true, - imports: [ - CommonModule, - FormsModule, - NgIcon, - UiCard, - NgbPaginationModule - ], - templateUrl: './list.html', -}) -export class HubUsersList implements OnInit, OnDestroy { - private usersService = inject(HubUsersService); - private roleService = inject(RoleManagementService); - private cdRef = inject(ChangeDetectorRef); - private destroy$ = new Subject(); - - readonly UserRole = UserRole; - readonly UserType = UserType; - - @Input() canCreateUsers: boolean = false; - @Input() canDeleteUsers: boolean = false; - - @Output() userSelected = new EventEmitter(); - @Output() openCreateModal = new EventEmitter(); - @Output() openResetPasswordModal = new EventEmitter(); - @Output() openDeleteUserModal = new EventEmitter(); - - // Données - allUsers: HubUserDto[] = []; - filteredUsers: HubUserDto[] = []; - displayedUsers: HubUserDto[] = []; - - // États - loading = false; - error = ''; - - // Recherche et filtres - searchTerm = ''; - statusFilter: 'all' | 'enabled' | 'disabled' = 'all'; - emailVerifiedFilter: 'all' | 'verified' | 'not-verified' = 'all'; - roleFilter: UserRole | 'all' = 'all'; - - // Pagination - currentPage = 1; - itemsPerPage = 10; - totalItems = 0; - totalPages = 0; - - // Tri - sortField: keyof HubUserDto = 'username'; - sortDirection: 'asc' | 'desc' = 'asc'; - - // Rôles disponibles pour le filtre - availableRoles = [ - { value: 'all' as const, label: 'Tous les rôles' }, - { value: UserRole.DCB_ADMIN, label: 'Administrateurs DCB' }, - { value: UserRole.DCB_SUPPORT, label: 'Support DCB' }, - { value: UserRole.DCB_PARTNER, label: 'Partenaires DCB' } - ]; - - ngOnInit() { - this.loadUsers(); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - loadUsers() { - this.loading = true; - this.error = ''; - - this.usersService.getHubUsers(this.currentPage, this.itemsPerPage) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (response: PaginatedUserResponse) => { - this.allUsers = response.users as HubUserDto[]; - this.totalItems = response.total; - this.totalPages = response.totalPages; - this.applyFiltersAndPagination(); - this.loading = false; - this.cdRef.detectChanges(); - }, - error: (error) => { - this.error = 'Erreur lors du chargement des utilisateurs Hub'; - this.loading = false; - this.cdRef.detectChanges(); - console.error('Error loading hub users:', error); - } - }); - } - - // Recherche et filtres - onSearch() { - this.currentPage = 1; - this.applyFiltersAndPagination(); - } - - onClearFilters() { - this.searchTerm = ''; - this.statusFilter = 'all'; - this.emailVerifiedFilter = 'all'; - this.roleFilter = 'all'; - this.currentPage = 1; - this.applyFiltersAndPagination(); - } - - applyFiltersAndPagination() { - // Appliquer les filtres - this.filteredUsers = this.allUsers.filter(user => { - // Filtre de recherche - const matchesSearch = !this.searchTerm || - user.username.toLowerCase().includes(this.searchTerm.toLowerCase()) || - user.email.toLowerCase().includes(this.searchTerm.toLowerCase()) || - user.firstName?.toLowerCase().includes(this.searchTerm.toLowerCase()) || - user.lastName?.toLowerCase().includes(this.searchTerm.toLowerCase()); - - // Filtre par statut - const matchesStatus = this.statusFilter === 'all' || - (this.statusFilter === 'enabled' && user.enabled) || - (this.statusFilter === 'disabled' && !user.enabled); - - // Filtre par email vérifié - const matchesEmailVerified = this.emailVerifiedFilter === 'all' || - (this.emailVerifiedFilter === 'verified' && user.emailVerified) || - (this.emailVerifiedFilter === 'not-verified' && !user.emailVerified); - - // Filtre par rôle - const matchesRole = this.roleFilter === 'all' || user.role === this.roleFilter; - - return matchesSearch && matchesStatus && matchesEmailVerified && matchesRole; - }); - - // Appliquer le tri - this.filteredUsers.sort((a, b) => { - const aValue = a[this.sortField]; - const bValue = b[this.sortField]; - - if (aValue === bValue) return 0; - - let comparison = 0; - if (typeof aValue === 'string' && typeof bValue === 'string') { - comparison = aValue.localeCompare(bValue); - } else if (typeof aValue === 'number' && typeof bValue === 'number') { - comparison = aValue - bValue; - } else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') { - comparison = (aValue === bValue) ? 0 : aValue ? -1 : 1; - } - - return this.sortDirection === 'asc' ? comparison : -comparison; - }); - - // Calculer la pagination - this.totalItems = this.filteredUsers.length; - this.totalPages = Math.ceil(this.totalItems / this.itemsPerPage); - - // Appliquer la pagination - const startIndex = (this.currentPage - 1) * this.itemsPerPage; - const endIndex = startIndex + this.itemsPerPage; - this.displayedUsers = this.filteredUsers.slice(startIndex, endIndex); - } - - // Tri - sort(field: keyof HubUserDto) { - if (this.sortField === field) { - this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; - } else { - this.sortField = field; - this.sortDirection = 'asc'; - } - this.applyFiltersAndPagination(); - } - - getSortIcon(field: keyof HubUserDto): string { - if (this.sortField !== field) return 'lucideArrowUpDown'; - return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown'; - } - - // Pagination - onPageChange(page: number) { - this.currentPage = page; - this.applyFiltersAndPagination(); - } - - getStartIndex(): number { - return (this.currentPage - 1) * this.itemsPerPage + 1; - } - - getEndIndex(): number { - return Math.min(this.currentPage * this.itemsPerPage, this.totalItems); - } - - // Actions - viewUserProfile(userId: string) { - this.userSelected.emit(userId); - } - - // Méthode pour réinitialiser le mot de passe - resetPassword(user: HubUserDto) { - this.openResetPasswordModal.emit(user.id); - } - - // Méthode pour ouvrir le modal de suppression - deleteUser(user: HubUserDto) { - if (this.canDeleteUsers) { - this.openDeleteUserModal.emit(user.id); - } - } - - enableUser(user: HubUserDto) { - this.usersService.enableHubUser(user.id) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (updatedUser) => { - // Mettre à jour l'utilisateur dans la liste - const index = this.allUsers.findIndex(u => u.id === user.id); - if (index !== -1) { - this.allUsers[index] = updatedUser; - } - this.applyFiltersAndPagination(); - this.cdRef.detectChanges(); - }, - error: (error) => { - console.error('Error enabling hub user:', error); - this.error = 'Erreur lors de l\'activation de l\'utilisateur'; - this.cdRef.detectChanges(); - } - }); - } - - disableUser(user: HubUserDto) { - this.usersService.disableHubUser(user.id) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (updatedUser) => { - // Mettre à jour l'utilisateur dans la liste - const index = this.allUsers.findIndex(u => u.id === user.id); - if (index !== -1) { - this.allUsers[index] = updatedUser; - } - this.applyFiltersAndPagination(); - this.cdRef.detectChanges(); - }, - error: (error) => { - console.error('Error disabling hub user:', error); - this.error = 'Erreur lors de la désactivation de l\'utilisateur'; - this.cdRef.detectChanges(); - } - }); - } - - // Utilitaires d'affichage - getStatusBadgeClass(user: HubUserDto): string { - if (!user.enabled) return 'badge bg-danger'; - if (!user.emailVerified) return 'badge bg-warning'; - return 'badge bg-success'; - } - - getStatusText(user: HubUserDto): string { - if (!user.enabled) return 'Désactivé'; - if (!user.emailVerified) return 'Email non vérifié'; - return 'Actif'; - } - - getRoleBadgeClass(role: UserRole): string { - return this.roleService.getRoleBadgeClass(role); - } - - getRoleLabel(role: UserRole): string { - return this.roleService.getRoleLabel(role); - } - - getRoleIcon(role: UserRole): string { - return this.roleService.getRoleIcon(role); - } - - formatTimestamp(timestamp: number): string { - if (!timestamp) return 'Non disponible'; - return new Date(timestamp).toLocaleDateString('fr-FR', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - } - - getUserInitials(user: HubUserDto): string { - return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U'; - } - - getUserDisplayName(user: HubUserDto): string { - if (user.firstName && user.lastName) { - return `${user.firstName} ${user.lastName}`; - } - return user.username; - } - - // Statistiques - getUsersCountByRole(role: UserRole): number { - return this.allUsers.filter(user => user.role === role).length; - } - - getEnabledUsersCount(): number { - return this.allUsers.filter(user => user.enabled).length; - } - - getDisabledUsersCount(): number { - return this.allUsers.filter(user => !user.enabled).length; - } - - getEmailVerifiedCount(): number { - return this.allUsers.filter(user => user.emailVerified).length; - } - - getEmailNotVerifiedCount(): number { - return this.allUsers.filter(user => !user.emailVerified).length; - } - - // Recherche rapide par rôle - filterByRole(role: UserRole | 'all') { - this.roleFilter = role; - this.currentPage = 1; - this.applyFiltersAndPagination(); - } - - // Recharger les données - refreshData() { - this.loadUsers(); - } - - // Méthode pour charger plus d'utilisateurs (scroll infini optionnel) - loadMoreUsers() { - if (this.currentPage < this.totalPages) { - this.currentPage++; - this.usersService.getHubUsers(this.currentPage, this.itemsPerPage) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (response: PaginatedUserResponse) => { - this.allUsers = [...this.allUsers, ...(response.users as HubUserDto[])]; - this.applyFiltersAndPagination(); - this.cdRef.detectChanges(); - }, - error: (error) => { - console.error('Error loading more hub users:', error); - this.error = 'Erreur lors du chargement des utilisateurs supplémentaires'; - this.cdRef.detectChanges(); - } - }); - } - } -} \ No newline at end of file diff --git a/src/app/modules/hub-users/models/hub-user.model.ts b/src/app/modules/hub-users/models/hub-user.model.ts deleted file mode 100644 index 9e7b94e..0000000 --- a/src/app/modules/hub-users/models/hub-user.model.ts +++ /dev/null @@ -1,115 +0,0 @@ -// src/app/core/models/user.model.ts - -export enum UserType { - HUB = 'HUB', - MERCHANT = 'MERCHANT', - MERCHANT_USER = 'MERCHANT_USER' -} - -export enum UserRole { - // HUB roles - DCB_ADMIN = 'DCB_ADMIN', - DCB_SUPPORT = 'DCB_SUPPORT', - DCB_PARTNER = 'DCB_PARTNER', - - // MERCHANT roles - DCB_PARTNER_ADMIN = 'DCB_PARTNER_ADMIN', - DCB_PARTNER_MANAGER = 'DCB_PARTNER_MANAGER', - DCB_PARTNER_SUPPORT = 'DCB_PARTNER_SUPPORT' -} - -// === BASE USER MODEL === -export interface BaseUserDto { - id: string; - username: string; - email: string; - firstName: string; - lastName: string; - role: UserRole; - enabled: boolean; - emailVerified: boolean; - createdBy: string; - createdByUsername: string; - createdTimestamp: number; - lastLogin?: number; - userType: UserType; -} - -// === EXTENSIONS === -export interface HubUserDto extends BaseUserDto { - userType: UserType.HUB; -} - -export interface MerchantUserDto extends BaseUserDto { - userType: UserType.MERCHANT; - merchantPartnerId: string; -} - -// === DTOs CRUD === -export interface CreateUserDto { - username: string; - email: string; - firstName: string; - lastName: string; - password: string; - role: UserRole; - enabled?: boolean; - emailVerified?: boolean; - merchantPartnerId?: string; // obligatoire si MERCHANT -} - -export interface UpdateUserDto { - firstName?: string; - lastName?: string; - email?: string; - enabled?: boolean; -} - -export interface ResetPasswordDto { - userId?: string; - newPassword: string; - temporary?: boolean; -} - -// === PAGINATION / STATS === -export interface PaginatedUserResponse { - users: BaseUserDto[]; - total: number; - page: number; - limit: number; - totalPages: number; -} - -export interface MerchantPartnerStatsResponse { - totalAdmins: number; - totalManagers: number; - totalSupport: number; - totalUsers: number; - activeUsers: number; - inactiveUsers: number; -} - -// === ROLES === -export interface AvailableRole { - value: UserRole; - label: string; - description: string; - allowedForCreation: boolean; -} - -export interface AvailableRolesResponse { - roles: AvailableRole[]; -} - -export interface RoleOperationResponse { - message: string; - success: boolean; -} - -// === SEARCH === -export interface SearchUsersParams { - query?: string; - role?: UserRole; - enabled?: boolean; - userType?: UserType; -} diff --git a/src/app/modules/hub-users/models/hub-userl-v1.mode.ts b/src/app/modules/hub-users/models/hub-userl-v1.mode.ts deleted file mode 100644 index 58d6f00..0000000 --- a/src/app/modules/hub-users/models/hub-userl-v1.mode.ts +++ /dev/null @@ -1,54 +0,0 @@ -// src/app/modules/users/models/user.model.ts -export enum UserRole { - DCB_ADMIN = 'DCB_ADMIN', - DCB_SUPPORT = 'DCB_SUPPORT', - DCB_PARTNER = 'DCB_PARTNER' -} - -export interface HubUserResponse { - id: string; - username: string; - email: string; - firstName: string; - lastName: string; - role: UserRole; - enabled: boolean; - emailVerified: boolean; - createdBy: string; - createdByUsername: string; - createdTimestamp: number; - lastLogin?: number; - userType: 'HUB'; -} - -export interface CreateHubUserDto { - username: string; - email: string; - firstName: string; - lastName: string; - password: string; - role: UserRole; - enabled?: boolean; - emailVerified?: boolean; -} - -export interface UpdateHubUserDto { - firstName?: string; - lastName?: string; - email?: string; - enabled?: boolean; -} - -export interface ResetPasswordDto { - userId: string; - newPassword: string; - temporary?: boolean; -} - -export interface PaginatedUserResponse { - users: HubUserResponse[]; - total: number; - page: number; - limit: number; - totalPages: number; -} \ No newline at end of file diff --git a/src/app/modules/hub-users/models/user.model.ts b/src/app/modules/hub-users/models/user.model.ts deleted file mode 100644 index e58c6ec..0000000 --- a/src/app/modules/hub-users/models/user.model.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { IsString, IsEmail, IsBoolean, IsOptional, IsArray, MinLength } from 'class-validator'; - -export class User { - id?: string; - username: string = ''; - email: string = ''; - firstName?: string = ''; - lastName?: string = ''; - enabled: boolean = true; - emailVerified: boolean = false; - attributes?: Record = {}; - clientRoles: string[] = []; - createdTimestamp?: number; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class UserCredentials { - type: string = 'password'; - value: string = ''; - temporary: boolean = false; - - constructor(type?: string, value?: string, temporary?: boolean) { - if (type) this.type = type; - if (value) this.value = value; - if (temporary !== undefined) this.temporary = temporary; - } -} - -export class CreateUserDto { - @IsString() - @MinLength(3) - username: string = ''; - - @IsEmail() - email: string = ''; - - @IsOptional() - @IsString() - firstName: string = ''; - - @IsOptional() - @IsString() - lastName: string = ''; - - @IsString() - @MinLength(8) - password: string = ''; - - @IsOptional() - @IsBoolean() - enabled: boolean = true; - - @IsOptional() - @IsBoolean() - emailVerified: boolean = false; - - @IsOptional() - attributes?: Record = {}; - - @IsOptional() - @IsArray() - clientRoles: string[] = []; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class UpdateUserDto { - @IsOptional() - @IsString() - username?: string; - - @IsOptional() - @IsEmail() - email?: string; - - @IsOptional() - @IsString() - firstName?: string; - - @IsOptional() - @IsString() - lastName?: string; - - @IsOptional() - @IsBoolean() - enabled?: boolean; - - @IsOptional() - @IsBoolean() - emailVerified?: boolean; - - @IsOptional() - attributes?: Record; - - @IsOptional() - @IsArray() - clientRoles?: string[]; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class UserQueryDto { - @IsOptional() - page: number = 1; - - @IsOptional() - limit: number = 10; - - @IsOptional() - @IsString() - search?: string; - - @IsOptional() - @IsBoolean() - enabled?: boolean; - - @IsOptional() - @IsBoolean() - emailVerified?: boolean; - - @IsOptional() - @IsString() - email?: string; - - @IsOptional() - @IsString() - username?: string; - - @IsOptional() - @IsString() - firstName?: string; - - @IsOptional() - @IsString() - lastName?: string; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class ResetPasswordDto { - @IsString() - userId: string = ''; - - @IsString() - @MinLength(8) - newPassword: string = ''; - - @IsOptional() - @IsBoolean() - temporary: boolean = false; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class UserResponse { - id: string = ''; - username: string = ''; - email: string = ''; - firstName: string = ''; - lastName: string = ''; - enabled: boolean = true; - emailVerified: boolean = false; - attributes: Record = {}; - clientRoles: string[] = []; - createdTimestamp: number = Date.now(); - - constructor(user?: any) { - if (user) { - this.id = user.id || ''; - this.username = user.username || ''; - this.email = user.email || ''; - this.firstName = user.firstName || ''; - this.lastName = user.lastName || ''; - this.enabled = user.enabled ?? true; - this.emailVerified = user.emailVerified ?? false; - this.attributes = user.attributes || {}; - this.clientRoles = user.clientRoles || []; - this.createdTimestamp = user.createdTimestamp || Date.now(); - } - } -} - -export class PaginatedUserResponse { - users: UserResponse[] = []; - total: number = 0; - page: number = 1; - limit: number = 10; - totalPages: number = 0; - - constructor(users: UserResponse[] = [], total: number = 0, page: number = 1, limit: number = 10) { - this.users = users; - this.total = total; - this.page = page; - this.limit = limit; - this.totalPages = Math.ceil(total / limit) || 0; - } -} - -export class AssignRolesDto { - @IsArray() - @IsString({ each: true }) - roles: string[] = []; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class LoginDto { - @IsString() - username: string = ''; - - @IsString() - password: string = ''; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class TokenResponse { - access_token: string = ''; - refresh_token?: string = ''; - expires_in: number = 0; - token_type: string = ''; - scope?: string = ''; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class ApiResponse { - data: T | null = null; - message: string = ''; - status: string = ''; - - constructor(partial?: Partial>) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class UserRoleMapping { - id: string = ''; - name: string = ''; - description?: string = ''; - composite?: boolean = false; - clientRole?: boolean = false; - containerId?: string = ''; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class UserSession { - id: string = ''; - username: string = ''; - userId: string = ''; - ipAddress: string = ''; - start: number = 0; - lastAccess: number = 0; - clients: Record = {}; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -// Types pour les rôles client -export type ClientRole = - | 'dcb-admin' - | 'dcb-partner' - | 'dcb-support' - | 'dcb-partner-admin' - | 'dcb-partner-manager' - | 'dcb-partner-support' - | 'dcb-partner-user'; \ No newline at end of file diff --git a/src/app/modules/hub-users/profile/profile.spec.ts b/src/app/modules/hub-users/profile/profile.spec.ts deleted file mode 100644 index 032e4d3..0000000 --- a/src/app/modules/hub-users/profile/profile.spec.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { UserProfile } from './profile'; -describe('UserProfile', () => {}); \ No newline at end of file diff --git a/src/app/modules/hub-users/profile/profile.ts b/src/app/modules/hub-users/profile/profile.ts deleted file mode 100644 index c955d45..0000000 --- a/src/app/modules/hub-users/profile/profile.ts +++ /dev/null @@ -1,378 +0,0 @@ -// src/app/modules/users/profile/profile.ts -import { Component, inject, OnInit, Input, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { NgIcon } from '@ng-icons/core'; -import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'; -import { Subject, takeUntil } from 'rxjs'; -import { HubUsersService } from '../services/hub-users.service'; -import { RoleManagementService } from '@core/services/role-management.service'; -import { AuthService } from '@core/services/auth.service'; - -import { - HubUserDto, - UpdateUserDto, - UserRole -} from '@core/models/dcb-bo-hub-user.model'; - -@Component({ - selector: 'app-hub-user-profile', - standalone: true, - imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule], - templateUrl: './profile.html', - styles: [` - .avatar-lg { - width: 80px; - height: 80px; - } - .fs-24 { - font-size: 24px; - } - `] -}) -export class HubUserProfile implements OnInit, OnDestroy { - private usersService = inject(HubUsersService); - private roleService = inject(RoleManagementService); - private authService = inject(AuthService); - private cdRef = inject(ChangeDetectorRef); - private destroy$ = new Subject(); - - @Input() userId!: string; - @Output() back = new EventEmitter(); - @Output() openResetPasswordModal = new EventEmitter(); - - user: HubUserDto | null = null; - loading = false; - saving = false; - error = ''; - success = ''; - - // Gestion des permissions - currentUserRole: UserRole | null = null; - canEditUsers = false; - canManageRoles = false; - canDeleteUsers = false; - - // Édition - isEditing = false; - editedUser: UpdateUserDto = {}; - - // Gestion des rôles - availableRoles: { value: UserRole; label: string; description: string }[] = []; - updatingRoles = false; - - ngOnInit() { - if (this.userId) { - this.initializeUserPermissions(); - this.loadAvailableRoles(); - this.loadUserProfile(); - } - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - /** - * Initialise les permissions de l'utilisateur courant - */ - private initializeUserPermissions(): void { - this.authService.loadUserProfile() - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (profile) => { - this.currentUserRole = profile?.role?.[0] as UserRole || null; - if (this.currentUserRole) { - this.canEditUsers = this.roleService.canEditUsers(this.currentUserRole); - this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole); - this.canDeleteUsers = this.roleService.canDeleteUsers(this.currentUserRole); - } - }, - error: (error) => { - console.error('Error loading user permissions:', error); - } - }); - } - - /** - * Charge les rôles disponibles - */ - private loadAvailableRoles(): void { - this.usersService.getAvailableHubRoles() - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (response) => { - this.availableRoles = response.roles.map(role => ({ - value: role.value, - label: role.label, - description: role.description - })); - }, - error: (error) => { - console.error('Error loading available hub roles:', error); - // Fallback - this.availableRoles = [ - { value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' }, - { value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' }, - { value: UserRole.DCB_PARTNER, label: 'DCB Partner', description: 'Partenaire commercial' } - ]; - } - }); - } - - loadUserProfile() { - this.loading = true; - this.error = ''; - - this.usersService.getHubUserById(this.userId) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (user) => { - this.user = user; - this.loading = false; - this.cdRef.detectChanges(); - }, - error: (error) => { - this.error = 'Erreur lors du chargement du profil utilisateur Hub'; - this.loading = false; - this.cdRef.detectChanges(); - console.error('Error loading hub user profile:', error); - } - }); - } - - startEditing() { - if (!this.canEditUsers) { - this.error = 'Vous n\'avez pas la permission de modifier les utilisateurs'; - return; - } - - this.isEditing = true; - this.editedUser = { - firstName: this.user?.firstName, - lastName: this.user?.lastName, - email: this.user?.email, - enabled: this.user?.enabled - }; - this.cdRef.detectChanges(); - } - - cancelEditing() { - this.isEditing = false; - this.editedUser = {}; - this.error = ''; - this.success = ''; - this.cdRef.detectChanges(); - } - - saveProfile() { - if (!this.user || !this.canEditUsers) return; - - this.saving = true; - this.error = ''; - this.success = ''; - - this.usersService.updateHubUser(this.user.id, this.editedUser) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (updatedUser) => { - this.user = updatedUser; - this.isEditing = false; - this.saving = false; - this.success = 'Profil mis à jour avec succès'; - this.editedUser = {}; - this.cdRef.detectChanges(); - }, - error: (error) => { - this.error = this.getErrorMessage(error); - this.saving = false; - this.cdRef.detectChanges(); - } - }); - } - - // Gestion des rôles - updateUserRole(newRole: UserRole) { - if (!this.user || !this.canManageRoles) return; - - // Vérifier que l'utilisateur peut attribuer ce rôle - if (!this.roleService.canAssignRole(this.currentUserRole, newRole)) { - this.error = 'Vous n\'avez pas la permission d\'attribuer ce rôle'; - return; - } - - // Vérifier que c'est un rôle Hub valide - const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]; - if (!hubRoles.includes(newRole)) { - this.error = 'Rôle Hub invalide'; - return; - } - - this.updatingRoles = true; - this.error = ''; - this.success = ''; - - this.usersService.updateHubUserRole(this.user.id, newRole) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (updatedUser) => { - this.user = updatedUser; - this.updatingRoles = false; - this.success = 'Rôle mis à jour avec succès'; - this.cdRef.detectChanges(); - }, - error: (error) => { - this.updatingRoles = false; - this.error = this.getErrorMessage(error); - this.cdRef.detectChanges(); - } - }); - } - - // Gestion du statut - enableUser() { - if (!this.user || !this.canEditUsers) return; - - this.usersService.enableHubUser(this.user.id) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (updatedUser) => { - this.user = updatedUser; - this.success = 'Utilisateur Hub activé avec succès'; - this.cdRef.detectChanges(); - }, - error: (error) => { - this.error = this.getErrorMessage(error); - this.cdRef.detectChanges(); - } - }); - } - - disableUser() { - if (!this.user || !this.canEditUsers) return; - - this.usersService.disableHubUser(this.user.id) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (updatedUser) => { - this.user = updatedUser; - this.success = 'Utilisateur Hub désactivé avec succès'; - this.cdRef.detectChanges(); - }, - error: (error) => { - this.error = this.getErrorMessage(error); - this.cdRef.detectChanges(); - } - }); - } - - // Réinitialisation du mot de passe - resetPassword() { - if (this.user) { - this.openResetPasswordModal.emit(this.user.id); - } - } - - // Gestion des erreurs - private getErrorMessage(error: any): string { - if (error.error?.message) { - return error.error.message; - } - if (error.status === 403) { - return 'Vous n\'avez pas les permissions nécessaires pour cette action'; - } - if (error.status === 404) { - return 'Utilisateur Hub non trouvé'; - } - if (error.status === 400) { - return 'Données invalides'; - } - return 'Une erreur est survenue. Veuillez réessayer.'; - } - - // Utilitaires d'affichage - getStatusBadgeClass(): string { - if (!this.user) return 'badge bg-secondary'; - if (!this.user.enabled) return 'badge bg-danger'; - if (!this.user.emailVerified) return 'badge bg-warning'; - return 'badge bg-success'; - } - - getStatusText(): string { - if (!this.user) return 'Inconnu'; - if (!this.user.enabled) return 'Désactivé'; - if (!this.user.emailVerified) return 'Email non vérifié'; - return 'Actif'; - } - - formatTimestamp(timestamp: number): string { - if (!timestamp) return 'Non disponible'; - return new Date(timestamp).toLocaleDateString('fr-FR', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - } - - getUserInitials(): string { - if (!this.user) return 'U'; - return (this.user.firstName?.charAt(0) || '') + (this.user.lastName?.charAt(0) || '') || 'U'; - } - - getUserDisplayName(): string { - if (!this.user) return 'Utilisateur Hub'; - if (this.user.firstName && this.user.lastName) { - return `${this.user.firstName} ${this.user.lastName}`; - } - return this.user.username; - } - - getRoleBadgeClass(role: UserRole): string { - return this.roleService.getRoleBadgeClass(role); - } - - getRoleLabel(role: UserRole): string { - return this.roleService.getRoleLabel(role); - } - - getRoleIcon(role: UserRole): string { - return this.roleService.getRoleIcon(role); - } - - getRoleDescription(role: UserRole): string { - const roleInfo = this.availableRoles.find(r => r.value === role); - return roleInfo?.description || 'Description non disponible'; - } - - // Vérification des permissions pour les actions - canAssignRole(targetRole: UserRole): boolean { - return this.roleService.canAssignRole(this.currentUserRole, targetRole); - } - - // Vérifie si c'est le profil de l'utilisateur courant - isCurrentUserProfile(): boolean { - if (!this.user || !this.user.id) return false; - return this.authService.isCurrentUserProfile(this.user.id); - } - - // Vérifier les permissions - canEditUser(): boolean { - if (this.isCurrentUserProfile()) return true; // Toujours éditer son profil - return this.authService.canManageHubUsers(); - } - - // Méthode pour obtenir les rôles Hub assignables - getAssignableHubRoles(): UserRole[] { - const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]; - return hubRoles.filter(role => this.canAssignRole(role)); - } - - // Vérifie si un rôle est un rôle Hub - isHubRole(role: UserRole): boolean { - const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]; - return hubRoles.includes(role); - } -} \ No newline at end of file diff --git a/src/app/modules/hub-users/services/api.service.ts b/src/app/modules/hub-users/services/api.service.ts deleted file mode 100644 index 9945345..0000000 --- a/src/app/modules/hub-users/services/api.service.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; -import { Observable, throwError } from 'rxjs'; -import { catchError } from 'rxjs/operators'; -import { environment } from '@environments/environment'; - -export interface ApiResponse { - data?: T; - message?: string; - success: boolean; - status?: string; -} - -@Injectable({ - providedIn: 'root' -}) -export class ApiService { - private http = inject(HttpClient); - private baseUrl = environment.iamApiUrl; - - private getHeaders(): HttpHeaders { - const token = localStorage.getItem('access_token'); - return new HttpHeaders({ - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }); - } - - get(endpoint: string, params?: any): Observable { - return this.http.get(`${this.baseUrl}/${endpoint}`, { - headers: this.getHeaders(), - params: this.createParams(params) - }).pipe( - catchError(this.handleError) - ); - } - - post(endpoint: string, data: any): Observable { - return this.http.post(`${this.baseUrl}/${endpoint}`, data, { - headers: this.getHeaders() - }).pipe( - catchError(this.handleError) - ); - } - - put(endpoint: string, data: any): Observable { - return this.http.put(`${this.baseUrl}/${endpoint}`, data, { - headers: this.getHeaders() - }).pipe( - catchError(this.handleError) - ); - } - - delete(endpoint: string): Observable { - return this.http.delete(`${this.baseUrl}/${endpoint}`, { - headers: this.getHeaders() - }).pipe( - catchError(this.handleError) - ); - } - - private createParams(params: any): HttpParams { - let httpParams = new HttpParams(); - if (params) { - Object.keys(params).forEach(key => { - if (params[key] !== null && params[key] !== undefined) { - httpParams = httpParams.set(key, params[key].toString()); - } - }); - } - return httpParams; - } - - private handleError(error: any) { - console.error('API Error:', error); - return throwError(() => error); - } -} \ No newline at end of file diff --git a/src/app/modules/hub-users/services/hub-users.service.ts b/src/app/modules/hub-users/services/hub-users.service.ts deleted file mode 100644 index 547766a..0000000 --- a/src/app/modules/hub-users/services/hub-users.service.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient, HttpParams } from '@angular/common/http'; -import { environment } from '@environments/environment'; -import { Observable, map, catchError, throwError, of } from 'rxjs'; - -import { - HubUserDto, - CreateUserDto, - UpdateUserDto, - ResetPasswordDto, - PaginatedUserResponse, - AvailableRolesResponse, - SearchUsersParams, - UserRole, - UserType, - MerchantUserDto -} from '@core/models/dcb-bo-hub-user.model'; - -@Injectable({ providedIn: 'root' }) -export class HubUsersService { - private http = inject(HttpClient); - private apiUrl = `${environment.iamApiUrl}/hub-users`; - - // === CRÉATION === - - /** - * Crée un nouvel utilisateur Hub - */ - createHubUser(createUserDto: CreateUserDto): Observable { - // Validation pour les utilisateurs Hub - if (!createUserDto.username?.trim()) { - return throwError(() => 'Username is required and cannot be empty'); - } - - if (!createUserDto.email?.trim()) { - return throwError(() => 'Email is required and cannot be empty'); - } - - if (!createUserDto.password || createUserDto.password.length < 8) { - return throwError(() => 'Password must be at least 8 characters'); - } - - if (!createUserDto.role) { - return throwError(() => 'Role is required'); - } - - // Vérification que le rôle est bien un rôle Hub - const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]; - if (!hubRoles.includes(createUserDto.role)) { - return throwError(() => 'Invalid role for Hub user'); - } - - // Nettoyage des données - const payload = { - ...createUserDto, - username: createUserDto.username.trim(), - email: createUserDto.email.trim(), - firstName: (createUserDto.firstName || '').trim(), - lastName: (createUserDto.lastName || '').trim(), - enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true, - emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : false, - }; - - return this.http.post(this.apiUrl, payload).pipe( - catchError(error => { - console.error('Error creating hub user:', error); - return throwError(() => error); - }) - ); - } - - // === LECTURE === - - /** - * Récupère tous les utilisateurs Hub avec pagination - */ - getHubUsers(page: number = 1, limit: number = 10, filters?: SearchUsersParams): Observable { - let params = new HttpParams() - .set('page', page.toString()) - .set('limit', limit.toString()) - .set('userType', UserType.HUB); - - if (filters) { - Object.keys(filters).forEach(key => { - if (filters[key as keyof SearchUsersParams] !== undefined && filters[key as keyof SearchUsersParams] !== null) { - params = params.set(key, filters[key as keyof SearchUsersParams]!.toString()); - } - }); - } - - return this.http.get(this.apiUrl, { params, observe: 'response' }).pipe( - map(response => { - const users = response.body || []; - const total = parseInt(response.headers.get('X-Total-Count') || '0'); - - return { - users, - total, - page, - limit, - totalPages: Math.ceil(total / limit) - }; - }), - catchError(error => { - console.error('Error loading hub users:', error); - return throwError(() => error); - }) - ); - } - - /** - * Get all merchant partners - */ - /** - * Récupère tous les utilisateurs Hub avec pagination - */ - findAllMerchantUsers(page: number = 1, limit: number = 10, filters?: any): Observable { - let params = new HttpParams() - .set('page', page.toString()) - .set('limit', limit.toString()); - - if (filters) { - Object.keys(filters).forEach(key => { - if (filters[key] !== undefined && filters[key] !== null) { - params = params.set(key, filters[key].toString()); - } - }); - } - - return this.http.get(`${this.apiUrl}/merchants/all`, { params, observe: 'response' }).pipe( - map(response => { - const users = response.body || []; - const total = parseInt(response.headers.get('X-Total-Count') || '0'); - - return { - users, - total, - page, - limit, - totalPages: Math.ceil(total / limit) - }; - }), - catchError(error => { - console.error('Error loading users:', error); - return throwError(() => error); - }) - ); - } - - /** - * Récupère un utilisateur Hub par ID - */ - getHubUserById(id: string): Observable { - return this.http.get(`${this.apiUrl}/${id}`).pipe( - catchError(error => { - console.error(`Error loading hub user ${id}:`, error); - return throwError(() => error); - }) - ); - } - - // === MISE À JOUR === - - /** - * Met à jour un utilisateur Hub - */ - updateHubUser(id: string, updateUserDto: UpdateUserDto): Observable { - return this.http.put(`${this.apiUrl}/${id}`, updateUserDto).pipe( - catchError(error => { - console.error(`Error updating hub user ${id}:`, error); - return throwError(() => error); - }) - ); - } - - /** - * Met à jour le rôle d'un utilisateur Hub - */ - updateHubUserRole(id: string, role: UserRole): Observable { - const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]; - if (!hubRoles.includes(role)) { - return throwError(() => 'Invalid role for Hub user'); - } - - return this.http.put(`${this.apiUrl}/${id}/role`, { role }).pipe( - catchError(error => { - console.error(`Error updating role for hub user ${id}:`, error); - return throwError(() => error); - }) - ); - } - - // === SUPPRESSION === - - /** - * Supprime un utilisateur Hub - */ - deleteHubUser(id: string): Observable<{ message: string }> { - return this.http.delete<{ message: string }>(`${this.apiUrl}/${id}`).pipe( - catchError(error => { - console.error(`Error deleting hub user ${id}:`, error); - return throwError(() => error); - }) - ); - } - - // === GESTION DES MOTS DE PASSE === - - /** - * Réinitialise le mot de passe d'un utilisateur Hub - */ - resetHubUserPassword(id: string, resetPasswordDto: ResetPasswordDto): Observable<{ message: string }> { - return this.http.post<{ message: string }>( - `${this.apiUrl}/${id}/reset-password`, - resetPasswordDto - ).pipe( - catchError(error => { - console.error(`Error resetting password for hub user ${id}:`, error); - return throwError(() => error); - }) - ); - } - - /** - * Envoie un email de réinitialisation de mot de passe - */ - sendHubUserPasswordResetEmail(id: string): Observable<{ message: string }> { - return this.http.post<{ message: string }>( - `${this.apiUrl}/${id}/send-reset-email`, - {} - ).pipe( - catchError(error => { - console.error(`Error sending reset email for hub user ${id}:`, error); - return throwError(() => error); - }) - ); - } - - // === GESTION DU STATUT === - - /** - * Active un utilisateur Hub - */ - enableHubUser(id: string): Observable { - return this.updateHubUser(id, { enabled: true }); - } - - /** - * Désactive un utilisateur Hub - */ - disableHubUser(id: string): Observable { - return this.updateHubUser(id, { enabled: false }); - } - - // === GESTION DES RÔLES === - - /** - * Récupère les rôles Hub disponibles - */ - getAvailableHubRoles(): Observable { - return this.http.get(`${this.apiUrl}/roles/available`).pipe( - catchError(error => { - console.error('Error loading available hub roles:', error); - // Fallback en cas d'erreur - return of({ - roles: [ - { - value: UserRole.DCB_ADMIN, - label: 'DCB Admin', - description: 'Full administrative access to the entire system', - allowedForCreation: true - }, - { - value: UserRole.DCB_SUPPORT, - label: 'DCB Support', - description: 'Support access with limited administrative capabilities', - allowedForCreation: true - }, - { - value: UserRole.DCB_PARTNER, - label: 'DCB Partner', - description: 'Partner access to merchant management', - allowedForCreation: true - } - ] - }); - }) - ); - } - - /** - * Récupère les utilisateurs par rôle spécifique - */ - getHubUsersByRole(role: UserRole): Observable { - return this.http.get(`${this.apiUrl}/role/${role}`).pipe( - catchError(error => { - console.error(`Error loading hub users with role ${role}:`, error); - return throwError(() => error); - }) - ); - } - - // === RECHERCHE === - - /** - * Recherche des utilisateurs Hub - */ - searchHubUsers(params: SearchUsersParams): Observable { - let httpParams = new HttpParams().set('userType', UserType.HUB); - - if (params.query) { - httpParams = httpParams.set('query', params.query); - } - - if (params.role) { - httpParams = httpParams.set('role', params.role); - } - - if (params.enabled !== undefined) { - httpParams = httpParams.set('enabled', params.enabled.toString()); - } - - return this.http.get(`${this.apiUrl}/search`, { params: httpParams }).pipe( - catchError(error => { - console.error('Error searching hub users:', error); - return throwError(() => error); - }) - ); - } - - // === STATISTIQUES === - - /** - * Récupère les statistiques des utilisateurs Hub - */ - getHubUsersStats(): Observable { - return this.http.get(`${this.apiUrl}/stats/overview`).pipe( - catchError(error => { - console.error('Error loading hub users stats:', error); - return throwError(() => error); - }) - ); - } - - // === UTILITAIRES === - - /** - * Vérifie si un nom d'utilisateur existe parmi les utilisateurs Hub - */ - hubUserExists(username: string): Observable<{ exists: boolean }> { - return this.searchHubUsers({ query: username }).pipe( - map(users => ({ - exists: users.some(user => user.username === username) - })), - catchError(error => { - console.error('Error checking if hub user exists:', error); - return of({ exists: false }); - }) - ); - } -} \ No newline at end of file diff --git a/src/app/modules/hub-users/structure.txt b/src/app/modules/hub-users/structure.txt deleted file mode 100644 index 435eb3d..0000000 --- a/src/app/modules/hub-users/structure.txt +++ /dev/null @@ -1,15 +0,0 @@ -modules/users/ -├── components/ # Composants réutilisables -│ ├── users-list/ -│ │ ├── users-list.ts # Logique du tableau utilisateurs -│ │ └── users-list.html -│ ├── users-profile/ -│ │ ├── users-profile.ts # Logique création / modification -│ │ └── users-profile.html -│ -├── services/ -│ └── users.service.ts # Service API centralisé (NestJS) -│ -├── users.module.ts # Module principal -├── users.routes.ts # Gestion des routes -└── users.html # Template global du module diff --git a/src/app/modules/merchant-users/config/config.html b/src/app/modules/merchant-users/config/config.html deleted file mode 100644 index 88c113b..0000000 --- a/src/app/modules/merchant-users/config/config.html +++ /dev/null @@ -1,436 +0,0 @@ - - - Payment Hub DCB - - -
- - - - - - - - @if (configError) { -
- - {{ configError }} -
- } - - @if (configSuccess) { -
- - {{ configSuccess }} -
- } - - -
- @for (step of wizardSteps; track $index; let i = $index) { -
-
- - @if (i === 0) { -
-
- -
- - @if (companyInfo.get('name')?.invalid && companyInfo.get('name')?.touched) { -
Le nom commercial est requis
- } -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- } - - - @if (i === 1) { -
-
-
Adresse de l'entreprise
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
- -
-
Contact technique
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- } - - - @if (i === 2) { -
-
- -
- -
Pourcentage prélevé sur chaque transaction
-
-
-
- -
- -
Plafond total des transactions par jour
-
-
-
- -
- -
Montant maximum par transaction
-
-
-
- -
- -
-
-
- -
- -
-
-
- } - - - @if (i === 3) { -
-
-
Header Enrichment
-
-
-
-
- - -
-
- - -
-
- -
-
- - -
- - @for (header of headerEnrichmentHeaders.controls; track $index; let idx = $index) { -
-
- - -
-
- - -
-
- -
-
- } -
-
-
-
- -
-
Webhooks Abonnements
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
- -
-
Webhooks Paiements
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-
-
-
- } - - - @if (i === 4) { -
-
-
- - Vérifiez les informations avant de créer le partenaire -
- -
-
-
Récapitulatif
-
-
- Informations Société:
- {{ companyInfo.value.name || 'Non renseigné' }}
- {{ companyInfo.value.legalName || 'Non renseigné' }}
- {{ companyInfo.value.email || 'Non renseigné' }}
- {{ companyInfo.value.phone || 'Non renseigné' }} -
-
- Configuration:
- Commission: {{ paymentConfig.value.commissionRate || 0 }}%
- Limite quotidienne: {{ (paymentConfig.value.dailyLimit || 0) | number }} XOF
- Limite transaction: {{ (paymentConfig.value.transactionLimit || 0) | number }} XOF -
-
-
-
-
-
- } -
- - -
- @if (i > 0) { - - } @else { -
- } - - @if (i < wizardSteps.length - 1) { - - } @else { - - } -
-
- } -
-
-
\ No newline at end of file diff --git a/src/app/modules/merchant-users/config/config.spec.ts b/src/app/modules/merchant-users/config/config.spec.ts deleted file mode 100644 index d27de8c..0000000 --- a/src/app/modules/merchant-users/config/config.spec.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { PartnerConfig } from './config'; -describe('PartnerConfig', () => {}); \ No newline at end of file diff --git a/src/app/modules/merchant-users/config/config.ts b/src/app/modules/merchant-users/config/config.ts deleted file mode 100644 index 1926065..0000000 --- a/src/app/modules/merchant-users/config/config.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { Component, inject, OnInit } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormGroup, FormControl } from '@angular/forms'; -import { NgIcon } from '@ng-icons/core'; -import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap'; -import { UiCard } from '@app/components/ui-card'; -import { PartnerConfigService } from '../services/partner-config.service'; -import { CreatePartnerDto, PartnerCategory } from '../models/partners-config.model'; -import { firstValueFrom } from 'rxjs'; - -@Component({ - selector: 'app-partner-config', - standalone: true, - imports: [ - CommonModule, - FormsModule, - ReactiveFormsModule, - NgIcon, - NgbProgressbarModule, - UiCard - ], - templateUrl: './config.html' -}) -export class PartnerConfig implements OnInit { - private fb = inject(FormBuilder); - private PartnerConfigService = inject(PartnerConfigService); - - // Configuration wizard - currentStep = 0; - wizardSteps = [ - { id: 'company-info', icon: 'lucideBuilding', title: 'Informations Société', subtitle: 'Détails entreprise' }, - { id: 'contact-info', icon: 'lucideUser', title: 'Contact Principal', subtitle: 'Personne de contact' }, - { id: 'payment-config', icon: 'lucideCreditCard', title: 'Configuration Paiements', subtitle: 'Paramètres DCB' }, - { id: 'webhooks', icon: 'lucideWebhook', title: 'Webhooks', subtitle: 'Notifications et retours' }, - { id: 'review', icon: 'lucideCheckCircle', title: 'Validation', subtitle: 'Vérification finale' } - ]; - - configLoading = false; - configError = ''; - configSuccess = ''; - - // Formulaires - partnerForm = this.fb.group({ - companyInfo: this.fb.group({ - name: ['', [Validators.required, Validators.minLength(2)]], - legalName: ['', [Validators.required]], - email: ['', [Validators.required, Validators.email]], - phone: ['', [Validators.required]], - website: [''], - category: ['E_COMMERCE', [Validators.required]], - country: ['CIV', [Validators.required]], - currency: ['XOF', [Validators.required]], - timezone: ['Africa/Abidjan', [Validators.required]] - }), - addressInfo: this.fb.group({ - street: ['', [Validators.required]], - city: ['', [Validators.required]], - state: ['', [Validators.required]], - postalCode: ['', [Validators.required]], - country: ['CIV', [Validators.required]] - }), - technicalContact: this.fb.group({ - name: ['', [Validators.required]], - email: ['', [Validators.required, Validators.email]], - phone: ['', [Validators.required]] - }), - paymentConfig: this.fb.group({ - commissionRate: [2.5, [Validators.required, Validators.min(0), Validators.max(100)]], - dailyLimit: [1000000, [Validators.required, Validators.min(1000)]], - transactionLimit: [50000, [Validators.required, Validators.min(100), Validators.max(500000)]], - minAmount: [100, [Validators.required, Validators.min(1)]], - maxAmount: [500000, [Validators.required, Validators.min(100)]] - }), - webhookConfig: this.fb.group({ - headerEnrichment: this.fb.group({ - url: ['', [Validators.pattern('https?://.+')]], - method: ['POST'], - headers: this.fb.array([]) - }), - subscription: this.fb.group({ - onCreate: ['', [Validators.pattern('https?://.+')]], - onRenew: ['', [Validators.pattern('https?://.+')]], - onCancel: ['', [Validators.pattern('https?://.+')]], - onExpire: ['', [Validators.pattern('https?://.+')]] - }), - payment: this.fb.group({ - onSuccess: ['', [Validators.pattern('https?://.+')]], - onFailure: ['', [Validators.pattern('https?://.+')]], - onRefund: ['', [Validators.pattern('https?://.+')]] - }), - authentication: this.fb.group({ - onSuccess: ['', [Validators.pattern('https?://.+')]], - onFailure: ['', [Validators.pattern('https?://.+')]] - }) - }) - }); - - // Données partagées - countries = [ - { code: 'CIV', name: 'Côte d\'Ivoire' }, - { code: 'SEN', name: 'Sénégal' }, - { code: 'CMR', name: 'Cameroun' }, - { code: 'GHA', name: 'Ghana' }, - { code: 'NGA', name: 'Nigeria' } - ]; - - categories = [ - { value: 'E_COMMERCE', label: 'E-Commerce' }, - { value: 'GAMING', label: 'Jeux & Gaming' }, - { value: 'ENTERTAINMENT', label: 'Divertissement' }, - { value: 'UTILITIES', label: 'Services Publics' }, - { value: 'DIGITAL_CONTENT', label: 'Contenu Digital' }, - { value: 'SERVICES', label: 'Services' }, - { value: 'OTHER', label: 'Autre' } - ]; - - currencies = [ - { code: 'XOF', name: 'Franc CFA' }, - { code: 'EUR', name: 'Euro' }, - { code: 'USD', name: 'Dollar US' } - ]; - - timezones = [ - { value: 'Africa/Abidjan', label: 'Abidjan (GMT)' }, - { value: 'Africa/Lagos', label: 'Lagos (WAT)' }, - { value: 'Africa/Johannesburg', label: 'Johannesburg (SAST)' } - ]; - - httpMethods = [ - { value: 'GET', label: 'GET' }, - { value: 'POST', label: 'POST' } - ]; - - ngOnInit() {} - - // Navigation du wizard - get progressValue(): number { - return ((this.currentStep + 1) / this.wizardSteps.length) * 100; - } - - nextStep() { - if (this.currentStep < this.wizardSteps.length - 1 && this.isStepValid(this.currentStep)) { - this.currentStep++; - } - } - - previousStep() { - if (this.currentStep > 0) { - this.currentStep--; - } - } - - goToStep(index: number) { - if (this.isStepAccessible(index)) { - this.currentStep = index; - } - } - - isStepAccessible(index: number): boolean { - if (index === 0) return true; - - for (let i = 0; i < index; i++) { - if (!this.isStepValid(i)) { - return false; - } - } - return true; - } - - isStepValid(stepIndex: number): boolean { - switch (stepIndex) { - case 0: // Company Info - return this.companyInfo.valid; - case 1: // Contact Info - return this.addressInfo.valid && this.technicalContact.valid; - case 2: // Payment Config - return this.paymentConfig.valid; - case 3: // Webhooks (toujours valide car optionnel) - return true; - case 4: // Review - return this.partnerForm.valid; - default: - return false; - } - } - - // Gestion des headers dynamiques - CORRECTION ICI - get headerEnrichmentHeaders(): FormArray { - return this.headerEnrichment.get('headers') as FormArray; - } - - // Méthode pour obtenir un FormControl sécurisé - NOUVELLE MÉTHODE - getHeaderControl(header: any, field: string): FormControl { - return header.get(field) as FormControl; - } - - addHeader() { - const headerGroup = this.fb.group({ - key: ['', Validators.required], - value: ['', Validators.required] - }); - this.headerEnrichmentHeaders.push(headerGroup); - } - - removeHeader(index: number) { - this.headerEnrichmentHeaders.removeAt(index); - } - - // Soumission du formulaire - async submitForm() { - if (this.partnerForm.valid) { - this.configLoading = true; - this.configError = ''; - - try { - const formData = this.partnerForm.value; - - const createPartnerDto: CreatePartnerDto = { - name: this.safeString(formData.companyInfo?.name) || '', - legalName: this.safeString(formData.companyInfo?.legalName) || '', - email: this.safeString(formData.companyInfo?.email) || '', - phone: this.safeString(formData.companyInfo?.phone) || '', - website: this.safeString(formData.companyInfo?.website) || '', - category: (this.safeString(formData.companyInfo?.category) as PartnerCategory) || 'OTHER', - country: this.safeString(formData.companyInfo?.country) || 'CIV', - currency: this.safeString(formData.companyInfo?.currency) || 'XOF', - timezone: this.safeString(formData.companyInfo?.timezone) || 'Africa/Abidjan', - commissionRate: this.safeNumber(formData.paymentConfig?.commissionRate) || 0, - dailyLimit: this.safeNumber(formData.paymentConfig?.dailyLimit) || 0, - transactionLimit: this.safeNumber(formData.paymentConfig?.transactionLimit) || 0, - minAmount: this.safeNumber(formData.paymentConfig?.minAmount) || 0, - maxAmount: this.safeNumber(formData.paymentConfig?.maxAmount) || 0, - }; - - const response = await firstValueFrom( - this.PartnerConfigService.createPartnerConfig(createPartnerDto) - ); - - if (response.success && response.data) { - this.configSuccess = `Partenaire créé avec succès! ID: ${response.data.id}`; - this.partnerForm.reset(); - this.currentStep = 0; - } else { - this.configError = response.error || 'Erreur lors de la création du partenaire'; - } - - } catch (error) { - this.configError = 'Erreur lors de la création du partenaire'; - console.error('Error creating partner:', error); - } finally { - this.configLoading = false; - } - } else { - this.configError = 'Veuillez corriger les erreurs dans le formulaire'; - this.markAllFieldsAsTouched(); - } - } - - // Méthodes utilitaires - private safeString(value: string | null | undefined): string { - return value || ''; - } - - private safeNumber(value: number | null | undefined): number { - return value || 0; - } - - private markAllFieldsAsTouched() { - Object.keys(this.partnerForm.controls).forEach(key => { - const control = this.partnerForm.get(key); - if (control instanceof FormGroup) { - Object.keys(control.controls).forEach(subKey => { - control.get(subKey)?.markAsTouched(); - }); - } else { - control?.markAsTouched(); - } - }); - } - - // Getters pour les formulaires - CORRECTION ICI (suppression des ?) - get companyInfo() { - return this.partnerForm.get('companyInfo') as FormGroup; - } - - get addressInfo() { - return this.partnerForm.get('addressInfo') as FormGroup; - } - - get technicalContact() { - return this.partnerForm.get('technicalContact') as FormGroup; - } - - get paymentConfig() { - return this.partnerForm.get('paymentConfig') as FormGroup; - } - - get webhookConfig() { - return this.partnerForm.get('webhookConfig') as FormGroup; - } - - get headerEnrichment() { - return this.webhookConfig.get('headerEnrichment') as FormGroup; - } - - get subscription() { - return this.webhookConfig.get('subscription') as FormGroup; - } - - get payment() { - return this.webhookConfig.get('payment') as FormGroup; - } - - get authentication() { - return this.webhookConfig.get('authentication') as FormGroup; - } -} \ No newline at end of file diff --git a/src/app/modules/merchant-users/list/list.spec.ts b/src/app/modules/merchant-users/list/list.spec.ts deleted file mode 100644 index 1919cb9..0000000 --- a/src/app/modules/merchant-users/list/list.spec.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { PartnerTeamList } from './list'; -describe('PartnerTeamList', () => {}); \ No newline at end of file diff --git a/src/app/modules/merchant-users/list/list.ts b/src/app/modules/merchant-users/list/list.ts deleted file mode 100644 index d95a26d..0000000 --- a/src/app/modules/merchant-users/list/list.ts +++ /dev/null @@ -1,695 +0,0 @@ -// src/app/modules/merchant-users/list/list.ts -import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { NgIcon } from '@ng-icons/core'; -import { NgbPaginationModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; -import { catchError, map, of, Subject, takeUntil } from 'rxjs'; - -import { - MerchantUserDto, - PaginatedUserResponse, - SearchUsersParams, - UserRole, - UserType -} from '@core/models/dcb-bo-hub-user.model'; - -import { MerchantUsersService } from '../services/merchant-users.service'; -import { HubUsersService } from '../../hub-users/services/hub-users.service'; -import { AuthService } from '@core/services/auth.service'; -import { UiCard } from '@app/components/ui-card'; - -@Component({ - selector: 'app-merchant-users-list', - standalone: true, - imports: [ - CommonModule, - FormsModule, - NgIcon, - UiCard, - NgbPaginationModule, - NgbDropdownModule - ], - templateUrl: './list.html', -}) -export class MerchantUsersList implements OnInit, OnDestroy { - private merchantUsersService = inject(MerchantUsersService); - private hubUsersService = inject(HubUsersService); - private authService = inject(AuthService); - private cdRef = inject(ChangeDetectorRef); - private destroy$ = new Subject(); - - readonly UserRole = UserRole; - readonly UserType = UserType; - - @Output() userSelected = new EventEmitter(); - @Output() openCreateModal = new EventEmitter(); - @Output() openResetPasswordModal = new EventEmitter(); - @Output() openDeleteUserModal = new EventEmitter(); - - // Données - allUsers: MerchantUserDto[] = []; - filteredUsers: MerchantUserDto[] = []; - displayedUsers: MerchantUserDto[] = []; - - // États - loading = false; - error = ''; - - // Recherche et filtres - searchTerm = ''; - statusFilter: 'all' | 'enabled' | 'disabled' = 'all'; - emailVerifiedFilter: 'all' | 'verified' | 'not-verified' = 'all'; - roleFilter: UserRole | 'all' = 'all'; - - // Pagination - currentPage = 1; - itemsPerPage = 10; - totalItems = 0; - totalPages = 0; - - // Tri - sortField: keyof MerchantUserDto = 'username'; - sortDirection: 'asc' | 'desc' = 'asc'; - - // Rôles disponibles pour le filtre - availableRoles: { value: UserRole | 'all'; label: string }[] = [ - { value: 'all', label: 'Tous les rôles' }, - { value: UserRole.DCB_PARTNER_ADMIN, label: 'Administrateurs' }, - { value: UserRole.DCB_PARTNER_MANAGER, label: 'Managers' }, - { value: UserRole.DCB_PARTNER_SUPPORT, label: 'Support' } - ]; - - // ID du merchant partner courant et permissions - currentMerchantPartnerId: string = ''; - currentUserRole: UserRole | null = null; - isHubAdminOrSupport = false; - canViewAllMerchants = false; - isDcbPartner = false; - - ngOnInit() { - this.loadCurrentUserPermissions(); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - private loadCurrentUserPermissions() { - this.authService.getProfile().subscribe({ - next: (user: any) => { - // Méthode robuste pour récupérer le rôle - this.currentUserRole = this.extractUserRole(user); - - // Déterminer le type d'utilisateur avec des méthodes plus robustes - this.isHubAdminOrSupport = this.isHubAdminOrSupportRole(this.currentUserRole); - this.isDcbPartner = this.isDcbPartnerRole(this.currentUserRole); - this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole); - - - // Déterminer le merchantPartnerId - if(this.isDcbPartner){ - this.currentMerchantPartnerId = user.id; - } - else if(!this.isDcbPartner || !this.isHubAdminOrSupport) { - this.currentMerchantPartnerId = this.extractMerchantPartnerId(user); - } - - this.loadUsers(); - }, - error: (error) => { - console.error('❌ Error loading current user permissions:', error); - // Fallback: utiliser les méthodes d'AuthService - this.fallbackPermissions(); - this.loadUsers(); // Charger quand même les utilisateurs - } - }); - } - - /** - * Méthode robuste pour extraire le rôle de l'utilisateur - */ - private extractUserRole(user: any): UserRole | null { - console.log('🔍 Extracting user role from:', user); - - // 1. Essayer depuis les rôles du token (méthode principale) - const tokenRoles = this.authService.getCurrentUserRoles(); - if (tokenRoles && tokenRoles.length > 0) { - console.log('✅ Role from token:', tokenRoles[0]); - return tokenRoles[0]; - } - - // 2. Essayer depuis le profil user.role - if (user?.role && Object.values(UserRole).includes(user.role)) { - console.log('✅ Role from user.role:', user.role); - return user.role as UserRole; - } - - // 3. Essayer depuis user.userType pour déduire le rôle - if (user?.userType) { - const roleFromType = this.getRoleFromUserType(user.userType); - if (roleFromType) { - console.log('✅ Role deduced from userType:', roleFromType); - return roleFromType; - } - } - - // 4. Essayer depuis les attributs étendus - if (user?.attributes?.role?.[0]) { - const roleFromAttributes = user.attributes.role[0]; - if (Object.values(UserRole).includes(roleFromAttributes as UserRole)) { - console.log('✅ Role from attributes:', roleFromAttributes); - return roleFromAttributes as UserRole; - } - } - - console.warn('❌ No valid role found in user profile'); - return null; - } - - /** - * Déduire le rôle à partir du userType - */ - private getRoleFromUserType(userType: string): UserRole | null { - const typeMapping: { [key: string]: UserRole } = { - [UserType.HUB]: UserRole.DCB_ADMIN, // Fallback pour HUB - [UserType.MERCHANT]: UserRole.DCB_PARTNER_ADMIN, // Fallback pour MERCHANT - }; - - return typeMapping[userType] || null; - } - - /** - * Vérifier si l'utilisateur est Hub Admin ou Support - */ - private isHubAdminOrSupportRole(role: UserRole | null): boolean { - if (!role) return false; - - const hubAdminSupportRoles = [ - UserRole.DCB_ADMIN, - UserRole.DCB_SUPPORT, - UserRole.DCB_PARTNER // DCB_PARTNER peut aussi être considéré comme Hub - ]; - - return hubAdminSupportRoles.includes(role); - } - - /** - * Vérifier si l'utilisateur est DCB Partner - */ - private isDcbPartnerRole(role: UserRole | null): boolean { - if (!role) return false; - - const partnerRoles = [ - UserRole.DCB_PARTNER, - UserRole.DCB_PARTNER_ADMIN, - UserRole.DCB_PARTNER_MANAGER, - UserRole.DCB_PARTNER_SUPPORT - ]; - - return partnerRoles.includes(role); - } - - /** - * Vérifier si l'utilisateur peut voir tous les merchants - */ - private canViewAllMerchantsCheck(role: UserRole | null): boolean { - if (!role) return false; - - const canViewAllRoles = [ - UserRole.DCB_ADMIN, - UserRole.DCB_SUPPORT - ]; - - return canViewAllRoles.includes(role); - } - - /** - * Extraire le merchantPartnerId de manière robuste - */ - private extractMerchantPartnerId(user: any): string { - console.log('🔍 Extracting merchantPartnerId from:', user); - - // 1. Essayer depuis merchantPartnerId direct - if (user?.merchantPartnerId) { - console.log('✅ merchantPartnerId from direct property:', user.merchantPartnerId); - return user.merchantPartnerId; - } - - // 2. Essayer depuis les attributs - if (user?.attributes?.merchantPartnerId?.[0]) { - console.log('✅ merchantPartnerId from attributes:', user.attributes.merchantPartnerId[0]); - return user.attributes.merchantPartnerId[0]; - } - - // 3. Essayer depuis AuthService - const authServiceMerchantId = this.authService.getCurrentMerchantPartnerId(); - if (authServiceMerchantId) { - console.log('✅ merchantPartnerId from AuthService:', authServiceMerchantId); - return authServiceMerchantId; - } - - // 4. Pour les rôles Hub, pas de merchantPartnerId - if (this.isHubAdminOrSupport) { - console.log('ℹ️ Hub user - no merchantPartnerId'); - return ''; - } - - console.warn('❌ No merchantPartnerId found'); - return ''; - } - - /** - * Fallback en cas d'erreur de chargement du profil - */ - private fallbackPermissions(): void { - console.warn('🔄 Using fallback permissions'); - - // Utiliser les méthodes d'AuthService comme fallback - this.currentUserRole = this.authService.getCurrentUserRole(); - this.isHubAdminOrSupport = this.authService.isHubUser() || - this.authService.hasRole(UserRole.DCB_ADMIN) || - this.authService.hasRole(UserRole.DCB_SUPPORT); - this.isDcbPartner = this.authService.isMerchantUser() || - this.authService.hasRole(UserRole.DCB_PARTNER); - this.canViewAllMerchants = this.authService.canViewAllMerchants(); - this.currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId() || ''; - - console.log('🔄 Fallback permissions:', { - currentUserRole: this.currentUserRole, - isHubAdminOrSupport: this.isHubAdminOrSupport, - isDcbPartner: this.isDcbPartner, - canViewAllMerchants: this.canViewAllMerchants, - currentMerchantPartnerId: this.currentMerchantPartnerId - }); - } - - loadUsers() { - this.loading = true; - this.error = ''; - - let usersObservable; - - if (this.canViewAllMerchants && !this.currentMerchantPartnerId) { - console.log('🔍 Loading ALL merchant users (Hub Admin/Support)'); - usersObservable = this.hubUsersService.findAllMerchantUsers(this.currentPage, this.itemsPerPage).pipe( - map((response: PaginatedUserResponse) => { - console.log('✅ All merchant users loaded:', response.users.length); - return response.users as MerchantUserDto[]; - }), - catchError(error => { - console.error('❌ Error loading all merchant users:', error); - this.error = 'Erreur lors du chargement de tous les utilisateurs marchands'; - return of([]); - }) - ); - } else if (this.currentMerchantPartnerId) { - console.log(`🔍 Loading merchant users for partner: ${this.currentMerchantPartnerId}`); - usersObservable = this.merchantUsersService.getMerchantUsersByPartner(this.currentMerchantPartnerId).pipe( - catchError(error => { - console.error('❌ Error loading merchant users by partner:', error); - this.error = 'Erreur lors du chargement des utilisateurs du partenaire'; - return of([]); - }) - ); - } else { - console.log('🔍 Loading my merchant users'); - usersObservable = this.merchantUsersService.getMyMerchantUsers(this.currentMerchantPartnerId).pipe( - catchError(error => { - console.error('❌ Error loading my merchant users:', error); - this.error = 'Erreur lors du chargement de mes utilisateurs marchands'; - return of([]); - }) - ); - } - - usersObservable - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (users: MerchantUserDto[]) => { - this.allUsers = users || []; - console.log(`✅ Loaded ${this.allUsers.length} merchant users`); - - this.applyFiltersAndPagination(); - this.loading = false; - this.cdRef.detectChanges(); - }, - error: (error: any) => { - this.error = 'Erreur lors du chargement des utilisateurs marchands'; - this.loading = false; - - this.allUsers = []; - this.filteredUsers = []; - this.displayedUsers = []; - this.cdRef.detectChanges(); - console.error('❌ Error in users subscription:', error); - } - }); - } - - // Recherche et filtres - onSearch() { - this.currentPage = 1; - this.applyFiltersAndPagination(); - } - - onClearFilters() { - this.searchTerm = ''; - this.statusFilter = 'all'; - this.emailVerifiedFilter = 'all'; - this.roleFilter = 'all'; - this.currentPage = 1; - this.applyFiltersAndPagination(); - } - - applyFiltersAndPagination() { - // Vérifier que allUsers est défini - if (!this.allUsers) { - this.allUsers = []; - } - - console.log(`🔍 Applying filters to ${this.allUsers.length} users`); - - // Appliquer les filtres - this.filteredUsers = this.allUsers.filter(user => { - // Filtre de recherche - const matchesSearch = !this.searchTerm || - user.username.toLowerCase().includes(this.searchTerm.toLowerCase()) || - user.email.toLowerCase().includes(this.searchTerm.toLowerCase()) || - (user.firstName && user.firstName.toLowerCase().includes(this.searchTerm.toLowerCase())) || - (user.lastName && user.lastName.toLowerCase().includes(this.searchTerm.toLowerCase())); - - // Filtre par statut - const matchesStatus = this.statusFilter === 'all' || - (this.statusFilter === 'enabled' && user.enabled) || - (this.statusFilter === 'disabled' && !user.enabled); - - // Filtre par email vérifié - const matchesEmailVerified = this.emailVerifiedFilter === 'all' || - (this.emailVerifiedFilter === 'verified' && user.emailVerified) || - (this.emailVerifiedFilter === 'not-verified' && !user.emailVerified); - - // Filtre par rôle - const matchesRole = this.roleFilter === 'all' || user.role === this.roleFilter; - - return matchesSearch && matchesStatus && matchesEmailVerified && matchesRole; - }); - - console.log(`✅ Filtered to ${this.filteredUsers.length} users`); - - // Appliquer le tri - this.filteredUsers.sort((a, b) => { - const aValue = a[this.sortField]; - const bValue = b[this.sortField]; - - if (aValue === bValue) return 0; - - let comparison = 0; - if (typeof aValue === 'string' && typeof bValue === 'string') { - comparison = aValue.localeCompare(bValue); - } else if (typeof aValue === 'number' && typeof bValue === 'number') { - comparison = aValue - bValue; - } else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') { - comparison = (aValue === bValue) ? 0 : aValue ? -1 : 1; - } - - return this.sortDirection === 'asc' ? comparison : -comparison; - }); - - // Calculer la pagination - this.totalItems = this.filteredUsers.length; - this.totalPages = Math.ceil(this.totalItems / this.itemsPerPage); - - // Appliquer la pagination - const startIndex = (this.currentPage - 1) * this.itemsPerPage; - const endIndex = startIndex + this.itemsPerPage; - this.displayedUsers = this.filteredUsers.slice(startIndex, endIndex); - - console.log(`📄 Pagination: page ${this.currentPage} of ${this.totalPages}, showing ${this.displayedUsers.length} users`); - } - - // Tri - sort(field: keyof MerchantUserDto) { - if (this.sortField === field) { - this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; - } else { - this.sortField = field; - this.sortDirection = 'asc'; - } - console.log(`🔀 Sorting by ${field} (${this.sortDirection})`); - this.applyFiltersAndPagination(); - } - - getSortIcon(field: keyof MerchantUserDto): string { - if (this.sortField !== field) return 'lucideArrowUpDown'; - return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown'; - } - - // Pagination - onPageChange(page: number) { - console.log(`📄 Changing to page ${page}`); - this.currentPage = page; - this.applyFiltersAndPagination(); - } - - getStartIndex(): number { - return (this.currentPage - 1) * this.itemsPerPage + 1; - } - - getEndIndex(): number { - return Math.min(this.currentPage * this.itemsPerPage, this.totalItems); - } - - // Actions - viewUserProfile(userId: string) { - console.log(`👤 Viewing user profile: ${userId}`); - this.userSelected.emit(userId); - } - - // Méthode pour réinitialiser le mot de passe - resetPassword(user: MerchantUserDto) { - console.log(`🔑 Resetting password for user: ${user.username}`); - this.openResetPasswordModal.emit(user.id); - } - - // Méthode pour ouvrir le modal de suppression - deleteUser(user: MerchantUserDto) { - console.log(`🗑️ Deleting user: ${user.username}`); - this.openDeleteUserModal.emit(user.id); - } - - // Activer un utilisateur - enableUser(user: MerchantUserDto) { - console.log(`✅ Enabling user: ${user.username}`); - this.merchantUsersService.enableMerchantUser(user.id) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (updatedUser) => { - console.log(`✅ User ${user.username} enabled successfully`); - // Mettre à jour l'utilisateur dans la liste - const index = this.allUsers.findIndex(u => u.id === user.id); - if (index !== -1) { - this.allUsers[index] = updatedUser; - } - this.applyFiltersAndPagination(); - this.cdRef.detectChanges(); - }, - error: (error) => { - console.error('❌ Error enabling merchant user:', error); - this.error = 'Erreur lors de l\'activation de l\'utilisateur'; - this.cdRef.detectChanges(); - } - }); - } - - // Désactiver un utilisateur - disableUser(user: MerchantUserDto) { - console.log(`❌ Disabling user: ${user.username}`); - this.merchantUsersService.disableMerchantUser(user.id) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (updatedUser) => { - console.log(`✅ User ${user.username} disabled successfully`); - // Mettre à jour l'utilisateur dans la liste - const index = this.allUsers.findIndex(u => u.id === user.id); - if (index !== -1) { - this.allUsers[index] = updatedUser; - } - this.applyFiltersAndPagination(); - this.cdRef.detectChanges(); - }, - error: (error) => { - console.error('❌ Error disabling merchant user:', error); - this.error = 'Erreur lors de la désactivation de l\'utilisateur'; - this.cdRef.detectChanges(); - } - }); - } - - // ==================== UTILITAIRES D'AFFICHAGE ==================== - - getStatusBadgeClass(user: MerchantUserDto): string { - if (!user.enabled) return 'badge bg-danger'; - if (!user.emailVerified) return 'badge bg-warning'; - return 'badge bg-success'; - } - - getStatusText(user: MerchantUserDto): string { - if (!user.enabled) return 'Désactivé'; - if (!user.emailVerified) return 'Email non vérifié'; - return 'Actif'; - } - - getRoleBadgeClass(role: UserRole): string { - switch (role) { - case UserRole.DCB_PARTNER_ADMIN: - return 'bg-danger'; - case UserRole.DCB_PARTNER_MANAGER: - return 'bg-warning text-dark'; - case UserRole.DCB_PARTNER_SUPPORT: - return 'bg-info text-white'; - default: - return 'bg-secondary'; - } - } - - getRoleDisplayName(role: UserRole): string { - const roleNames = { - [UserRole.DCB_ADMIN]: 'Administrateur', - [UserRole.DCB_PARTNER]: 'Manager', - [UserRole.DCB_SUPPORT]: 'Support', - [UserRole.DCB_PARTNER_ADMIN]: 'Admin Marchand', - [UserRole.DCB_PARTNER_MANAGER]: 'Manager Marchand', - [UserRole.DCB_PARTNER_SUPPORT]: 'Support Marchand' - }; - return roleNames[role] || role; - } - - getRoleIcon(role: UserRole): string { - switch (role) { - case UserRole.DCB_PARTNER_ADMIN: - return 'lucideShield'; - case UserRole.DCB_PARTNER_MANAGER: - return 'lucideUserCog'; - case UserRole.DCB_PARTNER_SUPPORT: - return 'lucideHeadphones'; - default: - return 'lucideUser'; - } - } - - formatTimestamp(timestamp: number): string { - if (!timestamp) return 'Non disponible'; - return new Date(timestamp).toLocaleDateString('fr-FR', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - } - - getUserInitials(user: MerchantUserDto): string { - return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U'; - } - - getUserDisplayName(user: MerchantUserDto): string { - if (user.firstName && user.lastName) { - return `${user.firstName} ${user.lastName}`; - } - return user.username; - } - - // ==================== STATISTIQUES ==================== - - getUsersCountByRole(role: UserRole): number { - return this.allUsers.filter(user => user.role === role).length; - } - - getEnabledUsersCount(): number { - return this.allUsers.filter(user => user.enabled).length; - } - - getDisabledUsersCount(): number { - return this.allUsers.filter(user => !user.enabled).length; - } - - getEmailVerifiedCount(): number { - return this.allUsers.filter(user => user.emailVerified).length; - } - - getTotalUsersCount(): number { - return this.allUsers.length; - } - - // ==================== MÉTHODES UTILITAIRES ==================== - - hasRole(user: MerchantUserDto, role: UserRole): boolean { - return user.role === role; - } - - isAdmin(user: MerchantUserDto): boolean { - return this.hasRole(user, UserRole.DCB_PARTNER_ADMIN); - } - - isManager(user: MerchantUserDto): boolean { - return this.hasRole(user, UserRole.DCB_PARTNER_MANAGER); - } - - isSupport(user: MerchantUserDto): boolean { - return this.hasRole(user, UserRole.DCB_PARTNER_SUPPORT); - } - - // Recherche rapide par rôle - filterByRole(role: UserRole | 'all') { - this.roleFilter = role; - this.currentPage = 1; - this.applyFiltersAndPagination(); - } - - // Recherche via le service (pour des recherches plus complexes) - searchUsers() { - if (this.searchTerm.trim()) { - this.loading = true; - const searchParams: SearchUsersParams = { - query: this.searchTerm, - userType: UserType.MERCHANT - }; - - if (this.roleFilter !== 'all') { - searchParams.role = this.roleFilter as UserRole; - } - if (this.statusFilter !== 'all') { - searchParams.enabled = this.statusFilter === 'enabled'; - } - - console.log('🔍 Performing advanced search:', searchParams); - - this.merchantUsersService.searchMerchantUsers(searchParams) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (users) => { - this.allUsers = users; - console.log(`✅ Advanced search found ${users.length} users`); - this.applyFiltersAndPagination(); - this.loading = false; - this.cdRef.detectChanges(); - }, - error: (error: any) => { - console.error('❌ Error searching users:', error); - this.loading = false; - this.cdRef.detectChanges(); - } - }); - } else { - this.loadUsers(); // Recharger tous les utilisateurs si la recherche est vide - } - } - - // Recharger les données - refreshData() { - console.log('🔄 Refreshing data...'); - this.loadUsers(); - } -} \ No newline at end of file diff --git a/src/app/modules/merchant-users/merchant-users.spec.ts b/src/app/modules/merchant-users/merchant-users.spec.ts deleted file mode 100644 index dba9a7d..0000000 --- a/src/app/modules/merchant-users/merchant-users.spec.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { MerchantUsers } from './merchant-users'; -describe('Merchant Users', () => {}); \ No newline at end of file diff --git a/src/app/modules/merchant-users/merchant-users.ts b/src/app/modules/merchant-users/merchant-users.ts deleted file mode 100644 index 88f7ed5..0000000 --- a/src/app/modules/merchant-users/merchant-users.ts +++ /dev/null @@ -1,744 +0,0 @@ -import { Component, inject, OnInit, TemplateRef, ViewChild, ChangeDetectorRef, OnDestroy } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { NgIcon } from '@ng-icons/core'; -import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { Subject, takeUntil } from 'rxjs'; -import { PageTitle } from '@app/components/page-title/page-title'; -import { MerchantUsersList } from './list/list'; -import { MerchantUserProfile } from './profile/profile'; -import { MerchantUsersService } from './services/merchant-users.service'; -import { AuthService } from '@core/services/auth.service'; - -import { - MerchantUserDto, - CreateUserDto, - ResetPasswordDto, - UserRole, - UserType, - AvailableRolesResponse, -} from '@core/models/dcb-bo-hub-user.model'; -import { RoleManagementService } from '@core/services/role-management.service'; - -@Component({ - selector: 'app-merchant-users', - standalone: true, - imports: [ - CommonModule, - FormsModule, - NgIcon, - NgbNavModule, - NgbModalModule, - PageTitle, - MerchantUsersList, - MerchantUserProfile - ], - templateUrl: './merchant-users.html', -}) -export class MerchantUsers implements OnInit, OnDestroy { - private modalService = inject(NgbModal); - private authService = inject(AuthService); - private merchantUsersService = inject(MerchantUsersService); - protected roleService = inject(RoleManagementService); - private cdRef = inject(ChangeDetectorRef); - private destroy$ = new Subject(); - - readonly UserRole = UserRole; - readonly UserType = UserType; - - // Ajouter cette propriété manquante - user: MerchantUserDto | null = null; - - activeTab: 'list' | 'profile' = 'list'; - selectedUserId: string | null = null; - currentMerchantPartnerId: string = ''; - - // Données pour la création d'utilisateur marchand - newMerchantUser: CreateUserDto = { - username: '', - email: '', - firstName: '', - lastName: '', - password: '', - role: UserRole.DCB_PARTNER_SUPPORT, - merchantPartnerId: '', - enabled: true, - emailVerified: false - }; - - availableRoles: AvailableRolesResponse | null = null; - creatingUser = false; - createUserError = ''; - currentUserRole: UserRole | null = null; - - // Données pour la réinitialisation de mot de passe - selectedUserForReset: MerchantUserDto | null = null; - newPassword = ''; - temporaryPassword = true; - resettingPassword = false; - resetPasswordError = ''; - resetPasswordSuccess = ''; - - selectedUserForDelete: MerchantUserDto | null = null; - deletingUser = false; - deleteUserError = ''; - - // Permissions utilisateur - isHubAdminOrSupport = false; - isDcbPartner = false; - canViewAllMerchants = false; - - ngOnInit() { - this.activeTab = 'list'; - this.loadCurrentUserPermissions(); - this.loadAvailableRoles(); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - private loadCurrentUserPermissions() { - this.authService.getProfile().subscribe({ - next: (user: any) => { - this.currentUserRole = this.extractUserRole(user); - - this.isHubAdminOrSupport = this.isHubAdminOrSupportRole(this.currentUserRole); - this.isDcbPartner = this.isDcbPartnerRole(this.currentUserRole); - this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole); - - - // Déterminer le merchantPartnerId - if(this.isDcbPartner){ - this.currentMerchantPartnerId = user.id; - } - else if(!this.isDcbPartner || !this.isHubAdminOrSupport) { - this.currentMerchantPartnerId = this.extractMerchantPartnerId(user); - } - - }, - error: (error) => { - this.fallbackPermissions(); - } - }); - } - - /** - * Méthode robuste pour extraire le rôle de l'utilisateur - */ - private extractUserRole(user: any): UserRole | null { - // 1. Essayer depuis les rôles du token (méthode principale) - const tokenRoles = this.authService.getCurrentUserRoles(); - if (tokenRoles && tokenRoles.length > 0) { - return tokenRoles[0]; - } - - // 2. Essayer depuis le profil user.role - if (user?.role && Object.values(UserRole).includes(user.role)) { - return user.role as UserRole; - } - - // 3. Essayer depuis user.userType pour déduire le rôle - if (user?.userType) { - const roleFromType = this.getRoleFromUserType(user.userType); - if (roleFromType) { - return roleFromType; - } - } - - // 4. Essayer depuis les attributs étendus - if (user?.attributes?.role?.[0]) { - const roleFromAttributes = user.attributes.role[0]; - if (Object.values(UserRole).includes(roleFromAttributes as UserRole)) { - return roleFromAttributes as UserRole; - } - } - - return null; - } - - /** - * Déduire le rôle à partir du userType - */ - private getRoleFromUserType(userType: string): UserRole | null { - const typeMapping: { [key: string]: UserRole } = { - [UserType.HUB]: UserRole.DCB_ADMIN, - [UserType.MERCHANT]: UserRole.DCB_PARTNER_ADMIN, - }; - - return typeMapping[userType] || null; - } - - /** - * Vérifier si l'utilisateur est Hub Admin ou Support - */ - private isHubAdminOrSupportRole(role: UserRole | null): boolean { - if (!role) return false; - - const hubAdminSupportRoles = [ - UserRole.DCB_ADMIN, - UserRole.DCB_SUPPORT - ]; - - return hubAdminSupportRoles.includes(role); - } - - /** - * Vérifier si l'utilisateur est DCB Partner - */ - private isDcbPartnerRole(role: UserRole | null): boolean { - if (!role) return false; - - const partnerRoles = [ - UserRole.DCB_PARTNER, - UserRole.DCB_PARTNER_ADMIN, - UserRole.DCB_PARTNER_MANAGER, - UserRole.DCB_PARTNER_SUPPORT - ]; - - return partnerRoles.includes(role); - } - - /** - * Vérifier si l'utilisateur peut voir tous les merchants - */ - private canViewAllMerchantsCheck(role: UserRole | null): boolean { - if (!role) return false; - - const canViewAllRoles = [ - UserRole.DCB_ADMIN, - UserRole.DCB_SUPPORT, - ]; - - return canViewAllRoles.includes(role); - } - - /** - * Extraire le merchantPartnerId de manière robuste - */ - private extractMerchantPartnerId(user: any): string { - // 1. Essayer depuis merchantPartnerId direct - if (user?.merchantPartnerId) { - return user.merchantPartnerId; - } - - // 2. Essayer depuis les attributs - if (user?.attributes?.merchantPartnerId?.[0]) { - return user.attributes.merchantPartnerId[0]; - } - - // 3. Essayer depuis AuthService - const authServiceMerchantId = this.authService.getCurrentMerchantPartnerId(); - if (authServiceMerchantId) { - return authServiceMerchantId; - } - - // 4. Pour les rôles Hub, pas de merchantPartnerId - if (this.isHubAdminOrSupport) { - return ''; - } - - return ''; - } - - /** - * Fallback en cas d'erreur de chargement du profil - */ - private fallbackPermissions(): void { - - // Utiliser les méthodes d'AuthService comme fallback - this.currentUserRole = this.authService.getCurrentUserRole(); - this.isHubAdminOrSupport = this.authService.isHubUser() || - this.authService.hasRole(UserRole.DCB_ADMIN) || - this.authService.hasRole(UserRole.DCB_SUPPORT); - this.isDcbPartner = this.authService.isMerchantUser() || - this.authService.hasRole(UserRole.DCB_PARTNER); - this.canViewAllMerchants = this.authService.canViewAllMerchants(); - this.currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId() || ''; - this.newMerchantUser.merchantPartnerId = this.currentMerchantPartnerId; - - } - - private loadAvailableRoles() { - this.merchantUsersService.getAvailableMerchantRoles() - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (roles) => { - this.availableRoles = roles; - - // Sélectionner le premier rôle disponible par défaut - const firstAllowedRole = roles.roles.find(role => role.allowedForCreation); - if (firstAllowedRole) { - this.newMerchantUser.role = firstAllowedRole.value; - } - }, - error: (error) => { - console.error('❌ Error loading available roles:', error); - } - }); - } - - showTab(tab: 'list' | 'profile', userId?: string) { - console.log(`Switching to tab: ${tab}`, userId ? `for user ${userId}` : ''); - this.activeTab = tab; - - if (userId) { - this.selectedUserId = userId; - } - } - - backToList() { - console.log('🔙 Returning to list view'); - this.activeTab = 'list'; - this.selectedUserId = null; - } - - // Méthodes de gestion des événements du composant enfant - onUserSelected(userId: string) { - this.showTab('profile', userId); - } - - onResetPasswordRequested(userId: string) { - this.openResetPasswordModal(userId); - } - - onDeleteUserRequested(userId: string) { - this.openDeleteUserModal(userId); - } - - // Méthodes pour les modals - openModal(content: TemplateRef, size: 'sm' | 'lg' | 'xl' = 'lg') { - this.modalService.open(content, { - size: size, - centered: true, - scrollable: true - }); - } - - // Méthode pour ouvrir le modal de création d'utilisateur - openCreateUserModal() { - this.resetUserForm(); - this.createUserError = ''; - this.openModal(this.createUserModal); - } - - // Réinitialiser le formulaire de création - private resetUserForm() { - this.newMerchantUser = { - username: '', - email: '', - firstName: '', - lastName: '', - password: '', - role: UserRole.DCB_PARTNER_SUPPORT, - merchantPartnerId: this.currentMerchantPartnerId, - enabled: true, - emailVerified: false - }; - console.log('🔄 User form reset'); - } - - // Méthode pour ouvrir le modal de réinitialisation de mot de passe - openResetPasswordModal(userId: string) { - this.merchantUsersService.getMerchantUserById(userId) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (user) => { - this.selectedUserForReset = user; - this.newPassword = ''; - this.temporaryPassword = true; - this.resetPasswordError = ''; - this.resetPasswordSuccess = ''; - this.openModal(this.resetPasswordModal); - console.log('✅ User loaded for password reset:', user.username); - }, - error: (error) => { - console.error('❌ Error loading user for password reset:', error); - this.resetPasswordError = 'Erreur lors du chargement de l\'utilisateur'; - this.cdRef.detectChanges(); - } - }); - } - - // Méthode pour ouvrir le modal de suppression - openDeleteUserModal(userId: string) { - console.log(`🗑️ Opening delete modal for user: ${userId}`); - this.merchantUsersService.getMerchantUserById(userId) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (user) => { - this.selectedUserForDelete = user; - this.deleteUserError = ''; - this.openModal(this.deleteUserModal); - console.log('✅ User loaded for deletion:', user.username); - }, - error: (error) => { - console.error('❌ Error loading user for deletion:', error); - this.deleteUserError = 'Erreur lors du chargement de l\'utilisateur'; - this.cdRef.detectChanges(); - } - }); - } - - // Créer un utilisateur marchand - createMerchantUser() { - console.log('🚀 Creating new merchant user...'); - const validation = this.validateUserForm(); - if (!validation.isValid) { - this.createUserError = validation.error!; - console.error('❌ Form validation failed:', validation.error); - return; - } - - this.creatingUser = true; - this.createUserError = ''; - - // S'assurer que le merchantPartnerId est défini - this.newMerchantUser.merchantPartnerId = this.currentMerchantPartnerId; - - this.merchantUsersService.createMerchantUser(this.newMerchantUser) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (createdUser) => { - this.creatingUser = false; - this.modalService.dismissAll(); - this.refreshUsersList(); - this.cdRef.detectChanges(); - }, - error: (error) => { - console.error('❌ Error creating user:', error); - this.creatingUser = false; - this.createUserError = this.getErrorMessage(error); - this.cdRef.detectChanges(); - } - }); - } - - // Réinitialiser le mot de passe - confirmResetPassword() { - if (!this.selectedUserForReset || !this.newPassword || this.newPassword.length < 8) { - this.resetPasswordError = 'Veuillez saisir un mot de passe valide (au moins 8 caractères).'; - console.error('❌ Password reset validation failed'); - return; - } - - console.log('🔑 Confirming password reset for user:', this.selectedUserForReset.username); - - this.resettingPassword = true; - this.resetPasswordError = ''; - this.resetPasswordSuccess = ''; - - const resetPasswordDto: ResetPasswordDto = { - newPassword: this.newPassword, - temporary: this.temporaryPassword - }; - - this.merchantUsersService.resetMerchantUserPassword( - this.selectedUserForReset.id, - resetPasswordDto - ).pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (response) => { - console.log('✅ Password reset successfully'); - this.resettingPassword = false; - this.resetPasswordSuccess = 'Mot de passe réinitialisé avec succès !'; - this.cdRef.detectChanges(); - - // Fermer le modal après 2 secondes - setTimeout(() => { - this.modalService.dismissAll(); - }, 2000); - }, - error: (error) => { - console.error('❌ Error resetting password:', error); - this.resettingPassword = false; - this.resetPasswordError = this.getResetPasswordErrorMessage(error); - this.cdRef.detectChanges(); - } - }); - } - - confirmDeleteUser() { - if (!this.selectedUserForDelete) { - console.error('❌ No user selected for deletion'); - return; - } - - console.log('🗑️ Confirming user deletion:', this.selectedUserForDelete.username); - - this.deletingUser = true; - this.deleteUserError = ''; - - this.merchantUsersService.deleteMerchantUser(this.selectedUserForDelete.id) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: () => { - console.log('✅ User deleted successfully'); - this.deletingUser = false; - this.modalService.dismissAll(); - this.refreshUsersList(); - this.cdRef.detectChanges(); - }, - error: (error) => { - console.error('❌ Error deleting user:', error); - this.deletingUser = false; - this.deleteUserError = this.getDeleteErrorMessage(error); - this.cdRef.detectChanges(); - } - }); - } - - @ViewChild(MerchantUsersList) usersListComponent!: MerchantUsersList; - - private refreshUsersList(): void { - if (this.usersListComponent && typeof this.usersListComponent.refreshData === 'function') { - console.log('🔄 Refreshing users list...'); - this.usersListComponent.refreshData(); - } else { - console.warn('❌ MerchantUsersList component not available for refresh'); - this.showTab('list'); - } - } - - // ==================== GESTION DES ERREURS ==================== - - private getErrorMessage(error: any): string { - if (error.error?.message) { - return error.error.message; - } - if (error.status === 400) { - return 'Données invalides. Vérifiez les champs du formulaire.'; - } - if (error.status === 409) { - return 'Un utilisateur avec ce nom d\'utilisateur ou email existe déjà.'; - } - if (error.status === 403) { - return 'Vous n\'avez pas les permissions pour créer cet utilisateur.'; - } - return 'Erreur lors de la création de l\'utilisateur. Veuillez réessayer.'; - } - - private getResetPasswordErrorMessage(error: any): string { - if (error.error?.message) { - return error.error.message; - } - if (error.status === 404) { - return 'Utilisateur non trouvé.'; - } - if (error.status === 400) { - return 'Le mot de passe ne respecte pas les critères de sécurité.'; - } - if (error.status === 403) { - return 'Vous n\'avez pas les permissions pour réinitialiser ce mot de passe.'; - } - return 'Erreur lors de la réinitialisation du mot de passe. Veuillez réessayer.'; - } - - private getDeleteErrorMessage(error: any): string { - if (error.error?.message) { - return error.error.message; - } - if (error.status === 404) { - return 'Utilisateur non trouvé.'; - } - if (error.status === 403) { - return 'Vous n\'avez pas les permissions pour supprimer cet utilisateur.'; - } - if (error.status === 409) { - return 'Impossible de supprimer cet utilisateur car il est associé à des données.'; - } - return 'Erreur lors de la suppression de l\'utilisateur. Veuillez réessayer.'; - } - - // ==================== VALIDATION DU FORMULAIRE ==================== - - private validateUserForm(): { isValid: boolean; error?: string } { - const requiredFields = [ - { field: this.newMerchantUser.username?.trim(), name: 'Nom d\'utilisateur' }, - { field: this.newMerchantUser.email?.trim(), name: 'Email' }, - { field: this.newMerchantUser.firstName?.trim(), name: 'Prénom' }, - { field: this.newMerchantUser.lastName?.trim(), name: 'Nom' } - ]; - - for (const { field, name } of requiredFields) { - if (!field) { - return { isValid: false, error: `${name} est requis` }; - } - } - - // Validation email - const email = this.newMerchantUser.email?.trim(); - if (!email) { - return { isValid: false, error: 'Email est requis' }; - } - - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { - return { isValid: false, error: 'Format d\'email invalide' }; - } - - if (!this.newMerchantUser.password || this.newMerchantUser.password.length < 8) { - return { isValid: false, error: 'Le mot de passe doit contenir au moins 8 caractères' }; - } - - if (!this.newMerchantUser.role) { - return { isValid: false, error: 'Le rôle est requis' }; - } - - if (!this.newMerchantUser.merchantPartnerId) { - return { isValid: false, error: 'Merchant Partner ID est requis' }; - } - - return { isValid: true }; - } - - // ==================== MÉTHODES UTILITAIRES ==================== - - getRoleDisplayName(role: UserRole): string { - // Seulement gérer les rôles marchands, ignorer les rôles Hub - switch (role) { - case UserRole.DCB_PARTNER_ADMIN: - return 'Administrateur Partenaire'; - case UserRole.DCB_PARTNER_MANAGER: - return 'Manager Partenaire'; - case UserRole.DCB_PARTNER_SUPPORT: - return 'Support Partenaire'; - default: - // Pour les rôles Hub, retourner le nom technique - return role; - } - } - - // Méthode utilitaire pour vérifier si un rôle est un rôle marchand - isMerchantRole(role: UserRole): boolean { - const merchantRoles = [ - UserRole.DCB_PARTNER_ADMIN, - UserRole.DCB_PARTNER_MANAGER, - UserRole.DCB_PARTNER_SUPPORT - ]; - return merchantRoles.includes(role); - } - - // Méthode pour filtrer les rôles disponibles (uniquement les rôles marchands) - getFilteredAvailableRoles(): { value: UserRole; label: string; description: string }[] { - if (!this.availableRoles) return []; - - return this.availableRoles.roles - .filter(role => this.isMerchantRole(role.value)) - .map(role => ({ - value: role.value, - label: this.getRoleDisplayName(role.value), - description: role.description - })); - } - - getAvailableRoles(): any[] { - if (!this.availableRoles) return []; - return this.availableRoles.roles; - } - - /** - * Vérifie si l'utilisateur peut attribuer un rôle spécifique - */ - canAssignRole(targetRole: UserRole): boolean { - // Seul DCB_PARTNER peut attribuer tous les rôles - if (this.currentUserRole === UserRole.DCB_PARTNER || this.currentUserRole === UserRole.DCB_ADMIN || this.currentUserRole === UserRole.DCB_SUPPORT) { - return true; - } - - // Les autres rôles ont des permissions limitées - return this.roleService.canAssignRole(this.currentUserRole, targetRole); - } - - canManageRoles(): boolean { - return this.currentUserRole === UserRole.DCB_PARTNER; - } - - /** - * Vérifie si l'utilisateur peut gérer les rôles - */ - get canManageAllRoles(): boolean { - return this.currentUserRole === UserRole.DCB_PARTNER; - } - - // Méthode pour obtenir uniquement les rôles marchands assignables - getAssignableMerchantRoles(): UserRole[] { - const merchantRoles = [ - UserRole.DCB_PARTNER_ADMIN, - UserRole.DCB_PARTNER_MANAGER, - UserRole.DCB_PARTNER_SUPPORT - ]; - - if (!this.availableRoles) return merchantRoles; - - return merchantRoles.filter(role => { - const roleInfo = this.availableRoles!.roles.find(r => r.value === role); - return roleInfo?.allowedForCreation !== false; - }); - } - - getRoleDescription(role: UserRole): string { - if (!this.availableRoles) return ''; - - const roleInfo = this.availableRoles.roles.find(r => r.value === role); - return roleInfo?.description || ''; - } - - isRoleAllowedForCreation(role: UserRole): boolean { - if (!this.availableRoles) return false; - - const roleInfo = this.availableRoles.roles.find(r => r.value === role); - return roleInfo?.allowedForCreation || false; - } - - // Méthodes utilitaires pour le template - getUserInitials(user: MerchantUserDto): string { - return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U'; - } - - getUserType(user: MerchantUserDto): string { - switch (user.role) { - case UserRole.DCB_PARTNER_ADMIN: - return 'Administrateur'; - case UserRole.DCB_PARTNER_MANAGER: - return 'Manager'; - case UserRole.DCB_PARTNER_SUPPORT: - return 'Support'; - default: - return 'Utilisateur'; - } - } - - getRoleBadgeClass(role: UserRole): string { - switch (role) { - case UserRole.DCB_PARTNER_ADMIN: - return 'bg-danger text-white'; - case UserRole.DCB_PARTNER_MANAGER: - return 'bg-warning text-dark'; - case UserRole.DCB_PARTNER_SUPPORT: - return 'bg-info text-white'; - default: - return 'bg-secondary text-white'; - } - } - - getRoleIcon(role: UserRole): string { - switch (role) { - case UserRole.DCB_PARTNER_ADMIN: - return 'lucideShield'; - case UserRole.DCB_PARTNER_MANAGER: - return 'lucideUserCog'; - case UserRole.DCB_PARTNER_SUPPORT: - return 'lucideHeadphones'; - default: - return 'lucideUser'; - } - } - - // ==================== RÉFÉRENCES AUX TEMPLATES ==================== - - @ViewChild('createUserModal') createUserModal!: TemplateRef; - @ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef; - @ViewChild('deleteUserModal') deleteUserModal!: TemplateRef; -} \ No newline at end of file diff --git a/src/app/modules/merchant-users/models/merchant-user.model.ts b/src/app/modules/merchant-users/models/merchant-user.model.ts deleted file mode 100644 index 9e7b94e..0000000 --- a/src/app/modules/merchant-users/models/merchant-user.model.ts +++ /dev/null @@ -1,115 +0,0 @@ -// src/app/core/models/user.model.ts - -export enum UserType { - HUB = 'HUB', - MERCHANT = 'MERCHANT', - MERCHANT_USER = 'MERCHANT_USER' -} - -export enum UserRole { - // HUB roles - DCB_ADMIN = 'DCB_ADMIN', - DCB_SUPPORT = 'DCB_SUPPORT', - DCB_PARTNER = 'DCB_PARTNER', - - // MERCHANT roles - DCB_PARTNER_ADMIN = 'DCB_PARTNER_ADMIN', - DCB_PARTNER_MANAGER = 'DCB_PARTNER_MANAGER', - DCB_PARTNER_SUPPORT = 'DCB_PARTNER_SUPPORT' -} - -// === BASE USER MODEL === -export interface BaseUserDto { - id: string; - username: string; - email: string; - firstName: string; - lastName: string; - role: UserRole; - enabled: boolean; - emailVerified: boolean; - createdBy: string; - createdByUsername: string; - createdTimestamp: number; - lastLogin?: number; - userType: UserType; -} - -// === EXTENSIONS === -export interface HubUserDto extends BaseUserDto { - userType: UserType.HUB; -} - -export interface MerchantUserDto extends BaseUserDto { - userType: UserType.MERCHANT; - merchantPartnerId: string; -} - -// === DTOs CRUD === -export interface CreateUserDto { - username: string; - email: string; - firstName: string; - lastName: string; - password: string; - role: UserRole; - enabled?: boolean; - emailVerified?: boolean; - merchantPartnerId?: string; // obligatoire si MERCHANT -} - -export interface UpdateUserDto { - firstName?: string; - lastName?: string; - email?: string; - enabled?: boolean; -} - -export interface ResetPasswordDto { - userId?: string; - newPassword: string; - temporary?: boolean; -} - -// === PAGINATION / STATS === -export interface PaginatedUserResponse { - users: BaseUserDto[]; - total: number; - page: number; - limit: number; - totalPages: number; -} - -export interface MerchantPartnerStatsResponse { - totalAdmins: number; - totalManagers: number; - totalSupport: number; - totalUsers: number; - activeUsers: number; - inactiveUsers: number; -} - -// === ROLES === -export interface AvailableRole { - value: UserRole; - label: string; - description: string; - allowedForCreation: boolean; -} - -export interface AvailableRolesResponse { - roles: AvailableRole[]; -} - -export interface RoleOperationResponse { - message: string; - success: boolean; -} - -// === SEARCH === -export interface SearchUsersParams { - query?: string; - role?: UserRole; - enabled?: boolean; - userType?: UserType; -} diff --git a/src/app/modules/merchant-users/models/partners-config.model.ts b/src/app/modules/merchant-users/models/partners-config.model.ts deleted file mode 100644 index 93cb0f8..0000000 --- a/src/app/modules/merchant-users/models/partners-config.model.ts +++ /dev/null @@ -1,531 +0,0 @@ -import { IsString, IsEmail, IsBoolean, IsOptional, IsArray, MinLength, IsNumber, IsEnum } from 'class-validator'; - -// ==================== TYPES AND ENUMS ==================== - -export type PartnerStatus = - | 'ACTIVE' - | 'INACTIVE' - -export type PartnerCategory = - | 'E_COMMERCE' - | 'GAMING' - | 'ENTERTAINMENT' - | 'UTILITIES' - | 'DIGITAL_CONTENT' - | 'SERVICES' - | 'OTHER'; - -// ==================== CALLBACK CONFIGURATION ==================== - -export interface CallbackConfiguration { - headerEnrichment?: { - url?: string; - method?: 'GET' | 'POST'; - headers?: { [key: string]: string }; - }; - subscription?: { - onCreate?: string; - onRenew?: string; - onCancel?: string; - onExpire?: string; - }; - payment?: { - onSuccess?: string; - onFailure?: string; - onRefund?: string; - }; - authentication?: { - onSuccess?: string; - onFailure?: string; - }; -} - -export class CallbackConfigurationImpl implements CallbackConfiguration { - headerEnrichment?: { - url?: string; - method?: 'GET' | 'POST'; - headers?: { [key: string]: string }; - } = {}; - - subscription?: { - onCreate?: string; - onRenew?: string; - onCancel?: string; - onExpire?: string; - } = {}; - - payment?: { - onSuccess?: string; - onFailure?: string; - onRefund?: string; - } = {}; - - authentication?: { - onSuccess?: string; - onFailure?: string; - } = {}; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -// ==================== CORE PARTNER MODELS ==================== - -export class PartnerAddress { - @IsString() - street: string = ''; - - @IsString() - city: string = ''; - - @IsString() - state: string = ''; - - @IsString() - postalCode: string = ''; - - @IsString() - country: string = ''; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class TechnicalContact { - @IsString() - name: string = ''; - - @IsEmail() - email: string = ''; - - @IsString() - phone: string = ''; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class PartnerStats { - @IsNumber() - totalTransactions: number = 0; - - @IsNumber() - totalRevenue: number = 0; - - @IsNumber() - successRate: number = 0; - - @IsNumber() - refundRate: number = 0; - - @IsNumber() - averageAmount: number = 0; - - @IsNumber() - todayTransactions: number = 0; - - @IsNumber() - todayRevenue: number = 0; - - @IsNumber() - activeProducts: number = 0; - - lastTransactionDate?: Date; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class Partner { - id: string = ''; - name: string = ''; - legalName: string = ''; - email: string = ''; - phone: string = ''; - website: string = ''; - - @IsEnum(['ACTIVE', 'INACTIVE']) - status: PartnerStatus = 'ACTIVE'; - - @IsEnum(['E_COMMERCE', 'GAMING', 'ENTERTAINMENT', 'UTILITIES', 'DIGITAL_CONTENT', 'SERVICES', 'OTHER']) - category: PartnerCategory = 'OTHER'; - - country: string = ''; - currency: string = ''; - timezone: string = ''; - - // Configuration technique - apiKey: string = ''; - secretKey: string = ''; - webhookUrl: string = ''; - - callbacks: CallbackConfiguration = new CallbackConfigurationImpl(); - - // Limites et commissions - @IsNumber() - commissionRate: number = 0; - - @IsNumber() - dailyLimit: number = 0; - - @IsNumber() - transactionLimit: number = 0; - - @IsNumber() - minAmount: number = 0; - - @IsNumber() - maxAmount: number = 0; - - // Adresse - address: PartnerAddress = new PartnerAddress(); - - // Contact technique - technicalContact: TechnicalContact = new TechnicalContact(); - - // Statistiques - stats: PartnerStats = new PartnerStats(); - - // Métadonnées - createdAt: Date = new Date(); - updatedAt: Date = new Date(); - createdBy: string = ''; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -// ==================== PARTNER DTOs ==================== - -export class CreatePartnerDto { - @IsString() - name: string = ''; - - @IsString() - legalName: string = ''; - - @IsEmail() - email: string = ''; - - @IsString() - phone: string = ''; - - @IsOptional() - @IsString() - website: string = ''; - - @IsEnum(['E_COMMERCE', 'GAMING', 'ENTERTAINMENT', 'UTILITIES', 'DIGITAL_CONTENT', 'SERVICES', 'OTHER']) - category: PartnerCategory = 'OTHER'; - - @IsString() - country: string = ''; - - @IsString() - currency: string = ''; - - @IsString() - timezone: string = ''; - - @IsNumber() - commissionRate: number = 0; - - @IsNumber() - dailyLimit: number = 0; - - @IsNumber() - transactionLimit: number = 0; - - @IsNumber() - minAmount: number = 0; - - @IsNumber() - maxAmount: number = 0; - - @IsOptional() - address?: PartnerAddress; - - @IsOptional() - technicalContact?: TechnicalContact; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class UpdatePartnerDto { - @IsOptional() - @IsString() - name?: string; - - @IsOptional() - @IsString() - legalName?: string; - - @IsOptional() - @IsEmail() - email?: string; - - @IsOptional() - @IsString() - phone?: string; - - @IsOptional() - @IsString() - website?: string; - - @IsOptional() - @IsEnum(['ACTIVE', 'INACTIVE', 'SUSPENDED', 'PENDING_VERIFICATION', 'BLOCKED']) - status?: PartnerStatus; - - @IsOptional() - @IsEnum(['E_COMMERCE', 'GAMING', 'ENTERTAINMENT', 'UTILITIES', 'DIGITAL_CONTENT', 'SERVICES', 'OTHER']) - category?: PartnerCategory; - - @IsOptional() - @IsNumber() - commissionRate?: number; - - @IsOptional() - @IsNumber() - dailyLimit?: number; - - @IsOptional() - @IsNumber() - transactionLimit?: number; - - @IsOptional() - @IsNumber() - minAmount?: number; - - @IsOptional() - @IsNumber() - maxAmount?: number; - - @IsOptional() - address?: PartnerAddress; - - @IsOptional() - technicalContact?: TechnicalContact; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class UpdateCallbacksDto { - @IsOptional() - callbacks?: CallbackConfiguration; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -// ==================== QUERY AND PAGINATION ==================== - -export class PartnerQuery { - @IsOptional() - page: number = 1; - - @IsOptional() - limit: number = 10; - - @IsOptional() - @IsString() - search?: string; - - @IsOptional() - @IsEnum(['ACTIVE', 'INACTIVE', 'SUSPENDED', 'PENDING_VERIFICATION', 'BLOCKED']) - status?: PartnerStatus; - - @IsOptional() - @IsEnum(['E_COMMERCE', 'GAMING', 'ENTERTAINMENT', 'UTILITIES', 'DIGITAL_CONTENT', 'SERVICES', 'OTHER']) - category?: PartnerCategory; - - @IsOptional() - @IsString() - country?: string; - - @IsOptional() - @IsString() - sortBy?: string; - - @IsOptional() - @IsEnum(['asc', 'desc']) - sortOrder: 'asc' | 'desc' = 'desc'; - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class PaginatedPartners { - data: Partner[] = []; - total: number = 0; - page: number = 1; - limit: number = 10; - totalPages: number = 0; - - constructor(data: Partner[] = [], total: number = 0, page: number = 1, limit: number = 10) { - this.data = data; - this.total = total; - this.page = page; - this.limit = limit; - this.totalPages = Math.ceil(total / limit) || 0; - } -} - -// ==================== API RESPONSES ==================== - -export class ApiResponse { - success: boolean = false; - data?: T = undefined; - error?: string = ''; - message?: string = ''; - - constructor(partial?: Partial>) { - if (partial) { - Object.assign(this, partial); - } - } -} - -export class ApiKeyResponse { - apiKey: string = ''; - secretKey: string = ''; - partnerId: string = ''; - createdAt: Date = new Date(); - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -// ==================== PARTNER FORM DATA ==================== - -export interface PartnerFormData { - companyInfo: { - name: string; - legalName: string; - taxId: string; - address: string; - country: string; - }; - contactInfo: { - email: string; - phone: string; - firstName: string; - lastName: string; - }; - paymentConfig: { - supportedOperators: string[]; - defaultCurrency: string; - maxTransactionAmount: number; - }; - webhookConfig: CallbackConfiguration; -} - -export class PartnerFormDataImpl implements PartnerFormData { - companyInfo: { - name: string; - legalName: string; - taxId: string; - address: string; - country: string; - } = { - name: '', - legalName: '', - taxId: '', - address: '', - country: 'CIV' - }; - - contactInfo: { - email: string; - phone: string; - firstName: string; - lastName: string; - } = { - email: '', - phone: '', - firstName: '', - lastName: '' - }; - - paymentConfig: { - supportedOperators: string[]; - defaultCurrency: string; - maxTransactionAmount: number; - } = { - supportedOperators: [], - defaultCurrency: 'XOF', - maxTransactionAmount: 50000 - }; - - webhookConfig: CallbackConfiguration = new CallbackConfigurationImpl(); - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} - -// ==================== PARTNER PRODUCT ==================== - -export class PartnerProduct { - id: string = ''; - partnerId: string = ''; - - @IsString() - name: string = ''; - - @IsString() - description: string = ''; - - @IsNumber() - price: number = 0; - - @IsString() - currency: string = ''; - - @IsString() - category: string = ''; - - @IsEnum(['ACTIVE', 'INACTIVE', 'SUSPENDED']) - status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED' = 'ACTIVE'; - - createdAt: Date = new Date(); - updatedAt: Date = new Date(); - - constructor(partial?: Partial) { - if (partial) { - Object.assign(this, partial); - } - } -} \ No newline at end of file diff --git a/src/app/modules/merchant-users/profile/profile.spec.ts b/src/app/modules/merchant-users/profile/profile.spec.ts deleted file mode 100644 index 334a750..0000000 --- a/src/app/modules/merchant-users/profile/profile.spec.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { PartnerTeamProfile } from './profile'; -describe('PartnerTeamProfile', () => {}); \ No newline at end of file diff --git a/src/app/modules/merchant-users/services/merchant-users.service.ts b/src/app/modules/merchant-users/services/merchant-users.service.ts deleted file mode 100644 index af45d4f..0000000 --- a/src/app/modules/merchant-users/services/merchant-users.service.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient, HttpParams } from '@angular/common/http'; -import { environment } from '@environments/environment'; -import { Observable, map, catchError, throwError, of } from 'rxjs'; - -import { - MerchantUserDto, - CreateUserDto, - UpdateUserDto, - ResetPasswordDto, - PaginatedUserResponse, - MerchantPartnerStatsResponse, - AvailableRolesResponse, - SearchUsersParams, - UserRole, - UserType -} from '@core/models/dcb-bo-hub-user.model'; - -@Injectable({ providedIn: 'root' }) -export class MerchantUsersService { - private http = inject(HttpClient); - private apiUrl = `${environment.iamApiUrl}/merchant-users`; - - // === CRÉATION === - - /** - * Crée un nouvel utilisateur marchand - */ - createMerchantUser(createUserDto: CreateUserDto): Observable { - // Validation spécifique aux marchands - if (!createUserDto.merchantPartnerId?.trim()) { - return throwError(() => 'Merchant Partner ID is required for merchant users'); - } - - if (!createUserDto.username?.trim()) { - return throwError(() => 'Username is required and cannot be empty'); - } - - if (!createUserDto.email?.trim()) { - return throwError(() => 'Email is required and cannot be empty'); - } - - if (!createUserDto.password || createUserDto.password.length < 8) { - return throwError(() => 'Password must be at least 8 characters'); - } - - if (!createUserDto.role) { - return throwError(() => 'Role is required'); - } - - // Vérification que le rôle est bien un rôle marchand - const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; - if (!merchantRoles.includes(createUserDto.role)) { - return throwError(() => 'Invalid role for merchant user'); - } - - // Nettoyage des données - const payload = { - ...createUserDto, - username: createUserDto.username.trim(), - email: createUserDto.email.trim(), - firstName: (createUserDto.firstName || '').trim(), - lastName: (createUserDto.lastName || '').trim(), - merchantPartnerId: createUserDto.merchantPartnerId.trim(), - enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true, - emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : false, - }; - - return this.http.post(this.apiUrl, payload).pipe( - catchError(error => { - console.error('Error creating merchant user:', error); - return throwError(() => error); - }) - ); - } - - // === LECTURE === - - /** - * Récupère les utilisateurs marchands de l'utilisateur courant - */ - getMyMerchantUsers(partnerId: string): Observable { - return this.http.get(this.apiUrl).pipe( - catchError(error => { - console.error('Error loading my merchant users:', error); - return throwError(() => error); - }) - ); - } - - /** - * Récupère les utilisateurs marchands par ID de partenaire - */ - getMerchantUsersByPartner(partnerId: string): Observable { - return this.http.get(`${this.apiUrl}/partner/${partnerId}`).pipe( - catchError(error => { - console.error(`Error loading merchant users for partner ${partnerId}:`, error); - return throwError(() => error); - }) - ); - } - - /** - * Récupère un utilisateur marchand par ID - */ - getMerchantUserById(id: string): Observable { - return this.http.get(`${this.apiUrl}/${id}`).pipe( - catchError(error => { - console.error(`Error loading merchant user ${id}:`, error); - return throwError(() => error); - }) - ); - } - - /** - * Récupère tous les utilisateurs marchands avec pagination - */ - getMerchantUsers(page: number = 1, limit: number = 10, filters?: SearchUsersParams): Observable { - let params = new HttpParams() - .set('page', page.toString()) - .set('limit', limit.toString()) - .set('userType', UserType.MERCHANT); - - if (filters) { - Object.keys(filters).forEach(key => { - if (filters[key as keyof SearchUsersParams] !== undefined && filters[key as keyof SearchUsersParams] !== null) { - params = params.set(key, filters[key as keyof SearchUsersParams]!.toString()); - } - }); - } - - return this.http.get(this.apiUrl, { params, observe: 'response' }).pipe( - map(response => { - const users = response.body || []; - const total = parseInt(response.headers.get('X-Total-Count') || '0'); - - return { - users, - total, - page, - limit, - totalPages: Math.ceil(total / limit) - }; - }), - catchError(error => { - console.error('Error loading merchant users:', error); - return throwError(() => error); - }) - ); - } - - // === MISE À JOUR === - - /** - * Met à jour un utilisateur marchand - */ - updateMerchantUser(id: string, updateUserDto: UpdateUserDto): Observable { - return this.http.put(`${this.apiUrl}/${id}`, updateUserDto).pipe( - catchError(error => { - console.error(`Error updating merchant user ${id}:`, error); - return throwError(() => error); - }) - ); - } - - // === SUPPRESSION === - - /** - * Supprime un utilisateur marchand - */ - deleteMerchantUser(id: string): Observable<{ message: string }> { - return this.http.delete<{ message: string }>(`${this.apiUrl}/${id}`).pipe( - catchError(error => { - console.error(`Error deleting merchant user ${id}:`, error); - return throwError(() => error); - }) - ); - } - - // === GESTION DES MOTS DE PASSE === - - /** - * Réinitialise le mot de passe d'un utilisateur marchand - */ - resetMerchantUserPassword(id: string, resetPasswordDto: ResetPasswordDto): Observable<{ message: string }> { - return this.http.post<{ message: string }>( - `${this.apiUrl}/${id}/reset-password`, - resetPasswordDto - ).pipe( - catchError(error => { - console.error(`Error resetting password for merchant user ${id}:`, error); - return throwError(() => error); - }) - ); - } - - // === GESTION DU STATUT === - - /** - * Active un utilisateur marchand - */ - enableMerchantUser(id: string): Observable { - return this.updateMerchantUser(id, { enabled: true }); - } - - /** - * Désactive un utilisateur marchand - */ - disableMerchantUser(id: string): Observable { - return this.updateMerchantUser(id, { enabled: false }); - } - - // === STATISTIQUES === - - /** - * Récupère les statistiques des utilisateurs marchands - */ - getMerchantPartnerStats(): Observable { - return this.http.get(`${this.apiUrl}/stats/overview`).pipe( - catchError(error => { - console.error('Error loading merchant users stats:', error); - return throwError(() => error); - }) - ); - } - - // === RECHERCHE === - - /** - * Recherche des utilisateurs marchands avec filtres - */ - searchMerchantUsers(params: SearchUsersParams): Observable { - let httpParams = new HttpParams().set('userType', UserType.MERCHANT); - - if (params.query) { - httpParams = httpParams.set('query', params.query); - } - - if (params.role) { - httpParams = httpParams.set('role', params.role); - } - - if (params.enabled !== undefined) { - httpParams = httpParams.set('enabled', params.enabled.toString()); - } - - return this.http.get(`${this.apiUrl}/search`, { params: httpParams }).pipe( - catchError(error => { - console.error('Error searching merchant users:', error); - return throwError(() => error); - }) - ); - } - - // === GESTION DES RÔLES === - - /** - * Récupère les rôles marchands disponibles - */ - getAvailableMerchantRoles(): Observable { - return this.http.get(`${this.apiUrl}/roles/available`).pipe( - catchError(error => { - console.error('Error loading available merchant roles:', error); - return of({ - roles: [ - { - value: UserRole.DCB_PARTNER_ADMIN, - label: 'Partner Admin', - description: 'Full administrative access within the merchant partner', - allowedForCreation: true - }, - { - value: UserRole.DCB_PARTNER_MANAGER, - label: 'Partner Manager', - description: 'Manager access with limited administrative capabilities', - allowedForCreation: true - }, - { - value: UserRole.DCB_PARTNER_SUPPORT, - label: 'Partner Support', - description: 'Support role with read-only and basic operational access', - allowedForCreation: true - } - ] - }); - }) - ); - } - - // === UTILITAIRES === - - /** - * Vérifie si un nom d'utilisateur existe parmi les utilisateurs marchands - */ - merchantUserExists(username: string): Observable<{ exists: boolean }> { - return this.searchMerchantUsers({ query: username }).pipe( - map(users => ({ - exists: users.some(user => user.username === username) - })), - catchError(error => { - console.error('Error checking if merchant user exists:', error); - return of({ exists: false }); - }) - ); - } - - /** - * Récupère les utilisateurs par rôle spécifique - */ - getMerchantUsersByRole(role: UserRole): Observable { - const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; - if (!merchantRoles.includes(role)) { - return throwError(() => 'Invalid role for merchant user'); - } - return this.searchMerchantUsers({ role }); - } - - /** - * Récupère uniquement les utilisateurs actifs - */ - getActiveMerchantUsers(): Observable { - return this.searchMerchantUsers({ enabled: true }); - } - - /** - * Récupère uniquement les utilisateurs inactifs - */ - getInactiveMerchantUsers(): Observable { - return this.searchMerchantUsers({ enabled: false }); - } -} \ No newline at end of file diff --git a/src/app/modules/merchant-users/services/partner-config.service.ts b/src/app/modules/merchant-users/services/partner-config.service.ts deleted file mode 100644 index 83a579d..0000000 --- a/src/app/modules/merchant-users/services/partner-config.service.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { environment } from '@environments/environment'; -import { catchError, Observable, of, throwError } from 'rxjs'; - -import { - Partner, - CreatePartnerDto, - UpdatePartnerDto, - PartnerQuery, - PaginatedPartners, - ApiResponse, - CallbackConfiguration, - UpdateCallbacksDto, - ApiKeyResponse, - PartnerStats -} from '../models/partners-config.model'; - -@Injectable({ providedIn: 'root' }) -export class PartnerConfigService { - private http = inject(HttpClient); - private apiUrl = `${environment.localServiceTestApiUrl}/partners/config`; - - // ==================== GESTION DES MARCHANDS (ADMIN) ==================== - - /** - * Créer une config marchand - */ - createPartnerConfig(createPartnerDto: CreatePartnerDto): Observable> { - return this.http.post>(`${this.apiUrl}`, createPartnerDto).pipe( - catchError(error => throwError(() => error)) - ); - } - - /** - * Obtenir toutes les une config marchands avec pagination - */ - findAllPartnersConfig(query: PartnerQuery = new PartnerQuery()): Observable { - const params = this.buildQueryParams(query); - - return this.http.get(`${this.apiUrl}`, { params }).pipe( - catchError(error => { - console.error('Error loading merchants:', error); - return of(new PaginatedPartners()); - }) - ); - } - - /** - * Obtenir une config marchand par son ID - */ - getPartnerConfigById(merchantId: string): Observable> { - return this.http.get>(`${this.apiUrl}/${merchantId}`).pipe( - catchError(error => throwError(() => error)) - ); - } - - /** - * Mettre à jour une config marchand - */ - updatePartnerConfig(merchantId: string, updateData: UpdatePartnerDto): Observable> { - return this.http.put>(`${this.apiUrl}/${merchantId}`, updateData).pipe( - catchError(error => throwError(() => error)) - ); - } - - /** - * Supprimer une config marchand - */ - deletePartnerConfig(merchantId: string): Observable> { - return this.http.delete>(`${this.apiUrl}/${merchantId}`).pipe( - catchError(error => throwError(() => error)) - ); - } - - // ==================== GESTION DES CALLBACKS ==================== - - /** - * Mettre à jour la configuration des callbacks d'un marchand - */ - updateCallbacksConfig(merchantId: string, updateData: UpdateCallbacksDto): Observable> { - return this.http.put>( - `${this.apiUrl}/${merchantId}/callbacks`, - updateData - ).pipe( - catchError(error => throwError(() => error)) - ); - } - - /** - * Obtenir la configuration des callbacks d'un marchand - */ - getCallbacksConfig(merchantId: string): Observable> { - return this.http.get>( - `${this.apiUrl}/${merchantId}/callbacks` - ).pipe( - catchError(error => throwError(() => error)) - ); - } - - /** - * Tester un webhook spécifique - */ - testWebhookConfig(merchantId: string, webhookType: string, payload: any = {}): Observable> { - return this.http.post>( - `${this.apiUrl}/${merchantId}/callbacks/test/${webhookType}`, - payload - ).pipe( - catchError(error => throwError(() => error)) - ); - } - - /** - * Obtenir les logs des webhooks d'un marchand - */ - getWebhookLogsConfig(merchantId: string, query: any = {}): Observable> { - const params = this.buildQueryParams(query); - return this.http.get>( - `${this.apiUrl}/${merchantId}/callbacks/logs`, - { params } - ).pipe( - catchError(error => throwError(() => error)) - ); - } - - // ==================== GESTION DES STATISTIQUES ==================== - - /** - * Obtenir les statistiques d'un marchand - */ - getPartnerStats(merchantId: string): Observable> { - return this.http.get>( - `${this.apiUrl}/${merchantId}/stats` - ).pipe( - catchError(error => throwError(() => error)) - ); - } - - /** - * Obtenir mes statistiques (marchand connecté) - */ - getMyStats(): Observable> { - return this.http.get>( - `${this.apiUrl}/me/stats` - ).pipe( - catchError(error => throwError(() => error)) - ); - } - - /** - * Obtenir les statistiques globales (admin seulement) - */ - getGlobalStats(): Observable> { - return this.http.get>( - `${this.apiUrl}/stats/global` - ).pipe( - catchError(error => throwError(() => error)) - ); - } - - // ==================== GESTION DES CLÉS API ==================== - - /** - * Générer de nouvelles clés API pour un marchand - */ - generateApiKeys(merchantId: string): Observable> { - return this.http.post>( - `${this.apiUrl}/${merchantId}/api-keys`, - {} - ).pipe( - catchError(error => throwError(() => error)) - ); - } - - /** - * Révoker les clés API d'un marchand - */ - revokeApiKeys(merchantId: string): Observable> { - return this.http.delete>( - `${this.apiUrl}/${merchantId}/api-keys` - ).pipe( - catchError(error => throwError(() => error)) - ); - } - - /** - * Régénérer la clé secrète d'un marchand - */ - regenerateSecretKey(merchantId: string): Observable> { - return this.http.put>( - `${this.apiUrl}/${merchantId}/api-keys/regenerate-secret`, - {} - ).pipe( - catchError(error => throwError(() => error)) - ); - } - - /** - * Obtenir les clés API d'un marchand - */ - getApiKeys(merchantId: string): Observable> { - return this.http.get>( - `${this.apiUrl}/${merchantId}/api-keys` - ).pipe( - catchError(error => throwError(() => error)) - ); - } - - // ==================== MÉTHODES UTILITAIRES ==================== - - private buildQueryParams(query: any): { [key: string]: string } { - const params: { [key: string]: string } = {}; - - Object.keys(query).forEach(key => { - if (query[key] !== undefined && query[key] !== null && query[key] !== '') { - params[key] = query[key].toString(); - } - }); - - return params; - } - - /** - * Valider la configuration d'un marchand - */ - validatePartnerConfig(merchantId: string): Observable> { - return this.http.get>( - `${this.apiUrl}/${merchantId}/validate` - ).pipe( - catchError(error => throwError(() => error)) - ); - } -} \ No newline at end of file diff --git a/src/app/modules/merchant-users/stats/stats.html b/src/app/modules/merchant-users/stats/stats.html deleted file mode 100644 index 72cd000..0000000 --- a/src/app/modules/merchant-users/stats/stats.html +++ /dev/null @@ -1,249 +0,0 @@ - - - Vue d'ensemble des utilisateurs de votre écosystème marchand - - -
- @if (!stats) { -
-
- Chargement... -
-

Chargement des statistiques...

-
- } - - @if (stats) { -
- -
-
-
-
-
-

Total Utilisateurs

-

{{ stats.totalUsers }}

-

- - - Équipe complète - -

-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-

Administrateurs

-

{{ stats.totalAdmins }}

-

- - - Accès complet - -

-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-

Managers

-

{{ stats.totalManagers }}

-

- - - Gestion opérationnelle - -

-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-

Support

-

{{ stats.totalSupport }}

-

- - - Assistance client - -

-
-
-
- -
-
-
-
-
-
- - -
-
-
-
Utilisateurs Actifs
-
-

{{ stats.activeUsers }}

-

Comptes activés

-
-
-
- {{ (stats.activeUsers / stats.totalUsers * 100).toFixed(1) }}% -
-
- - {{ stats.activeUsers }} sur {{ stats.totalUsers }} utilisateurs - -
-
-
- -
-
-
-
Utilisateurs Inactifs
-
-

{{ stats.inactiveUsers }}

-

Comptes désactivés

-
-
-
- {{ (stats.inactiveUsers / stats.totalUsers * 100).toFixed(1) }}% -
-
- - {{ stats.inactiveUsers }} sur {{ stats.totalUsers }} utilisateurs - -
-
-
- -
-
-
-
Répartition des Rôles
-
-
-

{{ stats.totalAdmins }}

- Admins -
-
-

{{ stats.totalManagers }}

- Managers -
-
-

{{ stats.totalSupport }}

- Support -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - -
-
-
-
Synthèse de l'Équipe
-
-
-
-
{{ stats.totalUsers }}
- Total Membres -
-
-
-
-
{{ stats.activeUsers }}
- Actifs -
-
-
-
-
{{ stats.inactiveUsers }}
- Inactifs -
-
-
-
{{ stats.totalAdmins + stats.totalManagers + stats.totalSupport }}
- Avec Rôle Défini -
-
- - -
-
- -
- - Votre équipe marchande est composée de {{ stats.totalAdmins }} administrateurs, - {{ stats.totalManagers }} managers et {{ stats.totalSupport }} agents de support. - {{ stats.activeUsers }} utilisateurs sont actuellement actifs. - -
-
-
-
-
-
-
- } -
-
\ No newline at end of file diff --git a/src/app/modules/merchant-users/stats/stats.spec.ts b/src/app/modules/merchant-users/stats/stats.spec.ts deleted file mode 100644 index 98ccd87..0000000 --- a/src/app/modules/merchant-users/stats/stats.spec.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { MerchantPartnerStats } from './stats'; -describe('Merchant Partner Stats', () => {}); \ No newline at end of file diff --git a/src/app/modules/merchant-users/stats/stats.ts b/src/app/modules/merchant-users/stats/stats.ts deleted file mode 100644 index b0fc903..0000000 --- a/src/app/modules/merchant-users/stats/stats.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { NgIcon } from '@ng-icons/core'; -import { UiCard } from '@app/components/ui-card'; -import { MerchantPartnerStatsResponse } from '@core/models/dcb-bo-hub-user.model'; - -@Component({ - selector: 'app-merchant-users-stats', - standalone: true, - imports: [CommonModule, NgIcon, UiCard], - templateUrl: './stats.html' -}) -export class MerchantPartnerStats { - @Input() stats: MerchantPartnerStatsResponse | null = null; -} \ No newline at end of file diff --git a/src/app/modules/merchant-users/types.ts b/src/app/modules/merchant-users/types.ts deleted file mode 100644 index d63fd5b..0000000 --- a/src/app/modules/merchant-users/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type WizardStepType = { - id: string - icon: string - title: string - subtitle: string -} diff --git a/src/app/modules/modules.routes.ts b/src/app/modules/modules.routes.ts index b1e1bb8..2c86436 100644 --- a/src/app/modules/modules.routes.ts +++ b/src/app/modules/modules.routes.ts @@ -1,26 +1,26 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { authGuard } from '../core/guards/auth.guard'; -import { roleGuard } from '../core/guards/role.guard'; -import { HubUsers } from '@modules/hub-users/hub-users'; +import { authGuard } from '@core/guards/auth.guard'; +import { roleGuard } from '@core/guards/role.guard'; +import { HubUsersManagement } from '@modules/hub-users-management/hub-users'; +import { MerchantUsersManagement } from '@modules/hub-users-management/merchant-users'; // Composants principaux -import { DcbDashboard } from './dcb-dashboard/dcb-dashboard'; -import { Team } from './team/team'; -import { Transactions } from './transactions/transactions'; -import { MerchantUsers } from './merchant-users/merchant-users'; -import { OperatorsConfig } from './operators/config/config'; -import { OperatorsStats } from './operators/stats/stats'; -import { WebhooksHistory } from './webhooks/history/history'; -import { WebhooksStatus } from './webhooks/status/status'; -import { WebhooksRetry } from './webhooks/retry/retry'; -import { Settings } from './settings/settings'; -import { Integrations } from './integrations/integrations'; -import { Support } from './support/support'; -import { MyProfile } from './profile/profile'; -import { Documentation } from './documentation/documentation'; -import { Help } from './help/help'; -import { About } from './about/about'; +import { DcbDashboard } from '@modules/dcb-dashboard/dcb-dashboard'; +import { Team } from '@modules/team/team'; +import { Transactions } from '@modules/transactions/transactions'; +import { OperatorsConfig } from '@modules/operators/config/config'; +import { OperatorsStats } from '@modules/operators/stats/stats'; +import { WebhooksHistory } from '@modules/webhooks/history/history'; +import { WebhooksStatus } from '@modules/webhooks/status/status'; +import { WebhooksRetry } from '@modules/webhooks/retry/retry'; +import { Settings } from '@modules/settings/settings'; +import { Integrations } from '@modules/integrations/integrations'; +import { Support } from '@modules/support/support'; +import { MyProfile } from '@modules/profile/profile'; +import { Documentation } from '@modules/documentation/documentation'; +import { Help } from '@modules/help/help'; +import { About } from '@modules/about/about'; const routes: Routes = [ // --------------------------- @@ -75,12 +75,13 @@ const routes: Routes = [ // Users (Admin seulement) // --------------------------- { - path: 'users', + path: 'hub-users-management', canActivate: [authGuard, roleGuard], - component: HubUsers, + component: HubUsersManagement, data: { title: 'Gestion des Utilisateurs', - module: 'users' + module: 'hub-users-management', + context: 'HUB', } }, @@ -88,12 +89,13 @@ const routes: Routes = [ // Partners // --------------------------- { - path: 'merchant-partners', - component: MerchantUsers, + path: 'merchant-users-management', + component: MerchantUsersManagement, canActivate: [authGuard, roleGuard], data: { title: 'Gestion Partners/Marchants', - module: 'merchant-partners', + module: 'merchant-users-management', + context: 'MERCHANT', requiredRoles: [ 'dcb-admin', 'dcb-support', diff --git a/src/app/modules/profile/profile.html b/src/app/modules/profile/profile.html index 0eafdd1..f97292a 100644 --- a/src/app/modules/profile/profile.html +++ b/src/app/modules/profile/profile.html @@ -8,22 +8,18 @@ @if (user) { {{ getUserDisplayName() }} } @else { - Profil Utilisateur + Mon Profil } @@ -31,34 +27,15 @@
- @if (user && canEditUsers && !isEditing) { + @if (user && !isEditing) { - - @if (user.enabled) { - - } @else { - - } -
- - @if (currentUserRole && !canEditUsers) { -
-
-
-
- -
- Permissions limitées : Vous ne pouvez que consulter ce profil -
-
-
-
-
- } - @if (error) {
@@ -115,7 +76,7 @@
Chargement...
-

Chargement du profil...

+

Chargement de votre profil...

} @@ -126,7 +87,9 @@
-
Profil Utilisateur Hub
+
+ Mon Profil +
@@ -139,8 +102,13 @@
{{ getUserDisplayName() }}

@{{ user.username }}

+ + + {{ getUserTypeDisplay() }} + + - + {{ getStatusText() }} @@ -155,12 +123,12 @@
- Créé le {{ formatTimestamp(user.createdTimestamp) }} + Membre depuis {{ getCreationDate() }}
- @if (user.lastLogin) { + @if (isMerchantUser()) {
- - Dernière connexion : {{ formatTimestamp(user.lastLogin) }} + + Partner ID: {{ getMerchantPartnerId() }}
}
@@ -169,108 +137,70 @@
-
-
Rôle Utilisateur
- @if (canManageRoles && !isEditing) { - Modifiable - } +
+
+ Mon Rôle +
- - @if (user.roles && user.roles.length > 0) { - @for (role of user.roles; track role; let first = $first) { - @if (first) { - - - {{ getRoleLabel(role) }} - - - {{ getRoleDescription(role) }} - - } - } + @if (getUserRole()) { +
+ + + {{ getRoleLabel() }} + +
- - @if (user.roles.length > 1) { - - + {{ user.roles.length - 1 }} autre(s) rôle(s) - - } + + + {{ getRoleDescription() }} + } @else { Aucun rôle }
- - @if (canManageRoles && !isEditing) { -
- - -
- @if (updatingRoles) { -
- Mise à jour... -
- Mise à jour en cours... - } @else { - Sélectionnez un nouveau rôle pour cet utilisateur - } -
-
- } @else if (!canManageRoles) { -
- - - Vous n'avez pas la permission de modifier les rôles - -
- } + +
+ + + Votre rôle ne peut pas être modifié depuis cette page + +
-
Informations de Création
+
Informations du Compte
- Créé par : -
{{ user.createdByUsername || 'Système' }}
+ ID Utilisateur : +
{{ user.id }}
Date de création : -
{{ formatTimestamp(user.createdTimestamp) }}
+
{{ getCreationDate() }}
- Type d'utilisateur : + Type de compte :
- {{ user.userType }} + {{ getUserTypeDisplay() }}
+ @if (isMerchantUser()) { +
+ Merchant Partner ID : +
+ {{ getMerchantPartnerId() }} +
+
+ }
@@ -283,10 +213,10 @@
@if (isEditing) { - Modification du Profil + Modification de Mon Profil } @else { - Détails du Compte + Mes Informations }
@@ -305,7 +235,7 @@ type="button" class="btn btn-success btn-sm" (click)="saveProfile()" - [disabled]="saving" + [disabled]="saving || !isFormValid()" > @if (saving) {
@@ -323,15 +253,21 @@
- + @if (isEditing) { + @if (!editedUser.firstName?.trim()) { +
+ Le prénom est obligatoire +
+ } } @else {
{{ user.firstName || 'Non renseigné' }} @@ -341,15 +277,21 @@
- + @if (isEditing) { + @if (!editedUser.lastName?.trim()) { +
+ Le nom est obligatoire +
+ } } @else {
{{ user.lastName || 'Non renseigné' }} @@ -364,7 +306,7 @@ {{ user.username }}
- Le nom d'utilisateur ne peut pas être modifié + Votre identifiant de connexion ne peut pas être modifié
@@ -376,8 +318,9 @@ type="email" class="form-control" [(ngModel)]="editedUser.email" - placeholder="email@exemple.com" + placeholder="votre.email@exemple.com" [disabled]="saving" + required > } @else {
@@ -389,35 +332,40 @@ }
- - @if (isEditing) { -
-
- - -
-
- L'utilisateur peut se connecter si activé -
+ +
+ +
+ + {{ getStatusText() }} +
- } @else { -
- -
- - {{ getStatusText() }} +
+ @if (!user.enabled) { + Votre compte est actuellement désactivé + } @else if (!user.emailVerified) { + Votre email n'est pas encore vérifié + } @else { + Votre compte est actif + } +
+
+ + +
+ +
+
+ + + {{ getRoleLabel() }}
- } +
+ {{ getRoleDescription() }} +
+
@if (!isEditing) { @@ -437,21 +385,23 @@
- {{ formatTimestamp(user.createdTimestamp) }} + {{ getCreationDate() }}
- +
- {{ user.createdByUsername || 'Système' }} + {{ getUserTypeDisplay() }}
-
- -
- {{ user.userType }} + @if (isMerchantUser()) { +
+ +
+ {{ getMerchantPartnerId() }} +
-
+ }
} @@ -459,49 +409,30 @@
- - @if (!isEditing && canEditUsers) { + + @if (!isEditing) {
-
Actions de Gestion
+
Actions Personnelles
-
+
-
- @if (user.enabled) { - - } @else { - - } -
-
+
@@ -511,4 +442,248 @@
}
-
\ No newline at end of file +
+ + + + + + + + + + \ No newline at end of file diff --git a/src/app/modules/profile/profile.ts b/src/app/modules/profile/profile.ts index f77f340..3584aa8 100644 --- a/src/app/modules/profile/profile.ts +++ b/src/app/modules/profile/profile.ts @@ -1,19 +1,24 @@ -// src/app/modules/users/profile/personal-profile.ts -import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core'; +import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, OnDestroy, ViewChild, TemplateRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { NgIcon } from '@ng-icons/core'; -import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbAlertModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { Subject, takeUntil } from 'rxjs'; -import { HubUsersService } from '../hub-users/services/hub-users.service'; -import { RoleManagementService } from '@core/services/role-management.service'; -import { AuthService } from '@core/services/auth.service'; import { + User, UpdateUserDto, - UserRole + UserRole, + UserType, + UserUtils, + ResetPasswordDto } from '@core/models/dcb-bo-hub-user.model'; +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 { AuthService } from '@core/services/auth.service'; + @Component({ selector: 'app-my-profile', standalone: true, @@ -27,42 +32,61 @@ import { .fs-24 { font-size: 24px; } + .profile-card { + max-width: 800px; + margin: 0 auto; + } `] }) export class MyProfile implements OnInit, OnDestroy { - private usersService = inject(HubUsersService); + private modalService = inject(NgbModal); + private hubUsersService = inject(HubUsersService); + private merchantUsersService = inject(MerchantUsersService); private roleService = inject(RoleManagementService); private authService = inject(AuthService); private cdRef = inject(ChangeDetectorRef); private destroy$ = new Subject(); - @Output() back = new EventEmitter(); - @Output() openResetPasswordModal = new EventEmitter(); + readonly UserRole = UserRole; + readonly UserUtils = UserUtils; - user: any | null = null; + @Output() back = new EventEmitter(); + + // Référence au modal MyProfile + @ViewChild('myProfileResetPasswordModal') myProfileResetPasswordModal!: TemplateRef; + + // États pour MyProfile + myProfileCurrentPassword: string = ''; + myProfileNewPassword: string = ''; + temporaryPassword = false; + myProfileConfirmPassword: string = ''; + resettingMyPassword = false; + myProfileResetSuccess: string = ''; + myProfileResetError: string = ''; + + // États pour afficher/masquer les mots de passe + showCurrentPassword = false; + showNewMyPassword = false; + showConfirmMyPassword = false; + + // Utilisateur connecté + + user: User | undefined; loading = false; saving = false; error = ''; success = ''; - // Gestion des permissions (toujours true pour le profil personnel) - currentUserRole: UserRole | null = null; - canEditUsers = true; // Toujours vrai pour son propre profil - canManageRoles = false; // Jamais vrai pour le profil personnel - canDeleteUsers = false; // Jamais vrai pour le profil personnel - // Édition isEditing = false; editedUser: UpdateUserDto = {}; - - // Gestion des rôles (simplifiée pour profil personnel) + + // Gestion des rôles (lecture seule) availableRoles: { value: UserRole; label: string; description: string }[] = []; - updatingRoles = false; ngOnInit() { - this.initializeUserPermissions(); - this.loadAvailableRoles(); this.loadUserProfile(); + this.loadAvailableRoles(); } ngOnDestroy(): void { @@ -70,31 +94,13 @@ export class MyProfile implements OnInit, OnDestroy { this.destroy$.complete(); } - /** - * Initialise les permissions de l'utilisateur courant - */ - private initializeUserPermissions(): void { - this.authService.loadUserProfile() - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (profile) => { - this.currentUserRole = profile?.role?.[0] as UserRole || null; - // Pour le profil personnel, on peut toujours éditer son propre profil - this.canEditUsers = true; - this.canManageRoles = false; // On ne peut pas gérer les rôles de son propre profil - this.canDeleteUsers = false; // On ne peut pas se supprimer soi-même - }, - error: (error) => { - console.error('Error loading user permissions:', error); - } - }); - } - /** * Charge les rôles disponibles (lecture seule pour profil personnel) */ private loadAvailableRoles(): void { - this.usersService.getAvailableHubRoles() + this.isHubUser() + ? this.hubUsersService.getAvailableHubRoles() + : this.merchantUsersService.getAvailableMerchantRoles() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (response) => { @@ -106,15 +112,8 @@ export class MyProfile implements OnInit, OnDestroy { }, error: (error) => { console.error('Error loading available roles:', error); - // Fallback avec tous les rôles - this.availableRoles = [ - { value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' }, - { value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' }, - { value: UserRole.DCB_PARTNER, label: 'DCB Partner', description: 'Partenaire commercial' }, - { value: UserRole.DCB_PARTNER_ADMIN, label: 'DCB Partner Admin', description: 'Admin Partenaire commercial' }, - { value: UserRole.DCB_PARTNER_MANAGER, label: 'DCB Partner Manager', description: 'Manager Partenaire commercial' }, - { value: UserRole.DCB_PARTNER_SUPPORT, label: 'DCB Partner Support', description: 'Support Partenaire commercial' } - ]; + // Fallback avec les rôles principaux + this.availableRoles = []; } }); } @@ -127,47 +126,52 @@ export class MyProfile implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe({ next: (profile) => { - this.user = profile; + this.user = profile || undefined; + console.log("Profile User : " + this.user?.role); this.loading = false; - this.cdRef.detectChanges(); + this.cdRef.detectChanges(); }, error: (error) => { this.error = 'Erreur lors du chargement de votre profil'; this.loading = false; - this.cdRef.detectChanges(); + this.cdRef.detectChanges(); console.error('Error loading user profile:', error); } }); } startEditing() { - // Pas de vérification de permission pour le profil personnel this.isEditing = true; this.editedUser = { firstName: this.user?.firstName, lastName: this.user?.lastName, email: this.user?.email - // On ne permet pas de modifier 'enabled' sur son propre profil }; - this.cdRef.detectChanges(); + this.clearMessages(); } cancelEditing() { this.isEditing = false; this.editedUser = {}; - this.error = ''; - this.success = ''; - this.cdRef.detectChanges(); + this.clearMessages(); } saveProfile() { if (!this.user) return; - this.saving = true; - this.error = ''; - this.success = ''; + if (!this.isFormValid()) { + this.error = 'Veuillez remplir tous les champs obligatoires correctement'; + return; + } - this.usersService.updateHubUser(this.user.id, this.editedUser) + this.saving = true; + this.clearMessages(); + + const updateObservable = this.isHubUser() + ? this.hubUsersService.updateHubUser(this.user.id, this.editedUser) + : this.merchantUsersService.updateMerchantUser(this.user.id, this.editedUser); + + updateObservable .pipe(takeUntil(this.destroy$)) .subscribe({ next: (updatedUser) => { @@ -176,61 +180,35 @@ export class MyProfile implements OnInit, OnDestroy { this.saving = false; this.success = 'Profil mis à jour avec succès'; this.editedUser = {}; - this.cdRef.detectChanges(); + this.cdRef.detectChanges(); }, error: (error) => { this.error = this.getErrorMessage(error); this.saving = false; - this.cdRef.detectChanges(); + this.cdRef.detectChanges(); } }); } - // Gestion des rôles - désactivée pour profil personnel - updateUserRole(newRole: UserRole) { - // Non autorisé pour le profil personnel - this.error = 'Vous ne pouvez pas modifier votre propre rôle'; - this.cdRef.detectChanges(); + // ==================== VALIDATION ==================== + + isFormValid(): boolean { + if (!this.editedUser.firstName?.trim() || !this.editedUser.lastName?.trim()) { + return false; + } + if (!this.editedUser.email?.trim() || !this.isValidEmail(this.editedUser.email)) { + return false; + } + return true; } - // Gestion du statut - désactivée pour profil personnel - enableUser() { - // Non autorisé pour le profil personnel - this.error = 'Vous ne pouvez pas vous activer/désactiver vous-même'; - this.cdRef.detectChanges(); + isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); } - disableUser() { - // Non autorisé pour le profil personnel - this.error = 'Vous ne pouvez pas vous activer/désactiver vous-même'; - this.cdRef.detectChanges(); - } + // ==================== UTILITAIRES D'AFFICHAGE ==================== - // Réinitialisation du mot de passe - resetPassword() { - if (this.user) { - this.openResetPasswordModal.emit(this.user.id); - } - } - - // Gestion des erreurs - private getErrorMessage(error: any): string { - if (error.error?.message) { - return error.error.message; - } - if (error.status === 403) { - return 'Vous n\'avez pas les permissions nécessaires pour cette action'; - } - if (error.status === 404) { - return 'Utilisateur non trouvé'; - } - if (error.status === 400) { - return 'Données invalides'; - } - return 'Une erreur est survenue. Veuillez réessayer.'; - } - - // Utilitaires d'affichage getStatusBadgeClass(): string { if (!this.user) return 'badge bg-secondary'; if (!this.user.enabled) return 'badge bg-danger'; @@ -269,58 +247,365 @@ export class MyProfile implements OnInit, OnDestroy { return this.user.username; } - getRoleBadgeClass(role: UserRole): string { - return this.roleService.getRoleBadgeClass(role); + getRoleBadgeClass(): string { + if (!this.user?.role) return 'badge bg-secondary'; + return this.roleService.getRoleBadgeClass(this.user.role); } - getRoleLabel(role: UserRole): string { - return this.roleService.getRoleLabel(role); + getRoleLabel(): string { + if (!this.user?.role) return 'Aucun rôle'; + return this.roleService.getRoleLabel(this.user.role); } - getRoleIcon(role: UserRole): string { + getRoleDescription(): string { + if (!this.user?.role) return 'Description non disponible'; + const roleInfo = this.availableRoles.find(r => r.value === this.user!.role); + return roleInfo?.description || this.roleService.getRoleDescription(this.user.role); + } + + getRoleIcon(role: string | UserRole): string { return this.roleService.getRoleIcon(role); } - getRoleDescription(role: UserRole): string { - const roleInfo = this.availableRoles.find(r => r.value === role); - return roleInfo?.description || 'Description non disponible'; + // Obtenir le rôle (peut être string ou UserRole) + getUserRole(): string | UserRole | undefined { + return this.user?.role; } - // Vérification des permissions pour les actions - toujours false pour les actions sensibles - canAssignRole(targetRole: UserRole): boolean { - return false; + // Pour le template, retourner un tableau pour la boucle + getUserRoles(): (string | UserRole)[] { + const role = this.user?.role; + if (!role) return []; + return Array.isArray(role) ? role : [role]; } - // Vérifie si c'est le profil de l'utilisateur courant - toujours true - isCurrentUserProfile(): boolean { - return true; + // Afficher le rôle + getUserRoleDisplay(): string { + if (!this.user?.role) return 'Aucun rôle'; + return this.getRoleLabel(); } - // Méthode utilitaire pour déterminer le type d'utilisateur - getUserType(): string { - if (!this.currentUserRole) return 'Utilisateur'; - - const roleNames: { [key in UserRole]?: string } = { - [UserRole.DCB_ADMIN]: 'Administrateur DCB', - [UserRole.DCB_SUPPORT]: 'Support DCB', - [UserRole.DCB_PARTNER]: 'Partenaire DCB', - [UserRole.DCB_PARTNER_ADMIN]: 'Administrateur Partenaire', - [UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire', - [UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire' - }; - - return roleNames[this.currentUserRole] || this.currentUserRole; + // ==================== GESTION DES ERREURS ==================== + + private getErrorMessage(error: any): string { + if (error.error?.message) { + return error.error.message; + } + if (error.status === 400) { + return 'Données invalides. Vérifiez les informations saisies.'; + } + if (error.status === 403) { + return 'Vous n\'avez pas les permissions nécessaires pour cette action'; + } + if (error.status === 404) { + return 'Utilisateur non trouvé'; + } + if (error.status === 409) { + return 'Cet email est déjà utilisé par un autre utilisateur'; + } + return 'Une erreur est survenue. Veuillez réessayer.'; } - // Vérifie si l'utilisateur est un utilisateur Hub + // ==================== MÉTHODES DE NAVIGATION ==================== + + goBack() { + this.back.emit(); + } + + // ==================== MÉTHODES UTILITAIRES ==================== + isHubUser(): boolean { - const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]; - return this.currentUserRole ? hubRoles.includes(this.currentUserRole) : false; + return UserUtils.isHubUser(this.user!); } - // Vérifie si l'utilisateur est un utilisateur marchand isMerchantUser(): boolean { - const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; - return this.currentUserRole ? merchantRoles.includes(this.currentUserRole) : false; + return UserUtils.isMerchantPartnerUser(this.user!); + } + + getUserTypeDisplay(): string { + if (!this.user) return ''; + return UserUtils.getUserTypeDisplayName(this.user.userType); + } + + getCreationDate(): string { + if (!this.user?.createdTimestamp) return 'Non disponible'; + return this.formatTimestamp(this.user.createdTimestamp); + } + + getMerchantPartnerId(): string { + return this.user?.merchantPartnerId || 'Non applicable'; + } + + refresh() { + this.loadUserProfile(); + } + + clearMessages() { + this.error = ''; + this.success = ''; + this.cdRef.detectChanges(); + } + + // ==================== GETTERS POUR LE TEMPLATE ==================== + + getProfileTitle(): string { + return 'Mon Profil'; + } + + getWelcomeMessage(): string { + if (!this.user) return 'Bienvenue'; + return `Bonjour, ${this.getUserDisplayName()}`; + } + + canEditProfile(): boolean { + return true; // Toujours vrai pour son propre profil + } + + // ==================== MÉTHODES D'ACTION ==================== + + openModal(content: TemplateRef, size: 'sm' | 'lg' | 'xl' = 'lg') { + this.modalService.open(content, { + size: size, + centered: true, + scrollable: true + }); + } + + /** + * Ouvre la modal de réinitialisation personnelle + */ + openMyProfileResetModal(): void { + this.resetMyProfileForm(); + this.openModal(this.myProfileResetPasswordModal); + } + + /** + * Ferme la modal de réinitialisation personnelle + */ + closeMyProfileResetModal(): void { + this.modalService.dismissAll(); + this.resetMyProfileForm(); + } + + /** + * Réinitialise le formulaire MyProfile + */ + resetMyProfileForm(): void { + this.myProfileCurrentPassword = ''; + this.myProfileNewPassword = ''; + this.myProfileConfirmPassword = ''; + this.myProfileResetSuccess = ''; + this.myProfileResetError = ''; + this.resettingMyPassword = false; + this.showCurrentPassword = false; + this.showNewMyPassword = false; + this.showConfirmMyPassword = false; + } + + /** + * Vérifie si le formulaire MyProfile est valide + */ + isMyProfileResetFormValid(): boolean { + const hasCurrentPassword = !!this.myProfileCurrentPassword; + const hasNewPassword = !!this.myProfileNewPassword && this.myProfileNewPassword.length >= 8; + const passwordsMatch = this.myProfileNewPassword === this.myProfileConfirmPassword; + const isStrongPassword = this.isStrongPassword(); + + return hasCurrentPassword && hasNewPassword && passwordsMatch && isStrongPassword; + } + + /** + * Vérifie la longueur minimale + */ + hasMinLength(): boolean { + return (this.myProfileNewPassword?.length || 0) >= 8; + } + + /** + * Vérifie la présence d'une lettre minuscule + */ + hasLowerCase(): boolean { + return /[a-z]/.test(this.myProfileNewPassword || ''); + } + + /** + * Vérifie la présence d'une lettre majuscule + */ + hasUpperCase(): boolean { + return /[A-Z]/.test(this.myProfileNewPassword || ''); + } + + /** + * Vérifie la présence d'un chiffre + */ + hasNumber(): boolean { + return /\d/.test(this.myProfileNewPassword || ''); + } + + /** + * Vérifie la présence d'un caractère spécial + */ + hasSpecialChar(): boolean { + return /[@$!%*?&]/.test(this.myProfileNewPassword || ''); + } + + /** + * Vérifie si tous les critères sont remplis + */ + isStrongPassword(): boolean { + return this.hasMinLength() && + this.hasLowerCase() && + this.hasUpperCase() && + this.hasNumber() && + this.hasSpecialChar(); + } + + /** + * Calcule la force du mot de passe (0-100) + */ + getPasswordStrength(password: string): number { + if (!password) return 0; + + let strength = 0; + + // Longueur + if (password.length >= 8) strength += 25; + if (password.length >= 12) strength += 10; + + // Complexité + if (/[a-z]/.test(password)) strength += 15; + if (/[A-Z]/.test(password)) strength += 15; + if (/[0-9]/.test(password)) strength += 15; + if (/[@$!%*?&]/.test(password)) strength += 20; + + return Math.min(strength, 100); + } + + /** + * Classe CSS pour la force du mot de passe + */ + getPasswordStrengthClass(password: string): string { + const strength = this.getPasswordStrength(password); + + if (strength < 40) return 'bg-danger'; + if (strength < 70) return 'bg-warning'; + return 'bg-success'; + } + + /** + * Texte pour la force du mot de passe + */ + getPasswordStrengthText(password: string): string { + const strength = this.getPasswordStrength(password); + + if (strength < 40) return 'Faible'; + if (strength < 70) return 'Moyen'; + return 'Fort'; + } + + /** + * Récupère les initiales de l'utilisateur connecté + */ + getMyInitials(): string { + if (!this.user) return '?'; + + const first = this.user.firstName?.[0] || ''; + const last = this.user.lastName?.[0] || ''; + + return (first + last).toUpperCase() || this.user.username?.[0]?.toUpperCase() || 'U'; + } + + /** + * Confirme la réinitialisation du mot de passe personnel + */ + confirmMyProfileResetPassword(): void { + if (!this.isMyProfileResetFormValid()) { + this.myProfileResetError = 'Veuillez corriger les erreurs dans le formulaire'; + return; + } + + this.resettingMyPassword = true; + this.myProfileResetError = ''; + + console.log('🔐 Réinitialisation du mot de passe personnel...'); + + const resetPasswordDto: ResetPasswordDto = { + newPassword: this.myProfileNewPassword, + temporary: this.temporaryPassword + }; + + if(this.user) { + + // Appel au service de réinitialisation + this.isHubUser() + ? this.hubUsersService.resetHubUserPassword( + this.user.id, + resetPasswordDto + ) + : this.merchantUsersService.resetMerchantUserPassword( + this.user.id, + resetPasswordDto + ).subscribe({ + next: (response) => { + this.resettingMyPassword = false; + this.myProfileResetSuccess = 'Votre mot de passe a été changé avec succès. Vous serez déconnecté dans quelques secondes...'; + + console.log('✅ Mot de passe personnel changé avec succès'); + + // Déconnexion automatique après 3 secondes + setTimeout(() => { + this.authService.logout(); + }, 3000); + }, + error: (error) => { + this.resettingMyPassword = false; + + if (error.status === 401) { + this.myProfileResetError = 'Mot de passe actuel incorrect'; + } else if (error.status === 400) { + this.myProfileResetError = 'Le nouveau mot de passe ne respecte pas les politiques de sécurité'; + } else { + this.myProfileResetError = 'Erreur lors du changement de mot de passe. Veuillez réessayer.'; + } + + console.error('❌ Erreur réinitialisation mot de passe personnel:', error); + } + }); + } + } + + + // Actions désactivées pour le profil personnel + updateUserRole(newRole: UserRole) { + this.error = 'Vous ne pouvez pas modifier votre propre rôle'; + this.cdRef.detectChanges(); + } + + enableUser() { + this.error = 'Vous ne pouvez pas vous activer/désactiver vous-même'; + this.cdRef.detectChanges(); + } + + disableUser() { + this.error = 'Vous ne pouvez pas vous activer/désactiver vous-même'; + this.cdRef.detectChanges(); + } + + // ==================== VÉRIFICATIONS DE PERMISSIONS ==================== + + canManageRoles(): boolean { + return false; // Jamais vrai pour le profil personnel + } + + canEnableDisableUser(): boolean { + return false; // Jamais vrai pour le profil personnel + } + + canDeleteUser(): boolean { + return false; // Jamais vrai pour le profil personnel + } + + isCurrentUserProfile(): boolean { + return true; // Toujours vrai pour MyProfile } } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index d27f430..2b5e287 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,6 @@ -import 'zone.js'; import { bootstrapApplication } from '@angular/platform-browser'; import { App } from './app/app'; -import { AuthService } from './app/core/services/auth.service'; import { appConfig } from './app/app.config'; -bootstrapApplication(App, { - providers: [ - ...appConfig.providers, - ] -}).then(async appRef => { - const authService = appRef.injector.get(AuthService); - await authService.initialize(); -}).catch(err => console.error('BO Admin error', err)); + +bootstrapApplication(App, appConfig) + .catch(err => console.error('❌ BO Admin bootstrap error', err));