feat: Add Health Check Endpoint

This commit is contained in:
diallolatoile 2026-01-17 13:27:07 +00:00
parent de4c725554
commit 326b9c8ec1
7 changed files with 730 additions and 70 deletions

View File

@ -1,7 +1,7 @@
import { Injectable, inject, EventEmitter } from '@angular/core'; import { Injectable, inject, EventEmitter } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { environment } from '@environments/environment'; import { environment } from '@environments/environment';
import { BehaviorSubject, Observable, throwError, tap, catchError, finalize, of, filter, take } from 'rxjs'; import { BehaviorSubject, Observable, throwError, tap, catchError, finalize, of, filter, take, map } from 'rxjs';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { import {
@ -325,13 +325,12 @@ export class AuthService {
return this.http.get<any>( return this.http.get<any>(
`${environment.iamApiUrl}/auth/profile` `${environment.iamApiUrl}/auth/profile`
).pipe( ).pipe(
tap(apiResponse => { map(apiResponse => {
// Déterminer le type d'utilisateur
const userType = this.determineUserType(apiResponse); const userType = this.determineUserType(apiResponse);
// Mapper vers le modèle User
const userProfile = this.mapToUserModel(apiResponse, userType); const userProfile = this.mapToUserModel(apiResponse, userType);
this.userProfile$.next(userProfile); this.userProfile$.next(userProfile);
return userProfile;
}), }),
catchError(error => { catchError(error => {
console.error('❌ Erreur chargement profil:', error); console.error('❌ Erreur chargement profil:', error);

View File

@ -15,14 +15,41 @@
<!-- États normal et erreur avec @if --> <!-- États normal et erreur avec @if -->
@if (!isLoading) { @if (!isLoading) {
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<img @if (user){
[src]="getUserAvatar()" @if (merchant){
class="rounded-circle me-2" @if (merchant.logo && merchant.logo.trim() !== '') {
width="36" <img
height="36" [src]="getMerchantLogoUrl(merchant.id, merchant.logo, merchant.name) | async"
alt="user-image" [alt]="merchant.name + ' logo'"
(error)="onAvatarError($event)" class="rounded-circle me-2"
/> width="36"
height="36"
loading="lazy"
(error)="onLogoError($event, merchant.name)"
/>
} @else {
<img
[src]="getDefaultLogoUrl(merchant.name)"
[alt]="merchant.name + ' logo'"
class="rounded-circle me-2"
width="36"
height="36"
loading="lazy"
(error)="onDefaultLogoError($event)"
/>
}
}@else {
<img
[src]="getDefaultLogoUrl(user.username)"
[alt]="user.username + ' logo'"
class="rounded-circle me-2"
width="36"
height="36"
loading="lazy"
(error)="onDefaultLogoError($event)"
/>
}
}
<div> <div>
<h5 class="my-0 fw-semibold"> <h5 class="my-0 fw-semibold">
{{ getDisplayName() || 'Utilisateur' }} {{ getDisplayName() || 'Utilisateur' }}

View File

@ -3,23 +3,45 @@ 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 { User, UserRole } from '@core/models/dcb-bo-hub-user.model';
import { Subject, takeUntil, distinctUntilChanged, filter, startWith } from 'rxjs'; import { Subject, takeUntil, distinctUntilChanged, filter, startWith, catchError, map, Observable, of, Subscription } from 'rxjs';
import { CommonModule } from '@angular/common';
import { Merchant } from '@core/models/merchant-config.model';
import { MinioService } from '@core/services/minio.service';
import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service';
@Component({ @Component({
selector: 'app-user-profile', selector: 'app-user-profile',
standalone: true, standalone: true,
imports: [NgbCollapseModule], imports: [NgbCollapseModule,CommonModule],
templateUrl: './user-profile.component.html', templateUrl: './user-profile.component.html',
}) })
export class UserProfileComponent implements OnInit, OnDestroy { export class UserProfileComponent implements OnInit, OnDestroy {
private authService = inject(AuthService); private authService = inject(AuthService);
private cdr = inject(ChangeDetectorRef); private merchantConfigService = inject(MerchantConfigService);
private subscription?: Subscription;
private minioService = inject(MinioService);
private cdRef = inject(ChangeDetectorRef);
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
user: User | null = null; // Cache des URLs de logos
private logoUrlCache = new Map<string, string>();
// Ajouter un cache pour les logos non trouvés
private logoErrorCache = new Set<string>();
// Cache
private merchantCache: { data: Merchant, timestamp: number } | null = null;
private readonly CACHE_TTL = 2 * 60 * 1000; // 2 minutes
// Permissions
currentUserRole: any = null;
isHubUser = false;
merchant: Merchant | null = null;
user: User | undefined;
merchanPartnerId: string | undefined
// États
isLoading = true; isLoading = true;
hasError = false; hasError = false;
hasSuccess = '';
currentProfileLoaded = false; currentProfileLoaded = false;
ngOnInit(): void { ngOnInit(): void {
@ -45,9 +67,9 @@ export class UserProfileComponent implements OnInit, OnDestroy {
// Le profil sera chargé via la subscription // Le profil sera chargé via la subscription
} else { } else {
console.log('🔐 User not authenticated'); console.log('🔐 User not authenticated');
this.user = null; this.user = undefined;
this.isLoading = false; this.isLoading = false;
this.cdr.detectChanges(); this.cdRef.detectChanges();
} }
} }
@ -76,22 +98,31 @@ export class UserProfileComponent implements OnInit, OnDestroy {
if (profile) { if (profile) {
console.log('📥 User profile updated:', profile.username); console.log('📥 User profile updated:', profile.username);
this.user = profile; this.user = profile;
this.currentUserRole = this.extractUserRole(profile);
this.isHubUser = this.checkIfHubUser();
if (!this.isHubUser) {
this.merchanPartnerId = profile?.merchantPartnerId;
this.loadMerchantProfile()
}
this.currentProfileLoaded = true; this.currentProfileLoaded = true;
} else { } else {
console.log('📭 User profile cleared'); console.log('📭 User profile cleared');
this.user = null; this.user = undefined;
this.currentProfileLoaded = false; this.currentProfileLoaded = false;
} }
this.isLoading = false; this.isLoading = false;
this.hasError = false; this.hasError = false;
this.cdr.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
console.error('❌ Error in profile subscription:', error); console.error('❌ Error in profile subscription:', error);
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
this.cdr.detectChanges(); this.cdRef.detectChanges();
} }
}); });
} }
@ -115,10 +146,10 @@ export class UserProfileComponent implements OnInit, OnDestroy {
} else { } else {
// Si l'utilisateur s'est déconnecté // Si l'utilisateur s'est déconnecté
console.log('👋 User logged out'); console.log('👋 User logged out');
this.user = null; this.user = undefined;
this.currentProfileLoaded = false; this.currentProfileLoaded = false;
this.isLoading = false; this.isLoading = false;
this.cdr.detectChanges(); this.cdRef.detectChanges();
} }
} }
}); });
@ -130,21 +161,20 @@ export class UserProfileComponent implements OnInit, OnDestroy {
loadUserProfile(): void { loadUserProfile(): void {
this.isLoading = true; this.isLoading = true;
this.hasError = false; this.hasError = false;
this.cdr.detectChanges(); this.cdRef.detectChanges();
this.authService.loadUserProfile() this.authService.loadUserProfile()
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (profile) => { next: (profile) => {
// Note: le profil sera automatiquement mis à jour via la subscription getUserProfile()
this.isLoading = false; this.isLoading = false;
this.cdr.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
console.error('❌ Failed to load user profile:', error); console.error('❌ Failed to load user profile:', error);
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
this.cdr.detectChanges(); this.cdRef.detectChanges();
// Essayer de rafraîchir le token si erreur 401 // Essayer de rafraîchir le token si erreur 401
if (error.status === 401) { if (error.status === 401) {
@ -162,6 +192,179 @@ export class UserProfileComponent implements OnInit, OnDestroy {
}); });
} }
/**
* Charge le profil COMPLET du merchant
*/
loadMerchantProfile() {
if (this.shouldUseCache()) {
this.merchant = this.merchantCache!.data;
this.isLoading = false;
this.cdRef.detectChanges();
return;
}
this.isLoading = true;
this.hasError = false;
console.log("📥 Chargement du profil complet du merchant:", this.merchanPartnerId);
this.merchantConfigService.getMerchantById(Number(this.merchanPartnerId))
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (merchant) => {
this.merchant = merchant;
// Mise en cache
this.merchantCache = {
data: merchant,
timestamp: Date.now()
};
console.log("✅ Profil merchant chargé:", merchant);
this.isLoading = false;
this.cdRef.detectChanges();
},
error: (error) => {
console.error('❌ Error loading merchant profile:', error);
this.hasError = true;
this.isLoading = false;
this.cdRef.detectChanges();
}
});
}
// ==================== AFFICHAGE DU LOGO ====================
/**
* Récupère l'URL du logo avec fallback automatique
*/
getMerchantLogoUrl(
merchanPartnerId: number | undefined,
logoFileName: string,
merchantName: string
): Observable<string> {
const newMerchantId = String(merchanPartnerId);
const cacheKey = `${merchanPartnerId}_${logoFileName}`;
// Vérifier si le logo est en cache d'erreur
if (this.logoErrorCache.has(cacheKey)) {
const defaultLogo = this.getDefaultLogoUrl(merchantName);
return of(defaultLogo);
}
// Vérifier le cache normal
if (this.logoUrlCache.has(cacheKey)) {
return of(this.logoUrlCache.get(cacheKey)!);
}
// Récupérer l'URL depuis l'API avec la nouvelle structure
return this.minioService.getMerchantLogoUrl(
newMerchantId,
logoFileName,
{ signed: true, expirySeconds: 3600 }
).pipe(
map(response => {
// Extraire l'URL de la réponse
const url = response.data.url ;
// Mettre en cache avec la clé composite
this.logoUrlCache.set(cacheKey, url);
return url;
}),
catchError(error => {
console.warn(`⚠️ Logo not found for merchant ${merchanPartnerId}: ${logoFileName}`, error);
// En cas d'erreur, ajouter au cache d'erreur
this.logoErrorCache.add(cacheKey);
// Générer un logo par défaut
const defaultLogo = this.getDefaultLogoUrl(merchantName);
// Mettre le logo par défaut dans le cache normal aussi
this.logoUrlCache.set(cacheKey, defaultLogo);
return of(defaultLogo);
})
);
}
/**
* Génère une URL de logo par défaut basée sur les initiales
*/
getDefaultLogoUrl(merchantName: string): string {
// Créer des initiales significatives
const initials = this.extractInitials(merchantName);
// Palette de couleurs agréables
const colors = [
'667eea', // Violet
'764ba2', // Violet foncé
'f56565', // Rouge
'4299e1', // Bleu
'48bb78', // Vert
'ed8936', // Orange
'FF6B6B', // Rouge clair
'4ECDC4', // Turquoise
'45B7D1', // Bleu clair
'96CEB4' // Vert menthe
];
const colorIndex = merchantName.length % colors.length;
const backgroundColor = colors[colorIndex];
// Taille fixe à 80px (l'API génère un carré de cette taille)
// L'image sera redimensionnée à 40px via CSS
return `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&background=${backgroundColor}&color=FFFFFF&size=80`;
}
/**
* Gère les erreurs de chargement des logos MinIO
*/
onLogoError(event: Event, merchantName: string): void {
const img = event.target as HTMLImageElement;
if (!img) return;
console.warn('Logo MinIO failed to load, using default for:', merchantName);
img.onerror = null;
img.src = this.getDefaultLogoUrl(merchantName);
}
/**
* Gère les erreurs de chargement des logos par défaut
*/
onDefaultLogoError(event: Event | string): void {
if (!(event instanceof Event)) {
console.error('Default logo error (non-event):', event);
return;
}
const img = event.target as HTMLImageElement | null;
if (!img) return;
console.error('Default logo also failed to load, using fallback SVG');
// SVG local
img.onerror = null; // éviter boucle infinie
img.src = 'assets/images/default-merchant-logo.svg';
// Dernier recours
img.onerror = (e) => {
if (!(e instanceof Event)) return;
const fallbackImg = e.target as HTMLImageElement | null;
if (!fallbackImg) return;
fallbackImg.onerror = null;
fallbackImg.src = this.generateFallbackDataUrl();
};
}
/** /**
* Méthode pour réessayer le chargement en cas d'erreur * Méthode pour réessayer le chargement en cas d'erreur
*/ */
@ -234,19 +437,91 @@ export class UserProfileComponent implements OnInit, OnDestroy {
return roleClassMap[this.user.role] || 'badge bg-secondary'; return roleClassMap[this.user.role] || 'badge bg-secondary';
} }
private extractUserRole(user: any): any {
const userRoles = this.authService.getCurrentUserRoles();
return userRoles && userRoles.length > 0 ? userRoles[0] : null;
}
private checkIfHubUser(): boolean {
if (!this.currentUserRole) return false;
const hubRoles = [
UserRole.DCB_ADMIN,
UserRole.DCB_SUPPORT
];
return hubRoles.includes(this.currentUserRole);
}
private shouldUseCache(): boolean {
if (!this.merchantCache) return false;
const cacheAge = Date.now() - this.merchantCache.timestamp;
return cacheAge < this.CACHE_TTL && this.merchantCache.data !== null;
}
private clearCache(): void {
this.merchantCache = null;
}
/** /**
* Obtient l'URL de l'avatar de l'utilisateur * Extrait les initiales de manière intelligente
*/ */
getUserAvatar(): string { private extractInitials(name: string): string {
return `assets/images/users/user-2.jpg`; if (!name || name.trim() === '') {
return '??';
}
// Nettoyer le nom
const cleanedName = name.trim().toUpperCase();
// Extraire les mots
const words = cleanedName.split(/\s+/);
// Si un seul mot, prendre les deux premières lettres
if (words.length === 1) {
return words[0].substring(0, 2) || '??';
}
// Prendre la première lettre des deux premiers mots
const initials = words
.slice(0, 2) // Prendre les 2 premiers mots
.map(word => word[0] || '')
.join('');
return initials || name.substring(0, 2).toUpperCase() || '??';
} }
/** /**
* Gère les erreurs de chargement d'avatar * Génère un fallback SVG en data URL
*/ */
onAvatarError(event: Event): void { private generateFallbackDataUrl(): string {
const img = event.target as HTMLImageElement; const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
img.src = 'assets/images/users/user-2.jpg'; <rect width="40" height="40" fill="#667eea" rx="20"/>
img.onerror = null; <text x="20" y="22" text-anchor="middle" fill="white" font-family="Arial" font-size="14" font-weight="bold">?</text>
</svg>`;
return 'data:image/svg+xml;base64,' + btoa(svg);
}
// ==================== GESTION DES ERREURS ====================
private getErrorMessage(error: any): string {
if (error.error?.message) {
return error.error.message;
}
if (error.status === 400) {
return 'Données invalides. Vérifiez les informations saisies.';
}
if (error.status === 403) {
return 'Vous n\'avez pas les permissions nécessaires pour cette action';
}
if (error.status === 404) {
return 'Utilisateur non trouvé';
}
if (error.status === 409) {
return 'Cet email est déjà utilisé par un autre utilisateur';
}
return 'Une erreur est survenue. Veuillez réessayer.';
} }
} }

View File

@ -4,12 +4,41 @@
ngbDropdownToggle ngbDropdownToggle
class="topbar-link dropdown-toggle drop-arrow-none px-2" class="topbar-link dropdown-toggle drop-arrow-none px-2"
> >
<img @if (user){
src="assets/images/users/user-2.jpg" @if (merchant){
width="32" @if (merchant.logo && merchant.logo.trim() !== '') {
class="rounded-circle d-flex" <img
alt="user-image" [src]="getMerchantLogoUrl(merchant.id, merchant.logo, merchant.name) | async"
/> [alt]="merchant.name + ' logo'"
class="rounded-circle me-2"
width="36"
height="36"
loading="lazy"
(error)="onLogoError($event, merchant.name)"
/>
} @else {
<img
[src]="getDefaultLogoUrl(merchant.name)"
[alt]="merchant.name + ' logo'"
class="rounded-circle me-2"
width="36"
height="36"
loading="lazy"
(error)="onDefaultLogoError($event)"
/>
}
}@else {
<img
[src]="getDefaultLogoUrl(user.username)"
[alt]="user.username + ' logo'"
class="rounded-circle me-2"
width="36"
height="36"
loading="lazy"
(error)="onDefaultLogoError($event)"
/>
}
}
</button> </button>
<div ngbDropdownMenu class="dropdown-menu dropdown-menu-end"> <div ngbDropdownMenu class="dropdown-menu dropdown-menu-end">
@for (item of menuItems; track $index; let i = $index) { @for (item of menuItems; track $index; let i = $index) {

View File

@ -1,4 +1,4 @@
import { Component, inject, OnInit, OnDestroy } from '@angular/core' import { Component, inject, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'
import { AuthService } from '@core/services/auth.service' import { AuthService } from '@core/services/auth.service'
import { MenuService } from '@core/services/menu.service' import { MenuService } from '@core/services/menu.service'
import { import {
@ -9,7 +9,12 @@ import {
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { NgIcon } from '@ng-icons/core' import { NgIcon } from '@ng-icons/core'
import { UserDropdownItemType } from '@/app/types/layout' import { UserDropdownItemType } from '@/app/types/layout'
import { Subscription } from 'rxjs' import { catchError, map, Observable, of, Subject, Subscription, takeUntil } from 'rxjs'
import { MinioService } from '@core/services/minio.service'
import { Merchant } from '@core/models/merchant-config.model'
import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service'
import { User, UserRole } from '@core/models/dcb-bo-hub-user.model'
import { CommonModule } from '@angular/common'
@Component({ @Component({
selector: 'app-user-profile-topbar', selector: 'app-user-profile-topbar',
@ -19,17 +24,46 @@ import { Subscription } from 'rxjs'
NgbDropdownToggle, NgbDropdownToggle,
RouterLink, RouterLink,
NgIcon, NgIcon,
CommonModule,
], ],
templateUrl: './user-profile.html', templateUrl: './user-profile.html',
}) })
export class UserProfile implements OnInit, OnDestroy { export class UserProfile implements OnInit, OnDestroy {
private authService = inject(AuthService) private authService = inject(AuthService);
private menuService = inject(MenuService) private merchantConfigService = inject(MerchantConfigService);
private subscription?: Subscription private menuService = inject(MenuService);
private subscription?: Subscription;
private minioService = inject(MinioService);
private cdRef = inject(ChangeDetectorRef);
private destroy$ = new Subject<void>();
// Cache des URLs de logos
private logoUrlCache = new Map<string, string>();
// Ajouter un cache pour les logos non trouvés
private logoErrorCache = new Set<string>();
// Cache
private merchantCache: { data: Merchant, timestamp: number } | null = null;
private readonly CACHE_TTL = 2 * 60 * 1000; // 2 minutes
// Permissions
currentUserRole: any = null;
isHubUser = false;
merchant: Merchant | null = null;
user: User | undefined;
// États
loading = false;
error = '';
success = '';
menuItems: UserDropdownItemType[] = [] menuItems: UserDropdownItemType[] = []
merchanPartnerId: string | undefined
ngOnInit() { ngOnInit() {
this.loadUserProfile()
this.loadDropdownItems() this.loadDropdownItems()
this.subscription = this.authService.onAuthState().subscribe(() => { this.subscription = this.authService.onAuthState().subscribe(() => {
@ -37,11 +71,305 @@ export class UserProfile implements OnInit, OnDestroy {
}) })
} }
ngOnDestroy() { ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.subscription?.unsubscribe() this.subscription?.unsubscribe()
} }
// ==================== CHARGEMENT DES DONNÉES ====================
loadUserProfile() {
this.loading = true;
this.error = '';
this.authService.loadUserProfile()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (profile) => {
this.user = profile;
console.log("Profile User : " + profile?.role);
this.currentUserRole = this.extractUserRole(profile);
this.isHubUser = this.checkIfHubUser();
if (!this.isHubUser) {
this.merchanPartnerId = profile.merchantPartnerId;
this.loadMerchantProfile()
}
this.loading = false;
this.cdRef.detectChanges();
},
error: (error) => {
this.error = 'Erreur lors du chargement de votre profil';
this.loading = false;
this.cdRef.detectChanges();
console.error('Error loading user profile:', error);
}
});
}
/**
* Charge le profil COMPLET du merchant
*/
loadMerchantProfile() {
if (this.shouldUseCache()) {
this.merchant = this.merchantCache!.data;
this.loading = false;
this.cdRef.detectChanges();
return;
}
this.loading = true;
this.error = '';
console.log("📥 Chargement du profil complet du merchant:", this.merchanPartnerId);
this.merchantConfigService.getMerchantById(Number(this.merchanPartnerId))
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (merchant) => {
this.merchant = merchant;
// Mise en cache
this.merchantCache = {
data: merchant,
timestamp: Date.now()
};
console.log("✅ Profil merchant chargé:", merchant);
this.loading = false;
this.cdRef.detectChanges();
},
error: (error) => {
console.error('❌ Error loading merchant profile:', error);
this.error = this.getErrorMessage(error);
this.loading = false;
this.cdRef.detectChanges();
}
});
}
// ==================== AFFICHAGE DU LOGO ====================
/**
* Récupère l'URL du logo avec fallback automatique
*/
getMerchantLogoUrl(
merchanPartnerId: number | undefined,
logoFileName: string,
merchantName: string
): Observable<string> {
const newMerchantId = String(merchanPartnerId);
const cacheKey = `${merchanPartnerId}_${logoFileName}`;
// Vérifier si le logo est en cache d'erreur
if (this.logoErrorCache.has(cacheKey)) {
const defaultLogo = this.getDefaultLogoUrl(merchantName);
return of(defaultLogo);
}
// Vérifier le cache normal
if (this.logoUrlCache.has(cacheKey)) {
return of(this.logoUrlCache.get(cacheKey)!);
}
// Récupérer l'URL depuis l'API avec la nouvelle structure
return this.minioService.getMerchantLogoUrl(
newMerchantId,
logoFileName,
{ signed: true, expirySeconds: 3600 }
).pipe(
map(response => {
// Extraire l'URL de la réponse
const url = response.data.url ;
// Mettre en cache avec la clé composite
this.logoUrlCache.set(cacheKey, url);
return url;
}),
catchError(error => {
console.warn(`⚠️ Logo not found for merchant ${merchanPartnerId}: ${logoFileName}`, error);
// En cas d'erreur, ajouter au cache d'erreur
this.logoErrorCache.add(cacheKey);
// Générer un logo par défaut
const defaultLogo = this.getDefaultLogoUrl(merchantName);
// Mettre le logo par défaut dans le cache normal aussi
this.logoUrlCache.set(cacheKey, defaultLogo);
return of(defaultLogo);
})
);
}
/**
* Génère une URL de logo par défaut basée sur les initiales
*/
getDefaultLogoUrl(merchantName: string): string {
// Créer des initiales significatives
const initials = this.extractInitials(merchantName);
// Palette de couleurs agréables
const colors = [
'667eea', // Violet
'764ba2', // Violet foncé
'f56565', // Rouge
'4299e1', // Bleu
'48bb78', // Vert
'ed8936', // Orange
'FF6B6B', // Rouge clair
'4ECDC4', // Turquoise
'45B7D1', // Bleu clair
'96CEB4' // Vert menthe
];
const colorIndex = merchantName.length % colors.length;
const backgroundColor = colors[colorIndex];
// Taille fixe à 80px (l'API génère un carré de cette taille)
// L'image sera redimensionnée à 40px via CSS
return `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&background=${backgroundColor}&color=FFFFFF&size=80`;
}
/**
* Gère les erreurs de chargement des logos MinIO
*/
onLogoError(event: Event, merchantName: string): void {
const img = event.target as HTMLImageElement;
if (!img) return;
console.warn('Logo MinIO failed to load, using default for:', merchantName);
img.onerror = null;
img.src = this.getDefaultLogoUrl(merchantName);
}
/**
* Gère les erreurs de chargement des logos par défaut
*/
onDefaultLogoError(event: Event | string): void {
if (!(event instanceof Event)) {
console.error('Default logo error (non-event):', event);
return;
}
const img = event.target as HTMLImageElement | null;
if (!img) return;
console.error('Default logo also failed to load, using fallback SVG');
// SVG local
img.onerror = null; // éviter boucle infinie
img.src = 'assets/images/default-merchant-logo.svg';
// Dernier recours
img.onerror = (e) => {
if (!(e instanceof Event)) return;
const fallbackImg = e.target as HTMLImageElement | null;
if (!fallbackImg) return;
fallbackImg.onerror = null;
fallbackImg.src = this.generateFallbackDataUrl();
};
}
private extractUserRole(user: any): any {
const userRoles = this.authService.getCurrentUserRoles();
return userRoles && userRoles.length > 0 ? userRoles[0] : null;
}
private checkIfHubUser(): boolean {
if (!this.currentUserRole) return false;
const hubRoles = [
UserRole.DCB_ADMIN,
UserRole.DCB_SUPPORT
];
return hubRoles.includes(this.currentUserRole);
}
private loadDropdownItems() { private loadDropdownItems() {
this.menuItems = this.menuService.getUserDropdownItems() this.menuItems = this.menuService.getUserDropdownItems()
} }
private shouldUseCache(): boolean {
if (!this.merchantCache) return false;
const cacheAge = Date.now() - this.merchantCache.timestamp;
return cacheAge < this.CACHE_TTL && this.merchantCache.data !== null;
}
private clearCache(): void {
this.merchantCache = null;
}
/**
* Extrait les initiales de manière intelligente
*/
private extractInitials(name: string): string {
if (!name || name.trim() === '') {
return '??';
}
// Nettoyer le nom
const cleanedName = name.trim().toUpperCase();
// Extraire les mots
const words = cleanedName.split(/\s+/);
// Si un seul mot, prendre les deux premières lettres
if (words.length === 1) {
return words[0].substring(0, 2) || '??';
}
// Prendre la première lettre des deux premiers mots
const initials = words
.slice(0, 2) // Prendre les 2 premiers mots
.map(word => word[0] || '')
.join('');
return initials || name.substring(0, 2).toUpperCase() || '??';
}
/**
* Génère un fallback SVG en data URL
*/
private generateFallbackDataUrl(): string {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
<rect width="40" height="40" fill="#667eea" rx="20"/>
<text x="20" y="22" text-anchor="middle" fill="white" font-family="Arial" font-size="14" font-weight="bold">?</text>
</svg>`;
return 'data:image/svg+xml;base64,' + btoa(svg);
}
// ==================== GESTION DES ERREURS ====================
private getErrorMessage(error: any): string {
if (error.error?.message) {
return error.error.message;
}
if (error.status === 400) {
return 'Données invalides. Vérifiez les informations saisies.';
}
if (error.status === 403) {
return 'Vous n\'avez pas les permissions nécessaires pour cette action';
}
if (error.status === 404) {
return 'Utilisateur non trouvé';
}
if (error.status === 409) {
return 'Cet email est déjà utilisé par un autre utilisateur';
}
return 'Une erreur est survenue. Veuillez réessayer.';
}
} }

