feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature

This commit is contained in:
diallolatoile 2025-11-10 01:28:04 +00:00
parent 191099d8a5
commit 4c8e3f229d
59 changed files with 6759 additions and 7595 deletions

9
package-lock.json generated
View File

@ -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"
}
}
}

View File

@ -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",

View File

@ -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<void> {
this.setupTitleListener();
setTimeout(async () => {
await this.initializeAuth();
});
}
private async initializeAuth(): Promise<void> {
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 {

View File

@ -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;
}
}

View File

@ -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<boolean>(this.isAuthenticated());
private userProfile$ = new BehaviorSubject<BaseUserDto | null>(null);
private userProfile$ = new BehaviorSubject<User | null>(null);
private initialized$ = new BehaviorSubject<boolean>(false);
// === INITIALISATION DE L'APPLICATION ===
@ -72,31 +69,42 @@ export class AuthService {
* Initialise l'authentification au démarrage de l'application
*/
async initialize(): Promise<boolean> {
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<BaseUserDto> {
return this.http.get<BaseUserDto>(
loadUserProfile(): Observable<User> {
return this.http.get<any>(
`${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<BaseUserDto | null> {
getUserProfile(): Observable<User | null> {
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<BaseUserDto | null> {
getProfile(): Observable<User | null> {
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 ===

View File

@ -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<AvailableRolesWithPermissions | null>(null);
private currentUserRole$ = new BehaviorSubject<UserRole | null>(null);
/**
* Charge les rôles disponibles depuis l'API et les enrichit avec les permissions
* Charge les rôles Hub disponibles
*/
loadAvailableRoles(): Observable<AvailableRolesWithPermissions> {
loadAvailableHubRoles(): Observable<AvailableRolesWithPermissions> {
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<AvailableRolesWithPermissions> {
const cached = this.availableRoles$.value;
if (cached) {
return of(cached);
}
return this.loadAvailableRoles();
loadAvailableMerchantRoles(): Observable<AvailableRolesWithPermissions> {
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<AvailableRoleResponse[]> {
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<AvailableRolesWithPermissions> {
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<AvailableRolesWithPermissions> {
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<AvailableRolesWithPermissions | null> {
return this.availableRoles$.asObservable();
}
}

View File

@ -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' },

View File

@ -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',

View File

@ -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' },

View File

@ -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'"
/>
<div>
<h5 class="my-0 fw-semibold">{{ user?.firstName }} - {{ user?.lastName }}</h5>
<h6 class="my-0 text-muted">Administrateur</h6>
<h5 class="my-0 fw-semibold">
{{ getUserInitials() }} | {{ getDisplayName() }}
</h5>
<h6 class="my-0 text-muted">
{{ getUserRole(user) }}
</h6>
</div>
</div>
</div>

View File

@ -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<void>();
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';
}
}

View File

@ -51,7 +51,7 @@ import { credits, currentYear } from '@/app/constants'
<div class="mt-4 p-3 bg-light rounded">
<small class="text-muted">
<i class="fas fa-info-circle me-1"></i>
Rôles requis : Administrateur, Gestionnaire ou Support
Rôles requis : Administrateur, Partenaire ou Support
</small>
</div>
</div>

View File

@ -1,12 +1,38 @@
<app-ui-card title="Liste des Utilisateurs Hub">
<app-ui-card [title]="'Liste des Utilisateurs Hub'">
<a
helper-text
href="javascript:void(0);"
class="icon-link icon-link-hover link-primary fw-semibold"
>Gérez les accès utilisateurs de votre plateforme DCB
>
<ng-icon name="lucideUsers" class="me-1"></ng-icon>
Gérez les accès utilisateurs de votre plateforme DCB
</a>
<div card-body>
<!-- Indicateur de contexte pour la vue admin -->
@if (isAdminView) {
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-warning">
<div class="d-flex align-items-center">
<ng-icon name="lucideShield" class="me-2"></ng-icon>
<div>
<strong>Vue Administrative Globale :</strong> Vous visualisez tous les utilisateurs Hub et Merchant de la plateforme
@if (showStatistics) {
<br>
<small class="text-muted">
{{ getHubUsersCount() }} utilisateurs Hub •
{{ getMerchantUsersCount() }} utilisateurs Merchant •
{{ getTotalUsersCountAdmin() }} total
</small>
}
</div>
</div>
</div>
</div>
</div>
}
<!-- Barre d'actions supérieure -->
<div class="row mb-3">
<div class="col-md-6">
@ -19,53 +45,127 @@
[class.active]="roleFilter === 'all'"
(click)="filterByRole('all')"
>
Tous ({{ allUsers.length }})
</button>
<button
type="button"
class="btn btn-outline-danger"
[class.active]="roleFilter === UserRole.DCB_ADMIN"
(click)="filterByRole(UserRole.DCB_ADMIN)"
>
Admins ({{ getUsersCountByRole(UserRole.DCB_ADMIN) }})
</button>
<button
type="button"
class="btn btn-outline-info"
[class.active]="roleFilter === UserRole.DCB_SUPPORT"
(click)="filterByRole(UserRole.DCB_SUPPORT)"
>
Support ({{ getUsersCountByRole(UserRole.DCB_SUPPORT) }})
</button>
<button
type="button"
class="btn btn-outline-success"
[class.active]="roleFilter === UserRole.DCB_PARTNER"
(click)="filterByRole(UserRole.DCB_PARTNER)"
>
Partenaires ({{ getUsersCountByRole(UserRole.DCB_PARTNER) }})
Tous ({{ getTotalUsersCount() }})
</button>
<!-- Boutons pour VUE ADMIN (Hub + Merchant) -->
@if (isAdminView) {
<button
type="button"
class="btn btn-outline-danger"
[class.active]="roleFilter === UserRole.DCB_ADMIN"
(click)="filterByRole(UserRole.DCB_ADMIN)"
>
DCB Admins ({{ getUsersCountByRole(UserRole.DCB_ADMIN) }})
</button>
<button
type="button"
class="btn btn-outline-info"
[class.active]="roleFilter === UserRole.DCB_SUPPORT"
(click)="filterByRole(UserRole.DCB_SUPPORT)"
>
DCB Support ({{ getUsersCountByRole(UserRole.DCB_SUPPORT) }})
</button>
<button
type="button"
class="btn btn-outline-success"
[class.active]="roleFilter === UserRole.DCB_PARTNER"
(click)="filterByRole(UserRole.DCB_PARTNER)"
>
DCB Partners ({{ getUsersCountByRole(UserRole.DCB_PARTNER) }})
</button>
<button
type="button"
class="btn btn-outline-warning text-dark"
[class.active]="roleFilter === UserRole.DCB_PARTNER_ADMIN"
(click)="filterByRole(UserRole.DCB_PARTNER_ADMIN)"
>
Partner Admins ({{ getUsersCountByRole(UserRole.DCB_PARTNER_ADMIN) }})
</button>
<button
type="button"
class="btn btn-outline-secondary"
[class.active]="roleFilter === UserRole.DCB_PARTNER_MANAGER"
(click)="filterByRole(UserRole.DCB_PARTNER_MANAGER)"
>
Partner Managers ({{ getUsersCountByRole(UserRole.DCB_PARTNER_MANAGER) }})
</button>
<button
type="button"
class="btn btn-outline-dark"
[class.active]="roleFilter === UserRole.DCB_PARTNER_SUPPORT"
(click)="filterByRole(UserRole.DCB_PARTNER_SUPPORT)"
>
Partner Support ({{ getUsersCountByRole(UserRole.DCB_PARTNER_SUPPORT) }})
</button>
} @else {
<!-- Boutons pour VUE NORMALE HUB -->
<button
type="button"
class="btn btn-outline-danger"
[class.active]="roleFilter === UserRole.DCB_ADMIN"
(click)="filterByRole(UserRole.DCB_ADMIN)"
>
Admins ({{ getUsersCountByRole(UserRole.DCB_ADMIN) }})
</button>
<button
type="button"
class="btn btn-outline-info"
[class.active]="roleFilter === UserRole.DCB_SUPPORT"
(click)="filterByRole(UserRole.DCB_SUPPORT)"
>
Support ({{ getUsersCountByRole(UserRole.DCB_SUPPORT) }})
</button>
<button
type="button"
class="btn btn-outline-success"
[class.active]="roleFilter === UserRole.DCB_PARTNER"
(click)="filterByRole(UserRole.DCB_PARTNER)"
>
Partenaires ({{ getUsersCountByRole(UserRole.DCB_PARTNER) }})
</button>
}
</div>
<!-- Filtre par contexte (uniquement vue admin) -->
@if (isAdminView) {
<div class="ms-2">
<select class="form-select form-select-sm" [(ngModel)]="contextFilter" (change)="applyFiltersAndPagination()">
<option value="all">Tous les contextes</option>
<option value="hub">Hub seulement</option>
<option value="merchant">Merchant seulement</option>
</select>
</div>
}
</div>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-end gap-2">
@if (canCreateUsers) {
@if (showCreateButton) {
<button
class="btn btn-primary"
(click)="openCreateModal.emit()"
(click)="openCreateUserModal.emit()"
>
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
Nouvel Utilisateur
Nouvel Utilisateur Hub
</button>
}
<button
class="btn btn-outline-secondary"
(click)="refreshData()"
[disabled]="loading"
>
<ng-icon name="lucideRefreshCw" class="me-1" [class.spin]="loading"></ng-icon>
Actualiser
</button>
</div>
</div>
</div>
<!-- Barre de recherche et filtres -->
<!-- Barre de recherche et filtres avancés -->
<div class="row mb-3">
<div class="col-md-3">
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">
<ng-icon name="lucideSearch"></ng-icon>
@ -73,44 +173,44 @@
<input
type="text"
class="form-control"
placeholder="Nom, email, username..."
placeholder="Rechercher par nom, email..."
[(ngModel)]="searchTerm"
(keyup.enter)="onSearch()"
(input)="onSearch()"
[disabled]="loading"
>
</div>
</div>
<div class="col-md-2">
<select class="form-select" [(ngModel)]="statusFilter" (change)="onSearch()">
<select class="form-select" [(ngModel)]="statusFilter" (change)="applyFiltersAndPagination()">
<option value="all">Tous les statuts</option>
<option value="enabled">Activés ({{ getEnabledUsersCount() }})</option>
<option value="disabled">Désactivés ({{ getDisabledUsersCount() }})</option>
<option value="enabled">Activés seulement</option>
<option value="disabled">Désactivés seulement</option>
</select>
</div>
<div class="col-md-2">
<select class="form-select" [(ngModel)]="emailVerifiedFilter" (change)="onSearch()">
<select class="form-select" [(ngModel)]="emailVerifiedFilter" (change)="applyFiltersAndPagination()">
<option value="all">Tous les emails</option>
<option value="verified">Email vérifié</option>
<option value="not-verified">Email non vérifié</option>
</select>
</div>
<div class="col-md-2">
<select class="form-select" [(ngModel)]="roleFilter" (change)="onSearch()">
<select class="form-select" [(ngModel)]="roleFilter" (change)="applyFiltersAndPagination()">
<option value="all">Tous les rôles</option>
@for (role of availableRoles; track role.value) {
<option [value]="role.value">{{ role.label }}</option>
}
</select>
</div>
<div class="col-md-3">
<div class="d-flex gap-2">
<button class="btn btn-outline-primary" (click)="onSearch()">
<ng-icon name="lucideFilter" class="me-1"></ng-icon>
Appliquer
</button>
<button class="btn btn-outline-secondary" (click)="onClearFilters()">
<ng-icon name="lucideX" class="me-1"></ng-icon>
Réinitialiser
</button>
</div>
<div class="col-md-2">
<button class="btn btn-outline-secondary w-100" (click)="onClearFilters()" [disabled]="loading">
<ng-icon name="lucideX" class="me-1"></ng-icon>
Effacer
</button>
</div>
</div>
@ -130,6 +230,7 @@
<div class="d-flex align-items-center">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
<div>{{ error }}</div>
<button class="btn-close ms-auto" (click)="error = ''"></button>
</div>
</div>
}
@ -140,6 +241,14 @@
<table class="table table-hover table-striped">
<thead class="table-light">
<tr>
<!-- Colonne Type d'utilisateur pour la vue admin -->
@if (showUserTypeColumn()) {
<th>Type</th>
}
<!-- Colonne Merchant Partner pour la vue admin -->
@if (showMerchantPartnerColumn()) {
<th>Merchant Partner</th>
}
<th (click)="sort('username')" class="cursor-pointer">
<div class="d-flex align-items-center">
<span>Utilisateur</span>
@ -152,12 +261,7 @@
<ng-icon [name]="getSortIcon('email')" class="ms-1 fs-12"></ng-icon>
</div>
</th>
<th (click)="sort('role')" class="cursor-pointer">
<div class="d-flex align-items-center">
<span>Rôle</span>
<ng-icon [name]="getSortIcon('role')" class="ms-1 fs-12"></ng-icon>
</div>
</th>
<th>Rôle Principal</th>
<th (click)="sort('enabled')" class="cursor-pointer">
<div class="d-flex align-items-center">
<span>Statut</span>
@ -176,6 +280,33 @@
<tbody>
@for (user of displayedUsers; track user.id) {
<tr>
<!-- Colonne Type d'utilisateur pour la vue admin -->
@if (showUserTypeColumn()) {
<td>
<span class="badge" [ngClass]="user.userType === UserType.HUB ? 'bg-primary' : 'bg-success'">
{{ user.userType === UserType.HUB ? 'Hub' : 'Merchant' }}
</span>
</td>
}
<!-- Colonne Merchant Partner pour la vue admin -->
@if (showMerchantPartnerColumn()) {
<td>
@if (user.merchantPartnerId) {
<div class="d-flex align-items-center">
<div class="avatar-sm bg-secondary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
<ng-icon name="lucideBuilding" class="text-secondary fs-12"></ng-icon>
</div>
<div>
<small class="text-muted font-monospace" [title]="user.merchantPartnerId">
{{ user.merchantPartnerId.substring(0, 8) }}...
</small>
</div>
</div>
} @else {
<span class="text-muted">-</span>
}
</td>
}
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
@ -229,7 +360,7 @@
</button>
<button
class="btn btn-outline-warning btn-sm"
(click)="resetPassword(user)"
(click)="resetPasswordRequested(user)"
title="Réinitialiser le mot de passe"
>
<ng-icon name="lucideKey"></ng-icon>
@ -251,10 +382,10 @@
<ng-icon name="lucideUserCheck"></ng-icon>
</button>
}
@if (canDeleteUsers) {
@if (showDeleteButton) {
<button
class="btn btn-outline-danger btn-sm"
(click)="deleteUser(user)"
(click)="deleteUserRequested(user)"
title="Supprimer l'utilisateur"
>
<ng-icon name="lucideTrash2"></ng-icon>
@ -266,13 +397,13 @@
}
@empty {
<tr>
<td colspan="6" class="text-center py-4">
<td [attr.colspan]="getColumnCount()" class="text-center py-4">
<div class="text-muted">
<ng-icon name="lucideUsers" class="fs-1 mb-3 opacity-50"></ng-icon>
<h5 class="mb-2">Aucun utilisateur trouvé</h5>
<p class="mb-3">Aucun utilisateur ne correspond à vos critères de recherche.</p>
@if (canCreateUsers) {
<button class="btn btn-primary" (click)="openCreateModal.emit()">
@if (showCreateButton) {
<button class="btn btn-primary" (click)="openCreateUserModal.emit()">
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
Créer le premier utilisateur
</button>

View File

@ -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<void>();
// Configuration
readonly UserRole = UserRole;
readonly UserType = UserType;
readonly UserUtils = UserUtils;
// Inputs
@Input() canCreateUsers: boolean = false;
@Input() canDeleteUsers: boolean = false;
// Outputs
@Output() userSelected = new EventEmitter<string>();
@Output() openCreateUserModal = new EventEmitter<void>();
@Output() openResetPasswordModal = new EventEmitter<string>();
@Output() openDeleteUserModal = new EventEmitter<string>();
// 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;
}
}

View File

@ -8,14 +8,14 @@
@if (user) {
{{ getUserDisplayName() }}
} @else {
Profil Utilisateur
Profil Utilisateur Hub
}
</h4>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item">
<a href="javascript:void(0)" (click)="back.emit()" class="text-decoration-none cursor-pointer">
Utilisateurs
<a href="javascript:void(0)" (click)="goBack()" class="text-decoration-none cursor-pointer">
Utilisateurs Hub
</a>
</li>
<li class="breadcrumb-item active" aria-current="page">
@ -31,7 +31,7 @@
<div class="d-flex gap-2">
<!-- Bouton de réinitialisation de mot de passe -->
@if (user && canEditUsers && !isEditing) {
@if (user && canEditUser() && !isEditing) {
<button
class="btn btn-warning"
(click)="resetPassword()"
@ -41,7 +41,7 @@
</button>
<!-- Bouton activation/désactivation -->
@if (user.enabled) {
@if (user.enabled && canToggleStatus()) {
<button
class="btn btn-outline-warning"
(click)="disableUser()"
@ -49,7 +49,7 @@
<ng-icon name="lucidePause" class="me-1"></ng-icon>
Désactiver
</button>
} @else {
} @else if (!user.enabled && canToggleStatus()) {
<button
class="btn btn-outline-success"
(click)="enableUser()"
@ -74,7 +74,7 @@
</div>
<!-- Indicateur de permissions -->
@if (currentUserRole && !canEditUsers) {
@if (currentUserRole && !canEditUser()) {
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-warning">
@ -95,6 +95,7 @@
<div class="d-flex align-items-center">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
<div>{{ error }}</div>
<button class="btn-close ms-auto" (click)="clearMessages()"></button>
</div>
</div>
}
@ -104,6 +105,7 @@
<div class="d-flex align-items-center">
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
<div>{{ success }}</div>
<button class="btn-close ms-auto" (click)="clearMessages()"></button>
</div>
</div>
}
@ -115,7 +117,7 @@
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
<p class="mt-2 text-muted">Chargement du profil...</p>
<p class="mt-2 text-muted">Chargement du profil utilisateur Hub...</p>
</div>
}
@ -139,8 +141,13 @@
<h5>{{ getUserDisplayName() }}</h5>
<p class="text-muted mb-2">@{{ user.username }}</p>
<!-- Type d'utilisateur -->
<span class="badge bg-primary mb-2">
Utilisateur Hub
</span>
<!-- Statut -->
<span [class]="getStatusBadgeClass()" class="mb-3">
<span [class]="getStatusBadgeClass()" class="mb-3 d-block">
{{ getStatusText() }}
</span>
@ -155,12 +162,12 @@
</div>
<div class="d-flex align-items-center">
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
<small>Créé le {{ formatTimestamp(user.createdTimestamp) }}</small>
<small>Créé le {{ getCreationDate() }}</small>
</div>
@if (user.lastLogin) {
<div class="d-flex align-items-center mt-2">
<ng-icon name="lucideLogIn" class="me-2 text-muted"></ng-icon>
<small>Dernière connexion : {{ formatTimestamp(user.lastLogin) }}</small>
<small>Dernière connexion : {{ getLastLoginDate() }}</small>
</div>
}
</div>
@ -170,60 +177,68 @@
<!-- Carte rôle utilisateur -->
<div class="card mt-3">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Rôle Utilisateur</h5>
@if (canManageRoles && !isEditing) {
<h5 class="card-title mb-0">
Rôle Utilisateur
</h5>
@if (showRoleManagement()) {
<span class="badge bg-info">Modifiable</span>
}
</div>
<div class="card-body">
<!-- Rôle actuel -->
<!-- Rôles actuels -->
<div class="text-center mb-3">
<span class="badge d-flex align-items-center justify-content-center" [ngClass]="getRoleBadgeClass(user.role)">
<ng-icon [name]="getRoleIcon(user.role)" class="me-2"></ng-icon>
{{ getRoleLabel(user.role) }}
</span>
<small class="text-muted d-block mt-1">
{{ getRoleDescription(user.role) }}
</small>
@if (getUserRole()) {
<div class="d-flex flex-wrap gap-1 justify-content-center mb-2">
<span class="badge d-flex align-items-center" [ngClass]="getRoleBadgeClass(user.role)">
<ng-icon [name]="getRoleIcon(user.role)" class="me-1" size="12"></ng-icon>
{{ getRoleLabel(user.role) }}
</span>
</div>
<!-- Description du rôle principal -->
<small class="text-muted d-block">
{{ getRoleDescription(user.role) }}
</small>
} @else {
<span class="badge bg-secondary">Aucun rôle</span>
}
</div>
<!-- Changement de rôle -->
@if (canManageRoles && !isEditing) {
@if (showRoleManagement()) {
<div class="mt-3">
<label class="form-label fw-semibold">Changer le rôle</label>
<label class="form-label fw-semibold">Changer le rôle principal</label>
<select
class="form-select"
[value]="user.role"
[value]="currentUserRole"
(change)="updateUserRole($any($event.target).value)"
[disabled]="updatingRoles"
[disabled]="updatingRole"
>
<option value="" disabled>Sélectionnez un nouveau rôle</option>
@for (role of availableRoles; track role.value) {
@for (role of getAssignableRoles(); track role) {
<option
[value]="role.value"
[disabled]="!canAssignRole(role.value) || role.value === user.role"
[value]="role"
[disabled]="role === currentUserRole"
>
{{ role.label }}
@if (!canAssignRole(role.value)) {
(Non autorisé)
} @else if (role.value === user.role) {
{{ getRoleLabel(role) }}
@if (role === currentUserRole) {
(Actuel)
}
</option>
}
</select>
<div class="form-text">
@if (updatingRoles) {
@if (updatingRole) {
<div class="spinner-border spinner-border-sm me-1" role="status">
<span class="visually-hidden">Mise à jour...</span>
</div>
Mise à jour en cours...
} @else {
Sélectionnez un nouveau rôle pour cet utilisateur
Sélectionnez un nouveau rôle principal pour cet utilisateur
}
</div>
</div>
} @else if (!canManageRoles) {
} @else if (!canManageRoles()) {
<div class="alert alert-info mt-3">
<small>
<ng-icon name="lucideShield" class="me-1"></ng-icon>
@ -243,16 +258,16 @@
<div class="row g-2 small">
<div class="col-12">
<strong>Créé par :</strong>
<div class="text-muted">{{ user.createdByUsername || 'Système' }}</div>
<div class="text-muted">{{ getCreatorName() }}</div>
</div>
<div class="col-12">
<strong>Date de création :</strong>
<div class="text-muted">{{ formatTimestamp(user.createdTimestamp) }}</div>
<div class="text-muted">{{ getCreationDate() }}</div>
</div>
<div class="col-12">
<strong>Type d'utilisateur :</strong>
<div class="text-muted">
<span class="badge bg-secondary">{{ user.userType }}</span>
<span class="badge bg-primary">Utilisateur Hub</span>
</div>
</div>
</div>
@ -289,7 +304,7 @@
type="button"
class="btn btn-success btn-sm"
(click)="saveProfile()"
[disabled]="saving"
[disabled]="saving || !isFormValid()"
>
@if (saving) {
<div class="spinner-border spinner-border-sm me-1" role="status">
@ -403,6 +418,24 @@
</div>
}
<!-- Rôles multiples -->
<div class="col-12">
<label class="form-label">
Rôle Assigné
</label>
<div class="form-control-plaintext">
<div class="d-flex flex-wrap gap-1">
<span class="badge d-flex align-items-center" [ngClass]="getRoleBadgeClass(user.role)">
<ng-icon [name]="getRoleIcon(user.role)" class="me-1" size="12"></ng-icon>
{{ getRoleLabel(user.role) }}
</span>
</div>
</div>
<div class="form-text">
{{ getUserRoleDisplay() }}
</div>
</div>
<!-- Informations système -->
@if (!isEditing) {
<div class="col-12">
@ -421,19 +454,19 @@
<div class="col-md-6">
<label class="form-label">Date de création</label>
<div class="form-control-plaintext">
{{ formatTimestamp(user.createdTimestamp) }}
{{ getCreationDate() }}
</div>
</div>
<div class="col-md-6">
<label class="form-label">Créé par</label>
<div class="form-control-plaintext">
{{ user.createdByUsername || 'Système' }}
{{ getCreatorName() }}
</div>
</div>
<div class="col-md-6">
<label class="form-label">Type d'utilisateur</label>
<div class="form-control-plaintext">
<span class="badge bg-secondary">{{ user.userType }}</span>
<span class="badge bg-primary">Utilisateur Hub</span>
</div>
</div>
</div>
@ -444,7 +477,7 @@
</div>
<!-- Actions supplémentaires -->
@if (!isEditing && canEditUsers) {
@if (!isEditing && canEditUser()) {
<div class="card mt-3">
<div class="card-header bg-light">
<h6 class="card-title mb-0">Actions de Gestion</h6>
@ -455,13 +488,14 @@
<button
class="btn btn-outline-warning w-100"
(click)="resetPassword()"
[disabled]="!canResetPassword()"
>
<ng-icon name="lucideKey" class="me-1"></ng-icon>
Réinitialiser MDP
</button>
</div>
<div class="col-md-4">
@if (user.enabled) {
@if (user.enabled && canToggleStatus()) {
<button
class="btn btn-outline-secondary w-100"
(click)="disableUser()"
@ -469,7 +503,7 @@
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
Désactiver
</button>
} @else {
} @else if (!user.enabled && canToggleStatus()) {
<button
class="btn btn-outline-success w-100"
(click)="enableUser()"

View File

@ -0,0 +1,545 @@
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 {
User,
UpdateUserDto,
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';
@Component({
selector: 'app-hub-user-profile',
standalone: true,
imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule],
templateUrl: './hub-users-profile.html',
styles: [`
.avatar-lg {
width: 80px;
height: 80px;
}
.fs-24 {
font-size: 24px;
}
`]
})
export class HubUserProfile implements OnInit, OnDestroy {
private hubUsersService = inject(HubUsersService);
private roleService = inject(RoleManagementService);
private authService = inject(AuthService);
private cdRef = inject(ChangeDetectorRef);
private destroy$ = new Subject<void>();
readonly UserRole = UserRole;
readonly UserType = UserType;
readonly UserUtils = UserUtils;
@Input() userId!: string;
@Output() back = new EventEmitter<void>();
@Output() resetPasswordRequested = new EventEmitter<string>();
user: User | null = null;
loading = false;
saving = false;
error = '';
success = '';
// Gestion des permissions
currentUserRole: UserRole | null = null;
// Édition
isEditing = false;
editedUser: UpdateUserDto = {};
// Gestion des rôles
availableRoles: { value: UserRole; label: string; description: string }[] = [];
updatingRole = false;
// Getters pour la logique conditionnelle
get isHubUser(): boolean {
return UserUtils.isHubUser(this.user!);
}
userHasRole(user: User, role: UserRole): boolean {
return UserUtils.hasRole(user, role);
}
ngOnInit() {
if (this.userId) {
this.loadCurrentUserPermissions();
this.loadAvailableRoles();
this.loadUserProfile();
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Charge les permissions de l'utilisateur courant
*/
private loadCurrentUserPermissions(): void {
this.authService.getUserProfile()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (profile) => {
this.currentUserRole = this.authService.getCurrentUserRole();
this.cdRef.detectChanges();
},
error: (error) => {
console.error('Error loading user permissions:', error);
}
});
}
/**
* Charge les rôles disponibles pour les utilisateurs Hub
*/
private loadAvailableRoles(): void {
this.hubUsersService.getAvailableHubRoles()
.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 Hub
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' }
];
this.cdRef.detectChanges();
}
});
}
loadUserProfile() {
this.loading = true;
this.error = '';
this.hubUsersService.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.canEditUser()) {
this.error = 'Vous n\'avez pas la permission de modifier cet utilisateur';
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.canEditUser()) return;
this.saving = true;
this.error = '';
this.success = '';
this.hubUsersService.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();
console.error('Error updating hub user:', error);
}
});
}
updateUserRole(newRole: UserRole) {
if (!this.user || !this.canManageRoles()) {
this.error = 'Vous n\'avez pas la permission de modifier les rôles';
return;
}
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 Hub
if (!this.isValidHubRole(newRole)) {
this.error = 'Rôle invalide pour un utilisateur Hub';
return;
}
this.updatingRole = true;
this.error = '';
this.success = '';
this.hubUsersService.updateHubUserRole(this.user.id, newRole)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (updatedUser) => {
this.user = updatedUser;
this.updatingRole = false;
this.success = 'Rôle mis à jour avec succès';
this.cdRef.detectChanges();
},
error: (error) => {
this.updatingRole = false;
this.error = this.getErrorMessage(error);
this.cdRef.detectChanges();
}
});
}
// Gestion du statut
enableUser() {
if (!this.user || !this.canEnableDisableUser()) return;
this.hubUsersService.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.canEnableDisableUser()) {
this.error = 'Vous n\'avez pas la permission de désactiver cet utilisateur';
return;
}
this.error = '';
this.success = '';
this.hubUsersService.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();
console.error('Error disabling hub user:', error);
}
});
}
// Réinitialisation du mot de passe
resetPassword() {
if (this.user && this.canResetPassword()) {
this.resetPasswordRequested.emit(this.user.id);
}
}
// ==================== VÉRIFICATIONS DE PERMISSIONS ====================
/**
* Vérifie si l'utilisateur peut éditer cet utilisateur
*/
canEditUser(): boolean {
// Toujours permettre d'éditer son propre profil
if (this.isCurrentUserProfile()) {
return true;
}
// Pour les utilisateurs Hub, 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 {
// Pour les Hub, utiliser les permissions du service de rôle
return this.roleService.canManageRoles(this.currentUserRole);
}
/**
* Vérifie si l'utilisateur peut activer/désactiver cet utilisateur
*/
canEnableDisableUser(): boolean {
// Empêcher la désactivation de soi-même
if (this.isCurrentUserProfile()) {
return false;
}
// Pour les Hub, 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 {
// Les utilisateurs peuvent réinitialiser leur propre mot de passe
if (this.isCurrentUserProfile()) {
return true;
}
// Pour les Hub, utiliser les permissions générales
return this.roleService.canEditUsers(this.currentUserRole);
}
/**
* Vérifie si l'utilisateur peut supprimer cet utilisateur
*/
canDeleteUser(): boolean {
// Empêcher la suppression de soi-même
if (this.isCurrentUserProfile()) {
return false;
}
// Pour les Hub, utiliser les permissions du service de rôle
return this.roleService.canDeleteUsers(this.currentUserRole);
}
// ==================== 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: 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';
}
// Obtenir le rôle (peut être string ou UserRole)
getUserRole(): string | UserRole | undefined {
return 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 ====================
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 pour effectuer cette action.';
}
if (error.status === 404) {
return 'Utilisateur non trouvé.';
}
if (error.status === 409) {
return 'Conflit de données. Cet utilisateur existe peut-être déjà.';
}
return 'Erreur lors de l\'opération. Veuillez réessayer.';
}
// ==================== MÉTHODES DE NAVIGATION ====================
goBack() {
this.back.emit();
}
// ==================== MÉTHODES DE 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;
}
private isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
private isValidHubRole(role: UserRole): boolean {
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
return hubRoles.includes(role);
}
// ==================== MÉTHODES UTILITAIRES ====================
isCurrentUserProfile(): boolean {
if (!this.user?.id) return false;
return this.authService.isCurrentUserProfile(this.user.id);
}
getCreationDate(): string {
if (!this.user?.createdTimestamp) return 'Non disponible';
return this.formatTimestamp(this.user.createdTimestamp);
}
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() {
this.loadUserProfile();
}
clearMessages() {
this.error = '';
this.success = '';
this.cdRef.detectChanges();
}
// Méthodes pour le template
getProfileTitle(): string {
return 'Profil Utilisateur Hub';
}
getContextDescription(): string {
return 'Gestion des utilisateurs de la plateforme DCB';
}
getAssignableRoles(): UserRole[] {
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
return hubRoles.filter(role => this.roleService.canAssignRole(this.currentUserRole, role));
}
// Méthodes pour les actions spécifiques
canChangeRole(): boolean {
return this.canManageRoles() && !this.isCurrentUserProfile();
}
canToggleStatus(): boolean {
return this.canEnableDisableUser() && !this.isCurrentUserProfile();
}
showRoleManagement(): boolean {
return this.canManageRoles() && !this.isCurrentUserProfile();
}
}

View File

@ -1,17 +1,52 @@
<!-- src/app/modules/merchant-users/merchant-users.html -->
<div class="container-fluid">
<app-page-title
title="Gestion des Utilisateurs Marchands"
subTitle="Administrez les utilisateurs de votre écosystème marchand"
[badge]="{icon:'lucideUsers', text:'Merchant Users'}"
[title]="pageTitle"
[subTitle]="pageSubtitle"
[badge]="badge"
/>
<!-- Navigation par onglets avec style bordered -->
<!-- Indicateur de permissions -->
@if (currentUserRole) {
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-info py-2">
<div class="d-flex align-items-center">
<ng-icon name="lucideInfo" class="me-2"></ng-icon>
<div class="flex-grow-1">
<small>
<strong>Rôle actuel :</strong>
<span class="badge" [ngClass]="getRoleBadgeClass(currentUserRole)">
{{ getRoleLabel(currentUserRole) }}
</span>
@if (!canCreateUsers) {
<span class="text-warning ms-2">
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Permissions limitées
</span>
}
</small>
</div>
@if (canCreateUsers) {
<button
class="btn btn-primary btn-sm"
(click)="openCreateUserModal()"
>
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
Nouvel Utilisateur Hub
</button>
}
</div>
</div>
</div>
</div>
}
<!-- Navigation par onglets -->
<div class="row mb-4">
<div class="col-12">
<ul
ngbNav
#merchantUsersNav="ngbNav"
#usersNav="ngbNav"
[activeId]="activeTab"
[destroyOnHide]="false"
class="nav nav-tabs nav-justified nav-bordered nav-bordered-primary mb-3"
@ -19,14 +54,17 @@
<li [ngbNavItem]="'list'">
<a ngbNavLink (click)="showTab('list')">
<ng-icon name="lucideUsers" class="fs-lg me-md-1 d-inline-flex align-middle" />
<span class="d-none d-md-inline-block align-middle">Équipe Marchande</span>
<span class="d-none d-md-inline-block align-middle">Liste des Utilisateurs Hub</span>
</a>
<ng-template ngbNavContent>
<app-merchant-users-list
<app-hub-users-list
#hubUsersList
[canCreateUsers]="canCreateUsers"
[canDeleteUsers]="canDeleteUsers"
(userSelected)="onUserSelected($event)"
(openCreateModal)="openCreateUserModal()"
(openResetPasswordModal)="onResetPasswordRequested($event)"
(openDeleteUserModal)="onDeleteUserRequested($event)"
(openCreateUserModal)="openCreateUserModal()"
(resetPasswordRequested)="onResetPasswordRequested($event)"
(deleteUserRequested)="onDeleteUserRequested($event)"
/>
</ng-template>
</li>
@ -38,10 +76,10 @@
</a>
<ng-template ngbNavContent>
@if (selectedUserId) {
<app-merchant-user-profile
<app-hub-user-profile
[userId]="selectedUserId"
(resetPasswordRequested)="onResetPasswordRequested($event)"
(back)="backToList()"
(openResetPasswordModal)="onResetPasswordRequested($event)"
/>
} @else {
<div class="alert alert-warning text-center">
@ -53,17 +91,17 @@
</li>
</ul>
<div class="tab-content" [ngbNavOutlet]="merchantUsersNav"></div>
<div class="tab-content" [ngbNavOutlet]="usersNav"></div>
</div>
</div>
</div>
<!-- Modal de création d'utilisateur marchand -->
<!-- Modal de création d'utilisateur Hub -->
<ng-template #createUserModal let-modal>
<div class="modal-header">
<h4 class="modal-title">
<ng-icon name="lucideUserPlus" class="me-2"></ng-icon>
Créer un nouvel utilisateur marchand
Créer un nouvel utilisateur Hub
</h4>
<button
type="button"
@ -83,30 +121,21 @@
</div>
}
<form (ngSubmit)="createMerchantUser()" #userForm="ngForm">
<!-- Avertissement permissions -->
@if (!canManageRoles && assignableRoles.length === 1) {
<div class="alert alert-warning">
<small>
<ng-icon name="lucideShield" class="me-1"></ng-icon>
<strong>Permissions limitées :</strong> Vous ne pouvez créer que des utilisateurs avec le rôle
<span class="badge" [ngClass]="getRoleBadgeClass(assignableRoles[0])">
{{ getRoleLabel(assignableRoles[0]) }}
</span>
</small>
</div>
}
<form (ngSubmit)="createUser()" #userForm="ngForm">
<div class="row g-3">
<!-- Merchant Partner ID (lecture seule) -->
<div class="col-md-6">
<label class="form-label">Merchant Partner ID</label>
<div class="form-control-plaintext font-monospace small">
{{ currentMerchantPartnerId || 'Chargement...' }}
<input
type="hidden"
[(ngModel)]="newMerchantUser.merchantPartnerId"
name="merchantPartnerId"
[value]="currentMerchantPartnerId"
required
>
</div>
@if (!currentMerchantPartnerId) {
<div class="form-text text-warning">
<ng-icon name="lucideAlertTriangle" class="me-1"></ng-icon>
Merchant Partner ID non disponible
</div>
}
</div>
<!-- Informations de base -->
<div class="col-md-6">
<label class="form-label">
@ -116,7 +145,7 @@
type="text"
class="form-control"
placeholder="Entrez le prénom"
[(ngModel)]="newMerchantUser.firstName"
[(ngModel)]="newUser.firstName"
name="firstName"
required
[disabled]="creatingUser"
@ -137,7 +166,7 @@
type="text"
class="form-control"
placeholder="Entrez le nom"
[(ngModel)]="newMerchantUser.lastName"
[(ngModel)]="newUser.lastName"
name="lastName"
required
[disabled]="creatingUser"
@ -158,7 +187,7 @@
type="text"
class="form-control"
placeholder="Nom d'utilisateur unique"
[(ngModel)]="newMerchantUser.username"
[(ngModel)]="newUser.username"
name="username"
required
[disabled]="creatingUser"
@ -180,7 +209,7 @@
type="email"
class="form-control"
placeholder="email@exemple.com"
[(ngModel)]="newMerchantUser.email"
[(ngModel)]="newUser.email"
name="email"
required
email
@ -203,17 +232,27 @@
<label class="form-label">
Mot de passe <span class="text-danger">*</span>
</label>
<input
type="password"
class="form-control"
placeholder="Mot de passe sécurisé"
[(ngModel)]="newMerchantUser.password"
name="password"
required
minlength="8"
[disabled]="creatingUser"
#password="ngModel"
>
<div class="input-group">
<input
[type]="showPassword ? 'text' : 'password'"
class="form-control"
placeholder="Mot de passe sécurisé"
[(ngModel)]="newUser.password"
name="password"
required
minlength="8"
[disabled]="creatingUser"
#password="ngModel"
>
<button
type="button"
class="btn btn-outline-secondary"
(click)="showPassword = !showPassword"
[disabled]="creatingUser"
>
<ng-icon [name]="showPassword ? 'lucideEyeOff' : 'lucideEye'"></ng-icon>
</button>
</div>
<div class="form-text">
Le mot de passe doit contenir au moins 8 caractères.
</div>
@ -229,56 +268,56 @@
}
</div>
<!-- Sélection du rôle -->
<!-- Sélection du rôle unique -->
<div class="col-12">
<label class="form-label">
Rôle <span class="text-danger">*</span>
Rôle Principal <span class="text-danger">*</span>
</label>
<select
class="form-select"
[(ngModel)]="newMerchantUser.role"
[value]="newUser.role"
(change)="onRoleSelectionChange($any($event.target).value)"
name="role"
required
[disabled]="creatingUser || !canManageAllRoles"
#roleSelect="ngModel"
[disabled]="creatingUser || !canManageRoles"
>
<option value="" disabled selected>Sélectionnez un rôle</option>
@for (role of getAvailableRoles(); track role.value) {
@if (isMerchantRole(role.value) && isRoleAllowedForCreation(role.value)) {
<option
[value]="role.value"
[disabled]="!canAssignRole(role.value)"
>
{{ getRoleDisplayName(role.value) }} - {{ role.description }}
@if (!canAssignRole(role.value)) {
(Non autorisé)
}
</option>
}
<option value="" disabled>Sélectionnez un rôle</option>
@for (role of availableRoles; track role.value) {
<option
[value]="role.value"
[disabled]="!canAssignRole(role.value)"
>
{{ role.label }} - {{ role.description }}
@if (!canAssignRole(role.value)) {
(Non autorisé)
}
</option>
}
</select>
<div class="form-text">
@if (canManageAllRoles) {
<span class="text-success">
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Vous pouvez attribuer tous les rôles
</span>
@if (canManageRoles) {
Sélectionnez le rôle principal à assigner à cet utilisateur
} @else {
<span class="text-warning">
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Permissions de rôle limitées
</span>
Vous ne pouvez pas modifier les rôles disponibles
}
</div>
@if (roleSelect.invalid && roleSelect.touched) {
<div class="text-danger small">
Le rôle est requis
</div>
}
</div>
<!-- Type d'utilisateur automatique -->
<div class="col-12">
<label class="form-label">Type d'utilisateur</label>
<div class="form-control-plaintext">
<span class="badge bg-primary">
Utilisateur Hub
</span>
</div>
<div class="form-text">
Tous les utilisateurs créés ici sont des utilisateurs Hub
</div>
</div>
<!-- Avertissement pour les non-DCB_PARTNER -->
@if (!canManageAllRoles) {
@if (!canManageRoles) {
<div class="col-12">
<div class="alert alert-warning">
<div class="d-flex align-items-center">
@ -295,23 +334,22 @@
</div>
}
<!-- Aperçu du rôle sélectionné -->
@if (newMerchantUser.role) {
@if (newUser.role) {
<div class="col-12">
<div class="alert alert-info">
<div class="d-flex align-items-center">
<ng-icon
[name]="getRoleIcon(newMerchantUser.role)"
[name]="getRoleIcon(newUser.role)"
class="me-2"
></ng-icon>
<div>
<strong>Rôle sélectionné :</strong>
<span class="badge ms-2" [ngClass]="getRoleBadgeClass(newMerchantUser.role)">
{{ getRoleDisplayName(newMerchantUser.role) }}
<span class="badge ms-2" [ngClass]="getRoleBadgeClass(newUser.role)">
{{ getRoleLabel(newUser.role) }}
</span>
<br>
<small class="text-muted">
{{ getRoleDescription(newMerchantUser.role) }}
{{ getRoleDescription(newUser.role) }}
</small>
</div>
</div>
@ -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 @@
<div class="alert alert-light">
<small class="text-muted">
<strong>Informations système :</strong><br>
• Merchant Partner ID : {{ currentMerchantPartnerId || 'Chargement...' }}<br>
• Type d'utilisateur : MERCHANT<br>
• Type d'utilisateur : HUB<br>
• Créé par : Utilisateur courant<br>
• Votre rôle : {{ currentUserRole || 'Non défini' }}
</small>
@ -382,7 +419,7 @@
<button
type="submit"
class="btn btn-primary"
[disabled]="!userForm.form.valid || creatingUser || !isRoleAllowedForCreation(newMerchantUser.role) || !currentMerchantPartnerId"
[disabled]="!userForm.form.valid || creatingUser"
>
@if (creatingUser) {
<div class="spinner-border spinner-border-sm me-2" role="status">
@ -391,7 +428,7 @@
Création...
} @else {
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
Créer l'utilisateur
Créer l'utilisateur Hub
}
</button>
</div>
@ -447,9 +484,9 @@
<br>
<small class="text-muted">
<span class="badge" [ngClass]="getRoleBadgeClass(selectedUserForReset.role)">
{{ getRoleDisplayName(selectedUserForReset.role) }}
{{ getRoleLabel(selectedUserForReset.role) }}
</span>
Merchant Partner: {{ selectedUserForReset.merchantPartnerId }}
Type: Hub
</small>
</div>
</div>
@ -460,17 +497,27 @@
<label class="form-label">
Nouveau mot de passe <span class="text-danger">*</span>
</label>
<input
type="password"
class="form-control"
placeholder="Entrez le nouveau mot de passe"
[(ngModel)]="newPassword"
name="newPassword"
required
minlength="8"
[disabled]="resettingPassword"
#newPasswordInput="ngModel"
>
<div class="input-group">
<input
[type]="showNewPassword ? 'text' : 'password'"
class="form-control"
placeholder="Entrez le nouveau mot de passe"
[(ngModel)]="newPassword"
name="newPassword"
required
minlength="8"
[disabled]="resettingPassword"
#newPasswordInput="ngModel"
>
<button
type="button"
class="btn btn-outline-secondary"
(click)="showNewPassword = !showNewPassword"
[disabled]="resettingPassword"
>
<ng-icon [name]="showNewPassword ? 'lucideEyeOff' : 'lucideEye'"></ng-icon>
</button>
</div>
<div class="form-text">
Le mot de passe doit contenir au moins 8 caractères.
</div>
@ -573,9 +620,13 @@
<div class="avatar-lg mx-auto mb-3 bg-danger bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<ng-icon name="lucideUserX" class="text-danger" style="font-size: 2rem;"></ng-icon>
</div>
<h5 class="text-danger mb-2">Êtes-vous sûr de vouloir supprimer cet utilisateur ?</h5>
<h5 class="text-danger mb-2">Êtes-vous sûr de vouloir supprimer cet utilisateur Hub ?</h5>
<p class="text-muted mb-0">
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) {
<strong>{{ selectedUserForDelete.username }}</strong>
}
seront définitivement perdues.
</p>
</div>
@ -592,12 +643,17 @@
<br>
<strong>Email :</strong> {{ selectedUserForDelete.email }}
<br>
<strong>Rôle :</strong>
<strong>Rôle Principal :</strong>
<span class="badge" [ngClass]="getRoleBadgeClass(selectedUserForDelete.role)">
{{ getRoleDisplayName(selectedUserForDelete.role) }}
{{ getRoleLabel(selectedUserForDelete.role) }}
</span>
@if (selectedUserForDelete.role && selectedUserForDelete.role.length > 1) {
<br>
<strong>Rôles supplémentaires :</strong>
{{ selectedUserForDelete.role.length - 1 }}
}
<br>
<strong>Merchant Partner :</strong> {{ selectedUserForDelete.merchantPartnerId }}
<strong>Type :</strong> Utilisateur Hub
</div>
</div>
</div>
@ -631,7 +687,7 @@
type="button"
class="btn btn-danger"
(click)="confirmDeleteUser()"
[disabled]="deletingUser || !selectedUserForDelete"
[disabled]="deletingUser || !selectedUserForDelete || !canDeleteUsers"
>
@if (deletingUser) {
<div class="spinner-border spinner-border-sm me-2" role="status">

View File

@ -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<GlobalUsersOverview> {
return this.http.get<GlobalUsersOverview>(`${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<UsersStatistics> {
return this.http.get<GlobalUsersOverview>(`${this.baseApiUrl}`).pipe(
map(response => response.statistics),
catchError(error => {
console.error('Error loading users statistics:', error);
return throwError(() => error);
})
);
}
createHubUser(createUserDto: CreateUserDto): Observable<User> {
// 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<User>(`${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<PaginatedUserResponse> {
return this.http.get<User[]>(`${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<PaginatedUserResponse> {
return this.http.get<User[]>(`${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<User> {
return this.http.get<User>(`${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<User> {
const payload: any = {
firstName: updateUserDto.firstName,
lastName: updateUserDto.lastName,
email: updateUserDto.email,
enabled: updateUserDto.enabled
};
return this.http.put<User>(`${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<User> {
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<User>(`${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<MessageResponse> {
return this.http.delete<MessageResponse>(`${this.baseApiUrl}/${id}`).pipe(
catchError(error => {
console.error(`Error deleting hub user ${id}:`, error);
return throwError(() => error);
})
);
}
resetHubUserPassword(id: string, resetPasswordDto: ResetPasswordDto): Observable<MessageResponse> {
const payload = {
newPassword: resetPasswordDto.newPassword,
temporary: resetPasswordDto.temporary !== undefined ? resetPasswordDto.temporary : true
};
return this.http.post<MessageResponse>(
`${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<User> {
return this.updateHubUser(id, { enabled: true });
}
disableHubUser(id: string): Observable<User> {
return this.updateHubUser(id, { enabled: false });
}
getAvailableHubRoles(): Observable<AvailableRolesResponse> {
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<User[]> {
return this.http.get<User[]>(`${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<User[]> {
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)
};
}
}

View File

@ -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<void>();
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<any>;
@ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef<any>;
@ViewChild('deleteUserModal') deleteUserModal!: TemplateRef<any>;
// 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<any>, 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<any>, 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<any>;
@ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef<any>;
@ViewChild('deleteUserModal') deleteUserModal!: TemplateRef<any>;
}

View File

@ -1,45 +1,15 @@
<!-- src/app/modules/merchant-users/list/list.html -->
<app-ui-card title="Équipe Marchande">
<a
<app-ui-card [title]="'Équipe Marchande'">
<a
helper-text
href="javascript:void(0);"
class="icon-link icon-link-hover link-primary fw-semibold"
>
@if (canViewAllMerchants) {
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Vue administrative - Tous les utilisateurs marchands
} @else if (isDcbPartner) {
<ng-icon name="lucideUsers" class="me-1"></ng-icon>
Votre équipe marchande
} @else {
<ng-icon name="lucideBuilding" class="me-1"></ng-icon>
Utilisateurs de votre partenaire marchand
}
<ng-icon name="lucideUsers" class="me-1"></ng-icon>
Gérez les accès Marchands de votre plateforme DCB
</a>
<div card-body>
<!-- Indicateur de contexte -->
@if (canViewAllMerchants) {
<div class="alert alert-info mb-3">
<div class="d-flex align-items-center">
<ng-icon name="lucideShield" class="me-2"></ng-icon>
<div>
<strong>Vue administrative DCB :</strong> Vous visualisez tous les utilisateurs marchands de la plateforme
</div>
</div>
</div>
} @else if (isDcbPartner) {
<div class="alert alert-primary mb-3">
<div class="d-flex align-items-center">
<ng-icon name="lucideBuilding" class="me-2"></ng-icon>
<div>
<strong>Vue partenaire marchand :</strong> Vous gérez les utilisateurs de votre propre équipe
<small class="d-block text-muted">Merchant Partner ID: {{ currentMerchantPartnerId }}</small>
</div>
</div>
</div>
}
<!-- Barre d'actions supérieure -->
<div class="row mb-3">
<div class="col-md-6">
@ -52,7 +22,7 @@
[class.active]="roleFilter === 'all'"
(click)="filterByRole('all')"
>
Tous ({{ allUsers.length }})
Tous ({{ getTotalUsersCount() }})
</button>
<button
type="button"
@ -81,24 +51,33 @@
</div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-end gap-2">
@if (!canViewAllMerchants) {
@if (showCreateButton && canCreateUsers) {
<button
class="btn btn-primary"
(click)="openCreateModal.emit()"
(click)="openCreateUserModal.emit()"
>
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
Nouvel Utilisateur
Nouvel Utilisateur Marchand
</button>
}
<button
class="btn btn-outline-secondary"
(click)="refreshData()"
[disabled]="loading"
>
<ng-icon name="lucideRefreshCw" class="me-1" [class.spin]="loading"></ng-icon>
Actualiser
</button>
</div>
</div>
</div>
<!-- Barre de recherche et filtres -->
<!-- Barre de recherche et filtres avancés -->
<div class="row mb-3">
<div class="col-md-3">
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">
<ng-icon name="lucideSearch"></ng-icon>
@ -106,44 +85,44 @@
<input
type="text"
class="form-control"
placeholder="Nom, email, username..."
placeholder="Rechercher par nom, email..."
[(ngModel)]="searchTerm"
(keyup.enter)="onSearch()"
(input)="onSearch()"
[disabled]="loading"
>
</div>
</div>
<div class="col-md-2">
<select class="form-select" [(ngModel)]="statusFilter" (change)="onSearch()">
<select class="form-select" [(ngModel)]="statusFilter" (change)="applyFiltersAndPagination()">
<option value="all">Tous les statuts</option>
<option value="enabled">Activés ({{ getEnabledUsersCount() }})</option>
<option value="disabled">Désactivés ({{ getDisabledUsersCount() }})</option>
<option value="enabled">Activés seulement</option>
<option value="disabled">Désactivés seulement</option>
</select>
</div>
<div class="col-md-2">
<select class="form-select" [(ngModel)]="emailVerifiedFilter" (change)="onSearch()">
<select class="form-select" [(ngModel)]="emailVerifiedFilter" (change)="applyFiltersAndPagination()">
<option value="all">Tous les emails</option>
<option value="verified">Email vérifié</option>
<option value="not-verified">Email non vérifié</option>
</select>
</div>
<div class="col-md-2">
<select class="form-select" [(ngModel)]="roleFilter" (change)="onSearch()">
<select class="form-select" [(ngModel)]="roleFilter" (change)="applyFiltersAndPagination()">
<option value="all">Tous les rôles</option>
@for (role of availableRoles; track role.value) {
<option [value]="role.value">{{ role.label }}</option>
}
</select>
</div>
<div class="col-md-3">
<div class="d-flex gap-2">
<button class="btn btn-outline-primary" (click)="onSearch()">
<ng-icon name="lucideFilter" class="me-1"></ng-icon>
Appliquer
</button>
<button class="btn btn-outline-secondary" (click)="onClearFilters()">
<ng-icon name="lucideX" class="me-1"></ng-icon>
Réinitialiser
</button>
</div>
<div class="col-md-2">
<button class="btn btn-outline-secondary w-100" (click)="onClearFilters()" [disabled]="loading">
<ng-icon name="lucideX" class="me-1"></ng-icon>
Effacer
</button>
</div>
</div>
@ -163,6 +142,7 @@
<div class="d-flex align-items-center">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
<div>{{ error }}</div>
<button class="btn-close ms-auto" (click)="error = ''"></button>
</div>
</div>
}
@ -173,14 +153,9 @@
<table class="table table-hover table-striped">
<thead class="table-light">
<tr>
<!-- Colonne Merchant Partner uniquement pour les admins -->
@if (canViewAllMerchants) {
<th (click)="sort('merchantPartnerId')" class="cursor-pointer">
<div class="d-flex align-items-center">
<span>Merchant Partner</span>
<ng-icon [name]="getSortIcon('merchantPartnerId')" class="ms-1 fs-12"></ng-icon>
</div>
</th>
<!-- Colonne Merchant Partner pour les admins -->
@if (showMerchantPartnerColumn) {
<th>Merchant Partner</th>
}
<th (click)="sort('username')" class="cursor-pointer">
<div class="d-flex align-items-center">
@ -194,12 +169,7 @@
<ng-icon [name]="getSortIcon('email')" class="ms-1 fs-12"></ng-icon>
</div>
</th>
<th (click)="sort('role')" class="cursor-pointer">
<div class="d-flex align-items-center">
<span>Rôle</span>
<ng-icon [name]="getSortIcon('role')" class="ms-1 fs-12"></ng-icon>
</div>
</th>
<th>Rôle Principal</th>
<th (click)="sort('enabled')" class="cursor-pointer">
<div class="d-flex align-items-center">
<span>Statut</span>
@ -218,8 +188,8 @@
<tbody>
@for (user of displayedUsers; track user.id) {
<tr>
<!-- Colonne Merchant Partner uniquement pour les admins -->
@if (canViewAllMerchants) {
<!-- Colonne Merchant Partner pour les admins -->
@if (showMerchantPartnerColumn) {
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-secondary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
@ -262,7 +232,7 @@
<td>
<span class="badge d-flex align-items-center" [ngClass]="getRoleBadgeClass(user.role)">
<ng-icon [name]="getRoleIcon(user.role)" class="me-1" size="14"></ng-icon>
{{ getRoleDisplayName(user.role) }}
{{ getRoleLabel(user.role) }}
</span>
</td>
<td>
@ -286,7 +256,7 @@
</button>
<button
class="btn btn-outline-warning btn-sm"
(click)="resetPassword(user)"
(click)="resetPasswordRequested(user)"
title="Réinitialiser le mot de passe"
>
<ng-icon name="lucideKey"></ng-icon>
@ -308,10 +278,10 @@
<ng-icon name="lucideUserCheck"></ng-icon>
</button>
}
@if (!canViewAllMerchants) {
@if (showDeleteButton) {
<button
class="btn btn-outline-danger btn-sm"
(click)="deleteUser(user)"
(click)="deleteUserRequested(user)"
title="Supprimer l'utilisateur"
>
<ng-icon name="lucideTrash2"></ng-icon>
@ -323,13 +293,13 @@
}
@empty {
<tr>
<td [attr.colspan]="canViewAllMerchants ? 7 : 6" class="text-center py-4">
<td [attr.colspan]="getColumnCount()" class="text-center py-4">
<div class="text-muted">
<ng-icon name="lucideUsers" class="fs-1 mb-3 opacity-50"></ng-icon>
<h5 class="mb-2">Aucun utilisateur marchand trouvé</h5>
<p class="mb-3">Aucun utilisateur ne correspond à vos critères de recherche.</p>
@if (!canViewAllMerchants) {
<button class="btn btn-primary" (click)="openCreateModal.emit()">
@if (showCreateButton) {
<button class="btn btn-primary" (click)="openCreateUserModal.emit()">
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
Créer le premier utilisateur
</button>

View File

@ -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<void>();
// Configuration
readonly UserRole = UserRole;
readonly UserType = UserType;
readonly UserUtils = UserUtils;
// Inputs
@Input() canCreateUsers: boolean = false;
@Input() canDeleteUsers: boolean = false;
// Outputs
@Output() userSelected = new EventEmitter<string>();
@Output() openCreateUserModal = new EventEmitter<void>();
@Output() openResetPasswordModal = new EventEmitter<string>();
@Output() openDeleteUserModal = new EventEmitter<string>();
// 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<User[]>;
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;
}
}

View File

@ -14,8 +14,8 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item">
<a href="javascript:void(0)" (click)="back.emit()" class="text-decoration-none cursor-pointer">
Équipe Marchande
<a href="javascript:void(0)" (click)="goBack()" class="text-decoration-none cursor-pointer">
Utilisateurs Marchand
</a>
</li>
<li class="breadcrumb-item active" aria-current="page">
@ -31,7 +31,7 @@
<div class="d-flex gap-2">
<!-- Bouton de réinitialisation de mot de passe -->
@if (user && !isEditing && canResetPassword()) {
@if (user && canEditUser() && !isEditing) {
<button
class="btn btn-warning"
(click)="resetPassword()"
@ -39,31 +39,27 @@
<ng-icon name="lucideKey" class="me-1"></ng-icon>
Réinitialiser MDP
</button>
}
<!-- Bouton activation/désactivation -->
@if (user && !isEditing && canEnableDisableUser()) {
@if (user.enabled) {
<!-- Bouton activation/désactivation -->
@if (user.enabled && canToggleStatus()) {
<button
class="btn btn-outline-warning"
(click)="disableUser()"
>
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
<ng-icon name="lucidePause" class="me-1"></ng-icon>
Désactiver
</button>
} @else {
} @else if (!user.enabled && canToggleStatus()) {
<button
class="btn btn-outline-success"
(click)="enableUser()"
>
<ng-icon name="lucideUserCheck" class="me-1"></ng-icon>
<ng-icon name="lucidePlay" class="me-1"></ng-icon>
Activer
</button>
}
}
<!-- Bouton modification -->
@if (user && !isEditing && canEditUser()) {
<!-- Bouton modification -->
<button
class="btn btn-primary"
(click)="startEditing()"
@ -77,12 +73,29 @@
</div>
</div>
<!-- Indicateur de permissions -->
@if (currentUserRole && !canEditUser()) {
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-warning">
<div class="d-flex align-items-center">
<ng-icon name="lucideShield" class="me-2"></ng-icon>
<div>
<strong>Permissions limitées :</strong> Vous ne pouvez que consulter ce profil
</div>
</div>
</div>
</div>
</div>
}
<!-- Messages d'alerte -->
@if (error) {
<div class="alert alert-danger">
<div class="d-flex align-items-center">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
<div>{{ error }}</div>
<button class="btn-close ms-auto" (click)="clearMessages()"></button>
</div>
</div>
}
@ -92,31 +105,7 @@
<div class="d-flex align-items-center">
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
<div>{{ success }}</div>
</div>
</div>
}
<!-- Indicateur de permissions -->
@if (user && !loading) {
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-info py-2">
<div class="d-flex align-items-center">
<ng-icon name="lucideShield" class="me-2"></ng-icon>
<div class="flex-grow-1">
<small>
<strong>Statut :</strong> {{ getPermissionStatus() }}
@if (currentUserRole === UserRole.DCB_PARTNER) {
<span class="badge bg-success ms-2">Accès complet</span>
} @else if (currentUserRole === UserRole.DCB_PARTNER_ADMIN) {
<span class="badge bg-warning ms-2">Accès limité au partenaire</span>
} @else {
<span class="badge bg-secondary ms-2">Permissions restreintes</span>
}
</small>
</div>
</div>
</div>
<button class="btn-close ms-auto" (click)="clearMessages()"></button>
</div>
</div>
}
@ -128,7 +117,7 @@
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
<p class="mt-2 text-muted">Chargement du profil...</p>
<p class="mt-2 text-muted">Chargement du profil utilisateur Marchand...</p>
</div>
}
@ -152,15 +141,13 @@
<h5>{{ getUserDisplayName() }}</h5>
<p class="text-muted mb-2">@{{ user.username }}</p>
<!-- Rôle principal -->
<span class="badge d-flex align-items-center justify-content-center mx-auto mb-3"
[ngClass]="getRoleBadgeClass(user.role)" style="max-width: 150px;">
<ng-icon [name]="getRoleIcon(user.role)" class="me-1"></ng-icon>
{{ getRoleDisplayName(user.role) }}
<!-- Type d'utilisateur -->
<span class="badge bg-primary mb-2">
Utilisateur Marchand
</span>
<!-- Statut -->
<span [class]="getStatusBadgeClass()" class="mb-3">
<span [class]="getStatusBadgeClass()" class="mb-3 d-block">
{{ getStatusText() }}
</span>
@ -173,12 +160,6 @@
<ng-icon name="lucideAlertTriangle" class="ms-1 text-warning" size="14" title="Email non vérifié"></ng-icon>
}
</div>
<div class="d-flex align-items-center mb-2">
<ng-icon name="lucideBuilding" class="me-2 text-muted"></ng-icon>
<small class="text-truncate" title="Merchant Partner ID">
{{ user.merchantPartnerId }}
</small>
</div>
<div class="d-flex align-items-center">
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
<small>Créé le {{ getCreationDate() }}</small>
@ -195,35 +176,73 @@
<!-- Carte rôle utilisateur -->
<div class="card mt-3">
<div class="card-header bg-light">
<h5 class="card-title mb-0">Rôle Utilisateur</h5>
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
Rôle Utilisateur
</h5>
@if (showRoleManagement()) {
<span class="badge bg-info">Modifiable</span>
}
</div>
<div class="card-body">
<!-- Rôle actuel -->
<!-- Rôles actuels -->
<div class="text-center mb-3">
<span class="badge d-flex align-items-center justify-content-center"
[ngClass]="getRoleBadgeClass(user.role)">
<ng-icon [name]="getRoleIcon(user.role)" class="me-2"></ng-icon>
{{ getRoleDisplayName(user.role) }}
</span>
<small class="text-muted d-block mt-2">
{{ getRoleDescription(user.role) }}
</small>
@if (getUserRole()) {
<div class="d-flex flex-wrap gap-1 justify-content-center mb-2">
<span class="badge d-flex align-items-center" [ngClass]="getRoleBadgeClass(user.role)">
<ng-icon [name]="getRoleIcon(user.role)" class="me-1" size="12"></ng-icon>
{{ getRoleLabel(user.role) }}
</span>
</div>
<!-- Description du rôle principal -->
<small class="text-muted d-block">
{{ getRoleDescription(user.role) }}
</small>
} @else {
<span class="badge bg-secondary">Aucun rôle</span>
}
</div>
<!-- Information sur la modification des rôles -->
@if (canManageRoles()) {
<div class="alert alert-success mt-3">
<!-- Changement de rôle -->
@if (showRoleManagement()) {
<div class="mt-3">
<label class="form-label fw-semibold">Changer le rôle principal</label>
<select
class="form-select"
[value]="user.role"
(change)="updateUserRole($any($event.target).value)"
[disabled]="updatingRole"
>
<option value="" disabled>Sélectionnez un nouveau rôle</option>
@for (role of getAssignableRoles(); track role) {
<option
[value]="role"
[disabled]="role === currentUserRole"
>
{{ getRoleLabel(role) }}
@if (role === currentUserRole) {
(Actuel)
}
</option>
}
</select>
<div class="form-text">
@if (updatingRole) {
<div class="spinner-border spinner-border-sm me-1" role="status">
<span class="visually-hidden">Mise à jour...</span>
</div>
Mise à jour en cours...
} @else {
Sélectionnez un nouveau rôle principal pour cet utilisateur
}
</div>
</div>
} @else if (!canManageRoles()) {
<div class="alert alert-info mt-3">
<small>
<ng-icon name="lucideShield" class="me-1"></ng-icon>
<strong>DCB Partner :</strong> Vous pouvez modifier le rôle de cet utilisateur.
</small>
</div>
} @else {
<div class="alert alert-warning mt-3">
<small>
<ng-icon name="lucideInfo" class="me-1"></ng-icon>
<strong>Information :</strong> Seul un <strong>DCB Partner</strong> peut modifier les rôles des utilisateurs marchands.
Vous n'avez pas la permission de modifier les rôles
</small>
</div>
}
@ -248,13 +267,7 @@
<div class="col-12">
<strong>Type d'utilisateur :</strong>
<div class="text-muted">
<span class="badge bg-primary">{{ user.userType }}</span>
</div>
</div>
<div class="col-12">
<strong>Merchant Partner :</strong>
<div class="text-muted font-monospace small">
{{ user.merchantPartnerId }}
<span class="badge bg-primary">Utilisateur Marchand</span>
</div>
</div>
</div>
@ -309,7 +322,7 @@
<div class="row g-3">
<!-- Prénom -->
<div class="col-md-6">
<label class="form-label">Prénom <span class="text-danger">*</span></label>
<label class="form-label">Prénom</label>
@if (isEditing) {
<input
type="text"
@ -317,7 +330,6 @@
[(ngModel)]="editedUser.firstName"
placeholder="Entrez le prénom"
[disabled]="saving"
required
>
} @else {
<div class="form-control-plaintext">
@ -328,7 +340,7 @@
<!-- Nom -->
<div class="col-md-6">
<label class="form-label">Nom <span class="text-danger">*</span></label>
<label class="form-label">Nom</label>
@if (isEditing) {
<input
type="text"
@ -336,7 +348,6 @@
[(ngModel)]="editedUser.lastName"
placeholder="Entrez le nom"
[disabled]="saving"
required
>
} @else {
<div class="form-control-plaintext">
@ -358,7 +369,7 @@
<!-- Email -->
<div class="col-md-6">
<label class="form-label">Email <span class="text-danger">*</span></label>
<label class="form-label">Email</label>
@if (isEditing) {
<input
type="email"
@ -366,13 +377,7 @@
[(ngModel)]="editedUser.email"
placeholder="email@exemple.com"
[disabled]="saving"
required
>
@if (editedUser.email && !isValidEmail(editedUser.email)) {
<div class="text-danger small mt-1">
Format d'email invalide
</div>
}
} @else {
<div class="form-control-plaintext">
{{ user.email }}
@ -383,88 +388,8 @@
}
</div>
<!-- Rôle (affichage seulement) -->
<div class="col-12">
<label class="form-label">Rôle Utilisateur</label>
<div class="form-control-plaintext">
<span class="badge" [ngClass]="getRoleBadgeClass(user.role)">
<ng-icon [name]="getRoleIcon(user.role)" class="me-1"></ng-icon>
{{ getRoleDisplayName(user.role) }}
</span>
</div>
<div class="form-text">
@if (canManageRoles()) {
<span class="text-success">
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Vous pouvez modifier ce rôle (DCB Partner)
</span>
} @else {
<span class="text-warning">
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Seul un DCB Partner peut modifier les rôles
</span>
}
</div>
</div>
<!-- Section modification du rôle (uniquement pour DCB_PARTNER) -->
@if (isEditing && canManageRoles()) {
<div class="col-12">
<div class="card border-warning">
<div class="card-header bg-warning bg-opacity-10">
<h6 class="card-title mb-0 text-warning">
<ng-icon name="lucideShield" class="me-2"></ng-icon>
Modification du Rôle (DCB Partner)
</h6>
</div>
<div class="card-body">
<label class="form-label">Nouveau rôle</label>
<select
class="form-select"
[(ngModel)]="user.role"
name="role"
[disabled]="updatingRole"
>
<option value="" disabled>Sélectionnez un nouveau rôle</option>
@for (role of availableRoles; track role) {
<option [value]="role">
{{ getRoleDisplayName(role) }}
</option>
}
</select>
<div class="form-text">
En tant que DCB Partner, vous pouvez modifier le rôle de cet utilisateur.
</div>
<button
type="button"
class="btn btn-warning mt-2"
(click)="updateUserRole(user.role)"
[disabled]="updatingRole"
>
@if (updatingRole) {
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
}
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Mettre à jour le rôle
</button>
</div>
</div>
</div>
}
<!-- Merchant Partner ID (lecture seule) -->
<div class="col-md-6">
<label class="form-label">Merchant Partner ID</label>
<div class="form-control-plaintext font-monospace small">
{{ user.merchantPartnerId }}
</div>
<div class="form-text">
Identifiant du partenaire marchand
</div>
</div>
<!-- Statut activé -->
@if (isEditing && canEnableDisableUser()) {
@if (isEditing) {
<div class="col-md-6">
<div class="form-check form-switch">
<input
@ -482,7 +407,7 @@
L'utilisateur peut se connecter si activé
</div>
</div>
} @else if (!isEditing) {
} @else {
<div class="col-md-6">
<label class="form-label">Statut du compte</label>
<div class="form-control-plaintext">
@ -493,6 +418,24 @@
</div>
}
<!-- Rôles multiples -->
<div class="col-12">
<label class="form-label">
Rôle Assigné
</label>
<div class="form-control-plaintext">
<div class="d-flex flex-wrap gap-1">
<span class="badge d-flex align-items-center" [ngClass]="getRoleBadgeClass(user.role)">
<ng-icon [name]="getRoleIcon(user.role)" class="me-1" size="12"></ng-icon>
{{ getRoleLabel(user.role) }}
</span>
</div>
</div>
<div class="form-text">
{{ getUserRoleDisplay() }}
</div>
</div>
<!-- Informations système -->
@if (!isEditing) {
<div class="col-12">
@ -523,17 +466,9 @@
<div class="col-md-6">
<label class="form-label">Type d'utilisateur</label>
<div class="form-control-plaintext">
<span class="badge bg-primary">{{ user.userType }}</span>
<span class="badge bg-primary">Utilisateur Marchand</span>
</div>
</div>
@if (user.lastLogin) {
<div class="col-md-6">
<label class="form-label">Dernière connexion</label>
<div class="form-control-plaintext">
{{ getLastLoginDate() }}
</div>
</div>
}
</div>
</div>
}
@ -542,77 +477,51 @@
</div>
<!-- Actions supplémentaires -->
@if (!isEditing) {
@if (!isEditing && canEditUser()) {
<div class="card mt-3">
<div class="card-header bg-light">
<h6 class="card-title mb-0">Actions de Gestion</h6>
</div>
<div class="card-body">
<div class="row g-2">
<!-- Réinitialisation MDP -->
@if (canResetPassword()) {
<div class="col-md-4">
<div class="col-md-4">
<button
class="btn btn-outline-warning w-100"
(click)="resetPassword()"
[disabled]="!canResetPassword()"
>
<ng-icon name="lucideKey" class="me-1"></ng-icon>
Réinitialiser MDP
</button>
</div>
<div class="col-md-4">
@if (user.enabled && canToggleStatus()) {
<button
class="btn btn-outline-warning w-100"
(click)="resetPassword()"
class="btn btn-outline-secondary w-100"
(click)="disableUser()"
>
<ng-icon name="lucideKey" class="me-1"></ng-icon>
Réinitialiser MDP
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
Désactiver
</button>
</div>
}
<!-- Activation/Désactivation -->
@if (canEnableDisableUser()) {
<div class="col-md-4">
@if (user.enabled) {
<button
class="btn btn-outline-secondary w-100"
(click)="disableUser()"
>
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
Désactiver
</button>
} @else {
<button
class="btn btn-outline-success w-100"
(click)="enableUser()"
>
<ng-icon name="lucideUserCheck" class="me-1"></ng-icon>
Activer
</button>
}
</div>
}
<!-- Modification -->
@if (canEditUser()) {
<div class="col-md-4">
} @else if (!user.enabled && canToggleStatus()) {
<button
class="btn btn-outline-primary w-100"
(click)="startEditing()"
class="btn btn-outline-success w-100"
(click)="enableUser()"
>
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
Modifier
<ng-icon name="lucideUserCheck" class="me-1"></ng-icon>
Activer
</button>
</div>
}
</div>
<!-- Avertissement pour les permissions -->
<div class="alert alert-light mt-3 mb-0">
<small>
@if (currentUserRole === UserRole.DCB_PARTNER) {
<ng-icon name="lucideShield" class="me-1 text-success"></ng-icon>
<strong>DCB Partner :</strong> Vous avez un accès complet à toutes les fonctionnalités de gestion.
} @else if (currentUserRole === UserRole.DCB_PARTNER_ADMIN) {
<ng-icon name="lucideInfo" class="me-1 text-warning"></ng-icon>
<strong>Admin Partenaire :</strong> Vous pouvez gérer les utilisateurs de votre partenaire marchand, mais pas modifier les rôles.
} @else {
<ng-icon name="lucideInfo" class="me-1 text-muted"></ng-icon>
<strong>Permissions limitées :</strong> Contactez un DCB Partner pour les actions de gestion avancées.
}
</small>
</div>
<div class="col-md-4">
<button
class="btn btn-outline-primary w-100"
(click)="startEditing()"
>
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
Modifier
</button>
</div>
</div>
</div>
</div>

View File

@ -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<void>();
readonly UserRole = UserRole;
readonly UserType = UserType;
readonly UserUtils = UserUtils;
@Input() userId!: string;
@Output() back = new EventEmitter<void>();
@Output() openResetPasswordModal = new EventEmitter<string>();
@Output() resetPasswordRequested = new EventEmitter<string>();
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();
}
}

View File

@ -0,0 +1,882 @@
<div class="container-fluid">
<app-page-title
[title]="pageTitle"
[subTitle]="pageSubtitle"
[badge]="badge"
/>
<!-- Indicateur de permissions -->
@if (currentUserRole) {
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-info py-2">
<div class="d-flex align-items-center">
<ng-icon name="lucideInfo" class="me-2"></ng-icon>
<div class="flex-grow-1">
<small>
<strong>Rôle actuel :</strong>
<span class="badge" [ngClass]="getRoleBadgeClass(currentUserRole)">
{{ getRoleLabel(currentUserRole) }}
</span>
@if (!canCreateUsers) {
<span class="text-warning ms-2">
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Permissions limitées
</span>
}
</small>
</div>
@if (canCreateUsers) {
<button
class="btn btn-primary btn-sm"
(click)="openCreateUserModal()"
>
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
Nouvel Utilisateur
</button>
}
</div>
</div>
</div>
</div>
}
<!-- Navigation par onglets -->
<div class="row mb-4">
<div class="col-12">
<ul
ngbNav
#usersNav="ngbNav"
[activeId]="activeTab"
[destroyOnHide]="false"
class="nav nav-tabs nav-justified nav-bordered nav-bordered-primary mb-3"
>
<li [ngbNavItem]="'list'">
<a ngbNavLink (click)="showTab('list')">
<ng-icon name="lucideUsers" class="fs-lg me-md-1 d-inline-flex align-middle" />
<span class="d-none d-md-inline-block align-middle">Équipe Marchande</span>
</a>
<ng-template ngbNavContent>
<app-merchant-users-list
#merchantUsersList
[canCreateUsers]="canCreateUsers"
[canDeleteUsers]="canDeleteUsers"
(userSelected)="onUserSelected($event)"
(openCreateUserModal)="openCreateUserModal()"
(resetPasswordRequested)="onResetPasswordRequested($event)"
(deleteUserRequested)="onDeleteUserRequested($event)"
/>
</ng-template>
</li>
<li [ngbNavItem]="'profile'" [hidden]="activeTab !== 'profile'">
<a ngbNavLink (click)="showTab('profile')">
<ng-icon name="lucideUser" class="fs-lg me-md-1 d-inline-flex align-middle" />
<span class="d-none d-md-inline-block align-middle">Profil Utilisateur</span>
</a>
<ng-template ngbNavContent>
@if (selectedUserId) {
<app-merchant-user-profile
[userId]="selectedUserId"
(resetPasswordRequested)="onResetPasswordRequested($event)"
(back)="backToList()"
/>
} @else {
<div class="alert alert-warning text-center">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
Aucun utilisateur sélectionné
</div>
}
</ng-template>
</li>
</ul>
<div class="tab-content" [ngbNavOutlet]="usersNav"></div>
</div>
</div>
</div>
<!-- Modal de création d'utilisateur Merchant -->
<ng-template #createUserModal let-modal>
<div class="modal-header">
<h4 class="modal-title">
<ng-icon name="lucideUserPlus" class="me-2"></ng-icon>
Créer un nouvel utilisateur marchand
</h4>
<button
type="button"
class="btn-close"
(click)="modal.dismiss()"
[disabled]="creatingUser"
aria-label="Fermer"
></button>
</div>
<div class="modal-body">
<!-- Message d'erreur -->
@if (createUserError) {
<div class="alert alert-danger d-flex align-items-center">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
<div>{{ createUserError }}</div>
</div>
}
<!-- Avertissement permissions -->
@if (!canManageRoles && assignableRoles.length === 1) {
<div class="alert alert-warning">
<small>
<ng-icon name="lucideShield" class="me-1"></ng-icon>
<strong>Permissions limitées :</strong> Vous ne pouvez créer que des utilisateurs avec le rôle
<span class="badge" [ngClass]="getRoleBadgeClass(assignableRoles[0])">
{{ getRoleLabel(assignableRoles[0]) }}
</span>
</small>
</div>
}
<form (ngSubmit)="createUser()" #userForm="ngForm">
<div class="row g-3">
<!-- Merchant Partner ID - Uniquement pour DCB_PARTNER et DCB_PARTNER_ADMIN -->
@if (showMerchantPartnerIdField) {
<div class="col-12">
<div class="card border-primary">
<div class="card-header bg-primary text-white py-2">
<div class="d-flex align-items-center">
<ng-icon name="lucideBuilding" class="me-2"></ng-icon>
<span class="small">Votre Organisation Marchande</span>
</div>
</div>
<div class="card-body">
<!-- Information du partenaire -->
<div class="row">
<div class="col-md-6">
<div class="form-control-plaintext">
VOUS
</div>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">Merchant Partner ID</label>
<div class="form-control-plaintext font-monospace small">
@if (currentMerchantPartnerId) {
<span class="text-success">{{ currentMerchantPartnerId }}</span>
<ng-icon name="lucideCheckCircle" class="text-success ms-1"></ng-icon>
} @else {
<span class="text-warning">Chargement...</span>
<ng-icon name="lucideLoader" class="text-warning ms-1"></ng-icon>
}
</div>
</div>
</div>
<!-- Champ caché pour le formulaire -->
<input
type="hidden"
[(ngModel)]="newUser.merchantPartnerId"
name="merchantPartnerId"
[value]="currentMerchantPartnerId"
required
>
<!-- Messages d'information -->
@if (!currentMerchantPartnerId) {
<div class="alert alert-warning mt-2 mb-0">
<div class="d-flex align-items-center">
<ng-icon name="lucideAlertTriangle" class="me-2"></ng-icon>
<div>
<small>
<strong>Merchant Partner ID non disponible</strong><br>
Impossible de récupérer votre identifiant de partenaire.
Veuillez contacter l'administrateur.
</small>
</div>
</div>
</div>
} @else {
<div class="alert alert-info mt-2 mb-0">
<div class="d-flex align-items-center">
<ng-icon name="lucideInfo" class="me-2"></ng-icon>
<div>
<small>
<strong>Information :</strong>
Cet utilisateur sera automatiquement associé à votre organisation marchande.
Vous ne pouvez pas modifier cette association.
</small>
</div>
</div>
</div>
}
</div>
</div>
</div>
}
<!-- Informations de base -->
<div class="col-md-6">
<label class="form-label">
Prénom <span class="text-danger">*</span>
</label>
<input
type="text"
class="form-control"
placeholder="Entrez le prénom"
[(ngModel)]="newUser.firstName"
name="firstName"
required
[disabled]="creatingUser"
#firstName="ngModel"
>
@if (firstName.invalid && firstName.touched) {
<div class="text-danger small">
Le prénom est requis
</div>
}
</div>
<div class="col-md-6">
<label class="form-label">
Nom <span class="text-danger">*</span>
</label>
<input
type="text"
class="form-control"
placeholder="Entrez le nom"
[(ngModel)]="newUser.lastName"
name="lastName"
required
[disabled]="creatingUser"
#lastName="ngModel"
>
@if (lastName.invalid && lastName.touched) {
<div class="text-danger small">
Le nom est requis
</div>
}
</div>
<div class="col-md-6">
<label class="form-label">
Nom d'utilisateur <span class="text-danger">*</span>
</label>
<input
type="text"
class="form-control"
placeholder="Nom d'utilisateur unique"
[(ngModel)]="newUser.username"
name="username"
required
[disabled]="creatingUser"
#username="ngModel"
>
<div class="form-text">Doit être unique dans le système</div>
@if (username.invalid && username.touched) {
<div class="text-danger small">
Le nom d'utilisateur est requis
</div>
}
</div>
<div class="col-md-6">
<label class="form-label">
Email <span class="text-danger">*</span>
</label>
<input
type="email"
class="form-control"
placeholder="email@exemple.com"
[(ngModel)]="newUser.email"
name="email"
required
email
[disabled]="creatingUser"
#email="ngModel"
>
@if (email.invalid && email.touched) {
<div class="text-danger small">
@if (email.errors?.['required']) {
L'email est requis
}
@if (email.errors?.['email']) {
Format d'email invalide
}
</div>
}
</div>
<div class="col-12">
<label class="form-label">
Mot de passe <span class="text-danger">*</span>
</label>
<div class="input-group">
<input
[type]="showPassword ? 'text' : 'password'"
class="form-control"
placeholder="Mot de passe sécurisé"
[(ngModel)]="newUser.password"
name="password"
required
minlength="8"
[disabled]="creatingUser"
#password="ngModel"
>
<button
type="button"
class="btn btn-outline-secondary"
(click)="showPassword = !showPassword"
[disabled]="creatingUser"
>
<ng-icon [name]="showPassword ? 'lucideEyeOff' : 'lucideEye'"></ng-icon>
</button>
</div>
<div class="form-text">
Le mot de passe doit contenir au moins 8 caractères.
</div>
@if (password.invalid && password.touched) {
<div class="text-danger small">
@if (password.errors?.['required']) {
Le mot de passe est requis
}
@if (password.errors?.['minlength']) {
Le mot de passe doit contenir au moins 8 caractères
}
</div>
}
</div>
<!-- Sélection du rôle unique -->
<div class="col-12">
<label class="form-label">
Rôle Principal <span class="text-danger">*</span>
</label>
<select
class="form-select"
[value]="newUser.role"
(change)="onRoleSelectionChange($any($event.target).value)"
name="role"
required
[disabled]="creatingUser || !canManageRoles"
>
<option value="" disabled>Sélectionnez un rôle</option>
@for (role of availableRoles; track role.value) {
<option
[value]="role.value"
[disabled]="!canAssignRole(role.value)"
>
{{ role.label }} - {{ role.description }}
@if (!canAssignRole(role.value)) {
(Non autorisé)
}
</option>
}
</select>
<div class="form-text">
@if (canManageRoles) {
Sélectionnez le rôle principal à assigner à cet utilisateur
} @else {
Vous ne pouvez pas modifier les rôles disponibles
}
</div>
</div>
<!-- Type d'utilisateur automatique -->
<div class="col-12">
<label class="form-label">Type d'utilisateur</label>
<div class="form-control-plaintext">
<span class="badge bg-success">
Utilisateur Marchand
</span>
</div>
<div class="form-text">
Tous les utilisateurs créés ici sont des utilisateurs Marchands
</div>
</div>
<!-- Avertissement pour les non-DCB_PARTNER -->
@if (!canManageRoles) {
<div class="col-12">
<div class="alert alert-warning">
<div class="d-flex align-items-center">
<ng-icon name="lucideInfo" class="me-2"></ng-icon>
<div>
<small>
<strong>Permissions limitées :</strong>
Vous ne pouvez créer que des utilisateurs avec des rôles spécifiques.
Seul un <strong>DCB Partner</strong> peut attribuer tous les rôles.
</small>
</div>
</div>
</div>
</div>
}
<!-- Sélection du partenaire marchand -->
@if (showMerchantPartnerField) {
<div class="col-12">
<label class="form-label">
Partenaire Marchand
@if (requireMerchantPartnerSelection) {
<span class="text-danger">*</span>
}
</label>
<!-- État de chargement -->
@if (loadingMerchantPartners) {
<div class="form-control">
<div class="d-flex align-items-center">
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
<span class="text-muted">Chargement des partenaires marchands...</span>
</div>
</div>
}
<!-- État d'erreur -->
@else if (merchantPartnersError) {
<div class="alert alert-warning">
<div class="d-flex align-items-center justify-content-between">
<div>
<ng-icon name="lucideAlertTriangle" class="me-2"></ng-icon>
{{ merchantPartnersError }}
</div>
<button
class="btn btn-sm btn-outline-warning"
(click)="reloadMerchantPartners()"
>
<ng-icon name="lucideRefreshCw" class="me-1"></ng-icon>
Réessayer
</button>
</div>
</div>
}
<!-- Sélecteur normal -->
@else if (merchantPartners.length > 0) {
<select
class="form-select"
[(ngModel)]="selectedMerchantPartnerId"
(change)="onPartnerSelectionChange($any($event.target).value)"
name="merchantPartnerId"
[required]="requireMerchantPartnerSelection"
[disabled]="creatingUser"
>
<option value="" disabled>
@if (merchantPartners.length > 1) {
Sélectionnez un partenaire marchand
} @else {
Partenaire unique disponible
}
</option>
<!-- Liste des partenaires pour les admins Hub -->
@for (partner of merchantPartners; track partner.id) {
<option [value]="partner.id">
{{ partner.username }}
@if (!partner.enabled) {
<span class="badge bg-warning ms-1">Inactif</span>
}
</option>
}
</select>
<div class="form-text">
{{ merchantSelectionHelpText }}
@if (merchantPartners.length === 1) {
<span class="text-info">
<ng-icon name="lucideInfo" class="me-1"></ng-icon>
Un seul partenaire disponible - sélectionné automatiquement
</span>
}
</div>
}
<!-- Aucun partenaire disponible -->
@else {
<div class="alert alert-danger">
<ng-icon name="lucideXCircle" class="me-2"></ng-icon>
Aucun partenaire marchand disponible.
Veuillez contacter l'administrateur système.
</div>
}
<!-- Message d'erreur de validation -->
@if (requireMerchantPartnerSelection && !selectedMerchantPartnerId && !loadingMerchantPartners) {
<div class="text-danger small mt-1">
<ng-icon name="lucideAlertCircle" class="me-1"></ng-icon>
La sélection d'un partenaire marchand est obligatoire
</div>
}
</div>
}
@if (newUser.role) {
<div class="col-12">
<div class="alert alert-info">
<div class="d-flex align-items-center">
<ng-icon
[name]="getRoleIcon(newUser.role)"
class="me-2"
></ng-icon>
<div>
<strong>Rôle sélectionné :</strong>
<span class="badge ms-2" [ngClass]="getRoleBadgeClass(newUser.role)">
{{ getRoleLabel(newUser.role) }}
</span>
<br>
<small class="text-muted">
{{ getRoleDescription(newUser.role) }}
</small>
</div>
</div>
</div>
</div>
}
<!-- Configuration du compte -->
<div class="col-md-6">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="enabledSwitch"
[(ngModel)]="newUser.enabled"
name="enabled"
[disabled]="creatingUser"
checked
>
<label class="form-check-label" for="enabledSwitch">
Compte activé
</label>
</div>
<div class="form-text">L'utilisateur peut se connecter immédiatement</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="emailVerifiedSwitch"
[(ngModel)]="newUser.emailVerified"
name="emailVerified"
[disabled]="creatingUser"
>
<label class="form-check-label" for="emailVerifiedSwitch">
Email vérifié
</label>
</div>
<div class="form-text">L'utilisateur n'aura pas à vérifier son email</div>
</div>
<!-- Informations système (lecture seule) -->
<div class="col-12">
<div class="alert alert-light">
<small class="text-muted">
<strong>Informations système :</strong><br>
• Merchant Partner ID : {{ currentMerchantPartnerId || 'Chargement...' }}<br>
• Type d'utilisateur : MERCHANT<br>
• Créé par : Utilisateur courant<br>
• Votre rôle : {{ currentUserRole || 'Non défini' }}
</small>
</div>
</div>
</div>
<div class="modal-footer mt-4">
<button
type="button"
class="btn btn-light"
(click)="modal.dismiss()"
[disabled]="creatingUser"
>
<ng-icon name="lucideX" class="me-1"></ng-icon>
Annuler
</button>
<button
type="submit"
class="btn btn-primary"
[disabled]="!userForm.form.valid || creatingUser || !selectedMerchantPartnerId"
>
@if (creatingUser) {
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
Création...
} @else {
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
Créer l'utilisateur Marchand
}
</button>
</div>
</form>
</div>
</ng-template>
<!-- Modal de réinitialisation de mot de passe -->
<ng-template #resetPasswordModal let-modal>
<div class="modal-header">
<h4 class="modal-title">
<ng-icon name="lucideKey" class="me-2"></ng-icon>
Réinitialiser le mot de passe
</h4>
<button
type="button"
class="btn-close"
(click)="modal.dismiss()"
[disabled]="resettingPassword"
aria-label="Fermer"
></button>
</div>
<div class="modal-body">
<!-- Message de succès -->
@if (resetPasswordSuccess) {
<div class="alert alert-success d-flex align-items-center">
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
<div>{{ resetPasswordSuccess }}</div>
</div>
}
<!-- Message d'erreur -->
@if (resetPasswordError) {
<div class="alert alert-danger d-flex align-items-center">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
<div>{{ resetPasswordError }}</div>
</div>
}
@if (!resetPasswordSuccess && selectedUserForReset) {
<div class="alert alert-info">
<div class="d-flex align-items-center">
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3">
<span class="text-primary fw-bold">{{ getUserInitials(selectedUserForReset) }}</span>
</div>
<div>
<strong>{{ selectedUserForReset.username }}</strong>
@if (selectedUserForReset.firstName || selectedUserForReset.lastName) {
<br>
{{ selectedUserForReset.firstName }} {{ selectedUserForReset.lastName }}
}
<br>
<small class="text-muted">
<span class="badge" [ngClass]="getRoleBadgeClass(selectedUserForReset.role)">
{{ getRoleLabel(selectedUserForReset.role) }}
</span>
• Type: Hub
</small>
</div>
</div>
</div>
<form (ngSubmit)="confirmResetPassword()" #resetForm="ngForm">
<div class="mb-3">
<label class="form-label">
Nouveau mot de passe <span class="text-danger">*</span>
</label>
<div class="input-group">
<input
[type]="showNewPassword ? 'text' : 'password'"
class="form-control"
placeholder="Entrez le nouveau mot de passe"
[(ngModel)]="newPassword"
name="newPassword"
required
minlength="8"
[disabled]="resettingPassword"
#newPasswordInput="ngModel"
>
<button
type="button"
class="btn btn-outline-secondary"
(click)="showNewPassword = !showNewPassword"
[disabled]="resettingPassword"
>
<ng-icon [name]="showNewPassword ? 'lucideEyeOff' : 'lucideEye'"></ng-icon>
</button>
</div>
<div class="form-text">
Le mot de passe doit contenir au moins 8 caractères.
</div>
@if (newPasswordInput.invalid && newPasswordInput.touched) {
<div class="text-danger small">
@if (newPasswordInput.errors?.['required']) {
Le mot de passe est requis
}
@if (newPasswordInput.errors?.['minlength']) {
Le mot de passe doit contenir au moins 8 caractères
}
</div>
}
</div>
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="temporaryPassword"
[(ngModel)]="temporaryPassword"
name="temporaryPassword"
[disabled]="resettingPassword"
checked
>
<label class="form-check-label" for="temporaryPassword">
Mot de passe temporaire
</label>
</div>
<div class="form-text">
L'utilisateur devra changer son mot de passe à la prochaine connexion.
</div>
</div>
</form>
} @else if (!selectedUserForReset) {
<div class="alert alert-warning text-center">
<ng-icon name="lucideAlertTriangle" class="me-2"></ng-icon>
Aucun utilisateur sélectionné pour la réinitialisation
</div>
}
</div>
<div class="modal-footer">
@if (resetPasswordSuccess) {
<button
type="button"
class="btn btn-success"
(click)="modal.close()"
>
<ng-icon name="lucideCheck" class="me-1"></ng-icon>
Fermer
</button>
} @else {
<button
type="button"
class="btn btn-light"
(click)="modal.dismiss()"
[disabled]="resettingPassword"
>
Annuler
</button>
<button
type="button"
class="btn btn-primary"
(click)="confirmResetPassword()"
[disabled]="!newPassword || newPassword.length < 8 || resettingPassword || !selectedUserForReset"
>
@if (resettingPassword) {
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
Réinitialisation...
} @else {
<ng-icon name="lucideKey" class="me-1"></ng-icon>
Réinitialiser le mot de passe
}
</button>
}
</div>
</ng-template>
<!-- Modal de confirmation de suppression -->
<ng-template #deleteUserModal let-modal>
<div class="modal-header">
<h4 class="modal-title text-danger">
<ng-icon name="lucideTrash2" class="me-2"></ng-icon>
Confirmer la suppression
</h4>
<button
type="button"
class="btn-close"
(click)="modal.dismiss()"
aria-label="Fermer"
></button>
</div>
<div class="modal-body text-center">
<div class="mb-4">
<div class="avatar-lg mx-auto mb-3 bg-danger bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<ng-icon name="lucideUserX" class="text-danger" style="font-size: 2rem;"></ng-icon>
</div>
<h5 class="text-danger mb-2">Êtes-vous sûr de vouloir supprimer cet utilisateur Hub ?</h5>
<p class="text-muted mb-0">
Cette action est irréversible. Toutes les données de
@if (selectedUserForDelete) {
<strong>{{ selectedUserForDelete.username }}</strong>
}
seront définitivement perdues.
</p>
</div>
@if (selectedUserForDelete) {
<div class="alert alert-warning">
<div class="d-flex align-items-start">
<ng-icon name="lucideAlertTriangle" class="me-2 mt-1 text-warning"></ng-icon>
<div>
<strong>Utilisateur :</strong> {{ selectedUserForDelete.username }}
@if (selectedUserForDelete.firstName || selectedUserForDelete.lastName) {
<br>
<strong>Nom :</strong> {{ selectedUserForDelete.firstName }} {{ selectedUserForDelete.lastName }}
}
<br>
<strong>Email :</strong> {{ selectedUserForDelete.email }}
<br>
<strong>Rôle Principal :</strong>
<span class="badge" [ngClass]="getRoleBadgeClass(selectedUserForDelete.role)">
{{ getRoleLabel(selectedUserForDelete.role) }}
</span>
@if (selectedUserForDelete.role && selectedUserForDelete.role.length > 1) {
<br>
<strong>Rôles supplémentaires :</strong>
{{ selectedUserForDelete.role.length - 1 }}
}
<br>
<strong>Type :</strong> Utilisateur Hub
</div>
</div>
</div>
} @else {
<div class="alert alert-warning">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
Aucun utilisateur sélectionné pour la suppression
</div>
}
<!-- Message d'erreur -->
@if (deleteUserError) {
<div class="alert alert-danger d-flex align-items-center">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
<div>{{ deleteUserError }}</div>
</div>
}
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-light"
(click)="modal.dismiss()"
[disabled]="deletingUser"
>
<ng-icon name="lucideX" class="me-1"></ng-icon>
Annuler
</button>
<button
type="button"
class="btn btn-danger"
(click)="confirmDeleteUser()"
[disabled]="deletingUser || !selectedUserForDelete || !canDeleteUsers"
>
@if (deletingUser) {
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Suppression...</span>
</div>
Suppression...
} @else {
<ng-icon name="lucideTrash2" class="me-1"></ng-icon>
Supprimer définitivement
}
</button>
</div>
</ng-template>

View File

@ -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<string | null> {
return this.http.get<MerchantPartnerIdResponse>(`${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<User> {
// 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<User>(`${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<User[]> {
return this.http.get<User[]>(`${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<User[]> {
return this.getMyMerchantUsers().pipe(
map(users => users.filter(user => user.merchantPartnerId === partnerId))
);
}
getMerchantUserById(id: string | undefined): Observable<User> {
return this.http.get<User>(`${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<PaginatedUserResponse> {
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<User> {
const payload: any = {
firstName: updateUserDto.firstName,
lastName: updateUserDto.lastName,
email: updateUserDto.email,
enabled: updateUserDto.enabled
};
return this.http.put<User>(`${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<MessageResponse> {
return this.http.delete<MessageResponse>(`${this.baseApiUrl}/${id}`).pipe(
catchError(error => {
console.error(`Error deleting merchant user ${id}:`, error);
return throwError(() => error);
})
);
}
resetMerchantUserPassword(id: string, resetPasswordDto: ResetPasswordDto): Observable<MessageResponse> {
const payload = {
newPassword: resetPasswordDto.newPassword,
temporary: resetPasswordDto.temporary !== undefined ? resetPasswordDto.temporary : true
};
return this.http.post<MessageResponse>(
`${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<User> {
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<User>(`${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<User> {
return this.updateMerchantUser(id, { enabled: true });
}
disableMerchantUser(id: string): Observable<User> {
return this.updateMerchantUser(id, { enabled: false });
}
getAvailableMerchantRoles(): Observable<AvailableRolesResponse> {
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<User[]> {
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<User[]> {
return this.searchMerchantUsers({ role });
}
getActiveMerchantUsers(): Observable<User[]> {
return this.searchMerchantUsers({ enabled: true });
}
getInactiveMerchantUsers(): Observable<User[]> {
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)
};
}
}

View File

@ -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<void>();
// 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<any>;
@ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef<any>;
@ViewChild('deleteUserModal') deleteUserModal!: TemplateRef<any>;
// 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<any>, 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 };
}
}

View File

@ -1,557 +0,0 @@
<div class="container-fluid">
<app-page-title
title="Gestion des Utilisateurs"
subTitle="Administrez les utilisateurs Keycloak de votre plateforme"
[badge]="{icon:'lucideUsers', text:'Keycloak Users'}"
/>
<!-- Indicateur de permissions -->
@if (currentUserRole) {
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-info py-2">
<div class="d-flex align-items-center">
<ng-icon name="lucideInfo" class="me-2"></ng-icon>
<div class="flex-grow-1">
<small>
<strong>Rôle actuel :</strong>
<span class="badge" [ngClass]="getRoleBadgeClass(currentUserRole)">
{{ roleService.getRoleLabel(currentUserRole) }}
</span>
@if (!canCreateUsers) {
<span class="text-warning ms-2">
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Permissions limitées
</span>
}
</small>
</div>
@if (canCreateUsers) {
<button
class="btn btn-primary btn-sm"
(click)="openCreateUserModal()"
>
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
Nouvel Utilisateur
</button>
}
</div>
</div>
</div>
</div>
}
<!-- Navigation par onglets avec style bordered -->
<div class="row mb-4">
<div class="col-12">
<ul
ngbNav
#usersNav="ngbNav"
[activeId]="activeTab"
[destroyOnHide]="false"
class="nav nav-tabs nav-justified nav-bordered nav-bordered-primary mb-3"
>
<li [ngbNavItem]="'list'">
<a ngbNavLink (click)="showTab('list')">
<ng-icon name="lucideUsers" class="fs-lg me-md-1 d-inline-flex align-middle" />
<span class="d-none d-md-inline-block align-middle">Liste des Utilisateurs</span>
</a>
<ng-template ngbNavContent>
<app-hub-users-list
[canCreateUsers]="canCreateUsers"
[canDeleteUsers]="canDeleteUsers"
(userSelected)="showTab('profile', $event)"
(openCreateModal)="openCreateUserModal()"
(openResetPasswordModal)="openResetPasswordModal($event)"
(openDeleteUserModal)="openDeleteUserModal($event)"
/>
</ng-template>
</li>
<li [ngbNavItem]="'profile'" [hidden]="activeTab !== 'profile'">
<a ngbNavLink (click)="showTab('profile')">
<ng-icon name="lucideUser" class="fs-lg me-md-1 d-inline-flex align-middle" />
<span class="d-none d-md-inline-block align-middle">Profil Utilisateur</span>
</a>
<ng-template ngbNavContent>
@if (selectedUserId) {
<app-hub-user-profile
[userId]="selectedUserId"
(back)="showTab('list')"
/>
}
</ng-template>
</li>
</ul>
<div class="tab-content" [ngbNavOutlet]="usersNav"></div>
</div>
</div>
</div>
<!-- Modal de création d'utilisateur Keycloak -->
<ng-template #createUserModal let-modal>
<div class="modal-header">
<h4 class="modal-title">
<ng-icon name="lucideUserPlus" class="me-2"></ng-icon>
Créer un nouvel utilisateur Keycloak
</h4>
<button
type="button"
class="btn-close"
(click)="modal.dismiss()"
[disabled]="creatingUser"
></button>
</div>
<div class="modal-body">
<!-- Message d'erreur -->
@if (createUserError) {
<div class="alert alert-danger d-flex align-items-center">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
<div>{{ createUserError }}</div>
</div>
}
<!-- Avertissement permissions -->
@if (!canManageRoles && assignableRoles.length === 1) {
<div class="alert alert-warning">
<small>
<ng-icon name="lucideShield" class="me-1"></ng-icon>
<strong>Permissions limitées :</strong> Vous ne pouvez créer que des utilisateurs avec le rôle
<span class="badge" [ngClass]="getRoleBadgeClass(assignableRoles[0])">
{{ roleService.getRoleLabel(assignableRoles[0]) }}
</span>
</small>
</div>
}
<form (ngSubmit)="createUser()" #userForm="ngForm">
<div class="row g-3">
<!-- Informations de base -->
<div class="col-md-6">
<label class="form-label">
Prénom <span class="text-danger">*</span>
</label>
<input
type="text"
class="form-control"
placeholder="Entrez le prénom"
[(ngModel)]="newUser.firstName"
name="firstName"
required
[disabled]="creatingUser"
>
</div>
<div class="col-md-6">
<label class="form-label">
Nom <span class="text-danger">*</span>
</label>
<input
type="text"
class="form-control"
placeholder="Entrez le nom"
[(ngModel)]="newUser.lastName"
name="lastName"
required
[disabled]="creatingUser"
>
</div>
<div class="col-md-6">
<label class="form-label">
Nom d'utilisateur <span class="text-danger">*</span>
</label>
<input
type="text"
class="form-control"
placeholder="Nom d'utilisateur unique"
[(ngModel)]="newUser.username"
name="username"
required
[disabled]="creatingUser"
>
<div class="form-text">Doit être unique dans Keycloak</div>
</div>
<div class="col-md-6">
<label class="form-label">
Email <span class="text-danger">*</span>
</label>
<input
type="email"
class="form-control"
placeholder="email@exemple.com"
[(ngModel)]="newUser.email"
name="email"
required
[disabled]="creatingUser"
>
</div>
<div class="col-12">
<label class="form-label">
Mot de passe <span class="text-danger">*</span>
</label>
<input
type="password"
class="form-control"
placeholder="Mot de passe sécurisé"
[(ngModel)]="newUser.password"
name="password"
required
minlength="8"
[disabled]="creatingUser"
>
<div class="form-text">
Le mot de passe doit contenir au moins 8 caractères.
</div>
</div>
<!-- Sélection du rôle -->
<div class="col-12">
<label class="form-label">
Rôle <span class="text-danger">*</span>
</label>
<select
class="form-select"
[(ngModel)]="newUser.role"
name="role"
required
[disabled]="creatingUser || !canManageRoles"
>
<option value="" disabled>Sélectionnez un rôle</option>
@for (role of availableRoles; track role.value) {
<option
[value]="role.value"
[disabled]="!canAssignRole(role.value)"
>
{{ role.label }} - {{ role.description }}
@if (!canAssignRole(role.value)) {
(Non autorisé)
}
</option>
}
</select>
<div class="form-text">
@if (canManageRoles) {
Sélectionnez le rôle à assigner à cet utilisateur
} @else {
Vous ne pouvez pas modifier les rôles disponibles
}
</div>
</div>
<!-- Aperçu du rôle sélectionné -->
<!-- Sélection du partenaire marchand (uniquement pour les rôles marchands) -->
@if (newUser.role && isMerchantRole(newUser.role)) {
<div class="col-12">
<label class="form-label">
Partenaire Marchand <span class="text-danger">*</span>
</label>
<select
class="form-select"
[(ngModel)]="selectedMerchantPartnerId"
name="merchantPartnerId"
required
[disabled]="creatingUser"
>
<option value="" disabled>Sélectionnez un partenaire marchand</option>
@for (partner of merchantPartners; track partner.id) {
<option [value]="partner.id">
{{ partner.name }}
</option>
}
</select>
<div class="form-text">
Sélectionnez le partenaire marchand auquel cet utilisateur sera associé
</div>
</div>
}
<!-- Configuration du compte -->
<div class="col-md-6">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="enabledSwitch"
[(ngModel)]="newUser.enabled"
name="enabled"
[disabled]="creatingUser"
>
<label class="form-check-label" for="enabledSwitch">
Compte activé
</label>
</div>
<div class="form-text">L'utilisateur peut se connecter immédiatement</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="emailVerifiedSwitch"
[(ngModel)]="newUser.emailVerified"
name="emailVerified"
[disabled]="creatingUser"
>
<label class="form-check-label" for="emailVerifiedSwitch">
Email vérifié
</label>
</div>
<div class="form-text">L'utilisateur n'aura pas à vérifier son email</div>
</div>
</div>
<div class="modal-footer mt-4">
<button
type="button"
class="btn btn-light"
(click)="modal.dismiss()"
[disabled]="creatingUser"
>
Annuler
</button>
<button
type="submit"
class="btn btn-primary"
[disabled]="!userForm.form.valid || creatingUser || !canAssignRole(newUser.role) || (isMerchantRole(newUser.role) && !selectedMerchantPartnerId)"
>
@if (creatingUser) {
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
Création...
} @else {
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
Créer l'utilisateur
}
</button>
</div>
</form>
</div>
</ng-template>
<!-- Modal de réinitialisation de mot de passe -->
<ng-template #resetPasswordModal let-modal>
<div class="modal-header">
<h4 class="modal-title">
<ng-icon name="lucideKey" class="me-2"></ng-icon>
Réinitialiser le mot de passe
</h4>
<button
type="button"
class="btn-close"
(click)="modal.dismiss()"
[disabled]="resettingPassword"
></button>
</div>
<div class="modal-body">
<!-- Message de succès -->
@if (resetPasswordSuccess) {
<div class="alert alert-success d-flex align-items-center">
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
<div>{{ resetPasswordSuccess }}</div>
</div>
}
<!-- Message d'erreur -->
@if (resetPasswordError) {
<div class="alert alert-danger d-flex align-items-center">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
<div>{{ resetPasswordError }}</div>
</div>
}
@if (!resetPasswordSuccess && selectedUserForReset) {
<div class="alert alert-info">
<div class="d-flex align-items-center">
<ng-icon
[name]="roleService.getRoleIcon(selectedUserForReset.role)"
class="me-2"
></ng-icon>
<div>
<strong>Utilisateur :</strong> {{ selectedUserForReset.username }}
@if (selectedUserForReset.firstName || selectedUserForReset.lastName) {
<br>
<strong>Nom :</strong> {{ selectedUserForReset.firstName }} {{ selectedUserForReset.lastName }}
}
<br>
<strong>Rôle :</strong>
<span class="badge ms-1" [ngClass]="getRoleBadgeClass(selectedUserForReset.role)">
{{ roleService.getRoleLabel(selectedUserForReset.role) }}
</span>
</div>
</div>
</div>
<form (ngSubmit)="confirmResetPassword()" #resetForm="ngForm">
<div class="mb-3">
<label class="form-label">
Nouveau mot de passe <span class="text-danger">*</span>
</label>
<input
type="password"
class="form-control"
placeholder="Entrez le nouveau mot de passe"
[(ngModel)]="newPassword"
name="newPassword"
required
minlength="8"
[disabled]="resettingPassword"
>
<div class="form-text">
Le mot de passe doit contenir au moins 8 caractères.
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="temporaryPassword"
[(ngModel)]="temporaryPassword"
name="temporaryPassword"
[disabled]="resettingPassword"
>
<label class="form-check-label" for="temporaryPassword">
Mot de passe temporaire
</label>
</div>
<div class="form-text">
L'utilisateur devra changer son mot de passe à la prochaine connexion.
</div>
</div>
</form>
}
</div>
<div class="modal-footer">
@if (resetPasswordSuccess) {
<button
type="button"
class="btn btn-success"
(click)="modal.close()"
>
<ng-icon name="lucideCheck" class="me-1"></ng-icon>
Fermer
</button>
} @else {
<button
type="button"
class="btn btn-light"
(click)="modal.dismiss()"
[disabled]="resettingPassword"
>
Annuler
</button>
<button
type="button"
class="btn btn-primary"
(click)="confirmResetPassword()"
[disabled]="!newPassword || newPassword.length < 8 || resettingPassword"
>
@if (resettingPassword) {
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
Réinitialisation...
} @else {
<ng-icon name="lucideKey" class="me-1"></ng-icon>
Réinitialiser le mot de passe
}
</button>
}
</div>
</ng-template>
<!-- Modal de confirmation de suppression -->
<ng-template #deleteUserModal let-modal>
<div class="modal-header">
<h4 class="modal-title text-danger">
<ng-icon name="lucideTrash2" class="me-2"></ng-icon>
Confirmer la suppression
</h4>
<button
type="button"
class="btn-close"
(click)="modal.dismiss()"
></button>
</div>
<div class="modal-body text-center">
<div class="mb-4">
<div class="avatar-lg mx-auto mb-3 bg-danger bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<ng-icon name="lucideUserX" class="text-danger" style="font-size: 2rem;"></ng-icon>
</div>
<h5 class="text-danger mb-2">Êtes-vous sûr de vouloir supprimer cet utilisateur ?</h5>
<p class="text-muted mb-0">
Cette action est irréversible. Toutes les données de
<strong>{{ selectedUserForDelete?.username }}</strong> seront définitivement perdues.
</p>
</div>
@if (selectedUserForDelete) {
<div class="alert alert-warning">
<div class="d-flex align-items-start">
<ng-icon name="lucideAlertTriangle" class="me-2 mt-1"></ng-icon>
<div>
<strong>Utilisateur :</strong> {{ selectedUserForDelete.username }}
@if (selectedUserForDelete.firstName || selectedUserForDelete.lastName) {
<br>
<strong>Nom :</strong> {{ selectedUserForDelete.firstName }} {{ selectedUserForDelete.lastName }}
}
<br>
<strong>Email :</strong> {{ selectedUserForDelete.email }}
<br>
<strong>Rôle :</strong>
<span class="badge ms-1" [ngClass]="getRoleBadgeClass(selectedUserForDelete.role)">
{{ roleService.getRoleLabel(selectedUserForDelete.role) }}
</span>
</div>
</div>
</div>
}
<!-- Message d'erreur -->
@if (deleteUserError) {
<div class="alert alert-danger d-flex align-items-center">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
<div>{{ deleteUserError }}</div>
</div>
}
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-light"
(click)="modal.dismiss()"
[disabled]="deletingUser"
>
<ng-icon name="lucideX" class="me-1"></ng-icon>
Annuler
</button>
<button
type="button"
class="btn btn-danger"
(click)="confirmDeleteUser()"
[disabled]="deletingUser || !canDeleteUsers"
>
@if (deletingUser) {
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Suppression...</span>
</div>
Suppression...
} @else {
<ng-icon name="lucideTrash2" class="me-1"></ng-icon>
Supprimer définitivement
}
</button>
</div>
</ng-template>

View File

@ -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
}
}
];

View File

@ -1,2 +0,0 @@
import { HubUsers } from './hub-users';
describe('Users', () => {});

View File

@ -1,2 +0,0 @@
import { UsersList } from './list';
describe('UsersList', () => {});

View File

@ -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<void>();
readonly UserRole = UserRole;
readonly UserType = UserType;
@Input() canCreateUsers: boolean = false;
@Input() canDeleteUsers: boolean = false;
@Output() userSelected = new EventEmitter<string>();
@Output() openCreateModal = new EventEmitter<void>();
@Output() openResetPasswordModal = new EventEmitter<string>();
@Output() openDeleteUserModal = new EventEmitter<string>();
// 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();
}
});
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<string, any> = {};
clientRoles: string[] = [];
createdTimestamp?: number;
constructor(partial?: Partial<User>) {
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<string, any> = {};
@IsOptional()
@IsArray()
clientRoles: string[] = [];
constructor(partial?: Partial<CreateUserDto>) {
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<string, any>;
@IsOptional()
@IsArray()
clientRoles?: string[];
constructor(partial?: Partial<UpdateUserDto>) {
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<UserQueryDto>) {
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<ResetPasswordDto>) {
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<string, any> = {};
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<AssignRolesDto>) {
if (partial) {
Object.assign(this, partial);
}
}
}
export class LoginDto {
@IsString()
username: string = '';
@IsString()
password: string = '';
constructor(partial?: Partial<LoginDto>) {
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<TokenResponse>) {
if (partial) {
Object.assign(this, partial);
}
}
}
export class ApiResponse<T> {
data: T | null = null;
message: string = '';
status: string = '';
constructor(partial?: Partial<ApiResponse<T>>) {
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<UserRoleMapping>) {
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<string, string> = {};
constructor(partial?: Partial<UserSession>) {
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';

View File

@ -1,2 +0,0 @@
import { UserProfile } from './profile';
describe('UserProfile', () => {});

View File

@ -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<void>();
@Input() userId!: string;
@Output() back = new EventEmitter<void>();
@Output() openResetPasswordModal = new EventEmitter<string>();
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);
}
}

View File

@ -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<T> {
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<T>(endpoint: string, params?: any): Observable<T> {
return this.http.get<T>(`${this.baseUrl}/${endpoint}`, {
headers: this.getHeaders(),
params: this.createParams(params)
}).pipe(
catchError(this.handleError)
);
}
post<T>(endpoint: string, data: any): Observable<T> {
return this.http.post<T>(`${this.baseUrl}/${endpoint}`, data, {
headers: this.getHeaders()
}).pipe(
catchError(this.handleError)
);
}
put<T>(endpoint: string, data: any): Observable<T> {
return this.http.put<T>(`${this.baseUrl}/${endpoint}`, data, {
headers: this.getHeaders()
}).pipe(
catchError(this.handleError)
);
}
delete<T>(endpoint: string): Observable<T> {
return this.http.delete<T>(`${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);
}
}

View File

@ -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<HubUserDto> {
// 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<HubUserDto>(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<PaginatedUserResponse> {
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<HubUserDto[]>(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<PaginatedUserResponse> {
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<MerchantUserDto[]>(`${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<HubUserDto> {
return this.http.get<HubUserDto>(`${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<HubUserDto> {
return this.http.put<HubUserDto>(`${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<HubUserDto> {
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<HubUserDto>(`${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<HubUserDto> {
return this.updateHubUser(id, { enabled: true });
}
/**
* Désactive un utilisateur Hub
*/
disableHubUser(id: string): Observable<HubUserDto> {
return this.updateHubUser(id, { enabled: false });
}
// === GESTION DES RÔLES ===
/**
* Récupère les rôles Hub disponibles
*/
getAvailableHubRoles(): Observable<AvailableRolesResponse> {
return this.http.get<AvailableRolesResponse>(`${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<HubUserDto[]> {
return this.http.get<HubUserDto[]>(`${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<HubUserDto[]> {
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<HubUserDto[]>(`${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<any> {
return this.http.get<any>(`${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 });
})
);
}
}

View File

@ -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

View File

@ -1,436 +0,0 @@
<app-ui-card title="Configuration Partenaire DCB">
<span helper-text class="badge badge-soft-success badge-label fs-xxs py-1">
Payment Hub DCB
</span>
<div class="ins-wizard" card-body>
<!-- Progress Bar -->
<ngb-progressbar
class="mb-4"
[value]="progressValue"
type="primary"
height="6px"
/>
<!-- Navigation Steps -->
<ul class="nav nav-tabs wizard-tabs" role="tablist">
@for (step of wizardSteps; track $index; let i = $index) {
<li class="nav-item">
<a
href="javascript:void(0);"
[class.active]="i === currentStep"
class="nav-link"
[class.disabled]="!isStepAccessible(i)"
[class.wizard-item-done]="i < currentStep"
(click)="goToStep(i)"
>
<span class="d-flex align-items-center">
<ng-icon [name]="step.icon" class="fs-32" />
<span class="flex-grow-1 ms-2 text-truncate">
<span class="mb-0 lh-base d-block fw-semibold text-body fs-base">
{{ step.title }}
</span>
<span class="mb-0 fw-normal">{{ step.subtitle }}</span>
</span>
</span>
</a>
</li>
}
</ul>
<!-- Messages -->
@if (configError) {
<div class="alert alert-danger mt-3">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
{{ configError }}
</div>
}
@if (configSuccess) {
<div class="alert alert-success mt-3">
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
{{ configSuccess }}
</div>
}
<!-- Contenu des Steps -->
<div class="tab-content pt-3">
@for (step of wizardSteps; track $index; let i = $index) {
<div
class="tab-pane fade"
[class.show]="currentStep === i"
[class.active]="currentStep === i"
>
<form [formGroup]="partnerForm">
<!-- Step 1: Informations Société -->
@if (i === 0) {
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Nom commercial *</label>
<div formGroupName="companyInfo">
<input type="text" class="form-control" formControlName="name"
placeholder="Nom commercial" />
@if (companyInfo.get('name')?.invalid && companyInfo.get('name')?.touched) {
<div class="text-danger small">Le nom commercial est requis</div>
}
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Raison sociale *</label>
<div formGroupName="companyInfo">
<input type="text" class="form-control" formControlName="legalName"
placeholder="Raison sociale" />
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Email *</label>
<div formGroupName="companyInfo">
<input type="email" class="form-control" formControlName="email"
placeholder="email@entreprise.com" />
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Téléphone *</label>
<div formGroupName="companyInfo">
<input type="tel" class="form-control" formControlName="phone"
placeholder="+225 XX XX XX XX" />
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Site web</label>
<div formGroupName="companyInfo">
<input type="url" class="form-control" formControlName="website"
placeholder="https://..." />
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Catégorie *</label>
<div formGroupName="companyInfo">
<select class="form-select" formControlName="category">
@for (cat of categories; track cat.value) {
<option [value]="cat.value">{{ cat.label }}</option>
}
</select>
</div>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Pays *</label>
<div formGroupName="companyInfo">
<select class="form-select" formControlName="country">
@for (country of countries; track country.code) {
<option [value]="country.code">{{ country.name }}</option>
}
</select>
</div>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Devise *</label>
<div formGroupName="companyInfo">
<select class="form-select" formControlName="currency">
@for (currency of currencies; track currency.code) {
<option [value]="currency.code">{{ currency.name }}</option>
}
</select>
</div>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Fuseau horaire *</label>
<div formGroupName="companyInfo">
<select class="form-select" formControlName="timezone">
@for (tz of timezones; track tz.value) {
<option [value]="tz.value">{{ tz.label }}</option>
}
</select>
</div>
</div>
</div>
}
<!-- Step 2: Adresse et Contact -->
@if (i === 1) {
<div class="row">
<div class="col-12 mb-4">
<h6 class="border-bottom pb-2">Adresse de l'entreprise</h6>
</div>
<div class="col-12 mb-3">
<label class="form-label">Rue *</label>
<div formGroupName="addressInfo">
<input type="text" class="form-control" formControlName="street"
placeholder="Adresse complète" />
</div>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Ville *</label>
<div formGroupName="addressInfo">
<input type="text" class="form-control" formControlName="city"
placeholder="Ville" />
</div>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Région *</label>
<div formGroupName="addressInfo">
<input type="text" class="form-control" formControlName="state"
placeholder="Région" />
</div>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Code postal *</label>
<div formGroupName="addressInfo">
<input type="text" class="form-control" formControlName="postalCode"
placeholder="Code postal" />
</div>
</div>
<div class="col-12 mb-4 mt-4">
<h6 class="border-bottom pb-2">Contact technique</h6>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Nom complet *</label>
<div formGroupName="technicalContact">
<input type="text" class="form-control" formControlName="name"
placeholder="Nom du contact technique" />
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Email *</label>
<div formGroupName="technicalContact">
<input type="email" class="form-control" formControlName="email"
placeholder="contact@entreprise.com" />
</div>
</div>
<div class="col-12 mb-3">
<label class="form-label">Téléphone *</label>
<div formGroupName="technicalContact">
<input type="tel" class="form-control" formControlName="phone"
placeholder="+225 XX XX XX XX" />
</div>
</div>
</div>
}
<!-- Step 3: Configuration Paiements -->
@if (i === 2) {
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Taux de commission (%) *</label>
<div formGroupName="paymentConfig">
<input type="number" class="form-control" formControlName="commissionRate"
min="0" max="100" step="0.1" />
<div class="form-text">Pourcentage prélevé sur chaque transaction</div>
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Limite quotidienne (XOF) *</label>
<div formGroupName="paymentConfig">
<input type="number" class="form-control" formControlName="dailyLimit"
min="1000" />
<div class="form-text">Plafond total des transactions par jour</div>
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Limite par transaction (XOF) *</label>
<div formGroupName="paymentConfig">
<input type="number" class="form-control" formControlName="transactionLimit"
min="100" max="500000" />
<div class="form-text">Montant maximum par transaction</div>
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Montant minimum (XOF) *</label>
<div formGroupName="paymentConfig">
<input type="number" class="form-control" formControlName="minAmount"
min="1" />
</div>
</div>
<div class="col-12 mb-3">
<label class="form-label">Montant maximum (XOF) *</label>
<div formGroupName="paymentConfig">
<input type="number" class="form-control" formControlName="maxAmount"
min="100" />
</div>
</div>
</div>
}
<!-- Step 4: Webhooks -->
@if (i === 3) {
<div class="row">
<div class="col-12 mb-4">
<h6 class="border-bottom pb-2">Header Enrichment</h6>
<div formGroupName="webhookConfig">
<div formGroupName="headerEnrichment">
<div class="row mb-3">
<div class="col-md-8">
<label class="form-label">URL de callback</label>
<input type="url" class="form-control" formControlName="url"
placeholder="https://votre-domaine.com/api/header-enrichment" />
</div>
<div class="col-md-4">
<label class="form-label">Méthode HTTP</label>
<select class="form-select" formControlName="method">
@for (method of httpMethods; track method.value) {
<option [value]="method.value">{{ method.label }}</option>
}
</select>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label">Headers HTTP</label>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="addHeader()">
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
Ajouter un header
</button>
</div>
@for (header of headerEnrichmentHeaders.controls; track $index; let idx = $index) {
<div class="row mb-2">
<div class="col-md-5">
<!-- CORRECTION ICI : Utilisation de getHeaderControl -->
<input type="text" class="form-control"
[formControl]="getHeaderControl(header, 'key')"
placeholder="Clé (ex: Authorization)" />
</div>
<div class="col-md-5">
<!-- CORRECTION ICI : Utilisation de getHeaderControl -->
<input type="text" class="form-control"
[formControl]="getHeaderControl(header, 'value')"
placeholder="Valeur" />
</div>
<div class="col-md-2">
<button type="button" class="btn btn-sm btn-outline-danger w-100"
(click)="removeHeader(idx)">
<ng-icon name="lucideTrash2"></ng-icon>
</button>
</div>
</div>
}
</div>
</div>
</div>
</div>
<div class="col-12 mb-4">
<h6 class="border-bottom pb-2">Webhooks Abonnements</h6>
<div formGroupName="webhookConfig">
<div formGroupName="subscription">
<div class="row">
<div class="col-md-6 mb-2">
<label class="form-label">Création d'abonnement</label>
<input type="url" class="form-control" formControlName="onCreate"
placeholder="https://votre-domaine.com/webhooks/subscription-created" />
</div>
<div class="col-md-6 mb-2">
<label class="form-label">Renouvellement</label>
<input type="url" class="form-control" formControlName="onRenew"
placeholder="https://votre-domaine.com/webhooks/subscription-renewed" />
</div>
<div class="col-md-6 mb-2">
<label class="form-label">Annulation</label>
<input type="url" class="form-control" formControlName="onCancel"
placeholder="https://votre-domaine.com/webhooks/subscription-cancelled" />
</div>
<div class="col-md-6 mb-2">
<label class="form-label">Expiration</label>
<input type="url" class="form-control" formControlName="onExpire"
placeholder="https://votre-domaine.com/webhooks/subscription-expired" />
</div>
</div>
</div>
</div>
</div>
<div class="col-12 mb-4">
<h6 class="border-bottom pb-2">Webhooks Paiements</h6>
<div formGroupName="webhookConfig">
<div formGroupName="payment">
<div class="row">
<div class="col-md-4 mb-2">
<label class="form-label">Paiement réussi</label>
<input type="url" class="form-control" formControlName="onSuccess"
placeholder="https://votre-domaine.com/webhooks/payment-success" />
</div>
<div class="col-md-4 mb-2">
<label class="form-label">Paiement échoué</label>
<input type="url" class="form-control" formControlName="onFailure"
placeholder="https://votre-domaine.com/webhooks/payment-failed" />
</div>
<div class="col-md-4 mb-2">
<label class="form-label">Remboursement</label>
<input type="url" class="form-control" formControlName="onRefund"
placeholder="https://votre-domaine.com/webhooks/payment-refunded" />
</div>
</div>
</div>
</div>
</div>
</div>
}
<!-- Step 5: Validation -->
@if (i === 4) {
<div class="row">
<div class="col-12">
<div class="alert alert-info">
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
Vérifiez les informations avant de créer le partenaire
</div>
<div class="card">
<div class="card-body">
<h6 class="card-title">Récapitulatif</h6>
<div class="row">
<div class="col-md-6">
<strong>Informations Société:</strong><br>
{{ companyInfo.value.name || 'Non renseigné' }}<br>
{{ companyInfo.value.legalName || 'Non renseigné' }}<br>
{{ companyInfo.value.email || 'Non renseigné' }}<br>
{{ companyInfo.value.phone || 'Non renseigné' }}
</div>
<div class="col-md-6">
<strong>Configuration:</strong><br>
Commission: {{ paymentConfig.value.commissionRate || 0 }}%<br>
Limite quotidienne: {{ (paymentConfig.value.dailyLimit || 0) | number }} XOF<br>
Limite transaction: {{ (paymentConfig.value.transactionLimit || 0) | number }} XOF
</div>
</div>
</div>
</div>
</div>
</div>
}
</form>
<!-- Navigation Buttons -->
<div class="d-flex justify-content-between mt-4">
@if (i > 0) {
<button type="button" class="btn btn-secondary" (click)="previousStep()">
← Précédent
</button>
} @else {
<div></div>
}
@if (i < wizardSteps.length - 1) {
<button type="button" class="btn btn-primary" (click)="nextStep()"
[disabled]="!isStepValid(i)">
Suivant →
</button>
} @else {
<button type="button" class="btn btn-success"
(click)="submitForm()" [disabled]="configLoading">
@if (configLoading) {
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
}
Créer le Partenaire
</button>
}
</div>
</div>
}
</div>
</div>
</app-ui-card>

View File

@ -1,2 +0,0 @@
import { PartnerConfig } from './config';
describe('PartnerConfig', () => {});

View File

@ -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;
}
}

View File

@ -1,2 +0,0 @@
import { PartnerTeamList } from './list';
describe('PartnerTeamList', () => {});

View File

@ -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<void>();
readonly UserRole = UserRole;
readonly UserType = UserType;
@Output() userSelected = new EventEmitter<string>();
@Output() openCreateModal = new EventEmitter<void>();
@Output() openResetPasswordModal = new EventEmitter<string>();
@Output() openDeleteUserModal = new EventEmitter<string>();
// 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();
}
}

View File

@ -1,2 +0,0 @@
import { MerchantUsers } from './merchant-users';
describe('Merchant Users', () => {});

View File

@ -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<void>();
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<any>, 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<any>;
@ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef<any>;
@ViewChild('deleteUserModal') deleteUserModal!: TemplateRef<any>;
}

View File

@ -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;
}

View File

@ -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<CallbackConfiguration>) {
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<PartnerAddress>) {
if (partial) {
Object.assign(this, partial);
}
}
}
export class TechnicalContact {
@IsString()
name: string = '';
@IsEmail()
email: string = '';
@IsString()
phone: string = '';
constructor(partial?: Partial<TechnicalContact>) {
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<PartnerStats>) {
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<Partner>) {
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<CreatePartnerDto>) {
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<UpdatePartnerDto>) {
if (partial) {
Object.assign(this, partial);
}
}
}
export class UpdateCallbacksDto {
@IsOptional()
callbacks?: CallbackConfiguration;
constructor(partial?: Partial<UpdateCallbacksDto>) {
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<PartnerQuery>) {
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<T> {
success: boolean = false;
data?: T = undefined;
error?: string = '';
message?: string = '';
constructor(partial?: Partial<ApiResponse<T>>) {
if (partial) {
Object.assign(this, partial);
}
}
}
export class ApiKeyResponse {
apiKey: string = '';
secretKey: string = '';
partnerId: string = '';
createdAt: Date = new Date();
constructor(partial?: Partial<ApiKeyResponse>) {
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<PartnerFormData>) {
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<PartnerProduct>) {
if (partial) {
Object.assign(this, partial);
}
}
}

View File

@ -1,2 +0,0 @@
import { PartnerTeamProfile } from './profile';
describe('PartnerTeamProfile', () => {});

View File

@ -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<MerchantUserDto> {
// 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<MerchantUserDto>(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<MerchantUserDto[]> {
return this.http.get<MerchantUserDto[]>(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<MerchantUserDto[]> {
return this.http.get<MerchantUserDto[]>(`${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<MerchantUserDto> {
return this.http.get<MerchantUserDto>(`${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<PaginatedUserResponse> {
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<MerchantUserDto[]>(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<MerchantUserDto> {
return this.http.put<MerchantUserDto>(`${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<MerchantUserDto> {
return this.updateMerchantUser(id, { enabled: true });
}
/**
* Désactive un utilisateur marchand
*/
disableMerchantUser(id: string): Observable<MerchantUserDto> {
return this.updateMerchantUser(id, { enabled: false });
}
// === STATISTIQUES ===
/**
* Récupère les statistiques des utilisateurs marchands
*/
getMerchantPartnerStats(): Observable<MerchantPartnerStatsResponse> {
return this.http.get<MerchantPartnerStatsResponse>(`${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<MerchantUserDto[]> {
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<MerchantUserDto[]>(`${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<AvailableRolesResponse> {
return this.http.get<AvailableRolesResponse>(`${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<MerchantUserDto[]> {
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<MerchantUserDto[]> {
return this.searchMerchantUsers({ enabled: true });
}
/**
* Récupère uniquement les utilisateurs inactifs
*/
getInactiveMerchantUsers(): Observable<MerchantUserDto[]> {
return this.searchMerchantUsers({ enabled: false });
}
}

View File

@ -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<ApiResponse<Partner>> {
return this.http.post<ApiResponse<Partner>>(`${this.apiUrl}`, createPartnerDto).pipe(
catchError(error => throwError(() => error))
);
}
/**
* Obtenir toutes les une config marchands avec pagination
*/
findAllPartnersConfig(query: PartnerQuery = new PartnerQuery()): Observable<PaginatedPartners> {
const params = this.buildQueryParams(query);
return this.http.get<PaginatedPartners>(`${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<ApiResponse<Partner>> {
return this.http.get<ApiResponse<Partner>>(`${this.apiUrl}/${merchantId}`).pipe(
catchError(error => throwError(() => error))
);
}
/**
* Mettre à jour une config marchand
*/
updatePartnerConfig(merchantId: string, updateData: UpdatePartnerDto): Observable<ApiResponse<Partner>> {
return this.http.put<ApiResponse<Partner>>(`${this.apiUrl}/${merchantId}`, updateData).pipe(
catchError(error => throwError(() => error))
);
}
/**
* Supprimer une config marchand
*/
deletePartnerConfig(merchantId: string): Observable<ApiResponse<void>> {
return this.http.delete<ApiResponse<void>>(`${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<ApiResponse<Partner>> {
return this.http.put<ApiResponse<Partner>>(
`${this.apiUrl}/${merchantId}/callbacks`,
updateData
).pipe(
catchError(error => throwError(() => error))
);
}
/**
* Obtenir la configuration des callbacks d'un marchand
*/
getCallbacksConfig(merchantId: string): Observable<ApiResponse<CallbackConfiguration>> {
return this.http.get<ApiResponse<CallbackConfiguration>>(
`${this.apiUrl}/${merchantId}/callbacks`
).pipe(
catchError(error => throwError(() => error))
);
}
/**
* Tester un webhook spécifique
*/
testWebhookConfig(merchantId: string, webhookType: string, payload: any = {}): Observable<ApiResponse<any>> {
return this.http.post<ApiResponse<any>>(
`${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<ApiResponse<any>> {
const params = this.buildQueryParams(query);
return this.http.get<ApiResponse<any>>(
`${this.apiUrl}/${merchantId}/callbacks/logs`,
{ params }
).pipe(
catchError(error => throwError(() => error))
);
}
// ==================== GESTION DES STATISTIQUES ====================
/**
* Obtenir les statistiques d'un marchand
*/
getPartnerStats(merchantId: string): Observable<ApiResponse<PartnerStats>> {
return this.http.get<ApiResponse<PartnerStats>>(
`${this.apiUrl}/${merchantId}/stats`
).pipe(
catchError(error => throwError(() => error))
);
}
/**
* Obtenir mes statistiques (marchand connecté)
*/
getMyStats(): Observable<ApiResponse<PartnerStats>> {
return this.http.get<ApiResponse<PartnerStats>>(
`${this.apiUrl}/me/stats`
).pipe(
catchError(error => throwError(() => error))
);
}
/**
* Obtenir les statistiques globales (admin seulement)
*/
getGlobalStats(): Observable<ApiResponse<any>> {
return this.http.get<ApiResponse<any>>(
`${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<ApiResponse<ApiKeyResponse>> {
return this.http.post<ApiResponse<ApiKeyResponse>>(
`${this.apiUrl}/${merchantId}/api-keys`,
{}
).pipe(
catchError(error => throwError(() => error))
);
}
/**
* Révoker les clés API d'un marchand
*/
revokeApiKeys(merchantId: string): Observable<ApiResponse<void>> {
return this.http.delete<ApiResponse<void>>(
`${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<ApiResponse<ApiKeyResponse>> {
return this.http.put<ApiResponse<ApiKeyResponse>>(
`${this.apiUrl}/${merchantId}/api-keys/regenerate-secret`,
{}
).pipe(
catchError(error => throwError(() => error))
);
}
/**
* Obtenir les clés API d'un marchand
*/
getApiKeys(merchantId: string): Observable<ApiResponse<ApiKeyResponse>> {
return this.http.get<ApiResponse<ApiKeyResponse>>(
`${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<ApiResponse<{ isValid: boolean; errors: string[] }>> {
return this.http.get<ApiResponse<{ isValid: boolean; errors: string[] }>>(
`${this.apiUrl}/${merchantId}/validate`
).pipe(
catchError(error => throwError(() => error))
);
}
}

View File

@ -1,249 +0,0 @@
<!-- src/app/modules/merchant-users/stats/stats.html -->
<app-ui-card title="Statistiques de l'Équipe Marchande">
<a
helper-text
href="javascript:void(0);"
class="icon-link icon-link-hover link-primary fw-semibold"
>Vue d'ensemble des utilisateurs de votre écosystème marchand
</a>
<div card-body>
@if (!stats) {
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
<p class="mt-2 text-muted">Chargement des statistiques...</p>
</div>
}
@if (stats) {
<div class="row">
<!-- KPI Cards -->
<div class="col-xl-3 col-md-6">
<div class="card card-animate">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<p class="text-uppercase fw-medium text-muted mb-0">Total Utilisateurs</p>
<h4 class="mt-2 mb-0 text-primary">{{ stats.totalUsers }}</h4>
<p class="mb-0">
<span class="badge bg-success-subtle text-success mt-1">
<ng-icon name="lucideUsers" class="me-1"></ng-icon>
Équipe complète
</span>
</p>
</div>
<div class="flex-shrink-0">
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<ng-icon name="lucideUsers" class="text-primary fs-20"></ng-icon>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card card-animate">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<p class="text-uppercase fw-medium text-muted mb-0">Administrateurs</p>
<h4 class="mt-2 mb-0 text-danger">{{ stats.totalAdmins }}</h4>
<p class="mb-0">
<span class="badge bg-danger-subtle text-danger mt-1">
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Accès complet
</span>
</p>
</div>
<div class="flex-shrink-0">
<div class="avatar-sm bg-danger bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<ng-icon name="lucideShield" class="text-danger fs-20"></ng-icon>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card card-animate">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<p class="text-uppercase fw-medium text-muted mb-0">Managers</p>
<h4 class="mt-2 mb-0 text-warning">{{ stats.totalManagers }}</h4>
<p class="mb-0">
<span class="badge bg-warning-subtle text-warning mt-1">
<ng-icon name="lucideUserCog" class="me-1"></ng-icon>
Gestion opérationnelle
</span>
</p>
</div>
<div class="flex-shrink-0">
<div class="avatar-sm bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<ng-icon name="lucideUserCog" class="text-warning fs-20"></ng-icon>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card card-animate">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<p class="text-uppercase fw-medium text-muted mb-0">Support</p>
<h4 class="mt-2 mb-0 text-info">{{ stats.totalSupport }}</h4>
<p class="mb-0">
<span class="badge bg-info-subtle text-info mt-1">
<ng-icon name="lucideHeadphones" class="me-1"></ng-icon>
Assistance client
</span>
</p>
</div>
<div class="flex-shrink-0">
<div class="avatar-sm bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<ng-icon name="lucideHeadphones" class="text-info fs-20"></ng-icon>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Statistiques d'activité -->
<div class="col-xl-4 col-md-6">
<div class="card">
<div class="card-body text-center">
<h6 class="card-title mb-3">Utilisateurs Actifs</h6>
<div class="mb-3">
<h2 class="text-success">{{ stats.activeUsers }}</h2>
<p class="text-muted mb-0">Comptes activés</p>
</div>
<div class="progress mb-2">
<div class="progress-bar bg-success"
[style.width]="(stats.activeUsers / stats.totalUsers * 100) + '%'">
{{ (stats.activeUsers / stats.totalUsers * 100).toFixed(1) }}%
</div>
</div>
<small class="text-muted">
{{ stats.activeUsers }} sur {{ stats.totalUsers }} utilisateurs
</small>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6">
<div class="card">
<div class="card-body text-center">
<h6 class="card-title mb-3">Utilisateurs Inactifs</h6>
<div class="mb-3">
<h2 class="text-danger">{{ stats.inactiveUsers }}</h2>
<p class="text-muted mb-0">Comptes désactivés</p>
</div>
<div class="progress mb-2">
<div class="progress-bar bg-danger"
[style.width]="(stats.inactiveUsers / stats.totalUsers * 100) + '%'">
{{ (stats.inactiveUsers / stats.totalUsers * 100).toFixed(1) }}%
</div>
</div>
<small class="text-muted">
{{ stats.inactiveUsers }} sur {{ stats.totalUsers }} utilisateurs
</small>
</div>
</div>
</div>
<div class="col-xl-4 col-md-12">
<div class="card">
<div class="card-body text-center">
<h6 class="card-title mb-3">Répartition des Rôles</h6>
<div class="d-flex justify-content-around text-center">
<div>
<h4 class="text-danger mb-1">{{ stats.totalAdmins }}</h4>
<small class="text-muted">Admins</small>
</div>
<div>
<h4 class="text-warning mb-1">{{ stats.totalManagers }}</h4>
<small class="text-muted">Managers</small>
</div>
<div>
<h4 class="text-info mb-1">{{ stats.totalSupport }}</h4>
<small class="text-muted">Support</small>
</div>
</div>
<div class="mt-3">
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-danger"
[style.width]="(stats.totalAdmins / stats.totalUsers * 100) + '%'"
title="Administrateurs">
</div>
<div class="progress-bar bg-warning"
[style.width]="(stats.totalManagers / stats.totalUsers * 100) + '%'"
title="Managers">
</div>
<div class="progress-bar bg-info"
[style.width]="(stats.totalSupport / stats.totalUsers * 100) + '%'"
title="Support">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Résumé textuel -->
<div class="col-12">
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Synthèse de l'Équipe</h6>
<div class="row text-center">
<div class="col-md-3">
<div class="border-end">
<h5 class="text-primary mb-1">{{ stats.totalUsers }}</h5>
<small class="text-muted">Total Membres</small>
</div>
</div>
<div class="col-md-3">
<div class="border-end">
<h5 class="text-success mb-1">{{ stats.activeUsers }}</h5>
<small class="text-muted">Actifs</small>
</div>
</div>
<div class="col-md-3">
<div class="border-end">
<h5 class="text-danger mb-1">{{ stats.inactiveUsers }}</h5>
<small class="text-muted">Inactifs</small>
</div>
</div>
<div class="col-md-3">
<h5 class="text-info mb-1">{{ stats.totalAdmins + stats.totalManagers + stats.totalSupport }}</h5>
<small class="text-muted">Avec Rôle Défini</small>
</div>
</div>
<!-- Message d'information -->
<div class="alert alert-light mt-3 mb-0">
<div class="d-flex align-items-center">
<ng-icon name="lucideInfo" class="me-2 text-info"></ng-icon>
<div>
<small>
Votre équipe marchande est composée de <strong>{{ stats.totalAdmins }} administrateurs</strong>,
<strong>{{ stats.totalManagers }} managers</strong> et <strong>{{ stats.totalSupport }} agents de support</strong>.
<strong>{{ stats.activeUsers }} utilisateurs</strong> sont actuellement actifs.
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
</div>
</app-ui-card>

View File

@ -1,2 +0,0 @@
import { MerchantPartnerStats } from './stats';
describe('Merchant Partner Stats', () => {});

View File

@ -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;
}

View File

@ -1,6 +0,0 @@
export type WizardStepType = {
id: string
icon: string
title: string
subtitle: string
}

View File

@ -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',

View File

@ -8,22 +8,18 @@
@if (user) {
{{ getUserDisplayName() }}
} @else {
Profil Utilisateur
Mon Profil
}
</h4>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item">
<a href="javascript:void(0)" (click)="back.emit()" class="text-decoration-none cursor-pointer">
Utilisateurs
<a href="javascript:void(0)" (click)="goBack()" class="text-decoration-none cursor-pointer">
Tableau de bord
</a>
</li>
<li class="breadcrumb-item active" aria-current="page">
@if (user) {
{{ getUserDisplayName() }}
} @else {
Profile
}
Mon Profil
</li>
</ol>
</nav>
@ -31,34 +27,15 @@
<div class="d-flex gap-2">
<!-- Bouton de réinitialisation de mot de passe -->
@if (user && canEditUsers && !isEditing) {
@if (user && !isEditing) {
<button
class="btn btn-warning"
(click)="resetPassword()"
(click)="openMyProfileResetModal()"
>
<ng-icon name="lucideKey" class="me-1"></ng-icon>
Réinitialiser MDP
Changer MDP
</button>
<!-- Bouton activation/désactivation -->
@if (user.enabled) {
<button
class="btn btn-outline-warning"
(click)="disableUser()"
>
<ng-icon name="lucidePause" class="me-1"></ng-icon>
Désactiver
</button>
} @else {
<button
class="btn btn-outline-success"
(click)="enableUser()"
>
<ng-icon name="lucidePlay" class="me-1"></ng-icon>
Activer
</button>
}
<!-- Bouton modification -->
<button
class="btn btn-primary"
@ -73,22 +50,6 @@
</div>
</div>
<!-- Indicateur de permissions -->
@if (currentUserRole && !canEditUsers) {
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-warning">
<div class="d-flex align-items-center">
<ng-icon name="lucideShield" class="me-2"></ng-icon>
<div>
<strong>Permissions limitées :</strong> Vous ne pouvez que consulter ce profil
</div>
</div>
</div>
</div>
</div>
}
<!-- Messages d'alerte -->
@if (error) {
<div class="alert alert-danger">
@ -115,7 +76,7 @@
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
<p class="mt-2 text-muted">Chargement du profil...</p>
<p class="mt-2 text-muted">Chargement de votre profil...</p>
</div>
}
@ -126,7 +87,9 @@
<!-- Carte profil -->
<div class="card">
<div class="card-header bg-light">
<h5 class="card-title mb-0">Profil Utilisateur Hub</h5>
<h5 class="card-title mb-0">
Mon Profil
</h5>
</div>
<div class="card-body text-center">
<!-- Avatar -->
@ -139,8 +102,13 @@
<h5>{{ getUserDisplayName() }}</h5>
<p class="text-muted mb-2">@{{ user.username }}</p>
<!-- Type d'utilisateur -->
<span class="badge bg-secondary mb-2">
{{ getUserTypeDisplay() }}
</span>
<!-- Statut -->
<span [class]="getStatusBadgeClass()" class="mb-3">
<span [class]="getStatusBadgeClass()" class="mb-3 d-block">
{{ getStatusText() }}
</span>
@ -155,12 +123,12 @@
</div>
<div class="d-flex align-items-center">
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
<small>Créé le {{ formatTimestamp(user.createdTimestamp) }}</small>
<small>Membre depuis {{ getCreationDate() }}</small>
</div>
@if (user.lastLogin) {
@if (isMerchantUser()) {
<div class="d-flex align-items-center mt-2">
<ng-icon name="lucideLogIn" class="me-2 text-muted"></ng-icon>
<small>Dernière connexion : {{ formatTimestamp(user.lastLogin) }}</small>
<ng-icon name="lucideBuilding" class="me-2 text-muted"></ng-icon>
<small>Partner ID: {{ getMerchantPartnerId() }}</small>
</div>
}
</div>
@ -169,108 +137,70 @@
<!-- Carte rôle utilisateur -->
<div class="card mt-3">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Rôle Utilisateur</h5>
@if (canManageRoles && !isEditing) {
<span class="badge bg-info">Modifiable</span>
}
<div class="card-header bg-light">
<h5 class="card-title mb-0">
Mon Rôle
</h5>
</div>
<div class="card-body">
<!-- Rôle actuel -->
<div class="text-center mb-3">
<!-- Afficher le premier rôle ou tous les rôles -->
@if (user.roles && user.roles.length > 0) {
@for (role of user.roles; track role; let first = $first) {
@if (first) { <!-- Afficher seulement le premier rôle pour l'instant -->
<span class="badge d-flex align-items-center justify-content-center" [ngClass]="getRoleBadgeClass(role)">
<ng-icon [name]="getRoleIcon(role)" class="me-2"></ng-icon>
{{ getRoleLabel(role) }}
</span>
<small class="text-muted d-block mt-1">
{{ getRoleDescription(role) }}
</small>
}
}
@if (getUserRole()) {
<div class="d-flex flex-wrap gap-1 justify-content-center mb-2">
<span class="badge d-flex align-items-center" [ngClass]="getRoleBadgeClass()">
<ng-icon [name]="getRoleIcon(user.role)" class="me-1" size="12"></ng-icon>
{{ getRoleLabel() }}
</span>
</div>
<!-- Indicateur si plusieurs rôles -->
@if (user.roles.length > 1) {
<small class="text-muted">
+ {{ user.roles.length - 1 }} autre(s) rôle(s)
</small>
}
<!-- Description du rôle -->
<small class="text-muted d-block">
{{ getRoleDescription() }}
</small>
} @else {
<span class="badge bg-secondary">Aucun rôle</span>
}
</div>
<!-- Changement de rôle -->
@if (canManageRoles && !isEditing) {
<div class="mt-3">
<label class="form-label fw-semibold">Changer le rôle</label>
<select
class="form-select"
[value]="user.roles[0]"
(change)="updateUserRole($any($event.target).value)"
[disabled]="updatingRoles"
>
<option value="" disabled>Sélectionnez un nouveau rôle</option>
@for (role of availableRoles; track role.value) {
<option
[value]="role.value"
[disabled]="!canAssignRole(role.value) || role.value === user.roles[0]"
>
{{ role.label }}
@if (!canAssignRole(role.value)) {
(Non autorisé)
} @else if (role.value === user.roles[0]) {
(Actuel)
}
</option>
}
</select>
<div class="form-text">
@if (updatingRoles) {
<div class="spinner-border spinner-border-sm me-1" role="status">
<span class="visually-hidden">Mise à jour...</span>
</div>
Mise à jour en cours...
} @else {
Sélectionnez un nouveau rôle pour cet utilisateur
}
</div>
</div>
} @else if (!canManageRoles) {
<div class="alert alert-info mt-3">
<small>
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Vous n'avez pas la permission de modifier les rôles
</small>
</div>
}
<!-- Information rôle non modifiable -->
<div class="alert alert-info mt-3">
<small>
<ng-icon name="lucideInfo" class="me-1"></ng-icon>
Votre rôle ne peut pas être modifié depuis cette page
</small>
</div>
</div>
</div>
<!-- Informations de création -->
<div class="card mt-3">
<div class="card-header bg-light">
<h6 class="card-title mb-0">Informations de Création</h6>
<h6 class="card-title mb-0">Informations du Compte</h6>
</div>
<div class="card-body">
<div class="row g-2 small">
<div class="col-12">
<strong>Créé par :</strong>
<div class="text-muted">{{ user.createdByUsername || 'Système' }}</div>
<strong>ID Utilisateur :</strong>
<div class="text-muted font-monospace small">{{ user.id }}</div>
</div>
<div class="col-12">
<strong>Date de création :</strong>
<div class="text-muted">{{ formatTimestamp(user.createdTimestamp) }}</div>
<div class="text-muted">{{ getCreationDate() }}</div>
</div>
<div class="col-12">
<strong>Type d'utilisateur :</strong>
<strong>Type de compte :</strong>
<div class="text-muted">
<span class="badge bg-secondary">{{ user.userType }}</span>
<span class="badge bg-secondary">{{ getUserTypeDisplay() }}</span>
</div>
</div>
@if (isMerchantUser()) {
<div class="col-12">
<strong>Merchant Partner ID :</strong>
<div class="text-muted font-monospace small">
{{ getMerchantPartnerId() }}
</div>
</div>
}
</div>
</div>
</div>
@ -283,10 +213,10 @@
<h5 class="card-title mb-0">
@if (isEditing) {
<ng-icon name="lucideEdit" class="me-2"></ng-icon>
Modification du Profil
Modification de Mon Profil
} @else {
<ng-icon name="lucideUser" class="me-2"></ng-icon>
Détails du Compte
Mes Informations
}
</h5>
@ -305,7 +235,7 @@
type="button"
class="btn btn-success btn-sm"
(click)="saveProfile()"
[disabled]="saving"
[disabled]="saving || !isFormValid()"
>
@if (saving) {
<div class="spinner-border spinner-border-sm me-1" role="status">
@ -323,15 +253,21 @@
<div class="row g-3">
<!-- Prénom -->
<div class="col-md-6">
<label class="form-label">Prénom</label>
<label class="form-label">Prénom <span class="text-danger">*</span></label>
@if (isEditing) {
<input
type="text"
class="form-control"
[(ngModel)]="editedUser.firstName"
placeholder="Entrez le prénom"
placeholder="Votre prénom"
[disabled]="saving"
required
>
@if (!editedUser.firstName?.trim()) {
<div class="form-text text-danger">
Le prénom est obligatoire
</div>
}
} @else {
<div class="form-control-plaintext">
{{ user.firstName || 'Non renseigné' }}
@ -341,15 +277,21 @@
<!-- Nom -->
<div class="col-md-6">
<label class="form-label">Nom</label>
<label class="form-label">Nom <span class="text-danger">*</span></label>
@if (isEditing) {
<input
type="text"
class="form-control"
[(ngModel)]="editedUser.lastName"
placeholder="Entrez le nom"
placeholder="Votre nom"
[disabled]="saving"
required
>
@if (!editedUser.lastName?.trim()) {
<div class="form-text text-danger">
Le nom est obligatoire
</div>
}
} @else {
<div class="form-control-plaintext">
{{ user.lastName || 'Non renseigné' }}
@ -364,7 +306,7 @@
{{ user.username }}
</div>
<div class="form-text">
Le nom d'utilisateur ne peut pas être modifié
Votre identifiant de connexion ne peut pas être modifié
</div>
</div>
@ -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 {
<div class="form-control-plaintext">
@ -389,35 +332,40 @@
}
</div>
<!-- Statut activé -->
@if (isEditing) {
<div class="col-md-6">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="enabledSwitch"
[(ngModel)]="editedUser.enabled"
[disabled]="saving"
>
<label class="form-check-label" for="enabledSwitch">
Compte activé
</label>
</div>
<div class="form-text">
L'utilisateur peut se connecter si activé
</div>
<!-- Statut du compte -->
<div class="col-md-6">
<label class="form-label">Statut du compte</label>
<div class="form-control-plaintext">
<span [class]="getStatusBadgeClass()">
{{ getStatusText() }}
</span>
</div>
} @else {
<div class="col-md-6">
<label class="form-label">Statut du compte</label>
<div class="form-control-plaintext">
<span [class]="getStatusBadgeClass()">
{{ getStatusText() }}
<div class="form-text">
@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
}
</div>
</div>
<!-- Rôle -->
<div class="col-md-6">
<label class="form-label">Rôle</label>
<div class="form-control-plaintext">
<div class="d-flex flex-wrap gap-1">
<span class="badge d-flex align-items-center" [ngClass]="getRoleBadgeClass()">
<ng-icon [name]="getRoleIcon(user.role)" class="me-1" size="12"></ng-icon>
{{ getRoleLabel() }}
</span>
</div>
</div>
}
<div class="form-text">
{{ getRoleDescription() }}
</div>
</div>
<!-- Informations système -->
@if (!isEditing) {
@ -437,21 +385,23 @@
<div class="col-md-6">
<label class="form-label">Date de création</label>
<div class="form-control-plaintext">
{{ formatTimestamp(user.createdTimestamp) }}
{{ getCreationDate() }}
</div>
</div>
<div class="col-md-6">
<label class="form-label">Créé par</label>
<label class="form-label">Type de compte</label>
<div class="form-control-plaintext">
{{ user.createdByUsername || 'Système' }}
<span class="badge bg-secondary">{{ getUserTypeDisplay() }}</span>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Type d'utilisateur</label>
<div class="form-control-plaintext">
<span class="badge bg-secondary">{{ user.userType }}</span>
@if (isMerchantUser()) {
<div class="col-md-6">
<label class="form-label">Merchant Partner ID</label>
<div class="form-control-plaintext font-monospace small">
{{ getMerchantPartnerId() }}
</div>
</div>
</div>
}
</div>
</div>
}
@ -459,49 +409,30 @@
</div>
</div>
<!-- Actions supplémentaires -->
@if (!isEditing && canEditUsers) {
<!-- Actions personnelles -->
@if (!isEditing) {
<div class="card mt-3">
<div class="card-header bg-light">
<h6 class="card-title mb-0">Actions de Gestion</h6>
<h6 class="card-title mb-0">Actions Personnelles</h6>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-md-4">
<div class="col-md-6">
<button
class="btn btn-outline-warning w-100"
(click)="resetPassword()"
(click)="openMyProfileResetModal()"
>
<ng-icon name="lucideKey" class="me-1"></ng-icon>
Réinitialiser MDP
Changer le mot de passe
</button>
</div>
<div class="col-md-4">
@if (user.enabled) {
<button
class="btn btn-outline-secondary w-100"
(click)="disableUser()"
>
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
Désactiver
</button>
} @else {
<button
class="btn btn-outline-success w-100"
(click)="enableUser()"
>
<ng-icon name="lucideUserCheck" class="me-1"></ng-icon>
Activer
</button>
}
</div>
<div class="col-md-4">
<div class="col-md-6">
<button
class="btn btn-outline-primary w-100"
(click)="startEditing()"
>
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
Modifier
Modifier mon profil
</button>
</div>
</div>
@ -511,4 +442,248 @@
</div>
}
</div>
</div>
</div>
<!-- Modal de réinitialisation de mot de passe personnel - MyProfile -->
<ng-template #myProfileResetPasswordModal let-modal>
<div class="modal-header bg-primary text-white">
<h4 class="modal-title">
<ng-icon name="lucideKeyRound" class="me-2"></ng-icon>
Réinitialiser mon mot de passe
</h4>
<button
type="button"
class="btn-close btn-close-white"
(click)="closeMyProfileResetModal()"
[disabled]="resettingMyPassword"
aria-label="Fermer"
></button>
</div>
<div class="modal-body">
<!-- Informations personnelles -->
<div class="card border-primary mb-4">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="avatar-lg bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-4">
<span class="text-primary fw-bold fs-4">{{ getMyInitials() }}</span>
</div>
<div>
<h5 class="card-title mb-1">{{ user?.firstName }} {{ user?.lastName }}</h5>
<p class="card-text text-muted mb-1">
<ng-icon name="lucideUser" class="me-1"></ng-icon>
{{ user?.username }}
</p>
<p class="card-text text-muted mb-0">
<ng-icon name="lucideMail" class="me-1"></ng-icon>
{{ user?.email }}
</p>
</div>
</div>
</div>
</div>
<!-- Message de succès -->
@if (myProfileResetSuccess) {
<div class="alert alert-success d-flex align-items-center">
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
<div>
<strong>Succès !</strong> {{ myProfileResetSuccess }}
</div>
</div>
}
<!-- Message d'erreur -->
@if (myProfileResetError) {
<div class="alert alert-danger d-flex align-items-center">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
<div>
<strong>Erreur :</strong> {{ myProfileResetError }}
</div>
</div>
}
<!-- Formulaire de réinitialisation -->
@if (!myProfileResetSuccess) {
<form (ngSubmit)="confirmMyProfileResetPassword()" #myProfileResetForm="ngForm">
<!-- Mot de passe actuel (pour sécurité) -->
<div class="mb-3">
<label class="form-label">
Mot de passe actuel <span class="text-danger">*</span>
</label>
<div class="input-group">
<input
[type]="showCurrentPassword ? 'text' : 'password'"
class="form-control"
placeholder="Entrez votre mot de passe actuel"
[(ngModel)]="myProfileCurrentPassword"
name="currentPassword"
required
[disabled]="resettingMyPassword"
#currentPasswordInput="ngModel"
>
<button
type="button"
class="btn btn-outline-secondary"
(click)="showCurrentPassword = !showCurrentPassword"
[disabled]="resettingMyPassword"
>
<ng-icon [name]="showCurrentPassword ? 'lucideEyeOff' : 'lucideEye'"></ng-icon>
</button>
</div>
@if (currentPasswordInput.invalid && currentPasswordInput.touched) {
<div class="text-danger small">
Votre mot de passe actuel est requis
</div>
}
</div>
<!-- Nouveau mot de passe -->
<div class="mb-3">
<label class="form-label">
Nouveau mot de passe <span class="text-danger">*</span>
</label>
<div class="input-group">
<input
[type]="showNewMyPassword ? 'text' : 'password'"
class="form-control"
placeholder="Entrez votre nouveau mot de passe"
[(ngModel)]="myProfileNewPassword"
name="newPassword"
required
minlength="8"
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$"
[disabled]="resettingMyPassword"
#newMyPasswordInput="ngModel"
>
<button
type="button"
class="btn btn-outline-secondary"
(click)="showNewMyPassword = !showNewMyPassword"
[disabled]="resettingMyPassword"
>
<ng-icon [name]="showNewMyPassword ? 'lucideEyeOff' : 'lucideEye'"></ng-icon>
</button>
</div>
<div class="form-text">
<ul class="small mb-0 ps-3">
<li [class.text-success]="hasMinLength()">Au moins 8 caractères</li>
<li [class.text-success]="hasLowerCase()">Une lettre minuscule</li>
<li [class.text-success]="hasUpperCase()">Une lettre majuscule</li>
<li [class.text-success]="hasNumber()">Un chiffre</li>
<li [class.text-success]="hasSpecialChar()">Un caractère spécial (@$!%*?&)</li>
</ul>
</div>
@if (newMyPasswordInput.invalid && newMyPasswordInput.touched) {
<div class="text-danger small">
@if (newMyPasswordInput.errors?.['required']) {
Le nouveau mot de passe est requis
}
@if (newMyPasswordInput.errors?.['minlength']) {
Le mot de passe doit contenir au moins 8 caractères
}
@if (newMyPasswordInput.errors?.['pattern']) {
Le mot de passe ne respecte pas les critères de sécurité
}
</div>
}
</div>
<!-- Confirmation du nouveau mot de passe -->
<div class="mb-3">
<label class="form-label">
Confirmer le nouveau mot de passe <span class="text-danger">*</span>
</label>
<div class="input-group">
<input
[type]="showConfirmMyPassword ? 'text' : 'password'"
class="form-control"
placeholder="Confirmez votre nouveau mot de passe"
[(ngModel)]="myProfileConfirmPassword"
name="confirmPassword"
required
[disabled]="resettingMyPassword"
#confirmMyPasswordInput="ngModel"
>
<button
type="button"
class="btn btn-outline-secondary"
(click)="showConfirmMyPassword = !showConfirmMyPassword"
[disabled]="resettingMyPassword"
>
<ng-icon [name]="showConfirmMyPassword ? 'lucideEyeOff' : 'lucideEye'"></ng-icon>
</button>
</div>
@if (confirmMyPasswordInput.invalid && confirmMyPasswordInput.touched) {
<div class="text-danger small">
La confirmation du mot de passe est requise
</div>
}
@if (myProfileNewPassword && myProfileConfirmPassword && myProfileNewPassword !== myProfileConfirmPassword) {
<div class="text-danger small">
Les mots de passe ne correspondent pas
</div>
}
</div>
<!-- Indicateur de force du mot de passe -->
@if (myProfileNewPassword) {
<div class="mb-3">
<label class="form-label small">Force du mot de passe</label>
<div class="progress mb-1" style="height: 6px;">
<div
class="progress-bar"
[ngClass]="getPasswordStrengthClass(myProfileNewPassword)"
[style.width.%]="getPasswordStrength(myProfileNewPassword)"
></div>
</div>
<div class="small text-muted text-center">
{{ getPasswordStrengthText(myProfileNewPassword) }}
</div>
</div>
}
</form>
}
</div>
<div class="modal-footer">
@if (myProfileResetSuccess) {
<button
type="button"
class="btn btn-success"
(click)="closeMyProfileResetModal()"
>
<ng-icon name="lucideCheck" class="me-1"></ng-icon>
Terminer
</button>
} @else {
<button
type="button"
class="btn btn-light"
(click)="closeMyProfileResetModal()"
[disabled]="resettingMyPassword"
>
<ng-icon name="lucideX" class="me-1"></ng-icon>
Annuler
</button>
<button
type="button"
class="btn btn-primary"
(click)="confirmMyProfileResetPassword()"
[disabled]="!isMyProfileResetFormValid() || resettingMyPassword"
>
@if (resettingMyPassword) {
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
Réinitialisation...
} @else {
<ng-icon name="lucideKeyRound" class="me-1"></ng-icon>
Changer mon mot de passe
}
</button>
}
</div>
</ng-template>

View File

@ -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<void>();
@Output() back = new EventEmitter<void>();
@Output() openResetPasswordModal = new EventEmitter<string>();
readonly UserRole = UserRole;
readonly UserUtils = UserUtils;
user: any | null = null;
@Output() back = new EventEmitter<void>();
// Référence au modal MyProfile
@ViewChild('myProfileResetPasswordModal') myProfileResetPasswordModal!: TemplateRef<any>;
// É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<any>, 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
}
}

View File

@ -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));