dcb-backoffice/src/app/modules/merchant-users/list/list.ts

695 lines
22 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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();
}
}