View File

@ -307,7 +307,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
constructor( constructor(
private accessService: DashboardAccessService, private accessService: DashboardAccessService,
private cdr: ChangeDetectorRef private cdRef: ChangeDetectorRef
) { ) {
Chart.register(...registerables); Chart.register(...registerables);
} }
@ -326,12 +326,14 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
console.log('✅ Dashboard: waitForReady() a émis - Initialisation...'); console.log('✅ Dashboard: waitForReady() a émis - Initialisation...');
this.dashboardInitialized = true; this.dashboardInitialized = true;
this.initializeDashboard(); this.initializeDashboard();
this.cdRef.detectChanges();
}, },
error: (err) => { error: (err) => {
console.error('❌ Dashboard: Erreur dans waitForReady():', err); console.error('❌ Dashboard: Erreur dans waitForReady():', err);
// Gérer l'erreur - peut-être rediriger vers une page d'erreur // Gérer l'erreur - peut-être rediriger vers une page d'erreur
this.addAlert('danger', 'Erreur d\'initialisation', this.addAlert('danger', 'Erreur d\'initialisation',
'Impossible de charger les informations d\'accès', 'Maintenant'); 'Impossible de charger les informations d\'accès', 'Maintenant');
this.cdRef.detectChanges();
} }
}) })
); );
@ -413,7 +415,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
this.accessService.getAvailableMerchants().subscribe({ this.accessService.getAvailableMerchants().subscribe({
next: (merchants) => { next: (merchants) => {
this.allowedMerchants = merchants; this.allowedMerchants = merchants;
this.cdr.detectChanges(); this.cdRef.detectChanges();
}, },
error: (err) => { error: (err) => {
console.error('Erreur lors du chargement des merchants:', err); console.error('Erreur lors du chargement des merchants:', err);
@ -445,14 +447,14 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
console.log('Données globales chargées avec succès'); console.log('Données globales chargées avec succès');
this.loading.globalData = false; this.loading.globalData = false;
this.calculateStats(); this.calculateStats();
this.cdr.detectChanges(); this.cdRef.detectChanges();
setTimeout(() => this.updateAllCharts(), 100); setTimeout(() => this.updateAllCharts(), 100);
}, },
error: (err) => { error: (err) => {
console.error('Erreur lors du chargement des données globales:', err); console.error('Erreur lors du chargement des données globales:', err);
this.loading.globalData = false; this.loading.globalData = false;
this.addAlert('danger', 'Erreur de chargement', 'Impossible de charger les données globales', 'Maintenant'); this.addAlert('danger', 'Erreur de chargement', 'Impossible de charger les données globales', 'Maintenant');
this.cdr.detectChanges(); this.cdRef.detectChanges();
} }
}) })
); );
@ -485,7 +487,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
console.log(`Données du merchant ${merchantId} chargées avec succès`); console.log(`Données du merchant ${merchantId} chargées avec succès`);
this.loading.merchantData = false; this.loading.merchantData = false;
this.calculateStats(); this.calculateStats();
this.cdr.detectChanges(); this.cdRef.detectChanges();
setTimeout(() => this.updateAllCharts(), 100); setTimeout(() => this.updateAllCharts(), 100);
}, },
error: (err) => { error: (err) => {
@ -493,7 +495,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
this.loading.merchantData = false; this.loading.merchantData = false;
this.addAlert('danger', 'Erreur de chargement', this.addAlert('danger', 'Erreur de chargement',
`Impossible de charger les données du merchant ${merchantId}`, 'Maintenant'); `Impossible de charger les données du merchant ${merchantId}`, 'Maintenant');
this.cdr.detectChanges(); this.cdRef.detectChanges();
} }
}) })
); );
@ -730,12 +732,12 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
(ctx as any).chart = newChart; (ctx as any).chart = newChart;
this.loading.chart = false; this.loading.chart = false;
this.cdr.detectChanges(); this.cdRef.detectChanges();
} catch (error) { } catch (error) {
console.error('Erreur lors de la création du graphique principal:', error); console.error('Erreur lors de la création du graphique principal:', error);
this.loading.chart = false; this.loading.chart = false;
this.cdr.detectChanges(); this.cdRef.detectChanges();
} }
} }
@ -1109,13 +1111,13 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
this.updateOverallHealth(); this.updateOverallHealth();
this.generateHealthAlerts(); this.generateHealthAlerts();
this.loading.healthCheck = false; this.loading.healthCheck = false;
this.cdr.detectChanges(); this.cdRef.detectChanges();
}), }),
catchError(err => { catchError(err => {
console.error('Erreur lors du health check:', err); console.error('Erreur lors du health check:', err);
this.addAlert('danger', 'Erreur de vérification', 'Impossible de vérifier la santé des services', 'Maintenant'); this.addAlert('danger', 'Erreur de vérification', 'Impossible de vérifier la santé des services', 'Maintenant');
this.loading.healthCheck = false; this.loading.healthCheck = false;
this.cdr.detectChanges(); this.cdRef.detectChanges();
return of(null); return of(null);
}) })
).subscribe() ).subscribe()
@ -1274,7 +1276,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
this.metricDropdown.close(); this.metricDropdown.close();
} }
this.cdr.detectChanges(); this.cdRef.detectChanges();
setTimeout(() => { setTimeout(() => {
this.updateMainChart(); this.updateMainChart();
@ -1284,7 +1286,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
changePeriod(period: ReportPeriod): void { changePeriod(period: ReportPeriod): void {
this.dataSelection.period = period; this.dataSelection.period = period;
this.cdr.detectChanges(); this.cdRef.detectChanges();
setTimeout(() => { setTimeout(() => {
this.updateMainChart(); this.updateMainChart();
@ -1294,7 +1296,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
changeChartType(type: ChartType): void { changeChartType(type: ChartType): void {
this.dataSelection.chartType = type; this.dataSelection.chartType = type;
this.cdr.detectChanges(); this.cdRef.detectChanges();
setTimeout(() => { setTimeout(() => {
this.updateMainChart(); this.updateMainChart();
@ -1302,7 +1304,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
} }
refreshChartData(): void { refreshChartData(): void {
this.cdr.detectChanges(); this.cdRef.detectChanges();
setTimeout(() => { setTimeout(() => {
this.updateAllCharts(); this.updateAllCharts();
}, 50); }, 50);

View File

@ -183,13 +183,18 @@ export class MerchantConfigView implements OnInit, OnDestroy {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private minioService = inject(MinioService); private minioService = inject(MinioService);
private sanitizer = inject(DomSanitizer);
// Cache des URLs de logos // Cache des URLs de logos
private logoUrlCache = new Map<string, string>(); private logoUrlCache = new Map<string, string>();
// Ajouter un cache pour les logos non trouvés // Ajouter un cache pour les logos non trouvés
private logoErrorCache = new Set<string>(); private logoErrorCache = new Set<string>();
private deleteModalRef: any = null;
// Cache
private merchantCache: { data: Merchant, timestamp: number } | null = null;
private readonly CACHE_TTL = 2 * 60 * 1000; // 2 minutes
readonly ConfigType = ConfigType; readonly ConfigType = ConfigType;
readonly Operator = Operator; readonly Operator = Operator;
readonly MerchantUtils = MerchantUtils; readonly MerchantUtils = MerchantUtils;
@ -218,8 +223,7 @@ export class MerchantConfigView implements OnInit, OnDestroy {
editingConfigId: number | null = null; editingConfigId: number | null = null;
editedConfig: UpdateMerchantConfigDto = {}; editedConfig: UpdateMerchantConfigDto = {};
configToDelete: MerchantConfig | null = null; configToDelete: MerchantConfig | null = null;
private deleteModalRef: any = null;
// Affichage des valeurs sensibles // Affichage des valeurs sensibles
showSensitiveValues: { [configId: number]: boolean } = {}; showSensitiveValues: { [configId: number]: boolean } = {};
@ -232,10 +236,6 @@ export class MerchantConfigView implements OnInit, OnDestroy {
page = 1; page = 1;
pageSize = 5; pageSize = 5;
// Cache
private merchantCache: { data: Merchant, timestamp: number } | null = null;
private readonly CACHE_TTL = 2 * 60 * 1000; // 2 minutes
ngOnInit() { ngOnInit() {
if (this.merchantId) { if (this.merchantId) {
this.loadCurrentUserPermissions(); this.loadCurrentUserPermissions();