feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
191099d8a5
commit
4c8e3f229d
9
package-lock.json
generated
9
package-lock.json
generated
@ -63,8 +63,7 @@
|
|||||||
"quill": "^2.0.3",
|
"quill": "^2.0.3",
|
||||||
"rxjs": "~7.8.2",
|
"rxjs": "~7.8.2",
|
||||||
"simplebar-angular": "^3.3.2",
|
"simplebar-angular": "^3.3.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1"
|
||||||
"zone.js": "^0.15.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^20.3.6",
|
"@angular/build": "^20.3.6",
|
||||||
@ -11060,12 +11059,6 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.24.1"
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,8 +66,7 @@
|
|||||||
"quill": "^2.0.3",
|
"quill": "^2.0.3",
|
||||||
"rxjs": "~7.8.2",
|
"rxjs": "~7.8.2",
|
||||||
"simplebar-angular": "^3.3.2",
|
"simplebar-angular": "^3.3.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1"
|
||||||
"zone.js": "^0.15.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^20.3.6",
|
"@angular/build": "^20.3.6",
|
||||||
|
|||||||
@ -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 { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
|
||||||
import * as tablerIcons from '@ng-icons/tabler-icons';
|
import * as tablerIcons from '@ng-icons/tabler-icons';
|
||||||
import * as tablerIconsFill from '@ng-icons/tabler-icons/fill';
|
import * as tablerIconsFill from '@ng-icons/tabler-icons/fill';
|
||||||
@ -22,33 +22,55 @@ export class App implements OnInit {
|
|||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private activatedRoute = inject(ActivatedRoute);
|
private activatedRoute = inject(ActivatedRoute);
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
|
private cdr = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
|
this.setupTitleListener();
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
await this.initializeAuth();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initializeAuth(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const isAuthenticated = await this.authService.initialize();
|
const isAuthenticated = await this.authService.initialize();
|
||||||
|
|
||||||
if (!isAuthenticated && this.router.url === '/') {
|
setTimeout(() => {
|
||||||
this.router.navigate(['/auth/login']);
|
this.handleInitialNavigation(isAuthenticated);
|
||||||
} else if (isAuthenticated && this.router.url === '/') {
|
});
|
||||||
this.router.navigate(['/dcb-dashboard']);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during authentication initialization:', error);
|
console.error('Error during authentication initialization:', error);
|
||||||
|
setTimeout(() => {
|
||||||
this.router.navigate(['/auth/login']);
|
this.router.navigate(['/auth/login']);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setupTitleListener();
|
private handleInitialNavigation(isAuthenticated: boolean): void {
|
||||||
}
|
|
||||||
|
|
||||||
private checkPublicRouteRedirection(): void {
|
|
||||||
const currentUrl = this.router.url;
|
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 (!isAuthenticated && this.shouldRedirectToLogin(currentUrl)) {
|
||||||
if (publicRoutes.includes(currentUrl)) {
|
this.router.navigate(['/auth/login']);
|
||||||
|
} else if (isAuthenticated && this.shouldRedirectToDashboard(currentUrl)) {
|
||||||
this.router.navigate(['/dcb-dashboard']);
|
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 {
|
private setupTitleListener(): void {
|
||||||
|
|||||||
@ -1,46 +1,61 @@
|
|||||||
|
// === ENUMS COHÉRENTS ===
|
||||||
export enum UserType {
|
export enum UserType {
|
||||||
HUB = 'HUB',
|
HUB = 'HUB',
|
||||||
MERCHANT = 'MERCHANT',
|
MERCHANT_PARTNER = 'MERCHANT'
|
||||||
MERCHANT_USER = 'MERCHANT_USER'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UserRole {
|
export enum UserRole {
|
||||||
// HUB roles
|
// Rôles Hub (sans merchantPartnerId)
|
||||||
DCB_ADMIN = 'DCB_ADMIN',
|
DCB_ADMIN = 'dcb-admin',
|
||||||
DCB_SUPPORT = 'DCB_SUPPORT',
|
DCB_SUPPORT = 'dcb-support',
|
||||||
DCB_PARTNER = 'DCB_PARTNER',
|
DCB_PARTNER = 'dcb-partner',
|
||||||
|
|
||||||
// MERCHANT roles
|
// Rôles Merchant Partner (avec merchantPartnerId obligatoire)
|
||||||
DCB_PARTNER_ADMIN = 'DCB_PARTNER_ADMIN',
|
DCB_PARTNER_ADMIN = 'dcb-partner-admin',
|
||||||
DCB_PARTNER_MANAGER = 'DCB_PARTNER_MANAGER',
|
DCB_PARTNER_MANAGER = 'dcb-partner-manager',
|
||||||
DCB_PARTNER_SUPPORT = 'DCB_PARTNER_SUPPORT'
|
DCB_PARTNER_SUPPORT = 'dcb-partner-support'
|
||||||
}
|
}
|
||||||
|
|
||||||
// === BASE USER MODEL ===
|
// Enum pour le contexte Angular (identique à l'ancien)
|
||||||
export interface BaseUserDto {
|
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;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
role: UserRole;
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
createdBy: string;
|
userType: UserType; // HUB ou MERCHANT
|
||||||
createdByUsername: string;
|
merchantPartnerId?: string;
|
||||||
|
role: UserRole;
|
||||||
|
createdBy?: string;
|
||||||
|
createdByUsername?: string;
|
||||||
createdTimestamp: number;
|
createdTimestamp: number;
|
||||||
lastLogin?: number;
|
lastLogin?: number;
|
||||||
userType: UserType;
|
profileImage?: string | null
|
||||||
}
|
|
||||||
|
|
||||||
// === EXTENSIONS ===
|
|
||||||
export interface HubUserDto extends BaseUserDto {
|
|
||||||
userType: UserType.HUB;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MerchantUserDto extends BaseUserDto {
|
|
||||||
userType: UserType.MERCHANT;
|
|
||||||
merchantPartnerId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === DTOs CRUD ===
|
// === DTOs CRUD ===
|
||||||
@ -50,10 +65,11 @@ export interface CreateUserDto {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
userType: UserType;
|
||||||
role: UserRole;
|
role: UserRole;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
emailVerified?: boolean;
|
emailVerified?: boolean;
|
||||||
merchantPartnerId?: string; // obligatoire si MERCHANT
|
merchantPartnerId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateUserDto {
|
export interface UpdateUserDto {
|
||||||
@ -61,6 +77,7 @@ export interface UpdateUserDto {
|
|||||||
lastName?: string;
|
lastName?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
role?: UserRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResetPasswordDto {
|
export interface ResetPasswordDto {
|
||||||
@ -71,7 +88,7 @@ export interface ResetPasswordDto {
|
|||||||
|
|
||||||
// === PAGINATION / STATS ===
|
// === PAGINATION / STATS ===
|
||||||
export interface PaginatedUserResponse {
|
export interface PaginatedUserResponse {
|
||||||
users: BaseUserDto[];
|
users: User[];
|
||||||
total: number;
|
total: number;
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
@ -93,6 +110,7 @@ export interface AvailableRole {
|
|||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
allowedForCreation: boolean;
|
allowedForCreation: boolean;
|
||||||
|
userType: UserType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AvailableRolesResponse {
|
export interface AvailableRolesResponse {
|
||||||
@ -110,4 +128,88 @@ export interface SearchUsersParams {
|
|||||||
role?: UserRole;
|
role?: UserRole;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
userType?: UserType;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -6,11 +6,9 @@ import { BehaviorSubject, Observable, throwError, tap, catchError } from 'rxjs';
|
|||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
User,
|
||||||
UserType,
|
UserType,
|
||||||
UserRole,
|
UserRole,
|
||||||
BaseUserDto,
|
|
||||||
HubUserDto,
|
|
||||||
MerchantUserDto
|
|
||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
// === INTERFACES DTO AUTH ===
|
// === INTERFACES DTO AUTH ===
|
||||||
@ -57,13 +55,12 @@ export interface TokenValidationResponseDto {
|
|||||||
})
|
})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly router = inject(Router);
|
|
||||||
|
|
||||||
private readonly tokenKey = 'access_token';
|
private readonly tokenKey = 'access_token';
|
||||||
private readonly refreshTokenKey = 'refresh_token';
|
private readonly refreshTokenKey = 'refresh_token';
|
||||||
|
|
||||||
private authState$ = new BehaviorSubject<boolean>(this.isAuthenticated());
|
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);
|
private initialized$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
// === INITIALISATION DE L'APPLICATION ===
|
// === INITIALISATION DE L'APPLICATION ===
|
||||||
@ -72,31 +69,42 @@ export class AuthService {
|
|||||||
* Initialise l'authentification au démarrage de l'application
|
* Initialise l'authentification au démarrage de l'application
|
||||||
*/
|
*/
|
||||||
async initialize(): Promise<boolean> {
|
async initialize(): Promise<boolean> {
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = this.getAccessToken();
|
const token = this.getAccessToken();
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
setTimeout(() => {
|
||||||
this.initialized$.next(true);
|
this.initialized$.next(true);
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isTokenExpired(token)) {
|
if (this.isTokenExpired(token)) {
|
||||||
const refreshSuccess = await this.tryRefreshToken();
|
const refreshSuccess = await this.tryRefreshToken();
|
||||||
|
setTimeout(() => {
|
||||||
this.initialized$.next(true);
|
this.initialized$.next(true);
|
||||||
|
});
|
||||||
return refreshSuccess;
|
return refreshSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token valide : charger le profil utilisateur
|
// Token valide : charger le profil utilisateur
|
||||||
await firstValueFrom(this.loadUserProfile());
|
await firstValueFrom(this.loadUserProfile());
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
this.authState$.next(true);
|
this.authState$.next(true);
|
||||||
this.initialized$.next(true);
|
this.initialized$.next(true);
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.clearAuthData();
|
this.clearAuthData();
|
||||||
|
setTimeout(() => {
|
||||||
this.initialized$.next(true);
|
this.initialized$.next(true);
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,7 +120,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await firstValueFrom(this.refreshAccessToken());
|
await firstValueFrom(this.refreshAccessToken());
|
||||||
await firstValueFrom(this.loadUserProfile());
|
await firstValueFrom(this.loadUserProfile());
|
||||||
this.authState$.next(true);
|
this.authState$.next(true);
|
||||||
return true;
|
return true;
|
||||||
@ -188,15 +196,66 @@ export class AuthService {
|
|||||||
/**
|
/**
|
||||||
* Chargement du profil utilisateur
|
* Chargement du profil utilisateur
|
||||||
*/
|
*/
|
||||||
loadUserProfile(): Observable<BaseUserDto> {
|
loadUserProfile(): Observable<User> {
|
||||||
return this.http.get<BaseUserDto>(
|
return this.http.get<any>(
|
||||||
`${environment.iamApiUrl}/auth/profile`
|
`${environment.iamApiUrl}/auth/profile`
|
||||||
).pipe(
|
).pipe(
|
||||||
tap(profile => this.userProfile$.next(profile)),
|
tap(apiResponse => {
|
||||||
catchError(error => throwError(() => error))
|
// 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 ===
|
// === GESTION DE SESSION ===
|
||||||
|
|
||||||
private handleLoginSuccess(response: LoginResponseDto): void {
|
private handleLoginSuccess(response: LoginResponseDto): void {
|
||||||
@ -232,7 +291,7 @@ export class AuthService {
|
|||||||
return this.authState$.asObservable();
|
return this.authState$.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserProfile(): Observable<BaseUserDto | null> {
|
getUserProfile(): Observable<User | null> {
|
||||||
return this.userProfile$.asObservable();
|
return this.userProfile$.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,55 +381,72 @@ export class AuthService {
|
|||||||
if (hubRoles.includes(role)) {
|
if (hubRoles.includes(role)) {
|
||||||
return UserType.HUB;
|
return UserType.HUB;
|
||||||
} else if (merchantRoles.includes(role)) {
|
} else if (merchantRoles.includes(role)) {
|
||||||
return UserType.MERCHANT;
|
return UserType.MERCHANT_PARTNER;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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
|
* Vérifie si l'utilisateur courant est un utilisateur Hub
|
||||||
*/
|
*/
|
||||||
isHubUser(): boolean {
|
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
|
* Vérifie si l'utilisateur courant est un utilisateur Marchand
|
||||||
*/
|
*/
|
||||||
isMerchantUser(): boolean {
|
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
|
* Vérifie si l'utilisateur courant a un rôle spécifique
|
||||||
*/
|
*/
|
||||||
hasRole(role: UserRole): boolean {
|
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
|
* Vérifie si l'utilisateur courant a au moins un des rôles spécifiés
|
||||||
*/
|
*/
|
||||||
hasAnyRole(roles: UserRole[]): boolean {
|
hasAnyRole(role: UserRole): boolean {
|
||||||
const userRoles = this.getCurrentUserRoles();
|
const userRoles = this.getCurrentUserClientRoles();
|
||||||
return roles.some(role => userRoles.includes(role));
|
return userRoles === role;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si l'utilisateur courant peut gérer les utilisateurs Hub
|
* Vérifie si l'utilisateur courant peut gérer les utilisateurs Hub
|
||||||
*/
|
*/
|
||||||
canManageHubUsers(): boolean {
|
canManageHubUsers(): boolean {
|
||||||
const hubAdminRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
|
return this.hasAnyRole(UserRole.DCB_ADMIN) || this.hasAnyRole(UserRole.DCB_SUPPORT);
|
||||||
return this.hasAnyRole(hubAdminRoles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si l'utilisateur courant peut gérer les utilisateurs Marchands
|
* Vérifie si l'utilisateur courant peut gérer les utilisateurs Marchands
|
||||||
*/
|
*/
|
||||||
canManageMerchantUsers(): boolean {
|
canManageMerchantUsers(): boolean {
|
||||||
const allowedRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
|
return this.hasAnyRole(UserRole.DCB_ADMIN) || this.hasAnyRole(UserRole.DCB_PARTNER);
|
||||||
return this.hasAnyRole(allowedRoles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === MÉTHODES UTILITAIRES ===
|
// === MÉTHODES UTILITAIRES ===
|
||||||
@ -379,7 +455,7 @@ export class AuthService {
|
|||||||
return this.authState$.asObservable();
|
return this.authState$.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
getProfile(): Observable<BaseUserDto | null> {
|
getProfile(): Observable<User | null> {
|
||||||
return this.getUserProfile();
|
return this.getUserProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,10 +472,7 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
getCurrentMerchantPartnerId(): string | null {
|
getCurrentMerchantPartnerId(): string | null {
|
||||||
const profile = this.userProfile$.value;
|
const profile = this.userProfile$.value;
|
||||||
if (profile && 'merchantPartnerId' in profile) {
|
return profile?.merchantPartnerId || null;
|
||||||
return (profile as MerchantUserDto).merchantPartnerId || null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -414,8 +487,7 @@ export class AuthService {
|
|||||||
* Vérifie si l'utilisateur peut visualiser tous les marchands
|
* Vérifie si l'utilisateur peut visualiser tous les marchands
|
||||||
*/
|
*/
|
||||||
canViewAllMerchants(): boolean {
|
canViewAllMerchants(): boolean {
|
||||||
const hubAdminRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
|
return this.hasAnyRole(UserRole.DCB_ADMIN) || this.hasAnyRole(UserRole.DCB_PARTNER);
|
||||||
return this.hasAnyRole(hubAdminRoles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === TOKENS ===
|
// === TOKENS ===
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
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 { 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 {
|
export interface RolePermission {
|
||||||
canCreateUsers: boolean;
|
canCreateUsers: boolean;
|
||||||
@ -13,28 +14,11 @@ export interface RolePermission {
|
|||||||
canAccessAdmin: boolean;
|
canAccessAdmin: boolean;
|
||||||
canAccessSupport: boolean;
|
canAccessSupport: boolean;
|
||||||
canAccessPartner: boolean;
|
canAccessPartner: boolean;
|
||||||
assignableRoles: UserRole[]; // Ajout de cette propriété
|
assignableRoles: UserRole[];
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AvailableRolesWithPermissions {
|
export interface AvailableRolesWithPermissions {
|
||||||
roles: AvailableRole[];
|
roles: (AvailableRole & { permissions: RolePermission })[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -42,102 +26,87 @@ export interface AvailableRolesWithPermissions {
|
|||||||
})
|
})
|
||||||
export class RoleManagementService {
|
export class RoleManagementService {
|
||||||
private hubUsersService = inject(HubUsersService);
|
private hubUsersService = inject(HubUsersService);
|
||||||
|
private merchantUsersService = inject(MerchantUsersService);
|
||||||
|
|
||||||
private availableRoles$ = new BehaviorSubject<AvailableRolesWithPermissions | null>(null);
|
private availableRoles$ = new BehaviorSubject<AvailableRolesWithPermissions | null>(null);
|
||||||
private currentUserRole$ = new BehaviorSubject<UserRole | 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(
|
return this.hubUsersService.getAvailableHubRoles().pipe(
|
||||||
map(apiResponse => {
|
map(apiResponse => ({
|
||||||
// Enrichir les rôles de l'API avec les permissions
|
roles: apiResponse.roles.map(role => ({
|
||||||
const rolesWithPermissions: AvailableRole[] = apiResponse.roles.map(role => ({
|
|
||||||
...role,
|
...role,
|
||||||
permissions: this.getPermissionsForRole(role.value)
|
permissions: this.getPermissionsForRole(role.value)
|
||||||
}));
|
}))
|
||||||
|
})),
|
||||||
|
tap(roles => this.availableRoles$.next(roles)),
|
||||||
|
catchError(error => this.getDefaultHubRoles(error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const result: AvailableRolesWithPermissions = {
|
/**
|
||||||
roles: rolesWithPermissions
|
* Charge les rôles Marchands disponibles
|
||||||
};
|
*/
|
||||||
|
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))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.availableRoles$.next(result);
|
/**
|
||||||
return result;
|
* Rôles Hub par défaut en cas d'erreur
|
||||||
}),
|
*/
|
||||||
catchError(error => {
|
private getDefaultHubRoles(error?: any): Observable<AvailableRolesWithPermissions> {
|
||||||
console.error('Error loading available roles:', error);
|
console.error('Error loading hub roles:', error);
|
||||||
// Fallback avec les rôles par défaut
|
|
||||||
const defaultRoles: AvailableRolesWithPermissions = {
|
const defaultRoles: AvailableRolesWithPermissions = {
|
||||||
roles: [
|
roles: [
|
||||||
{
|
UserRole.DCB_ADMIN,
|
||||||
value: UserRole.DCB_ADMIN,
|
UserRole.DCB_SUPPORT,
|
||||||
label: 'DCB Admin',
|
UserRole.DCB_PARTNER
|
||||||
description: 'Full administrative access to the entire system',
|
].map(role => ({
|
||||||
permissions: this.getPermissionsForRole(UserRole.DCB_ADMIN)
|
value: role,
|
||||||
},
|
label: this.getRoleLabel(role),
|
||||||
{
|
description: this.getRoleDescription(role),
|
||||||
value: UserRole.DCB_SUPPORT,
|
allowedForCreation: true,
|
||||||
label: 'DCB Support',
|
userType: UserType.HUB,
|
||||||
description: 'Support access with limited administrative capabilities',
|
permissions: this.getPermissionsForRole(role)
|
||||||
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);
|
this.availableRoles$.next(defaultRoles);
|
||||||
return of(defaultRoles);
|
return of(defaultRoles);
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère les rôles disponibles depuis le cache ou l'API
|
* Rôles Marchands par défaut en cas d'erreur
|
||||||
*/
|
*/
|
||||||
getAvailableRoles(): Observable<AvailableRolesWithPermissions> {
|
private getDefaultMerchantRoles(error?: any): Observable<AvailableRolesWithPermissions> {
|
||||||
const cached = this.availableRoles$.value;
|
console.error('Error loading merchant roles:', error);
|
||||||
if (cached) {
|
const defaultRoles: AvailableRolesWithPermissions = {
|
||||||
return of(cached);
|
roles: [
|
||||||
}
|
UserRole.DCB_PARTNER_ADMIN,
|
||||||
return this.loadAvailableRoles();
|
UserRole.DCB_PARTNER_MANAGER,
|
||||||
}
|
UserRole.DCB_PARTNER_SUPPORT
|
||||||
|
].map(role => ({
|
||||||
/**
|
value: role,
|
||||||
* Récupère les rôles disponibles sous forme simplifiée (pour les selects)
|
label: this.getRoleLabel(role),
|
||||||
*/
|
description: this.getRoleDescription(role),
|
||||||
getAvailableRolesSimple(): Observable<AvailableRoleResponse[]> {
|
allowedForCreation: true,
|
||||||
return this.getAvailableRoles().pipe(
|
userType: UserType.MERCHANT_PARTNER,
|
||||||
map(response => response.roles.map(role => ({
|
permissions: this.getPermissionsForRole(role)
|
||||||
value: role.value,
|
}))
|
||||||
label: role.label,
|
};
|
||||||
description: role.description,
|
this.availableRoles$.next(defaultRoles);
|
||||||
allowedForCreation: role.allowedForCreation
|
return of(defaultRoles);
|
||||||
})))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -154,11 +123,22 @@ export class RoleManagementService {
|
|||||||
return this.currentUserRole$.asObservable();
|
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
|
* Récupère les permissions détaillées selon le rôle
|
||||||
*/
|
*/
|
||||||
getPermissionsForRole(role: UserRole): RolePermission {
|
getPermissionsForRole(role: UserRole | null): RolePermission {
|
||||||
const allRoles = Object.values(UserRole);
|
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 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];
|
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
|
||||||
|
|
||||||
@ -198,10 +178,10 @@ export class RoleManagementService {
|
|||||||
canDeleteUsers: true,
|
canDeleteUsers: true,
|
||||||
canManageRoles: true,
|
canManageRoles: true,
|
||||||
canViewStats: true,
|
canViewStats: true,
|
||||||
canManageMerchants: true,
|
canManageMerchants: false,
|
||||||
canAccessAdmin: true,
|
canAccessAdmin: false,
|
||||||
canAccessSupport: true,
|
canAccessSupport: false,
|
||||||
canAccessPartner: true,
|
canAccessPartner: false,
|
||||||
assignableRoles: merchantRoles
|
assignableRoles: merchantRoles
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -209,20 +189,20 @@ export class RoleManagementService {
|
|||||||
return {
|
return {
|
||||||
canCreateUsers: true,
|
canCreateUsers: true,
|
||||||
canEditUsers: true,
|
canEditUsers: true,
|
||||||
canDeleteUsers: false,
|
canDeleteUsers: true,
|
||||||
canManageRoles: true,
|
canManageRoles: true,
|
||||||
canViewStats: true,
|
canViewStats: true,
|
||||||
canManageMerchants: true,
|
canManageMerchants: true,
|
||||||
canAccessAdmin: false,
|
canAccessAdmin: false,
|
||||||
canAccessSupport: false,
|
canAccessSupport: false,
|
||||||
canAccessPartner: true,
|
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:
|
case UserRole.DCB_PARTNER_MANAGER:
|
||||||
return {
|
return {
|
||||||
canCreateUsers: true,
|
canCreateUsers: false,
|
||||||
canEditUsers: true,
|
canEditUsers: false,
|
||||||
canDeleteUsers: false,
|
canDeleteUsers: false,
|
||||||
canManageRoles: false,
|
canManageRoles: false,
|
||||||
canViewStats: true,
|
canViewStats: true,
|
||||||
@ -243,25 +223,32 @@ export class RoleManagementService {
|
|||||||
canManageMerchants: false,
|
canManageMerchants: false,
|
||||||
canAccessAdmin: false,
|
canAccessAdmin: false,
|
||||||
canAccessSupport: false,
|
canAccessSupport: false,
|
||||||
canAccessPartner: false,
|
canAccessPartner: true,
|
||||||
assignableRoles: []
|
assignableRoles: []
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
return this.getDefaultPermissions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permissions par défaut (rôle inconnu ou non défini)
|
||||||
|
*/
|
||||||
|
private getDefaultPermissions(): RolePermission {
|
||||||
return {
|
return {
|
||||||
canCreateUsers: false,
|
canCreateUsers: false,
|
||||||
canEditUsers: false,
|
canEditUsers: false,
|
||||||
canDeleteUsers: false,
|
canDeleteUsers: false,
|
||||||
canManageRoles: false,
|
canManageRoles: false,
|
||||||
canViewStats: true,
|
canViewStats: false,
|
||||||
canManageMerchants: true,
|
canManageMerchants: false,
|
||||||
canAccessAdmin: false,
|
canAccessAdmin: false,
|
||||||
canAccessSupport: false,
|
canAccessSupport: false,
|
||||||
canAccessPartner: false,
|
canAccessPartner: false,
|
||||||
assignableRoles: []
|
assignableRoles: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si un rôle peut être attribué par l'utilisateur courant
|
* Vérifie si un rôle peut être attribué par l'utilisateur courant
|
||||||
@ -269,8 +256,14 @@ export class RoleManagementService {
|
|||||||
canAssignRole(currentUserRole: UserRole | null, targetRole: UserRole): boolean {
|
canAssignRole(currentUserRole: UserRole | null, targetRole: UserRole): boolean {
|
||||||
if (!currentUserRole) return false;
|
if (!currentUserRole) return false;
|
||||||
|
|
||||||
// SEUL DCB_PARTNER peut attribuer tous les rôles
|
// Rôles qui peuvent attribuer tous les rôles
|
||||||
if (currentUserRole === UserRole.DCB_PARTNER, currentUserRole === UserRole.DCB_ADMIN, currentUserRole === UserRole.DCB_SUPPORT) {
|
const fullPermissionRoles = [
|
||||||
|
UserRole.DCB_ADMIN,
|
||||||
|
UserRole.DCB_SUPPORT,
|
||||||
|
UserRole.DCB_PARTNER
|
||||||
|
];
|
||||||
|
|
||||||
|
if (fullPermissionRoles.includes(currentUserRole)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,35 +338,60 @@ export class RoleManagementService {
|
|||||||
/**
|
/**
|
||||||
* Récupère le libellé d'un rôle
|
* Récupère le libellé d'un rôle
|
||||||
*/
|
*/
|
||||||
getRoleLabel(role: UserRole): string {
|
getRoleLabel(role: string): string {
|
||||||
const roleLabels: { [key in UserRole]: string } = {
|
const userRole = role as UserRole;
|
||||||
[UserRole.DCB_ADMIN]: 'Administrateur DCB',
|
switch (userRole) {
|
||||||
[UserRole.DCB_SUPPORT]: 'Support DCB',
|
case UserRole.DCB_ADMIN:
|
||||||
[UserRole.DCB_PARTNER]: 'Partenaire DCB',
|
return 'Administrateur DCB';
|
||||||
[UserRole.DCB_PARTNER_ADMIN]: 'Admin Partenaire',
|
case UserRole.DCB_SUPPORT:
|
||||||
[UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire',
|
return 'Support DCB';
|
||||||
[UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire'
|
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
|
* Récupère la classe CSS pour un badge de rôle
|
||||||
*/
|
*/
|
||||||
getRoleBadgeClass(role: UserRole): string {
|
getRoleBadgeClass(role: string): string {
|
||||||
switch (role) {
|
const userRole = role as UserRole;
|
||||||
|
switch (userRole) {
|
||||||
case UserRole.DCB_ADMIN:
|
case UserRole.DCB_ADMIN:
|
||||||
return 'bg-danger';
|
return 'bg-danger';
|
||||||
case UserRole.DCB_SUPPORT:
|
case UserRole.DCB_SUPPORT:
|
||||||
return 'bg-info';
|
return 'bg-info';
|
||||||
case UserRole.DCB_PARTNER:
|
case UserRole.DCB_PARTNER:
|
||||||
return 'bg-success';
|
return 'bg-primary';
|
||||||
case UserRole.DCB_PARTNER_ADMIN:
|
case UserRole.DCB_PARTNER_ADMIN:
|
||||||
return 'bg-danger';
|
return 'bg-warning';
|
||||||
case UserRole.DCB_PARTNER_MANAGER:
|
case UserRole.DCB_PARTNER_MANAGER:
|
||||||
return 'bg-warning text-dark';
|
return 'bg-success';
|
||||||
case UserRole.DCB_PARTNER_SUPPORT:
|
case UserRole.DCB_PARTNER_SUPPORT:
|
||||||
return 'bg-info text-white';
|
return 'bg-secondary';
|
||||||
default:
|
default:
|
||||||
return 'bg-secondary';
|
return 'bg-secondary';
|
||||||
}
|
}
|
||||||
@ -382,8 +400,9 @@ export class RoleManagementService {
|
|||||||
/**
|
/**
|
||||||
* Récupère l'icône pour un rôle
|
* Récupère l'icône pour un rôle
|
||||||
*/
|
*/
|
||||||
getRoleIcon(role: UserRole): string {
|
getRoleIcon(role: string): string {
|
||||||
switch (role) {
|
const userRole = role as UserRole;
|
||||||
|
switch (userRole) {
|
||||||
case UserRole.DCB_ADMIN:
|
case UserRole.DCB_ADMIN:
|
||||||
return 'lucideShield';
|
return 'lucideShield';
|
||||||
case UserRole.DCB_SUPPORT:
|
case UserRole.DCB_SUPPORT:
|
||||||
@ -391,7 +410,7 @@ export class RoleManagementService {
|
|||||||
case UserRole.DCB_PARTNER:
|
case UserRole.DCB_PARTNER:
|
||||||
return 'lucideBuilding';
|
return 'lucideBuilding';
|
||||||
case UserRole.DCB_PARTNER_ADMIN:
|
case UserRole.DCB_PARTNER_ADMIN:
|
||||||
return 'lucideShield';
|
return 'lucideShieldCheck';
|
||||||
case UserRole.DCB_PARTNER_MANAGER:
|
case UserRole.DCB_PARTNER_MANAGER:
|
||||||
return 'lucideUserCog';
|
return 'lucideUserCog';
|
||||||
case UserRole.DCB_PARTNER_SUPPORT:
|
case UserRole.DCB_PARTNER_SUPPORT:
|
||||||
@ -401,6 +420,7 @@ export class RoleManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si un rôle est un rôle administrateur
|
* 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
|
* Vérifie si un rôle est un rôle marchand
|
||||||
*/
|
*/
|
||||||
isMerchantRole(role: UserRole): boolean {
|
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);
|
return merchantRoles.includes(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -442,23 +466,71 @@ export class RoleManagementService {
|
|||||||
*/
|
*/
|
||||||
getAssignableRoles(currentUserRole: UserRole | null): UserRole[] {
|
getAssignableRoles(currentUserRole: UserRole | null): UserRole[] {
|
||||||
if (!currentUserRole) return [];
|
if (!currentUserRole) return [];
|
||||||
|
return this.getPermissionsForRole(currentUserRole).assignableRoles;
|
||||||
const permissions = this.getPermissionsForRole(currentUserRole);
|
|
||||||
return permissions.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[] {
|
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
|
* Récupère uniquement les rôles Marchands
|
||||||
*/
|
*/
|
||||||
getMerchantRoles(): UserRole[] {
|
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 {
|
clearCache(): void {
|
||||||
this.availableRoles$.next(null);
|
this.availableRoles$.next(null);
|
||||||
|
this.currentUserRole$.next(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les rôles disponibles (observable)
|
||||||
|
*/
|
||||||
|
getAvailableRoles(): Observable<AvailableRolesWithPermissions | null> {
|
||||||
|
return this.availableRoles$.asObservable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,11 +76,6 @@ export class MenuService {
|
|||||||
icon: 'lucideCreditCard',
|
icon: 'lucideCreditCard',
|
||||||
url: '/transactions',
|
url: '/transactions',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Gestions Merchants/Partenaires',
|
|
||||||
icon: 'lucideStore',
|
|
||||||
url: '/merchant-partners'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Opérateurs',
|
label: 'Opérateurs',
|
||||||
icon: 'lucideServer',
|
icon: 'lucideServer',
|
||||||
@ -104,12 +99,18 @@ export class MenuService {
|
|||||||
|
|
||||||
{ label: 'Utilisateurs & Sécurité', isTitle: true },
|
{ label: 'Utilisateurs & Sécurité', isTitle: true },
|
||||||
{
|
{
|
||||||
label: 'Gestion des Utilisateurs',
|
label: 'Utilisateurs Hub',
|
||||||
icon: 'lucideUsers',
|
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: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' },
|
||||||
{ label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' },
|
{ label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' },
|
||||||
|
|
||||||
|
|||||||
@ -23,8 +23,14 @@ export class PermissionsService {
|
|||||||
module: 'transactions',
|
module: 'transactions',
|
||||||
roles: this.allRoles,
|
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,
|
roles: this.allRoles,
|
||||||
},
|
},
|
||||||
// Operators - Admin seulement
|
// Operators - Admin seulement
|
||||||
@ -46,16 +52,18 @@ export class PermissionsService {
|
|||||||
'retry': [UserRole.DCB_ADMIN]
|
'retry': [UserRole.DCB_ADMIN]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Users - Admin et Support
|
|
||||||
{
|
|
||||||
module: 'users',
|
|
||||||
roles: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT]
|
|
||||||
},
|
|
||||||
// Settings - Tout le monde
|
// Settings - Tout le monde
|
||||||
{
|
{
|
||||||
module: 'settings',
|
module: 'settings',
|
||||||
roles: this.allRoles
|
roles: this.allRoles
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Settings - Tout le monde
|
||||||
|
{
|
||||||
|
module: 'merchant-configs',
|
||||||
|
roles: this.allRoles
|
||||||
|
},
|
||||||
|
|
||||||
// Integrations - Admin seulement
|
// Integrations - Admin seulement
|
||||||
{
|
{
|
||||||
module: 'integrations',
|
module: 'integrations',
|
||||||
|
|||||||
@ -59,11 +59,6 @@ export const menuItems: MenuItemType[] = [
|
|||||||
icon: 'lucideCreditCard',
|
icon: 'lucideCreditCard',
|
||||||
url: '/transactions',
|
url: '/transactions',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Gestions Merchants/Partners',
|
|
||||||
icon: 'lucideStore',
|
|
||||||
url: '/merchant-partners'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Opérateurs',
|
label: 'Opérateurs',
|
||||||
icon: 'lucideServer',
|
icon: 'lucideServer',
|
||||||
@ -104,18 +99,26 @@ export const menuItems: MenuItemType[] = [
|
|||||||
// ---------------------------
|
// ---------------------------
|
||||||
{ label: 'Utilisateurs & Sécurité', isTitle: true },
|
{ label: 'Utilisateurs & Sécurité', isTitle: true },
|
||||||
{
|
{
|
||||||
label: 'Gestion des Utilisateurs',
|
label: 'Utilisateurs Hub',
|
||||||
icon: 'lucideUsers',
|
icon: 'lucideUsers',
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
children: [
|
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
|
// 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: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' },
|
||||||
{ label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' },
|
{ label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' },
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,16 @@
|
|||||||
src="assets/images/users/user-2.jpg"
|
src="assets/images/users/user-2.jpg"
|
||||||
class="rounded-circle me-2"
|
class="rounded-circle me-2"
|
||||||
width="36"
|
width="36"
|
||||||
|
height="36"
|
||||||
alt="user-image"
|
alt="user-image"
|
||||||
|
onerror="this.src='assets/images/users/user-default.jpg'"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h5 class="my-0 fw-semibold">{{ user?.firstName }} - {{ user?.lastName }}</h5>
|
<h5 class="my-0 fw-semibold">
|
||||||
<h6 class="my-0 text-muted">Administrateur</h6>
|
{{ getUserInitials() }} | {{ getDisplayName() }}
|
||||||
|
</h5>
|
||||||
|
<h6 class="my-0 text-muted">
|
||||||
|
{{ getUserRole(user) }}
|
||||||
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -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 { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { userDropdownItems } from '@layouts/components/data';
|
import { userDropdownItems } from '@layouts/components/data';
|
||||||
import { AuthService } from '@/app/core/services/auth.service';
|
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({
|
@Component({
|
||||||
selector: 'app-user-profile',
|
selector: 'app-user-profile',
|
||||||
@ -9,27 +11,87 @@ import { AuthService } from '@/app/core/services/auth.service';
|
|||||||
imports: [NgbCollapseModule],
|
imports: [NgbCollapseModule],
|
||||||
templateUrl: './user-profile.component.html',
|
templateUrl: './user-profile.component.html',
|
||||||
})
|
})
|
||||||
export class UserProfileComponent {
|
export class UserProfileComponent implements OnInit, OnDestroy {
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
private cdr = inject(ChangeDetectorRef);
|
private cdr = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
user: any = null;
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
constructor() {
|
user: User | null = null;
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
this.loadUser();
|
this.loadUser();
|
||||||
this.authService.onAuthState().subscribe(() => this.loadUser());
|
|
||||||
}
|
|
||||||
|
|
||||||
loadUser() {
|
// Subscribe to auth state changes
|
||||||
this.authService.getProfile().subscribe({
|
this.authService.getAuthState()
|
||||||
next: profile => {
|
.pipe(takeUntil(this.destroy$))
|
||||||
this.user = profile;
|
.subscribe({
|
||||||
this.cdr.detectChanges();
|
next: (isAuthenticated) => {
|
||||||
},
|
if (isAuthenticated) {
|
||||||
error: () => {
|
this.loadUser();
|
||||||
|
} else {
|
||||||
this.user = null;
|
this.user = null;
|
||||||
this.cdr.detectChanges();
|
this.isLoading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -51,7 +51,7 @@ import { credits, currentYear } from '@/app/constants'
|
|||||||
<div class="mt-4 p-3 bg-light rounded">
|
<div class="mt-4 p-3 bg-light rounded">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<i class="fas fa-info-circle me-1"></i>
|
<i class="fas fa-info-circle me-1"></i>
|
||||||
Rôles requis : Administrateur, Gestionnaire ou Support
|
Rôles requis : Administrateur, Partenaire ou Support
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,12 +1,38 @@
|
|||||||
<app-ui-card title="Liste des Utilisateurs Hub">
|
<app-ui-card [title]="'Liste des Utilisateurs Hub'">
|
||||||
<a
|
<a
|
||||||
helper-text
|
helper-text
|
||||||
href="javascript:void(0);"
|
href="javascript:void(0);"
|
||||||
class="icon-link icon-link-hover link-primary fw-semibold"
|
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>
|
</a>
|
||||||
|
|
||||||
<div card-body>
|
<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 -->
|
<!-- Barre d'actions supérieure -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@ -19,8 +45,61 @@
|
|||||||
[class.active]="roleFilter === 'all'"
|
[class.active]="roleFilter === 'all'"
|
||||||
(click)="filterByRole('all')"
|
(click)="filterByRole('all')"
|
||||||
>
|
>
|
||||||
Tous ({{ allUsers.length }})
|
Tous ({{ getTotalUsersCount() }})
|
||||||
</button>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-danger"
|
class="btn btn-outline-danger"
|
||||||
@ -45,27 +124,48 @@
|
|||||||
>
|
>
|
||||||
Partenaires ({{ getUsersCountByRole(UserRole.DCB_PARTNER) }})
|
Partenaires ({{ getUsersCountByRole(UserRole.DCB_PARTNER) }})
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="d-flex justify-content-end gap-2">
|
<div class="d-flex justify-content-end gap-2">
|
||||||
@if (canCreateUsers) {
|
@if (showCreateButton) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
(click)="openCreateModal.emit()"
|
(click)="openCreateUserModal.emit()"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
||||||
Nouvel Utilisateur
|
Nouvel Utilisateur Hub
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Barre de recherche et filtres -->
|
<!-- Barre de recherche et filtres avancés -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-3">
|
<div class="col-md-4">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<ng-icon name="lucideSearch"></ng-icon>
|
<ng-icon name="lucideSearch"></ng-icon>
|
||||||
@ -73,46 +173,46 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Nom, email, username..."
|
placeholder="Rechercher par nom, email..."
|
||||||
[(ngModel)]="searchTerm"
|
[(ngModel)]="searchTerm"
|
||||||
(keyup.enter)="onSearch()"
|
(input)="onSearch()"
|
||||||
|
[disabled]="loading"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<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="all">Tous les statuts</option>
|
||||||
<option value="enabled">Activés ({{ getEnabledUsersCount() }})</option>
|
<option value="enabled">Activés seulement</option>
|
||||||
<option value="disabled">Désactivés ({{ getDisabledUsersCount() }})</option>
|
<option value="disabled">Désactivés seulement</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<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="all">Tous les emails</option>
|
||||||
<option value="verified">Email vérifié</option>
|
<option value="verified">Email vérifié</option>
|
||||||
<option value="not-verified">Email non vérifié</option>
|
<option value="not-verified">Email non vérifié</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<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) {
|
@for (role of availableRoles; track role.value) {
|
||||||
<option [value]="role.value">{{ role.label }}</option>
|
<option [value]="role.value">{{ role.label }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="d-flex gap-2">
|
<div class="col-md-2">
|
||||||
<button class="btn btn-outline-primary" (click)="onSearch()">
|
<button class="btn btn-outline-secondary w-100" (click)="onClearFilters()" [disabled]="loading">
|
||||||
<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>
|
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||||
Réinitialiser
|
Effacer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
@if (loading) {
|
@if (loading) {
|
||||||
@ -130,6 +230,7 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
<div>{{ error }}</div>
|
<div>{{ error }}</div>
|
||||||
|
<button class="btn-close ms-auto" (click)="error = ''"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -140,6 +241,14 @@
|
|||||||
<table class="table table-hover table-striped">
|
<table class="table table-hover table-striped">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<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">
|
<th (click)="sort('username')" class="cursor-pointer">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span>Utilisateur</span>
|
<span>Utilisateur</span>
|
||||||
@ -152,12 +261,7 @@
|
|||||||
<ng-icon [name]="getSortIcon('email')" class="ms-1 fs-12"></ng-icon>
|
<ng-icon [name]="getSortIcon('email')" class="ms-1 fs-12"></ng-icon>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th (click)="sort('role')" class="cursor-pointer">
|
<th>Rôle Principal</th>
|
||||||
<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 (click)="sort('enabled')" class="cursor-pointer">
|
<th (click)="sort('enabled')" class="cursor-pointer">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span>Statut</span>
|
<span>Statut</span>
|
||||||
@ -176,6 +280,33 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@for (user of displayedUsers; track user.id) {
|
@for (user of displayedUsers; track user.id) {
|
||||||
<tr>
|
<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>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<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">
|
<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>
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-warning btn-sm"
|
class="btn btn-outline-warning btn-sm"
|
||||||
(click)="resetPassword(user)"
|
(click)="resetPasswordRequested(user)"
|
||||||
title="Réinitialiser le mot de passe"
|
title="Réinitialiser le mot de passe"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideKey"></ng-icon>
|
<ng-icon name="lucideKey"></ng-icon>
|
||||||
@ -251,10 +382,10 @@
|
|||||||
<ng-icon name="lucideUserCheck"></ng-icon>
|
<ng-icon name="lucideUserCheck"></ng-icon>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (canDeleteUsers) {
|
@if (showDeleteButton) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-danger btn-sm"
|
class="btn btn-outline-danger btn-sm"
|
||||||
(click)="deleteUser(user)"
|
(click)="deleteUserRequested(user)"
|
||||||
title="Supprimer l'utilisateur"
|
title="Supprimer l'utilisateur"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideTrash2"></ng-icon>
|
<ng-icon name="lucideTrash2"></ng-icon>
|
||||||
@ -266,13 +397,13 @@
|
|||||||
}
|
}
|
||||||
@empty {
|
@empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="text-center py-4">
|
<td [attr.colspan]="getColumnCount()" class="text-center py-4">
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
<ng-icon name="lucideUsers" class="fs-1 mb-3 opacity-50"></ng-icon>
|
<ng-icon name="lucideUsers" class="fs-1 mb-3 opacity-50"></ng-icon>
|
||||||
<h5 class="mb-2">Aucun utilisateur trouvé</h5>
|
<h5 class="mb-2">Aucun utilisateur trouvé</h5>
|
||||||
<p class="mb-3">Aucun utilisateur ne correspond à vos critères de recherche.</p>
|
<p class="mb-3">Aucun utilisateur ne correspond à vos critères de recherche.</p>
|
||||||
@if (canCreateUsers) {
|
@if (showCreateButton) {
|
||||||
<button class="btn btn-primary" (click)="openCreateModal.emit()">
|
<button class="btn btn-primary" (click)="openCreateUserModal.emit()">
|
||||||
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
||||||
Créer le premier utilisateur
|
Créer le premier utilisateur
|
||||||
</button>
|
</button>
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,14 +8,14 @@
|
|||||||
@if (user) {
|
@if (user) {
|
||||||
{{ getUserDisplayName() }}
|
{{ getUserDisplayName() }}
|
||||||
} @else {
|
} @else {
|
||||||
Profil Utilisateur
|
Profil Utilisateur Hub
|
||||||
}
|
}
|
||||||
</h4>
|
</h4>
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb mb-0">
|
<ol class="breadcrumb mb-0">
|
||||||
<li class="breadcrumb-item">
|
<li class="breadcrumb-item">
|
||||||
<a href="javascript:void(0)" (click)="back.emit()" class="text-decoration-none cursor-pointer">
|
<a href="javascript:void(0)" (click)="goBack()" class="text-decoration-none cursor-pointer">
|
||||||
Utilisateurs
|
Utilisateurs Hub
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">
|
<li class="breadcrumb-item active" aria-current="page">
|
||||||
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<!-- Bouton de réinitialisation de mot de passe -->
|
<!-- Bouton de réinitialisation de mot de passe -->
|
||||||
@if (user && canEditUsers && !isEditing) {
|
@if (user && canEditUser() && !isEditing) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-warning"
|
class="btn btn-warning"
|
||||||
(click)="resetPassword()"
|
(click)="resetPassword()"
|
||||||
@ -41,7 +41,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Bouton activation/désactivation -->
|
<!-- Bouton activation/désactivation -->
|
||||||
@if (user.enabled) {
|
@if (user.enabled && canToggleStatus()) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-warning"
|
class="btn btn-outline-warning"
|
||||||
(click)="disableUser()"
|
(click)="disableUser()"
|
||||||
@ -49,7 +49,7 @@
|
|||||||
<ng-icon name="lucidePause" class="me-1"></ng-icon>
|
<ng-icon name="lucidePause" class="me-1"></ng-icon>
|
||||||
Désactiver
|
Désactiver
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else if (!user.enabled && canToggleStatus()) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-success"
|
class="btn btn-outline-success"
|
||||||
(click)="enableUser()"
|
(click)="enableUser()"
|
||||||
@ -74,7 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Indicateur de permissions -->
|
<!-- Indicateur de permissions -->
|
||||||
@if (currentUserRole && !canEditUsers) {
|
@if (currentUserRole && !canEditUser()) {
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
@ -95,6 +95,7 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
<div>{{ error }}</div>
|
<div>{{ error }}</div>
|
||||||
|
<button class="btn-close ms-auto" (click)="clearMessages()"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -104,6 +105,7 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||||
<div>{{ success }}</div>
|
<div>{{ success }}</div>
|
||||||
|
<button class="btn-close ms-auto" (click)="clearMessages()"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -115,7 +117,7 @@
|
|||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Chargement...</span>
|
<span class="visually-hidden">Chargement...</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-muted">Chargement du profil...</p>
|
<p class="mt-2 text-muted">Chargement du profil utilisateur Hub...</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,8 +141,13 @@
|
|||||||
<h5>{{ getUserDisplayName() }}</h5>
|
<h5>{{ getUserDisplayName() }}</h5>
|
||||||
<p class="text-muted mb-2">@{{ user.username }}</p>
|
<p class="text-muted mb-2">@{{ user.username }}</p>
|
||||||
|
|
||||||
|
<!-- Type d'utilisateur -->
|
||||||
|
<span class="badge bg-primary mb-2">
|
||||||
|
Utilisateur Hub
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- Statut -->
|
<!-- Statut -->
|
||||||
<span [class]="getStatusBadgeClass()" class="mb-3">
|
<span [class]="getStatusBadgeClass()" class="mb-3 d-block">
|
||||||
{{ getStatusText() }}
|
{{ getStatusText() }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@ -155,12 +162,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
||||||
<small>Créé le {{ formatTimestamp(user.createdTimestamp) }}</small>
|
<small>Créé le {{ getCreationDate() }}</small>
|
||||||
</div>
|
</div>
|
||||||
@if (user.lastLogin) {
|
@if (user.lastLogin) {
|
||||||
<div class="d-flex align-items-center mt-2">
|
<div class="d-flex align-items-center mt-2">
|
||||||
<ng-icon name="lucideLogIn" class="me-2 text-muted"></ng-icon>
|
<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>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -170,60 +177,68 @@
|
|||||||
<!-- Carte rôle utilisateur -->
|
<!-- Carte rôle utilisateur -->
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title mb-0">Rôle Utilisateur</h5>
|
<h5 class="card-title mb-0">
|
||||||
@if (canManageRoles && !isEditing) {
|
Rôle Utilisateur
|
||||||
|
</h5>
|
||||||
|
@if (showRoleManagement()) {
|
||||||
<span class="badge bg-info">Modifiable</span>
|
<span class="badge bg-info">Modifiable</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- Rôle actuel -->
|
<!-- Rôles actuels -->
|
||||||
<div class="text-center mb-3">
|
<div class="text-center mb-3">
|
||||||
<span class="badge d-flex align-items-center justify-content-center" [ngClass]="getRoleBadgeClass(user.role)">
|
@if (getUserRole()) {
|
||||||
<ng-icon [name]="getRoleIcon(user.role)" class="me-2"></ng-icon>
|
<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) }}
|
{{ getRoleLabel(user.role) }}
|
||||||
</span>
|
</span>
|
||||||
<small class="text-muted d-block mt-1">
|
</div>
|
||||||
|
|
||||||
|
<!-- Description du rôle principal -->
|
||||||
|
<small class="text-muted d-block">
|
||||||
{{ getRoleDescription(user.role) }}
|
{{ getRoleDescription(user.role) }}
|
||||||
</small>
|
</small>
|
||||||
|
} @else {
|
||||||
|
<span class="badge bg-secondary">Aucun rôle</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Changement de rôle -->
|
<!-- Changement de rôle -->
|
||||||
@if (canManageRoles && !isEditing) {
|
@if (showRoleManagement()) {
|
||||||
<div class="mt-3">
|
<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
|
<select
|
||||||
class="form-select"
|
class="form-select"
|
||||||
[value]="user.role"
|
[value]="currentUserRole"
|
||||||
(change)="updateUserRole($any($event.target).value)"
|
(change)="updateUserRole($any($event.target).value)"
|
||||||
[disabled]="updatingRoles"
|
[disabled]="updatingRole"
|
||||||
>
|
>
|
||||||
<option value="" disabled>Sélectionnez un nouveau rôle</option>
|
<option value="" disabled>Sélectionnez un nouveau rôle</option>
|
||||||
@for (role of availableRoles; track role.value) {
|
@for (role of getAssignableRoles(); track role) {
|
||||||
<option
|
<option
|
||||||
[value]="role.value"
|
[value]="role"
|
||||||
[disabled]="!canAssignRole(role.value) || role.value === user.role"
|
[disabled]="role === currentUserRole"
|
||||||
>
|
>
|
||||||
{{ role.label }}
|
{{ getRoleLabel(role) }}
|
||||||
@if (!canAssignRole(role.value)) {
|
@if (role === currentUserRole) {
|
||||||
(Non autorisé)
|
|
||||||
} @else if (role.value === user.role) {
|
|
||||||
(Actuel)
|
(Actuel)
|
||||||
}
|
}
|
||||||
</option>
|
</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
@if (updatingRoles) {
|
@if (updatingRole) {
|
||||||
<div class="spinner-border spinner-border-sm me-1" role="status">
|
<div class="spinner-border spinner-border-sm me-1" role="status">
|
||||||
<span class="visually-hidden">Mise à jour...</span>
|
<span class="visually-hidden">Mise à jour...</span>
|
||||||
</div>
|
</div>
|
||||||
Mise à jour en cours...
|
Mise à jour en cours...
|
||||||
} @else {
|
} @else {
|
||||||
Sélectionnez un nouveau rôle pour cet utilisateur
|
Sélectionnez un nouveau rôle principal pour cet utilisateur
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} @else if (!canManageRoles) {
|
} @else if (!canManageRoles()) {
|
||||||
<div class="alert alert-info mt-3">
|
<div class="alert alert-info mt-3">
|
||||||
<small>
|
<small>
|
||||||
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||||
@ -243,16 +258,16 @@
|
|||||||
<div class="row g-2 small">
|
<div class="row g-2 small">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<strong>Créé par :</strong>
|
<strong>Créé par :</strong>
|
||||||
<div class="text-muted">{{ user.createdByUsername || 'Système' }}</div>
|
<div class="text-muted">{{ getCreatorName() }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<strong>Date de création :</strong>
|
<strong>Date de création :</strong>
|
||||||
<div class="text-muted">{{ formatTimestamp(user.createdTimestamp) }}</div>
|
<div class="text-muted">{{ getCreationDate() }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<strong>Type d'utilisateur :</strong>
|
<strong>Type d'utilisateur :</strong>
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
<span class="badge bg-secondary">{{ user.userType }}</span>
|
<span class="badge bg-primary">Utilisateur Hub</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -289,7 +304,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn btn-success btn-sm"
|
class="btn btn-success btn-sm"
|
||||||
(click)="saveProfile()"
|
(click)="saveProfile()"
|
||||||
[disabled]="saving"
|
[disabled]="saving || !isFormValid()"
|
||||||
>
|
>
|
||||||
@if (saving) {
|
@if (saving) {
|
||||||
<div class="spinner-border spinner-border-sm me-1" role="status">
|
<div class="spinner-border spinner-border-sm me-1" role="status">
|
||||||
@ -403,6 +418,24 @@
|
|||||||
</div>
|
</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 -->
|
<!-- Informations système -->
|
||||||
@if (!isEditing) {
|
@if (!isEditing) {
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@ -421,19 +454,19 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Date de création</label>
|
<label class="form-label">Date de création</label>
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
{{ formatTimestamp(user.createdTimestamp) }}
|
{{ getCreationDate() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Créé par</label>
|
<label class="form-label">Créé par</label>
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
{{ user.createdByUsername || 'Système' }}
|
{{ getCreatorName() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Type d'utilisateur</label>
|
<label class="form-label">Type d'utilisateur</label>
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
<span class="badge bg-secondary">{{ user.userType }}</span>
|
<span class="badge bg-primary">Utilisateur Hub</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -444,7 +477,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions supplémentaires -->
|
<!-- Actions supplémentaires -->
|
||||||
@if (!isEditing && canEditUsers) {
|
@if (!isEditing && canEditUser()) {
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header bg-light">
|
||||||
<h6 class="card-title mb-0">Actions de Gestion</h6>
|
<h6 class="card-title mb-0">Actions de Gestion</h6>
|
||||||
@ -455,13 +488,14 @@
|
|||||||
<button
|
<button
|
||||||
class="btn btn-outline-warning w-100"
|
class="btn btn-outline-warning w-100"
|
||||||
(click)="resetPassword()"
|
(click)="resetPassword()"
|
||||||
|
[disabled]="!canResetPassword()"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
||||||
Réinitialiser MDP
|
Réinitialiser MDP
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
@if (user.enabled) {
|
@if (user.enabled && canToggleStatus()) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-secondary w-100"
|
class="btn btn-outline-secondary w-100"
|
||||||
(click)="disableUser()"
|
(click)="disableUser()"
|
||||||
@ -469,7 +503,7 @@
|
|||||||
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
|
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
|
||||||
Désactiver
|
Désactiver
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else if (!user.enabled && canToggleStatus()) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-success w-100"
|
class="btn btn-outline-success w-100"
|
||||||
(click)="enableUser()"
|
(click)="enableUser()"
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,52 @@
|
|||||||
<!-- src/app/modules/merchant-users/merchant-users.html -->
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<app-page-title
|
<app-page-title
|
||||||
title="Gestion des Utilisateurs Marchands"
|
[title]="pageTitle"
|
||||||
subTitle="Administrez les utilisateurs de votre écosystème marchand"
|
[subTitle]="pageSubtitle"
|
||||||
[badge]="{icon:'lucideUsers', text:'Merchant Users'}"
|
[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="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<ul
|
<ul
|
||||||
ngbNav
|
ngbNav
|
||||||
#merchantUsersNav="ngbNav"
|
#usersNav="ngbNav"
|
||||||
[activeId]="activeTab"
|
[activeId]="activeTab"
|
||||||
[destroyOnHide]="false"
|
[destroyOnHide]="false"
|
||||||
class="nav nav-tabs nav-justified nav-bordered nav-bordered-primary mb-3"
|
class="nav nav-tabs nav-justified nav-bordered nav-bordered-primary mb-3"
|
||||||
@ -19,14 +54,17 @@
|
|||||||
<li [ngbNavItem]="'list'">
|
<li [ngbNavItem]="'list'">
|
||||||
<a ngbNavLink (click)="showTab('list')">
|
<a ngbNavLink (click)="showTab('list')">
|
||||||
<ng-icon name="lucideUsers" class="fs-lg me-md-1 d-inline-flex align-middle" />
|
<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>
|
</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<app-merchant-users-list
|
<app-hub-users-list
|
||||||
|
#hubUsersList
|
||||||
|
[canCreateUsers]="canCreateUsers"
|
||||||
|
[canDeleteUsers]="canDeleteUsers"
|
||||||
(userSelected)="onUserSelected($event)"
|
(userSelected)="onUserSelected($event)"
|
||||||
(openCreateModal)="openCreateUserModal()"
|
(openCreateUserModal)="openCreateUserModal()"
|
||||||
(openResetPasswordModal)="onResetPasswordRequested($event)"
|
(resetPasswordRequested)="onResetPasswordRequested($event)"
|
||||||
(openDeleteUserModal)="onDeleteUserRequested($event)"
|
(deleteUserRequested)="onDeleteUserRequested($event)"
|
||||||
/>
|
/>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
@ -38,10 +76,10 @@
|
|||||||
</a>
|
</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
@if (selectedUserId) {
|
@if (selectedUserId) {
|
||||||
<app-merchant-user-profile
|
<app-hub-user-profile
|
||||||
[userId]="selectedUserId"
|
[userId]="selectedUserId"
|
||||||
|
(resetPasswordRequested)="onResetPasswordRequested($event)"
|
||||||
(back)="backToList()"
|
(back)="backToList()"
|
||||||
(openResetPasswordModal)="onResetPasswordRequested($event)"
|
|
||||||
/>
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="alert alert-warning text-center">
|
<div class="alert alert-warning text-center">
|
||||||
@ -53,17 +91,17 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content" [ngbNavOutlet]="merchantUsersNav"></div>
|
<div class="tab-content" [ngbNavOutlet]="usersNav"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal de création d'utilisateur marchand -->
|
<!-- Modal de création d'utilisateur Hub -->
|
||||||
<ng-template #createUserModal let-modal>
|
<ng-template #createUserModal let-modal>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">
|
<h4 class="modal-title">
|
||||||
<ng-icon name="lucideUserPlus" class="me-2"></ng-icon>
|
<ng-icon name="lucideUserPlus" class="me-2"></ng-icon>
|
||||||
Créer un nouvel utilisateur marchand
|
Créer un nouvel utilisateur Hub
|
||||||
</h4>
|
</h4>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -83,30 +121,21 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<form (ngSubmit)="createMerchantUser()" #userForm="ngForm">
|
<!-- Avertissement permissions -->
|
||||||
|
@if (!canManageRoles && assignableRoles.length === 1) {
|
||||||
<div class="row g-3">
|
<div class="alert alert-warning">
|
||||||
<!-- Merchant Partner ID (lecture seule) -->
|
<small>
|
||||||
<div class="col-md-6">
|
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||||
<label class="form-label">Merchant Partner ID</label>
|
<strong>Permissions limitées :</strong> Vous ne pouvez créer que des utilisateurs avec le rôle
|
||||||
<div class="form-control-plaintext font-monospace small">
|
<span class="badge" [ngClass]="getRoleBadgeClass(assignableRoles[0])">
|
||||||
{{ currentMerchantPartnerId || 'Chargement...' }}
|
{{ getRoleLabel(assignableRoles[0]) }}
|
||||||
<input
|
</span>
|
||||||
type="hidden"
|
</small>
|
||||||
[(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>
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<form (ngSubmit)="createUser()" #userForm="ngForm">
|
||||||
|
<div class="row g-3">
|
||||||
<!-- Informations de base -->
|
<!-- Informations de base -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
@ -116,7 +145,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Entrez le prénom"
|
placeholder="Entrez le prénom"
|
||||||
[(ngModel)]="newMerchantUser.firstName"
|
[(ngModel)]="newUser.firstName"
|
||||||
name="firstName"
|
name="firstName"
|
||||||
required
|
required
|
||||||
[disabled]="creatingUser"
|
[disabled]="creatingUser"
|
||||||
@ -137,7 +166,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Entrez le nom"
|
placeholder="Entrez le nom"
|
||||||
[(ngModel)]="newMerchantUser.lastName"
|
[(ngModel)]="newUser.lastName"
|
||||||
name="lastName"
|
name="lastName"
|
||||||
required
|
required
|
||||||
[disabled]="creatingUser"
|
[disabled]="creatingUser"
|
||||||
@ -158,7 +187,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Nom d'utilisateur unique"
|
placeholder="Nom d'utilisateur unique"
|
||||||
[(ngModel)]="newMerchantUser.username"
|
[(ngModel)]="newUser.username"
|
||||||
name="username"
|
name="username"
|
||||||
required
|
required
|
||||||
[disabled]="creatingUser"
|
[disabled]="creatingUser"
|
||||||
@ -180,7 +209,7 @@
|
|||||||
type="email"
|
type="email"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="email@exemple.com"
|
placeholder="email@exemple.com"
|
||||||
[(ngModel)]="newMerchantUser.email"
|
[(ngModel)]="newUser.email"
|
||||||
name="email"
|
name="email"
|
||||||
required
|
required
|
||||||
email
|
email
|
||||||
@ -203,17 +232,27 @@
|
|||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
Mot de passe <span class="text-danger">*</span>
|
Mot de passe <span class="text-danger">*</span>
|
||||||
</label>
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
type="password"
|
[type]="showPassword ? 'text' : 'password'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Mot de passe sécurisé"
|
placeholder="Mot de passe sécurisé"
|
||||||
[(ngModel)]="newMerchantUser.password"
|
[(ngModel)]="newUser.password"
|
||||||
name="password"
|
name="password"
|
||||||
required
|
required
|
||||||
minlength="8"
|
minlength="8"
|
||||||
[disabled]="creatingUser"
|
[disabled]="creatingUser"
|
||||||
#password="ngModel"
|
#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">
|
<div class="form-text">
|
||||||
Le mot de passe doit contenir au moins 8 caractères.
|
Le mot de passe doit contenir au moins 8 caractères.
|
||||||
</div>
|
</div>
|
||||||
@ -229,56 +268,56 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sélection du rôle -->
|
<!-- Sélection du rôle unique -->
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
Rôle <span class="text-danger">*</span>
|
Rôle Principal <span class="text-danger">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
class="form-select"
|
class="form-select"
|
||||||
[(ngModel)]="newMerchantUser.role"
|
[value]="newUser.role"
|
||||||
|
(change)="onRoleSelectionChange($any($event.target).value)"
|
||||||
name="role"
|
name="role"
|
||||||
required
|
required
|
||||||
[disabled]="creatingUser || !canManageAllRoles"
|
[disabled]="creatingUser || !canManageRoles"
|
||||||
#roleSelect="ngModel"
|
|
||||||
>
|
>
|
||||||
<option value="" disabled selected>Sélectionnez un rôle</option>
|
<option value="" disabled>Sélectionnez un rôle</option>
|
||||||
@for (role of getAvailableRoles(); track role.value) {
|
@for (role of availableRoles; track role.value) {
|
||||||
@if (isMerchantRole(role.value) && isRoleAllowedForCreation(role.value)) {
|
|
||||||
<option
|
<option
|
||||||
[value]="role.value"
|
[value]="role.value"
|
||||||
[disabled]="!canAssignRole(role.value)"
|
[disabled]="!canAssignRole(role.value)"
|
||||||
>
|
>
|
||||||
{{ getRoleDisplayName(role.value) }} - {{ role.description }}
|
{{ role.label }} - {{ role.description }}
|
||||||
@if (!canAssignRole(role.value)) {
|
@if (!canAssignRole(role.value)) {
|
||||||
(Non autorisé)
|
(Non autorisé)
|
||||||
}
|
}
|
||||||
</option>
|
</option>
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
@if (canManageAllRoles) {
|
@if (canManageRoles) {
|
||||||
<span class="text-success">
|
Sélectionnez le rôle principal à assigner à cet utilisateur
|
||||||
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
|
||||||
Vous pouvez attribuer tous les rôles
|
|
||||||
</span>
|
|
||||||
} @else {
|
} @else {
|
||||||
<span class="text-warning">
|
Vous ne pouvez pas modifier les rôles disponibles
|
||||||
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
}
|
||||||
Permissions de rôle limitées
|
</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>
|
</span>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
@if (roleSelect.invalid && roleSelect.touched) {
|
<div class="form-text">
|
||||||
<div class="text-danger small">
|
Tous les utilisateurs créés ici sont des utilisateurs Hub
|
||||||
Le rôle est requis
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Avertissement pour les non-DCB_PARTNER -->
|
<!-- Avertissement pour les non-DCB_PARTNER -->
|
||||||
@if (!canManageAllRoles) {
|
@if (!canManageRoles) {
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
@ -295,23 +334,22 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Aperçu du rôle sélectionné -->
|
@if (newUser.role) {
|
||||||
@if (newMerchantUser.role) {
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon
|
<ng-icon
|
||||||
[name]="getRoleIcon(newMerchantUser.role)"
|
[name]="getRoleIcon(newUser.role)"
|
||||||
class="me-2"
|
class="me-2"
|
||||||
></ng-icon>
|
></ng-icon>
|
||||||
<div>
|
<div>
|
||||||
<strong>Rôle sélectionné :</strong>
|
<strong>Rôle sélectionné :</strong>
|
||||||
<span class="badge ms-2" [ngClass]="getRoleBadgeClass(newMerchantUser.role)">
|
<span class="badge ms-2" [ngClass]="getRoleBadgeClass(newUser.role)">
|
||||||
{{ getRoleDisplayName(newMerchantUser.role) }}
|
{{ getRoleLabel(newUser.role) }}
|
||||||
</span>
|
</span>
|
||||||
<br>
|
<br>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
{{ getRoleDescription(newMerchantUser.role) }}
|
{{ getRoleDescription(newUser.role) }}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -326,7 +364,7 @@
|
|||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="enabledSwitch"
|
id="enabledSwitch"
|
||||||
[(ngModel)]="newMerchantUser.enabled"
|
[(ngModel)]="newUser.enabled"
|
||||||
name="enabled"
|
name="enabled"
|
||||||
[disabled]="creatingUser"
|
[disabled]="creatingUser"
|
||||||
checked
|
checked
|
||||||
@ -344,7 +382,7 @@
|
|||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="emailVerifiedSwitch"
|
id="emailVerifiedSwitch"
|
||||||
[(ngModel)]="newMerchantUser.emailVerified"
|
[(ngModel)]="newUser.emailVerified"
|
||||||
name="emailVerified"
|
name="emailVerified"
|
||||||
[disabled]="creatingUser"
|
[disabled]="creatingUser"
|
||||||
>
|
>
|
||||||
@ -360,8 +398,7 @@
|
|||||||
<div class="alert alert-light">
|
<div class="alert alert-light">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<strong>Informations système :</strong><br>
|
<strong>Informations système :</strong><br>
|
||||||
• Merchant Partner ID : {{ currentMerchantPartnerId || 'Chargement...' }}<br>
|
• Type d'utilisateur : HUB<br>
|
||||||
• Type d'utilisateur : MERCHANT<br>
|
|
||||||
• Créé par : Utilisateur courant<br>
|
• Créé par : Utilisateur courant<br>
|
||||||
• Votre rôle : {{ currentUserRole || 'Non défini' }}
|
• Votre rôle : {{ currentUserRole || 'Non défini' }}
|
||||||
</small>
|
</small>
|
||||||
@ -382,7 +419,7 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
[disabled]="!userForm.form.valid || creatingUser || !isRoleAllowedForCreation(newMerchantUser.role) || !currentMerchantPartnerId"
|
[disabled]="!userForm.form.valid || creatingUser"
|
||||||
>
|
>
|
||||||
@if (creatingUser) {
|
@if (creatingUser) {
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status">
|
<div class="spinner-border spinner-border-sm me-2" role="status">
|
||||||
@ -391,7 +428,7 @@
|
|||||||
Création...
|
Création...
|
||||||
} @else {
|
} @else {
|
||||||
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
||||||
Créer l'utilisateur
|
Créer l'utilisateur Hub
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -447,9 +484,9 @@
|
|||||||
<br>
|
<br>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<span class="badge" [ngClass]="getRoleBadgeClass(selectedUserForReset.role)">
|
<span class="badge" [ngClass]="getRoleBadgeClass(selectedUserForReset.role)">
|
||||||
{{ getRoleDisplayName(selectedUserForReset.role) }}
|
{{ getRoleLabel(selectedUserForReset.role) }}
|
||||||
</span>
|
</span>
|
||||||
• Merchant Partner: {{ selectedUserForReset.merchantPartnerId }}
|
• Type: Hub
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -460,8 +497,9 @@
|
|||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
Nouveau mot de passe <span class="text-danger">*</span>
|
Nouveau mot de passe <span class="text-danger">*</span>
|
||||||
</label>
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
type="password"
|
[type]="showNewPassword ? 'text' : 'password'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Entrez le nouveau mot de passe"
|
placeholder="Entrez le nouveau mot de passe"
|
||||||
[(ngModel)]="newPassword"
|
[(ngModel)]="newPassword"
|
||||||
@ -471,6 +509,15 @@
|
|||||||
[disabled]="resettingPassword"
|
[disabled]="resettingPassword"
|
||||||
#newPasswordInput="ngModel"
|
#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">
|
<div class="form-text">
|
||||||
Le mot de passe doit contenir au moins 8 caractères.
|
Le mot de passe doit contenir au moins 8 caractères.
|
||||||
</div>
|
</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">
|
<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>
|
<ng-icon name="lucideUserX" class="text-danger" style="font-size: 2rem;"></ng-icon>
|
||||||
</div>
|
</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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -592,12 +643,17 @@
|
|||||||
<br>
|
<br>
|
||||||
<strong>Email :</strong> {{ selectedUserForDelete.email }}
|
<strong>Email :</strong> {{ selectedUserForDelete.email }}
|
||||||
<br>
|
<br>
|
||||||
<strong>Rôle :</strong>
|
<strong>Rôle Principal :</strong>
|
||||||
<span class="badge" [ngClass]="getRoleBadgeClass(selectedUserForDelete.role)">
|
<span class="badge" [ngClass]="getRoleBadgeClass(selectedUserForDelete.role)">
|
||||||
{{ getRoleDisplayName(selectedUserForDelete.role) }}
|
{{ getRoleLabel(selectedUserForDelete.role) }}
|
||||||
</span>
|
</span>
|
||||||
|
@if (selectedUserForDelete.role && selectedUserForDelete.role.length > 1) {
|
||||||
<br>
|
<br>
|
||||||
<strong>Merchant Partner :</strong> {{ selectedUserForDelete.merchantPartnerId }}
|
<strong>Rôles supplémentaires :</strong>
|
||||||
|
{{ selectedUserForDelete.role.length - 1 }}
|
||||||
|
}
|
||||||
|
<br>
|
||||||
|
<strong>Type :</strong> Utilisateur Hub
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -631,7 +687,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
(click)="confirmDeleteUser()"
|
(click)="confirmDeleteUser()"
|
||||||
[disabled]="deletingUser || !selectedUserForDelete"
|
[disabled]="deletingUser || !selectedUserForDelete || !canDeleteUsers"
|
||||||
>
|
>
|
||||||
@if (deletingUser) {
|
@if (deletingUser) {
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status">
|
<div class="spinner-border spinner-border-sm me-2" role="status">
|
||||||
390
src/app/modules/hub-users-management/hub-users.service.ts
Normal file
390
src/app/modules/hub-users-management/hub-users.service.ts
Normal 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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,21 +3,19 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { catchError, map, of, Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
|
||||||
import { HubUsersList } from './list/list';
|
import { HubUsersService } from './hub-users.service';
|
||||||
import { HubUserProfile } from './profile/profile';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { HubUsersService } from './services/hub-users.service';
|
|
||||||
import { RoleManagementService } from '@core/services/role-management.service';
|
|
||||||
import { AuthService } from '@core/services/auth.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 {
|
import {
|
||||||
HubUserDto,
|
|
||||||
CreateUserDto,
|
|
||||||
ResetPasswordDto,
|
ResetPasswordDto,
|
||||||
UserRole,
|
UserRole,
|
||||||
PaginatedUserResponse,
|
UserType
|
||||||
MerchantUserDto,
|
|
||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -31,19 +29,27 @@ import {
|
|||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
HubUsersList,
|
HubUsersList,
|
||||||
HubUserProfile,
|
HubUserProfile
|
||||||
],
|
],
|
||||||
templateUrl: './hub-users.html',
|
templateUrl: './hub-users.html',
|
||||||
})
|
})
|
||||||
export class HubUsers implements OnInit, OnDestroy {
|
export class HubUsersManagement implements OnInit, OnDestroy {
|
||||||
private modalService = inject(NgbModal);
|
private modalService = inject(NgbModal);
|
||||||
private usersService = inject(HubUsersService);
|
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
|
private hubUsersService = inject(HubUsersService);
|
||||||
|
protected roleService = inject(RoleManagementService);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
private destroy$ = new Subject<void>();
|
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';
|
activeTab: 'list' | 'profile' = 'list';
|
||||||
selectedUserId: string | null = null;
|
selectedUserId: string | null = null;
|
||||||
|
|
||||||
@ -54,45 +60,56 @@ export class HubUsers implements OnInit, OnDestroy {
|
|||||||
canDeleteUsers = false;
|
canDeleteUsers = false;
|
||||||
canManageRoles = false;
|
canManageRoles = false;
|
||||||
|
|
||||||
// Données pour la création d'utilisateur
|
// Formulaire de création
|
||||||
newUser: CreateUserDto = {
|
newUser: {
|
||||||
username: '',
|
username: string;
|
||||||
email: '',
|
email: string;
|
||||||
firstName: '',
|
firstName: string;
|
||||||
lastName: '',
|
lastName: string;
|
||||||
password: '',
|
password: string;
|
||||||
role: UserRole.DCB_SUPPORT,
|
role: UserRole;
|
||||||
enabled: true,
|
enabled: boolean;
|
||||||
emailVerified: false
|
emailVerified: boolean;
|
||||||
};
|
userType: UserType
|
||||||
|
} = this.getDefaultUserForm();
|
||||||
// Liste des partenaires marchands (à récupérer depuis votre API)
|
|
||||||
merchantPartners: any[] = [];
|
|
||||||
selectedMerchantPartnerId: string = '';
|
|
||||||
|
|
||||||
availableRoles: { value: UserRole; label: string; description: string }[] = [];
|
|
||||||
assignableRoles: UserRole[] = [];
|
|
||||||
|
|
||||||
|
// États des opérations
|
||||||
creatingUser = false;
|
creatingUser = false;
|
||||||
createUserError = '';
|
createUserError = '';
|
||||||
|
|
||||||
// Données pour la réinitialisation de mot de passe
|
|
||||||
selectedUserForReset: HubUserDto | null = null;
|
|
||||||
newPassword = '';
|
|
||||||
temporaryPassword = false;
|
|
||||||
resettingPassword = false;
|
resettingPassword = false;
|
||||||
resetPasswordError = '';
|
resetPasswordError = '';
|
||||||
resetPasswordSuccess = '';
|
resetPasswordSuccess = '';
|
||||||
|
|
||||||
selectedUserForDelete: HubUserDto | null = null;
|
newPassword = '';
|
||||||
|
temporaryPassword = false;
|
||||||
|
|
||||||
deletingUser = false;
|
deletingUser = false;
|
||||||
deleteUserError = '';
|
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() {
|
ngOnInit() {
|
||||||
this.activeTab = 'list';
|
this.activeTab = 'list';
|
||||||
this.initializeUserPermissions();
|
this.loadCurrentUserPermissions();
|
||||||
this.loadAvailableRoles();
|
this.loadAvailableRoles();
|
||||||
this.loadMerchantPartners();
|
this.newUser.role = UserRole.DCB_SUPPORT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -103,12 +120,14 @@ export class HubUsers implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Initialise les permissions de l'utilisateur courant
|
* Initialise les permissions de l'utilisateur courant
|
||||||
*/
|
*/
|
||||||
private initializeUserPermissions(): void {
|
private loadCurrentUserPermissions(): void {
|
||||||
this.authService.getUserProfile()
|
this.authService.getUserProfile()
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (profile) => {
|
next: (user) => {
|
||||||
this.currentUserRole = profile?.role?.[0] as UserRole || null;
|
this.currentUserRole = this.extractUserRole(user);
|
||||||
|
|
||||||
|
console.log(`HUB User ROLE: ${this.currentUserRole}`);
|
||||||
|
|
||||||
if (this.currentUserRole) {
|
if (this.currentUserRole) {
|
||||||
this.roleService.setCurrentUserRole(this.currentUserRole);
|
this.roleService.setCurrentUserRole(this.currentUserRole);
|
||||||
@ -118,19 +137,45 @@ export class HubUsers implements OnInit, OnDestroy {
|
|||||||
this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole);
|
this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole);
|
||||||
|
|
||||||
this.assignableRoles = this.roleService.getAssignableRoles(this.currentUserRole);
|
this.assignableRoles = this.roleService.getAssignableRoles(this.currentUserRole);
|
||||||
|
console.log('Assignable roles:', this.assignableRoles);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Error loading user profile:', 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
|
* Charge les rôles disponibles
|
||||||
*/
|
*/
|
||||||
private loadAvailableRoles(): void {
|
private loadAvailableRoles(): void {
|
||||||
this.usersService.getAvailableHubRoles()
|
this.hubUsersService.getAvailableHubRoles()
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
@ -139,47 +184,243 @@ export class HubUsers implements OnInit, OnDestroy {
|
|||||||
label: role.label,
|
label: role.label,
|
||||||
description: role.description
|
description: role.description
|
||||||
}));
|
}));
|
||||||
|
console.log('Available hub roles loaded:', this.availableRoles);
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Error loading available hub roles:', error);
|
console.error('Error loading available roles:', error);
|
||||||
this.availableRoles = [
|
this.availableRoles = this.getFallbackRoles();
|
||||||
{ 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' }
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Charge la liste des partenaires marchands
|
* Rôles par défaut en cas d'erreur
|
||||||
*/
|
*/
|
||||||
private loadMerchantPartners(): void {
|
private getFallbackRoles(): any[] {
|
||||||
this.usersService.findAllMerchantUsers().pipe(
|
return [
|
||||||
map((response: PaginatedUserResponse) => {
|
{ value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' },
|
||||||
return response.users as MerchantUserDto[];
|
{ value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' },
|
||||||
}),
|
{ value: UserRole.DCB_PARTNER, label: 'DCB Partner', description: 'Partenaire commercial' }
|
||||||
catchError(error => {
|
];
|
||||||
console.error('❌ Error loading all merchant users:', error);
|
|
||||||
return of([]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private getDefaultUserForm() {
|
||||||
* Vérifie si un rôle est un rôle marchand
|
return {
|
||||||
*/
|
username: '',
|
||||||
isMerchantRole(role: UserRole): boolean {
|
email: '',
|
||||||
const merchantRoles = [
|
firstName: '',
|
||||||
UserRole.DCB_PARTNER_ADMIN,
|
lastName: '',
|
||||||
UserRole.DCB_PARTNER_MANAGER,
|
password: '',
|
||||||
UserRole.DCB_PARTNER_SUPPORT
|
role: UserRole.DCB_SUPPORT,
|
||||||
];
|
enabled: true,
|
||||||
return merchantRoles.includes(role);
|
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);
|
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
|
// Méthodes proxy pour le template
|
||||||
getRoleBadgeClass(role: UserRole): string {
|
getRoleBadgeClass(role: UserRole): string {
|
||||||
return this.roleService.getRoleBadgeClass(role);
|
return this.roleService.getRoleBadgeClass(role);
|
||||||
@ -207,215 +534,12 @@ export class HubUsers implements OnInit, OnDestroy {
|
|||||||
return roleInfo?.description || 'Description non disponible';
|
return roleInfo?.description || 'Description non disponible';
|
||||||
}
|
}
|
||||||
|
|
||||||
showTab(tab: 'list' | 'profile', userId?: string) {
|
getUserInitials(user: any): string {
|
||||||
this.activeTab = tab;
|
return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U';
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
this.selectedUserId = userId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
backToList() {
|
// ==================== GESTION DES ERREURS ====================
|
||||||
this.activeTab = 'list';
|
|
||||||
this.selectedUserId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
private getErrorMessage(error: any): string {
|
||||||
if (error.error?.message) {
|
if (error.error?.message) {
|
||||||
return 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.';
|
return 'Données invalides. Vérifiez les champs du formulaire.';
|
||||||
}
|
}
|
||||||
if (error.status === 409) {
|
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) {
|
if (error.status === 403) {
|
||||||
return 'Vous n\'avez pas les permissions nécessaires pour cette action.';
|
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) {
|
if (error.status === 403) {
|
||||||
return 'Vous n\'avez pas les permissions pour supprimer cet utilisateur.';
|
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.';
|
return 'Erreur lors de la suppression de l\'utilisateur. Veuillez réessayer.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation du formulaire
|
// ==================== VALIDATION DU FORMULAIRE ====================
|
||||||
|
|
||||||
private validateUserForm(): { isValid: boolean; error?: string } {
|
private validateUserForm(): { isValid: boolean; error?: string } {
|
||||||
const requiredFields = [
|
const requiredFields = [
|
||||||
{ field: this.newUser.username?.trim(), name: 'Nom d\'utilisateur' },
|
{ field: this.newUser.username?.trim(), name: 'Nom d\'utilisateur' },
|
||||||
@ -477,8 +605,13 @@ export class HubUsers implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validation email
|
// Validation email
|
||||||
|
const email = this.newUser.email?.trim();
|
||||||
|
if (!email) {
|
||||||
|
return { isValid: false, error: 'Email est requis' };
|
||||||
|
}
|
||||||
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
if (!emailRegex.test(this.newUser.email)) {
|
if (!emailRegex.test(email)) {
|
||||||
return { isValid: false, error: 'Format d\'email invalide' };
|
return { isValid: false, error: 'Format d\'email invalide' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -492,11 +625,4 @@ export class HubUsers implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
return { isValid: true };
|
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>;
|
|
||||||
}
|
}
|
||||||
@ -1,44 +1,14 @@
|
|||||||
<!-- src/app/modules/merchant-users/list/list.html -->
|
<app-ui-card [title]="'Équipe Marchande'">
|
||||||
<app-ui-card title="Équipe Marchande">
|
|
||||||
<a
|
<a
|
||||||
helper-text
|
helper-text
|
||||||
href="javascript:void(0);"
|
href="javascript:void(0);"
|
||||||
class="icon-link icon-link-hover link-primary fw-semibold"
|
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>
|
<ng-icon name="lucideUsers" class="me-1"></ng-icon>
|
||||||
Votre équipe marchande
|
Gérez les accès Marchands de votre plateforme DCB
|
||||||
} @else {
|
|
||||||
<ng-icon name="lucideBuilding" class="me-1"></ng-icon>
|
|
||||||
Utilisateurs de votre partenaire marchand
|
|
||||||
}
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div card-body>
|
<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 -->
|
<!-- Barre d'actions supérieure -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
@ -52,7 +22,7 @@
|
|||||||
[class.active]="roleFilter === 'all'"
|
[class.active]="roleFilter === 'all'"
|
||||||
(click)="filterByRole('all')"
|
(click)="filterByRole('all')"
|
||||||
>
|
>
|
||||||
Tous ({{ allUsers.length }})
|
Tous ({{ getTotalUsersCount() }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -81,24 +51,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="d-flex justify-content-end gap-2">
|
<div class="d-flex justify-content-end gap-2">
|
||||||
@if (!canViewAllMerchants) {
|
@if (showCreateButton && canCreateUsers) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
(click)="openCreateModal.emit()"
|
(click)="openCreateUserModal.emit()"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
||||||
Nouvel Utilisateur
|
Nouvel Utilisateur Marchand
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Barre de recherche et filtres -->
|
<!-- Barre de recherche et filtres avancés -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-3">
|
<div class="col-md-4">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<ng-icon name="lucideSearch"></ng-icon>
|
<ng-icon name="lucideSearch"></ng-icon>
|
||||||
@ -106,46 +85,46 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Nom, email, username..."
|
placeholder="Rechercher par nom, email..."
|
||||||
[(ngModel)]="searchTerm"
|
[(ngModel)]="searchTerm"
|
||||||
(keyup.enter)="onSearch()"
|
(input)="onSearch()"
|
||||||
|
[disabled]="loading"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<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="all">Tous les statuts</option>
|
||||||
<option value="enabled">Activés ({{ getEnabledUsersCount() }})</option>
|
<option value="enabled">Activés seulement</option>
|
||||||
<option value="disabled">Désactivés ({{ getDisabledUsersCount() }})</option>
|
<option value="disabled">Désactivés seulement</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<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="all">Tous les emails</option>
|
||||||
<option value="verified">Email vérifié</option>
|
<option value="verified">Email vérifié</option>
|
||||||
<option value="not-verified">Email non vérifié</option>
|
<option value="not-verified">Email non vérifié</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<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) {
|
@for (role of availableRoles; track role.value) {
|
||||||
<option [value]="role.value">{{ role.label }}</option>
|
<option [value]="role.value">{{ role.label }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="d-flex gap-2">
|
<div class="col-md-2">
|
||||||
<button class="btn btn-outline-primary" (click)="onSearch()">
|
<button class="btn btn-outline-secondary w-100" (click)="onClearFilters()" [disabled]="loading">
|
||||||
<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>
|
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||||
Réinitialiser
|
Effacer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
@if (loading) {
|
@if (loading) {
|
||||||
@ -163,6 +142,7 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
<div>{{ error }}</div>
|
<div>{{ error }}</div>
|
||||||
|
<button class="btn-close ms-auto" (click)="error = ''"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -173,14 +153,9 @@
|
|||||||
<table class="table table-hover table-striped">
|
<table class="table table-hover table-striped">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<!-- Colonne Merchant Partner uniquement pour les admins -->
|
<!-- Colonne Merchant Partner pour les admins -->
|
||||||
@if (canViewAllMerchants) {
|
@if (showMerchantPartnerColumn) {
|
||||||
<th (click)="sort('merchantPartnerId')" class="cursor-pointer">
|
<th>Merchant Partner</th>
|
||||||
<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>
|
|
||||||
}
|
}
|
||||||
<th (click)="sort('username')" class="cursor-pointer">
|
<th (click)="sort('username')" class="cursor-pointer">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
@ -194,12 +169,7 @@
|
|||||||
<ng-icon [name]="getSortIcon('email')" class="ms-1 fs-12"></ng-icon>
|
<ng-icon [name]="getSortIcon('email')" class="ms-1 fs-12"></ng-icon>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th (click)="sort('role')" class="cursor-pointer">
|
<th>Rôle Principal</th>
|
||||||
<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 (click)="sort('enabled')" class="cursor-pointer">
|
<th (click)="sort('enabled')" class="cursor-pointer">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span>Statut</span>
|
<span>Statut</span>
|
||||||
@ -218,8 +188,8 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@for (user of displayedUsers; track user.id) {
|
@for (user of displayedUsers; track user.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<!-- Colonne Merchant Partner uniquement pour les admins -->
|
<!-- Colonne Merchant Partner pour les admins -->
|
||||||
@if (canViewAllMerchants) {
|
@if (showMerchantPartnerColumn) {
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<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">
|
<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>
|
<td>
|
||||||
<span class="badge d-flex align-items-center" [ngClass]="getRoleBadgeClass(user.role)">
|
<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>
|
<ng-icon [name]="getRoleIcon(user.role)" class="me-1" size="14"></ng-icon>
|
||||||
{{ getRoleDisplayName(user.role) }}
|
{{ getRoleLabel(user.role) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@ -286,7 +256,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-warning btn-sm"
|
class="btn btn-outline-warning btn-sm"
|
||||||
(click)="resetPassword(user)"
|
(click)="resetPasswordRequested(user)"
|
||||||
title="Réinitialiser le mot de passe"
|
title="Réinitialiser le mot de passe"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideKey"></ng-icon>
|
<ng-icon name="lucideKey"></ng-icon>
|
||||||
@ -308,10 +278,10 @@
|
|||||||
<ng-icon name="lucideUserCheck"></ng-icon>
|
<ng-icon name="lucideUserCheck"></ng-icon>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (!canViewAllMerchants) {
|
@if (showDeleteButton) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-danger btn-sm"
|
class="btn btn-outline-danger btn-sm"
|
||||||
(click)="deleteUser(user)"
|
(click)="deleteUserRequested(user)"
|
||||||
title="Supprimer l'utilisateur"
|
title="Supprimer l'utilisateur"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideTrash2"></ng-icon>
|
<ng-icon name="lucideTrash2"></ng-icon>
|
||||||
@ -323,13 +293,13 @@
|
|||||||
}
|
}
|
||||||
@empty {
|
@empty {
|
||||||
<tr>
|
<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">
|
<div class="text-muted">
|
||||||
<ng-icon name="lucideUsers" class="fs-1 mb-3 opacity-50"></ng-icon>
|
<ng-icon name="lucideUsers" class="fs-1 mb-3 opacity-50"></ng-icon>
|
||||||
<h5 class="mb-2">Aucun utilisateur marchand trouvé</h5>
|
<h5 class="mb-2">Aucun utilisateur marchand trouvé</h5>
|
||||||
<p class="mb-3">Aucun utilisateur ne correspond à vos critères de recherche.</p>
|
<p class="mb-3">Aucun utilisateur ne correspond à vos critères de recherche.</p>
|
||||||
@if (!canViewAllMerchants) {
|
@if (showCreateButton) {
|
||||||
<button class="btn btn-primary" (click)="openCreateModal.emit()">
|
<button class="btn btn-primary" (click)="openCreateUserModal.emit()">
|
||||||
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
||||||
Créer le premier utilisateur
|
Créer le premier utilisateur
|
||||||
</button>
|
</button>
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,8 +14,8 @@
|
|||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb mb-0">
|
<ol class="breadcrumb mb-0">
|
||||||
<li class="breadcrumb-item">
|
<li class="breadcrumb-item">
|
||||||
<a href="javascript:void(0)" (click)="back.emit()" class="text-decoration-none cursor-pointer">
|
<a href="javascript:void(0)" (click)="goBack()" class="text-decoration-none cursor-pointer">
|
||||||
Équipe Marchande
|
Utilisateurs Marchand
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">
|
<li class="breadcrumb-item active" aria-current="page">
|
||||||
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<!-- Bouton de réinitialisation de mot de passe -->
|
<!-- Bouton de réinitialisation de mot de passe -->
|
||||||
@if (user && !isEditing && canResetPassword()) {
|
@if (user && canEditUser() && !isEditing) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-warning"
|
class="btn btn-warning"
|
||||||
(click)="resetPassword()"
|
(click)="resetPassword()"
|
||||||
@ -39,31 +39,27 @@
|
|||||||
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
||||||
Réinitialiser MDP
|
Réinitialiser MDP
|
||||||
</button>
|
</button>
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Bouton activation/désactivation -->
|
<!-- Bouton activation/désactivation -->
|
||||||
@if (user && !isEditing && canEnableDisableUser()) {
|
@if (user.enabled && canToggleStatus()) {
|
||||||
@if (user.enabled) {
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-warning"
|
class="btn btn-outline-warning"
|
||||||
(click)="disableUser()"
|
(click)="disableUser()"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
|
<ng-icon name="lucidePause" class="me-1"></ng-icon>
|
||||||
Désactiver
|
Désactiver
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else if (!user.enabled && canToggleStatus()) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-success"
|
class="btn btn-outline-success"
|
||||||
(click)="enableUser()"
|
(click)="enableUser()"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideUserCheck" class="me-1"></ng-icon>
|
<ng-icon name="lucidePlay" class="me-1"></ng-icon>
|
||||||
Activer
|
Activer
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Bouton modification -->
|
<!-- Bouton modification -->
|
||||||
@if (user && !isEditing && canEditUser()) {
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
(click)="startEditing()"
|
(click)="startEditing()"
|
||||||
@ -77,12 +73,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Messages d'alerte -->
|
||||||
@if (error) {
|
@if (error) {
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
<div>{{ error }}</div>
|
<div>{{ error }}</div>
|
||||||
|
<button class="btn-close ms-auto" (click)="clearMessages()"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -92,31 +105,7 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||||
<div>{{ success }}</div>
|
<div>{{ success }}</div>
|
||||||
</div>
|
<button class="btn-close ms-auto" (click)="clearMessages()"></button>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -128,7 +117,7 @@
|
|||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Chargement...</span>
|
<span class="visually-hidden">Chargement...</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-muted">Chargement du profil...</p>
|
<p class="mt-2 text-muted">Chargement du profil utilisateur Marchand...</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,15 +141,13 @@
|
|||||||
<h5>{{ getUserDisplayName() }}</h5>
|
<h5>{{ getUserDisplayName() }}</h5>
|
||||||
<p class="text-muted mb-2">@{{ user.username }}</p>
|
<p class="text-muted mb-2">@{{ user.username }}</p>
|
||||||
|
|
||||||
<!-- Rôle principal -->
|
<!-- Type d'utilisateur -->
|
||||||
<span class="badge d-flex align-items-center justify-content-center mx-auto mb-3"
|
<span class="badge bg-primary mb-2">
|
||||||
[ngClass]="getRoleBadgeClass(user.role)" style="max-width: 150px;">
|
Utilisateur Marchand
|
||||||
<ng-icon [name]="getRoleIcon(user.role)" class="me-1"></ng-icon>
|
|
||||||
{{ getRoleDisplayName(user.role) }}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Statut -->
|
<!-- Statut -->
|
||||||
<span [class]="getStatusBadgeClass()" class="mb-3">
|
<span [class]="getStatusBadgeClass()" class="mb-3 d-block">
|
||||||
{{ getStatusText() }}
|
{{ getStatusText() }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@ -173,12 +160,6 @@
|
|||||||
<ng-icon name="lucideAlertTriangle" class="ms-1 text-warning" size="14" title="Email non vérifié"></ng-icon>
|
<ng-icon name="lucideAlertTriangle" class="ms-1 text-warning" size="14" title="Email non vérifié"></ng-icon>
|
||||||
}
|
}
|
||||||
</div>
|
</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">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
||||||
<small>Créé le {{ getCreationDate() }}</small>
|
<small>Créé le {{ getCreationDate() }}</small>
|
||||||
@ -195,35 +176,73 @@
|
|||||||
|
|
||||||
<!-- Carte rôle utilisateur -->
|
<!-- Carte rôle utilisateur -->
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title mb-0">Rôle Utilisateur</h5>
|
<h5 class="card-title mb-0">
|
||||||
|
Rôle Utilisateur
|
||||||
|
</h5>
|
||||||
|
@if (showRoleManagement()) {
|
||||||
|
<span class="badge bg-info">Modifiable</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- Rôle actuel -->
|
<!-- Rôles actuels -->
|
||||||
<div class="text-center mb-3">
|
<div class="text-center mb-3">
|
||||||
<span class="badge d-flex align-items-center justify-content-center"
|
@if (getUserRole()) {
|
||||||
[ngClass]="getRoleBadgeClass(user.role)">
|
<div class="d-flex flex-wrap gap-1 justify-content-center mb-2">
|
||||||
<ng-icon [name]="getRoleIcon(user.role)" class="me-2"></ng-icon>
|
<span class="badge d-flex align-items-center" [ngClass]="getRoleBadgeClass(user.role)">
|
||||||
{{ getRoleDisplayName(user.role) }}
|
<ng-icon [name]="getRoleIcon(user.role)" class="me-1" size="12"></ng-icon>
|
||||||
|
{{ getRoleLabel(user.role) }}
|
||||||
</span>
|
</span>
|
||||||
<small class="text-muted d-block mt-2">
|
|
||||||
{{ getRoleDescription(user.role) }}
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Information sur la modification des rôles -->
|
<!-- Description du rôle principal -->
|
||||||
@if (canManageRoles()) {
|
<small class="text-muted d-block">
|
||||||
<div class="alert alert-success mt-3">
|
{{ getRoleDescription(user.role) }}
|
||||||
|
</small>
|
||||||
|
} @else {
|
||||||
|
<span class="badge bg-secondary">Aucun rôle</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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>
|
<small>
|
||||||
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||||
<strong>DCB Partner :</strong> Vous pouvez modifier le rôle de cet utilisateur.
|
Vous n'avez pas la permission de modifier les rôles
|
||||||
</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.
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -248,13 +267,7 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<strong>Type d'utilisateur :</strong>
|
<strong>Type d'utilisateur :</strong>
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
<span class="badge bg-primary">{{ user.userType }}</span>
|
<span class="badge bg-primary">Utilisateur Marchand</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<strong>Merchant Partner :</strong>
|
|
||||||
<div class="text-muted font-monospace small">
|
|
||||||
{{ user.merchantPartnerId }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -309,7 +322,7 @@
|
|||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<!-- Prénom -->
|
<!-- Prénom -->
|
||||||
<div class="col-md-6">
|
<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) {
|
@if (isEditing) {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -317,7 +330,6 @@
|
|||||||
[(ngModel)]="editedUser.firstName"
|
[(ngModel)]="editedUser.firstName"
|
||||||
placeholder="Entrez le prénom"
|
placeholder="Entrez le prénom"
|
||||||
[disabled]="saving"
|
[disabled]="saving"
|
||||||
required
|
|
||||||
>
|
>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
@ -328,7 +340,7 @@
|
|||||||
|
|
||||||
<!-- Nom -->
|
<!-- Nom -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Nom <span class="text-danger">*</span></label>
|
<label class="form-label">Nom</label>
|
||||||
@if (isEditing) {
|
@if (isEditing) {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -336,7 +348,6 @@
|
|||||||
[(ngModel)]="editedUser.lastName"
|
[(ngModel)]="editedUser.lastName"
|
||||||
placeholder="Entrez le nom"
|
placeholder="Entrez le nom"
|
||||||
[disabled]="saving"
|
[disabled]="saving"
|
||||||
required
|
|
||||||
>
|
>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
@ -358,7 +369,7 @@
|
|||||||
|
|
||||||
<!-- Email -->
|
<!-- Email -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Email <span class="text-danger">*</span></label>
|
<label class="form-label">Email</label>
|
||||||
@if (isEditing) {
|
@if (isEditing) {
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
@ -366,13 +377,7 @@
|
|||||||
[(ngModel)]="editedUser.email"
|
[(ngModel)]="editedUser.email"
|
||||||
placeholder="email@exemple.com"
|
placeholder="email@exemple.com"
|
||||||
[disabled]="saving"
|
[disabled]="saving"
|
||||||
required
|
|
||||||
>
|
>
|
||||||
@if (editedUser.email && !isValidEmail(editedUser.email)) {
|
|
||||||
<div class="text-danger small mt-1">
|
|
||||||
Format d'email invalide
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
} @else {
|
} @else {
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
{{ user.email }}
|
{{ user.email }}
|
||||||
@ -383,88 +388,8 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</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é -->
|
<!-- Statut activé -->
|
||||||
@if (isEditing && canEnableDisableUser()) {
|
@if (isEditing) {
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input
|
<input
|
||||||
@ -482,7 +407,7 @@
|
|||||||
L'utilisateur peut se connecter si activé
|
L'utilisateur peut se connecter si activé
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} @else if (!isEditing) {
|
} @else {
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Statut du compte</label>
|
<label class="form-label">Statut du compte</label>
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
@ -493,6 +418,24 @@
|
|||||||
</div>
|
</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 -->
|
<!-- Informations système -->
|
||||||
@if (!isEditing) {
|
@if (!isEditing) {
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@ -523,17 +466,9 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Type d'utilisateur</label>
|
<label class="form-label">Type d'utilisateur</label>
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
<span class="badge bg-primary">{{ user.userType }}</span>
|
<span class="badge bg-primary">Utilisateur Marchand</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -542,30 +477,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions supplémentaires -->
|
<!-- Actions supplémentaires -->
|
||||||
@if (!isEditing) {
|
@if (!isEditing && canEditUser()) {
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header bg-light">
|
||||||
<h6 class="card-title mb-0">Actions de Gestion</h6>
|
<h6 class="card-title mb-0">Actions de Gestion</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<!-- Réinitialisation MDP -->
|
|
||||||
@if (canResetPassword()) {
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-warning w-100"
|
class="btn btn-outline-warning w-100"
|
||||||
(click)="resetPassword()"
|
(click)="resetPassword()"
|
||||||
|
[disabled]="!canResetPassword()"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
||||||
Réinitialiser MDP
|
Réinitialiser MDP
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Activation/Désactivation -->
|
|
||||||
@if (canEnableDisableUser()) {
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
@if (user.enabled) {
|
@if (user.enabled && canToggleStatus()) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-secondary w-100"
|
class="btn btn-outline-secondary w-100"
|
||||||
(click)="disableUser()"
|
(click)="disableUser()"
|
||||||
@ -573,7 +503,7 @@
|
|||||||
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
|
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
|
||||||
Désactiver
|
Désactiver
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else if (!user.enabled && canToggleStatus()) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-success w-100"
|
class="btn btn-outline-success w-100"
|
||||||
(click)="enableUser()"
|
(click)="enableUser()"
|
||||||
@ -583,10 +513,6 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Modification -->
|
|
||||||
@if (canEditUser()) {
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-primary w-100"
|
class="btn btn-outline-primary w-100"
|
||||||
@ -596,23 +522,6 @@
|
|||||||
Modifier
|
Modifier
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -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 { Component, inject, OnInit, Input, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
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 {
|
import {
|
||||||
MerchantUserDto,
|
User,
|
||||||
UpdateUserDto,
|
UpdateUserDto,
|
||||||
UserRole
|
UserRole,
|
||||||
|
UserType,
|
||||||
|
UserUtils
|
||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} 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({
|
@Component({
|
||||||
selector: 'app-merchant-user-profile',
|
selector: 'app-merchant-user-profile',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule],
|
imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule],
|
||||||
templateUrl: './profile.html',
|
templateUrl: './merchant-users-profile.html',
|
||||||
styles: [`
|
styles: [`
|
||||||
.avatar-lg {
|
.avatar-lg {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
@ -32,17 +34,20 @@ import {
|
|||||||
})
|
})
|
||||||
export class MerchantUserProfile implements OnInit, OnDestroy {
|
export class MerchantUserProfile implements OnInit, OnDestroy {
|
||||||
private merchantUsersService = inject(MerchantUsersService);
|
private merchantUsersService = inject(MerchantUsersService);
|
||||||
private authService = inject(AuthService);
|
|
||||||
private roleService = inject(RoleManagementService);
|
private roleService = inject(RoleManagementService);
|
||||||
|
private authService = inject(AuthService);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
readonly UserRole = UserRole;
|
readonly UserRole = UserRole;
|
||||||
|
readonly UserType = UserType;
|
||||||
|
readonly UserUtils = UserUtils;
|
||||||
|
|
||||||
@Input() userId!: string;
|
@Input() userId!: string;
|
||||||
@Output() back = new EventEmitter<void>();
|
@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;
|
loading = false;
|
||||||
saving = false;
|
saving = false;
|
||||||
error = '';
|
error = '';
|
||||||
@ -56,16 +61,22 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
editedUser: UpdateUserDto = {};
|
editedUser: UpdateUserDto = {};
|
||||||
|
|
||||||
// Gestion des rôles
|
// Gestion des rôles
|
||||||
availableRoles: UserRole[] = [
|
availableRoles: { value: UserRole; label: string; description: string }[] = [];
|
||||||
UserRole.DCB_PARTNER_ADMIN,
|
|
||||||
UserRole.DCB_PARTNER_MANAGER,
|
|
||||||
UserRole.DCB_PARTNER_SUPPORT
|
|
||||||
];
|
|
||||||
updatingRole = false;
|
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() {
|
ngOnInit() {
|
||||||
if (this.userId) {
|
if (this.userId) {
|
||||||
this.loadCurrentUserPermissions();
|
this.loadCurrentUserPermissions();
|
||||||
|
this.loadAvailableRoles();
|
||||||
this.loadUserProfile();
|
this.loadUserProfile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,6 +95,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
.subscribe({
|
.subscribe({
|
||||||
next: (profile) => {
|
next: (profile) => {
|
||||||
this.currentUserRole = this.authService.getCurrentUserRole();
|
this.currentUserRole = this.authService.getCurrentUserRole();
|
||||||
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Error loading user permissions:', 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() {
|
loadUserProfile() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
@ -104,7 +140,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
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.loading = false;
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
console.error('Error loading merchant user profile:', error);
|
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) {
|
updateUserRole(newRole: UserRole) {
|
||||||
if (!this.user || !this.canManageRoles()) {
|
if (!this.user || !this.canManageRoles()) {
|
||||||
this.error = 'Vous n\'avez pas la permission de modifier les rôles';
|
this.error = 'Vous n\'avez pas la permission de modifier les rôles';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier que le nouveau rôle est différent
|
if (newRole === this.currentUserRole) {
|
||||||
if (newRole === this.user.role) {
|
this.error = 'L\'utilisateur a déjà ce rôle comme rôle principal';
|
||||||
this.error = 'L\'utilisateur a déjà ce rôle';
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,14 +226,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
this.error = '';
|
this.error = '';
|
||||||
this.success = '';
|
this.success = '';
|
||||||
|
|
||||||
// Note: La modification de rôle nécessite une méthode spécifique
|
this.merchantUsersService.updateMerchantUserRole(this.user.id, newRole)
|
||||||
// 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)
|
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (updatedUser) => {
|
next: (updatedUser) => {
|
||||||
@ -206,26 +245,19 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Gestion du statut
|
// Gestion du statut
|
||||||
enableUser() {
|
enableUser() {
|
||||||
if (!this.user || !this.canEnableDisableUser()) {
|
if (!this.user || !this.canEnableDisableUser()) return;
|
||||||
this.error = 'Vous n\'avez pas la permission d\'activer cet utilisateur';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.error = '';
|
|
||||||
this.success = '';
|
|
||||||
|
|
||||||
this.merchantUsersService.enableMerchantUser(this.user.id)
|
this.merchantUsersService.enableMerchantUser(this.user.id)
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (updatedUser) => {
|
next: (updatedUser) => {
|
||||||
this.user = updatedUser;
|
this.user = updatedUser;
|
||||||
this.success = 'Utilisateur marchand activé avec succès';
|
this.success = 'Utilisateur Merchant activé avec succès';
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.error = this.getErrorMessage(error);
|
this.error = this.getErrorMessage(error);
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
console.error('Error enabling merchant user:', error);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -244,7 +276,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
.subscribe({
|
.subscribe({
|
||||||
next: (updatedUser) => {
|
next: (updatedUser) => {
|
||||||
this.user = 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();
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
@ -258,7 +290,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
// Réinitialisation du mot de passe
|
// Réinitialisation du mot de passe
|
||||||
resetPassword() {
|
resetPassword() {
|
||||||
if (this.user && this.canResetPassword()) {
|
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
|
* Vérifie si l'utilisateur peut éditer cet utilisateur
|
||||||
*/
|
*/
|
||||||
canEditUser(): boolean {
|
canEditUser(): boolean {
|
||||||
// Seul DCB_PARTNER peut éditer tous les utilisateurs marchands
|
// Toujours permettre d'éditer son propre profil
|
||||||
if (this.currentUserRole === UserRole.DCB_PARTNER) {
|
if (this.isCurrentUserProfile()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Les administrateurs marchands peuvent éditer les utilisateurs de leur partenaire
|
// Pour les utilisateurs Merchant, utiliser les permissions du service de rôle
|
||||||
if (this.currentUserRole === UserRole.DCB_PARTNER_ADMIN) {
|
return this.roleService.canEditUsers(this.currentUserRole);
|
||||||
const currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId();
|
|
||||||
return this.user?.merchantPartnerId === currentMerchantPartnerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si l'utilisateur peut gérer les rôles
|
* Vérifie si l'utilisateur peut gérer les rôles
|
||||||
*/
|
*/
|
||||||
canManageRoles(): boolean {
|
canManageRoles(): boolean {
|
||||||
// SEUL DCB_PARTNER peut modifier les rôles des utilisateurs marchands
|
// Pour les Merchant, utiliser les permissions du service de rôle
|
||||||
return this.currentUserRole === UserRole.DCB_PARTNER;
|
return this.roleService.canManageRoles(this.currentUserRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -299,41 +326,21 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seul DCB_PARTNER peut activer/désactiver les utilisateurs marchands
|
// Pour les Merchant, utiliser les permissions du service de rôle
|
||||||
if (this.currentUserRole === UserRole.DCB_PARTNER) {
|
return this.roleService.canEditUsers(this.currentUserRole);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si l'utilisateur peut réinitialiser le mot de passe
|
* Vérifie si l'utilisateur peut réinitialiser le mot de passe
|
||||||
*/
|
*/
|
||||||
canResetPassword(): boolean {
|
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
|
// Les utilisateurs peuvent réinitialiser leur propre mot de passe
|
||||||
if (this.isCurrentUserProfile()) {
|
if (this.isCurrentUserProfile()) {
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seul DCB_PARTNER peut supprimer les utilisateurs marchands
|
// Pour les Merchant, utiliser les permissions du service de rôle
|
||||||
return this.currentUserRole === UserRole.DCB_PARTNER;
|
return this.roleService.canDeleteUsers(this.currentUserRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== UTILITAIRES D'AFFICHAGE ====================
|
// ==================== UTILITAIRES D'AFFICHAGE ====================
|
||||||
@ -382,42 +389,46 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUserDisplayName(): string {
|
getUserDisplayName(): string {
|
||||||
if (!this.user) return 'Utilisateur Marchand';
|
if (!this.user) return 'Utilisateur Merchant';
|
||||||
if (this.user.firstName && this.user.lastName) {
|
if (this.user.firstName && this.user.lastName) {
|
||||||
return `${this.user.firstName} ${this.user.lastName}`;
|
return `${this.user.firstName} ${this.user.lastName}`;
|
||||||
}
|
}
|
||||||
return this.user.username;
|
return this.user.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleBadgeClass(role: UserRole): string {
|
getRoleBadgeClass(role: string | UserRole): string {
|
||||||
return this.roleService.getRoleBadgeClass(role);
|
return this.roleService.getRoleBadgeClass(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleDisplayName(role: UserRole): string {
|
getRoleLabel(role: string | UserRole): string {
|
||||||
return this.roleService.getRoleLabel(role);
|
return this.roleService.getRoleLabel(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleIcon(role: UserRole): string {
|
getRoleIcon(role: string | UserRole): string {
|
||||||
return this.roleService.getRoleIcon(role);
|
return this.roleService.getRoleIcon(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleDescription(role: UserRole): string {
|
getRoleDescription(role: string | UserRole): string {
|
||||||
const descriptions: { [key in UserRole]?: string } = {
|
const roleInfo = this.availableRoles.find(r => r.value === role);
|
||||||
[UserRole.DCB_PARTNER_ADMIN]: 'Accès administratif complet au sein du partenaire marchand',
|
return roleInfo?.description || 'Description non disponible';
|
||||||
[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';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserType(): string {
|
// Obtenir le rôle (peut être string ou UserRole)
|
||||||
if (!this.user) return 'Utilisateur Marchand';
|
getUserRole(): string | UserRole | undefined {
|
||||||
return this.roleService.getRoleLabel(this.user.role);
|
return this.user?.role;
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserTypeBadgeClass(): string {
|
// Pour le template, retourner un tableau pour la boucle
|
||||||
if (!this.user) return 'bg-secondary';
|
getUserRoles(): (string | UserRole)[] {
|
||||||
return this.roleService.getRoleBadgeClass(this.user.role);
|
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 ====================
|
// ==================== GESTION DES ERREURS ====================
|
||||||
@ -433,7 +444,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
return 'Vous n\'avez pas les permissions pour effectuer cette action.';
|
return 'Vous n\'avez pas les permissions pour effectuer cette action.';
|
||||||
}
|
}
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
return 'Utilisateur marchand non trouvé.';
|
return 'Utilisateur non trouvé.';
|
||||||
}
|
}
|
||||||
if (error.status === 409) {
|
if (error.status === 409) {
|
||||||
return 'Conflit de données. Cet utilisateur existe peut-être déjà.';
|
return 'Conflit de données. Cet utilisateur existe peut-être déjà.';
|
||||||
@ -459,23 +470,36 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected isValidEmail(email: string): boolean {
|
private isValidEmail(email: string): boolean {
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
return emailRegex.test(email);
|
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 ====================
|
// ==================== MÉTHODES UTILITAIRES ====================
|
||||||
|
|
||||||
isAdmin(): boolean {
|
isCurrentUserProfile(): boolean {
|
||||||
return this.user?.role === UserRole.DCB_PARTNER_ADMIN;
|
if (!this.user?.id) return false;
|
||||||
|
return this.authService.isCurrentUserProfile(this.user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
isManager(): boolean {
|
getCreationDate(): string {
|
||||||
return this.user?.role === UserRole.DCB_PARTNER_MANAGER;
|
if (!this.user?.createdTimestamp) return 'Non disponible';
|
||||||
|
return this.formatTimestamp(this.user.createdTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSupport(): boolean {
|
getLastLoginDate(): string {
|
||||||
return this.user?.role === UserRole.DCB_PARTNER_SUPPORT;
|
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() {
|
refresh() {
|
||||||
@ -488,62 +512,30 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifie si c'est le profil de l'utilisateur courant
|
// Méthodes pour le template
|
||||||
isCurrentUserProfile(): boolean {
|
getProfileTitle(): string {
|
||||||
if (!this.user?.id) return false;
|
return 'Profil Utilisateur Merchant';
|
||||||
return this.authService.isCurrentUserProfile(this.user.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthode pour obtenir la date de création formatée
|
getContextDescription(): string {
|
||||||
getCreationDate(): string {
|
return 'Gestion des utilisateurs de la plateforme DCB';
|
||||||
if (!this.user?.createdTimestamp) return 'Non disponible';
|
|
||||||
return this.formatTimestamp(this.user.createdTimestamp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthode pour obtenir la date de dernière connexion formatée
|
getAssignableRoles(): UserRole[] {
|
||||||
getLastLoginDate(): string {
|
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
|
||||||
if (!this.user?.lastLogin) return 'Jamais connecté';
|
return merchantRoles.filter(role => this.roleService.canAssignRole(this.currentUserRole, role));
|
||||||
return this.formatTimestamp(this.user.lastLogin);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthode pour obtenir le nom du créateur
|
// Méthodes pour les actions spécifiques
|
||||||
getCreatorName(): string {
|
canChangeRole(): boolean {
|
||||||
if (!this.user?.createdByUsername) return 'Non disponible';
|
return this.canManageRoles() && !this.isCurrentUserProfile();
|
||||||
return this.user.createdByUsername;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthode pour obtenir l'ID du partenaire marchand
|
canToggleStatus(): boolean {
|
||||||
getMerchantPartnerId(): string {
|
return this.canEnableDisableUser() && !this.isCurrentUserProfile();
|
||||||
return this.user?.merchantPartnerId || 'Non disponible';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifie si l'utilisateur a accès à ce profil marchand
|
showRoleManagement(): boolean {
|
||||||
canAccessMerchantProfile(): boolean {
|
return this.canManageRoles() && !this.isCurrentUserProfile();
|
||||||
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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
882
src/app/modules/hub-users-management/merchant-users.html
Normal file
882
src/app/modules/hub-users-management/merchant-users.html
Normal 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>
|
||||||
351
src/app/modules/hub-users-management/merchant-users.service.ts
Normal file
351
src/app/modules/hub-users-management/merchant-users.service.ts
Normal 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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
806
src/app/modules/hub-users-management/merchant-users.ts
Normal file
806
src/app/modules/hub-users-management/merchant-users.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { HubUsers } from './hub-users';
|
|
||||||
describe('Users', () => {});
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { UsersList } from './list';
|
|
||||||
describe('UsersList', () => {});
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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';
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { UserProfile } from './profile';
|
|
||||||
describe('UserProfile', () => {});
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 });
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
@ -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>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { PartnerConfig } from './config';
|
|
||||||
describe('PartnerConfig', () => {});
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { PartnerTeamList } from './list';
|
|
||||||
describe('PartnerTeamList', () => {});
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { MerchantUsers } from './merchant-users';
|
|
||||||
describe('Merchant Users', () => {});
|
|
||||||
@ -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>;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { PartnerTeamProfile } from './profile';
|
|
||||||
describe('PartnerTeamProfile', () => {});
|
|
||||||
@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { MerchantPartnerStats } from './stats';
|
|
||||||
describe('Merchant Partner Stats', () => {});
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export type WizardStepType = {
|
|
||||||
id: string
|
|
||||||
icon: string
|
|
||||||
title: string
|
|
||||||
subtitle: string
|
|
||||||
}
|
|
||||||
@ -1,26 +1,26 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { authGuard } from '../core/guards/auth.guard';
|
import { authGuard } from '@core/guards/auth.guard';
|
||||||
import { roleGuard } from '../core/guards/role.guard';
|
import { roleGuard } from '@core/guards/role.guard';
|
||||||
import { HubUsers } from '@modules/hub-users/hub-users';
|
import { HubUsersManagement } from '@modules/hub-users-management/hub-users';
|
||||||
|
import { MerchantUsersManagement } from '@modules/hub-users-management/merchant-users';
|
||||||
|
|
||||||
// Composants principaux
|
// Composants principaux
|
||||||
import { DcbDashboard } from './dcb-dashboard/dcb-dashboard';
|
import { DcbDashboard } from '@modules/dcb-dashboard/dcb-dashboard';
|
||||||
import { Team } from './team/team';
|
import { Team } from '@modules/team/team';
|
||||||
import { Transactions } from './transactions/transactions';
|
import { Transactions } from '@modules/transactions/transactions';
|
||||||
import { MerchantUsers } from './merchant-users/merchant-users';
|
import { OperatorsConfig } from '@modules/operators/config/config';
|
||||||
import { OperatorsConfig } from './operators/config/config';
|
import { OperatorsStats } from '@modules/operators/stats/stats';
|
||||||
import { OperatorsStats } from './operators/stats/stats';
|
import { WebhooksHistory } from '@modules/webhooks/history/history';
|
||||||
import { WebhooksHistory } from './webhooks/history/history';
|
import { WebhooksStatus } from '@modules/webhooks/status/status';
|
||||||
import { WebhooksStatus } from './webhooks/status/status';
|
import { WebhooksRetry } from '@modules/webhooks/retry/retry';
|
||||||
import { WebhooksRetry } from './webhooks/retry/retry';
|
import { Settings } from '@modules/settings/settings';
|
||||||
import { Settings } from './settings/settings';
|
import { Integrations } from '@modules/integrations/integrations';
|
||||||
import { Integrations } from './integrations/integrations';
|
import { Support } from '@modules/support/support';
|
||||||
import { Support } from './support/support';
|
import { MyProfile } from '@modules/profile/profile';
|
||||||
import { MyProfile } from './profile/profile';
|
import { Documentation } from '@modules/documentation/documentation';
|
||||||
import { Documentation } from './documentation/documentation';
|
import { Help } from '@modules/help/help';
|
||||||
import { Help } from './help/help';
|
import { About } from '@modules/about/about';
|
||||||
import { About } from './about/about';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@ -75,12 +75,13 @@ const routes: Routes = [
|
|||||||
// Users (Admin seulement)
|
// Users (Admin seulement)
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
{
|
{
|
||||||
path: 'users',
|
path: 'hub-users-management',
|
||||||
canActivate: [authGuard, roleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
component: HubUsers,
|
component: HubUsersManagement,
|
||||||
data: {
|
data: {
|
||||||
title: 'Gestion des Utilisateurs',
|
title: 'Gestion des Utilisateurs',
|
||||||
module: 'users'
|
module: 'hub-users-management',
|
||||||
|
context: 'HUB',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -88,12 +89,13 @@ const routes: Routes = [
|
|||||||
// Partners
|
// Partners
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
{
|
{
|
||||||
path: 'merchant-partners',
|
path: 'merchant-users-management',
|
||||||
component: MerchantUsers,
|
component: MerchantUsersManagement,
|
||||||
canActivate: [authGuard, roleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
title: 'Gestion Partners/Marchants',
|
title: 'Gestion Partners/Marchants',
|
||||||
module: 'merchant-partners',
|
module: 'merchant-users-management',
|
||||||
|
context: 'MERCHANT',
|
||||||
requiredRoles: [
|
requiredRoles: [
|
||||||
'dcb-admin',
|
'dcb-admin',
|
||||||
'dcb-support',
|
'dcb-support',
|
||||||
|
|||||||
@ -8,22 +8,18 @@
|
|||||||
@if (user) {
|
@if (user) {
|
||||||
{{ getUserDisplayName() }}
|
{{ getUserDisplayName() }}
|
||||||
} @else {
|
} @else {
|
||||||
Profil Utilisateur
|
Mon Profil
|
||||||
}
|
}
|
||||||
</h4>
|
</h4>
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb mb-0">
|
<ol class="breadcrumb mb-0">
|
||||||
<li class="breadcrumb-item">
|
<li class="breadcrumb-item">
|
||||||
<a href="javascript:void(0)" (click)="back.emit()" class="text-decoration-none cursor-pointer">
|
<a href="javascript:void(0)" (click)="goBack()" class="text-decoration-none cursor-pointer">
|
||||||
Utilisateurs
|
Tableau de bord
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">
|
<li class="breadcrumb-item active" aria-current="page">
|
||||||
@if (user) {
|
Mon Profil
|
||||||
{{ getUserDisplayName() }}
|
|
||||||
} @else {
|
|
||||||
Profile
|
|
||||||
}
|
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
@ -31,34 +27,15 @@
|
|||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<!-- Bouton de réinitialisation de mot de passe -->
|
<!-- Bouton de réinitialisation de mot de passe -->
|
||||||
@if (user && canEditUsers && !isEditing) {
|
@if (user && !isEditing) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-warning"
|
class="btn btn-warning"
|
||||||
(click)="resetPassword()"
|
(click)="openMyProfileResetModal()"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
||||||
Réinitialiser MDP
|
Changer MDP
|
||||||
</button>
|
</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 -->
|
<!-- Bouton modification -->
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
@ -73,22 +50,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Messages d'alerte -->
|
||||||
@if (error) {
|
@if (error) {
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
@ -115,7 +76,7 @@
|
|||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Chargement...</span>
|
<span class="visually-hidden">Chargement...</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-muted">Chargement du profil...</p>
|
<p class="mt-2 text-muted">Chargement de votre profil...</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +87,9 @@
|
|||||||
<!-- Carte profil -->
|
<!-- Carte profil -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-light">
|
<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>
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<!-- Avatar -->
|
<!-- Avatar -->
|
||||||
@ -139,8 +102,13 @@
|
|||||||
<h5>{{ getUserDisplayName() }}</h5>
|
<h5>{{ getUserDisplayName() }}</h5>
|
||||||
<p class="text-muted mb-2">@{{ user.username }}</p>
|
<p class="text-muted mb-2">@{{ user.username }}</p>
|
||||||
|
|
||||||
|
<!-- Type d'utilisateur -->
|
||||||
|
<span class="badge bg-secondary mb-2">
|
||||||
|
{{ getUserTypeDisplay() }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- Statut -->
|
<!-- Statut -->
|
||||||
<span [class]="getStatusBadgeClass()" class="mb-3">
|
<span [class]="getStatusBadgeClass()" class="mb-3 d-block">
|
||||||
{{ getStatusText() }}
|
{{ getStatusText() }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@ -155,12 +123,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
||||||
<small>Créé le {{ formatTimestamp(user.createdTimestamp) }}</small>
|
<small>Membre depuis {{ getCreationDate() }}</small>
|
||||||
</div>
|
</div>
|
||||||
@if (user.lastLogin) {
|
@if (isMerchantUser()) {
|
||||||
<div class="d-flex align-items-center mt-2">
|
<div class="d-flex align-items-center mt-2">
|
||||||
<ng-icon name="lucideLogIn" class="me-2 text-muted"></ng-icon>
|
<ng-icon name="lucideBuilding" class="me-2 text-muted"></ng-icon>
|
||||||
<small>Dernière connexion : {{ formatTimestamp(user.lastLogin) }}</small>
|
<small>Partner ID: {{ getMerchantPartnerId() }}</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -169,108 +137,70 @@
|
|||||||
|
|
||||||
<!-- Carte rôle utilisateur -->
|
<!-- Carte rôle utilisateur -->
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
<div class="card-header bg-light">
|
||||||
<h5 class="card-title mb-0">Rôle Utilisateur</h5>
|
<h5 class="card-title mb-0">
|
||||||
@if (canManageRoles && !isEditing) {
|
Mon Rôle
|
||||||
<span class="badge bg-info">Modifiable</span>
|
</h5>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- Rôle actuel -->
|
<!-- Rôle actuel -->
|
||||||
<div class="text-center mb-3">
|
<div class="text-center mb-3">
|
||||||
<!-- Afficher le premier rôle ou tous les rôles -->
|
@if (getUserRole()) {
|
||||||
@if (user.roles && user.roles.length > 0) {
|
<div class="d-flex flex-wrap gap-1 justify-content-center mb-2">
|
||||||
@for (role of user.roles; track role; let first = $first) {
|
<span class="badge d-flex align-items-center" [ngClass]="getRoleBadgeClass()">
|
||||||
@if (first) { <!-- Afficher seulement le premier rôle pour l'instant -->
|
<ng-icon [name]="getRoleIcon(user.role)" class="me-1" size="12"></ng-icon>
|
||||||
<span class="badge d-flex align-items-center justify-content-center" [ngClass]="getRoleBadgeClass(role)">
|
{{ getRoleLabel() }}
|
||||||
<ng-icon [name]="getRoleIcon(role)" class="me-2"></ng-icon>
|
|
||||||
{{ getRoleLabel(role) }}
|
|
||||||
</span>
|
</span>
|
||||||
<small class="text-muted d-block mt-1">
|
</div>
|
||||||
{{ getRoleDescription(role) }}
|
|
||||||
</small>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Indicateur si plusieurs rôles -->
|
<!-- Description du rôle -->
|
||||||
@if (user.roles.length > 1) {
|
<small class="text-muted d-block">
|
||||||
<small class="text-muted">
|
{{ getRoleDescription() }}
|
||||||
+ {{ user.roles.length - 1 }} autre(s) rôle(s)
|
|
||||||
</small>
|
</small>
|
||||||
}
|
|
||||||
} @else {
|
} @else {
|
||||||
<span class="badge bg-secondary">Aucun rôle</span>
|
<span class="badge bg-secondary">Aucun rôle</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Changement de rôle -->
|
<!-- Information rôle non modifiable -->
|
||||||
@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">
|
<div class="alert alert-info mt-3">
|
||||||
<small>
|
<small>
|
||||||
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
<ng-icon name="lucideInfo" class="me-1"></ng-icon>
|
||||||
Vous n'avez pas la permission de modifier les rôles
|
Votre rôle ne peut pas être modifié depuis cette page
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Informations de création -->
|
<!-- Informations de création -->
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header bg-light">
|
<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>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-2 small">
|
<div class="row g-2 small">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<strong>Créé par :</strong>
|
<strong>ID Utilisateur :</strong>
|
||||||
<div class="text-muted">{{ user.createdByUsername || 'Système' }}</div>
|
<div class="text-muted font-monospace small">{{ user.id }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<strong>Date de création :</strong>
|
<strong>Date de création :</strong>
|
||||||
<div class="text-muted">{{ formatTimestamp(user.createdTimestamp) }}</div>
|
<div class="text-muted">{{ getCreationDate() }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<strong>Type d'utilisateur :</strong>
|
<strong>Type de compte :</strong>
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
<span class="badge bg-secondary">{{ user.userType }}</span>
|
<span class="badge bg-secondary">{{ getUserTypeDisplay() }}</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -283,10 +213,10 @@
|
|||||||
<h5 class="card-title mb-0">
|
<h5 class="card-title mb-0">
|
||||||
@if (isEditing) {
|
@if (isEditing) {
|
||||||
<ng-icon name="lucideEdit" class="me-2"></ng-icon>
|
<ng-icon name="lucideEdit" class="me-2"></ng-icon>
|
||||||
Modification du Profil
|
Modification de Mon Profil
|
||||||
} @else {
|
} @else {
|
||||||
<ng-icon name="lucideUser" class="me-2"></ng-icon>
|
<ng-icon name="lucideUser" class="me-2"></ng-icon>
|
||||||
Détails du Compte
|
Mes Informations
|
||||||
}
|
}
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
@ -305,7 +235,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn btn-success btn-sm"
|
class="btn btn-success btn-sm"
|
||||||
(click)="saveProfile()"
|
(click)="saveProfile()"
|
||||||
[disabled]="saving"
|
[disabled]="saving || !isFormValid()"
|
||||||
>
|
>
|
||||||
@if (saving) {
|
@if (saving) {
|
||||||
<div class="spinner-border spinner-border-sm me-1" role="status">
|
<div class="spinner-border spinner-border-sm me-1" role="status">
|
||||||
@ -323,15 +253,21 @@
|
|||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<!-- Prénom -->
|
<!-- Prénom -->
|
||||||
<div class="col-md-6">
|
<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) {
|
@if (isEditing) {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
[(ngModel)]="editedUser.firstName"
|
[(ngModel)]="editedUser.firstName"
|
||||||
placeholder="Entrez le prénom"
|
placeholder="Votre prénom"
|
||||||
[disabled]="saving"
|
[disabled]="saving"
|
||||||
|
required
|
||||||
>
|
>
|
||||||
|
@if (!editedUser.firstName?.trim()) {
|
||||||
|
<div class="form-text text-danger">
|
||||||
|
Le prénom est obligatoire
|
||||||
|
</div>
|
||||||
|
}
|
||||||
} @else {
|
} @else {
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
{{ user.firstName || 'Non renseigné' }}
|
{{ user.firstName || 'Non renseigné' }}
|
||||||
@ -341,15 +277,21 @@
|
|||||||
|
|
||||||
<!-- Nom -->
|
<!-- Nom -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Nom</label>
|
<label class="form-label">Nom <span class="text-danger">*</span></label>
|
||||||
@if (isEditing) {
|
@if (isEditing) {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
[(ngModel)]="editedUser.lastName"
|
[(ngModel)]="editedUser.lastName"
|
||||||
placeholder="Entrez le nom"
|
placeholder="Votre nom"
|
||||||
[disabled]="saving"
|
[disabled]="saving"
|
||||||
|
required
|
||||||
>
|
>
|
||||||
|
@if (!editedUser.lastName?.trim()) {
|
||||||
|
<div class="form-text text-danger">
|
||||||
|
Le nom est obligatoire
|
||||||
|
</div>
|
||||||
|
}
|
||||||
} @else {
|
} @else {
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
{{ user.lastName || 'Non renseigné' }}
|
{{ user.lastName || 'Non renseigné' }}
|
||||||
@ -364,7 +306,7 @@
|
|||||||
{{ user.username }}
|
{{ user.username }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
Le nom d'utilisateur ne peut pas être modifié
|
Votre identifiant de connexion ne peut pas être modifié
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -376,8 +318,9 @@
|
|||||||
type="email"
|
type="email"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
[(ngModel)]="editedUser.email"
|
[(ngModel)]="editedUser.email"
|
||||||
placeholder="email@exemple.com"
|
placeholder="votre.email@exemple.com"
|
||||||
[disabled]="saving"
|
[disabled]="saving"
|
||||||
|
required
|
||||||
>
|
>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
@ -389,26 +332,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statut activé -->
|
<!-- Statut du compte -->
|
||||||
@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>
|
|
||||||
</div>
|
|
||||||
} @else {
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Statut du compte</label>
|
<label class="form-label">Statut du compte</label>
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
@ -416,8 +340,32 @@
|
|||||||
{{ getStatusText() }}
|
{{ getStatusText() }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<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 -->
|
<!-- Informations système -->
|
||||||
@if (!isEditing) {
|
@if (!isEditing) {
|
||||||
@ -437,21 +385,23 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Date de création</label>
|
<label class="form-label">Date de création</label>
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
{{ formatTimestamp(user.createdTimestamp) }}
|
{{ getCreationDate() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<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">
|
<div class="form-control-plaintext">
|
||||||
{{ user.createdByUsername || 'Système' }}
|
<span class="badge bg-secondary">{{ getUserTypeDisplay() }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (isMerchantUser()) {
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Type d'utilisateur</label>
|
<label class="form-label">Merchant Partner ID</label>
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext font-monospace small">
|
||||||
<span class="badge bg-secondary">{{ user.userType }}</span>
|
{{ getMerchantPartnerId() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -459,49 +409,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions supplémentaires -->
|
<!-- Actions personnelles -->
|
||||||
@if (!isEditing && canEditUsers) {
|
@if (!isEditing) {
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header bg-light">
|
<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>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-md-4">
|
<div class="col-md-6">
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-warning w-100"
|
class="btn btn-outline-warning w-100"
|
||||||
(click)="resetPassword()"
|
(click)="openMyProfileResetModal()"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
||||||
Réinitialiser MDP
|
Changer le mot de passe
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-6">
|
||||||
@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">
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-primary w-100"
|
class="btn btn-outline-primary w-100"
|
||||||
(click)="startEditing()"
|
(click)="startEditing()"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
|
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
|
||||||
Modifier
|
Modifier mon profil
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -512,3 +443,247 @@
|
|||||||
}
|
}
|
||||||
</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>
|
||||||
@ -1,19 +1,24 @@
|
|||||||
// src/app/modules/users/profile/personal-profile.ts
|
import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, OnDestroy, ViewChild, TemplateRef } from '@angular/core';
|
||||||
import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
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 { 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 {
|
import {
|
||||||
|
User,
|
||||||
UpdateUserDto,
|
UpdateUserDto,
|
||||||
UserRole
|
UserRole,
|
||||||
|
UserType,
|
||||||
|
UserUtils,
|
||||||
|
ResetPasswordDto
|
||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} 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({
|
@Component({
|
||||||
selector: 'app-my-profile',
|
selector: 'app-my-profile',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@ -27,42 +32,61 @@ import {
|
|||||||
.fs-24 {
|
.fs-24 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
.profile-card {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class MyProfile implements OnInit, OnDestroy {
|
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 roleService = inject(RoleManagementService);
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
@Output() back = new EventEmitter<void>();
|
readonly UserRole = UserRole;
|
||||||
@Output() openResetPasswordModal = new EventEmitter<string>();
|
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;
|
loading = false;
|
||||||
saving = false;
|
saving = false;
|
||||||
error = '';
|
error = '';
|
||||||
success = '';
|
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
|
// Édition
|
||||||
isEditing = false;
|
isEditing = false;
|
||||||
editedUser: UpdateUserDto = {};
|
editedUser: UpdateUserDto = {};
|
||||||
|
|
||||||
// Gestion des rôles (simplifiée pour profil personnel)
|
// Gestion des rôles (lecture seule)
|
||||||
availableRoles: { value: UserRole; label: string; description: string }[] = [];
|
availableRoles: { value: UserRole; label: string; description: string }[] = [];
|
||||||
updatingRoles = false;
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.initializeUserPermissions();
|
|
||||||
this.loadAvailableRoles();
|
|
||||||
this.loadUserProfile();
|
this.loadUserProfile();
|
||||||
|
this.loadAvailableRoles();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -70,31 +94,13 @@ export class MyProfile implements OnInit, OnDestroy {
|
|||||||
this.destroy$.complete();
|
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)
|
* Charge les rôles disponibles (lecture seule pour profil personnel)
|
||||||
*/
|
*/
|
||||||
private loadAvailableRoles(): void {
|
private loadAvailableRoles(): void {
|
||||||
this.usersService.getAvailableHubRoles()
|
this.isHubUser()
|
||||||
|
? this.hubUsersService.getAvailableHubRoles()
|
||||||
|
: this.merchantUsersService.getAvailableMerchantRoles()
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
@ -106,15 +112,8 @@ export class MyProfile implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Error loading available roles:', error);
|
console.error('Error loading available roles:', error);
|
||||||
// Fallback avec tous les rôles
|
// Fallback avec les rôles principaux
|
||||||
this.availableRoles = [
|
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' }
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -127,7 +126,8 @@ export class MyProfile implements OnInit, OnDestroy {
|
|||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (profile) => {
|
next: (profile) => {
|
||||||
this.user = profile;
|
this.user = profile || undefined;
|
||||||
|
console.log("Profile User : " + this.user?.role);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
@ -141,33 +141,37 @@ export class MyProfile implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startEditing() {
|
startEditing() {
|
||||||
// Pas de vérification de permission pour le profil personnel
|
|
||||||
this.isEditing = true;
|
this.isEditing = true;
|
||||||
this.editedUser = {
|
this.editedUser = {
|
||||||
firstName: this.user?.firstName,
|
firstName: this.user?.firstName,
|
||||||
lastName: this.user?.lastName,
|
lastName: this.user?.lastName,
|
||||||
email: this.user?.email
|
email: this.user?.email
|
||||||
// On ne permet pas de modifier 'enabled' sur son propre profil
|
|
||||||
};
|
};
|
||||||
this.cdRef.detectChanges();
|
this.clearMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelEditing() {
|
cancelEditing() {
|
||||||
this.isEditing = false;
|
this.isEditing = false;
|
||||||
this.editedUser = {};
|
this.editedUser = {};
|
||||||
this.error = '';
|
this.clearMessages();
|
||||||
this.success = '';
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveProfile() {
|
saveProfile() {
|
||||||
if (!this.user) return;
|
if (!this.user) return;
|
||||||
|
|
||||||
this.saving = true;
|
if (!this.isFormValid()) {
|
||||||
this.error = '';
|
this.error = 'Veuillez remplir tous les champs obligatoires correctement';
|
||||||
this.success = '';
|
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$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (updatedUser) => {
|
next: (updatedUser) => {
|
||||||
@ -186,51 +190,25 @@ export class MyProfile implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion des rôles - désactivée pour profil personnel
|
// ==================== VALIDATION ====================
|
||||||
updateUserRole(newRole: UserRole) {
|
|
||||||
// Non autorisé pour le profil personnel
|
isFormValid(): boolean {
|
||||||
this.error = 'Vous ne pouvez pas modifier votre propre rôle';
|
if (!this.editedUser.firstName?.trim() || !this.editedUser.lastName?.trim()) {
|
||||||
this.cdRef.detectChanges();
|
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
|
isValidEmail(email: string): boolean {
|
||||||
enableUser() {
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
// Non autorisé pour le profil personnel
|
return emailRegex.test(email);
|
||||||
this.error = 'Vous ne pouvez pas vous activer/désactiver vous-même';
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disableUser() {
|
// ==================== UTILITAIRES D'AFFICHAGE ====================
|
||||||
// Non autorisé pour le profil personnel
|
|
||||||
this.error = 'Vous ne pouvez pas vous activer/désactiver vous-même';
|
|
||||||
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 non trouvé';
|
|
||||||
}
|
|
||||||
if (error.status === 400) {
|
|
||||||
return 'Données invalides';
|
|
||||||
}
|
|
||||||
return 'Une erreur est survenue. Veuillez réessayer.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utilitaires d'affichage
|
|
||||||
getStatusBadgeClass(): string {
|
getStatusBadgeClass(): string {
|
||||||
if (!this.user) return 'badge bg-secondary';
|
if (!this.user) return 'badge bg-secondary';
|
||||||
if (!this.user.enabled) return 'badge bg-danger';
|
if (!this.user.enabled) return 'badge bg-danger';
|
||||||
@ -269,58 +247,365 @@ export class MyProfile implements OnInit, OnDestroy {
|
|||||||
return this.user.username;
|
return this.user.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleBadgeClass(role: UserRole): string {
|
getRoleBadgeClass(): string {
|
||||||
return this.roleService.getRoleBadgeClass(role);
|
if (!this.user?.role) return 'badge bg-secondary';
|
||||||
|
return this.roleService.getRoleBadgeClass(this.user.role);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleLabel(role: UserRole): string {
|
getRoleLabel(): string {
|
||||||
return this.roleService.getRoleLabel(role);
|
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);
|
return this.roleService.getRoleIcon(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleDescription(role: UserRole): string {
|
// Obtenir le rôle (peut être string ou UserRole)
|
||||||
const roleInfo = this.availableRoles.find(r => r.value === role);
|
getUserRole(): string | UserRole | undefined {
|
||||||
return roleInfo?.description || 'Description non disponible';
|
return this.user?.role;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérification des permissions pour les actions - toujours false pour les actions sensibles
|
// Pour le template, retourner un tableau pour la boucle
|
||||||
canAssignRole(targetRole: UserRole): boolean {
|
getUserRoles(): (string | UserRole)[] {
|
||||||
return false;
|
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
|
// Afficher le rôle
|
||||||
isCurrentUserProfile(): boolean {
|
getUserRoleDisplay(): string {
|
||||||
return true;
|
if (!this.user?.role) return 'Aucun rôle';
|
||||||
|
return this.getRoleLabel();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthode utilitaire pour déterminer le type d'utilisateur
|
// ==================== GESTION DES ERREURS ====================
|
||||||
getUserType(): string {
|
|
||||||
if (!this.currentUserRole) return 'Utilisateur';
|
|
||||||
|
|
||||||
const roleNames: { [key in UserRole]?: string } = {
|
private getErrorMessage(error: any): string {
|
||||||
[UserRole.DCB_ADMIN]: 'Administrateur DCB',
|
if (error.error?.message) {
|
||||||
[UserRole.DCB_SUPPORT]: 'Support DCB',
|
return error.error.message;
|
||||||
[UserRole.DCB_PARTNER]: 'Partenaire DCB',
|
}
|
||||||
[UserRole.DCB_PARTNER_ADMIN]: 'Administrateur Partenaire',
|
if (error.status === 400) {
|
||||||
[UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire',
|
return 'Données invalides. Vérifiez les informations saisies.';
|
||||||
[UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire'
|
}
|
||||||
|
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.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MÉTHODES DE NAVIGATION ====================
|
||||||
|
|
||||||
|
goBack() {
|
||||||
|
this.back.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MÉTHODES UTILITAIRES ====================
|
||||||
|
|
||||||
|
isHubUser(): boolean {
|
||||||
|
return UserUtils.isHubUser(this.user!);
|
||||||
|
}
|
||||||
|
|
||||||
|
isMerchantUser(): boolean {
|
||||||
|
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
|
||||||
};
|
};
|
||||||
|
|
||||||
return roleNames[this.currentUserRole] || this.currentUserRole;
|
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.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifie si l'utilisateur est un utilisateur Hub
|
console.error('❌ Erreur réinitialisation mot de passe personnel:', error);
|
||||||
isHubUser(): boolean {
|
}
|
||||||
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
|
});
|
||||||
return this.currentUserRole ? hubRoles.includes(this.currentUserRole) : false;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifie si l'utilisateur est un utilisateur marchand
|
|
||||||
isMerchantUser(): boolean {
|
// Actions désactivées pour le profil personnel
|
||||||
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
|
updateUserRole(newRole: UserRole) {
|
||||||
return this.currentUserRole ? merchantRoles.includes(this.currentUserRole) : false;
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
13
src/main.ts
13
src/main.ts
@ -1,13 +1,6 @@
|
|||||||
import 'zone.js';
|
|
||||||
import { bootstrapApplication } from '@angular/platform-browser';
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
import { App } from './app/app';
|
import { App } from './app/app';
|
||||||
import { AuthService } from './app/core/services/auth.service';
|
|
||||||
import { appConfig } from './app/app.config';
|
import { appConfig } from './app/app.config';
|
||||||
bootstrapApplication(App, {
|
|
||||||
providers: [
|
bootstrapApplication(App, appConfig)
|
||||||
...appConfig.providers,
|
.catch(err => console.error('❌ BO Admin bootstrap error', err));
|
||||||
]
|
|
||||||
}).then(async appRef => {
|
|
||||||
const authService = appRef.injector.get(AuthService);
|
|
||||||
await authService.initialize();
|
|
||||||
}).catch(err => console.error('BO Admin error', err));
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user