From 326b9c8ec138e138924f64156081b6e957531fc7 Mon Sep 17 00:00:00 2001 From: diallolatoile Date: Sat, 17 Jan 2026 13:27:07 +0000 Subject: [PATCH] feat: Add Health Check Endpoint --- src/app/core/services/auth.service.ts | 9 +- .../user-profile/user-profile.component.html | 43 ++- .../user-profile/user-profile.component.ts | 323 +++++++++++++++-- .../components/user-profile/user-profile.html | 41 ++- .../components/user-profile/user-profile.ts | 340 +++++++++++++++++- .../dcb-dashboard/dcb-reporting-dashboard.ts | 30 +- .../merchant-config-view.ts | 14 +- 7 files changed, 730 insertions(+), 70 deletions(-) diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts index e8b6041..2cef6dc 100644 --- a/src/app/core/services/auth.service.ts +++ b/src/app/core/services/auth.service.ts @@ -1,7 +1,7 @@ import { Injectable, inject, EventEmitter } from '@angular/core'; import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; 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 { @@ -325,13 +325,12 @@ export class AuthService { return this.http.get( `${environment.iamApiUrl}/auth/profile` ).pipe( - tap(apiResponse => { - // Déterminer le type d'utilisateur + map(apiResponse => { const userType = this.determineUserType(apiResponse); - // Mapper vers le modèle User const userProfile = this.mapToUserModel(apiResponse, userType); - + this.userProfile$.next(userProfile); + return userProfile; }), catchError(error => { console.error('❌ Erreur chargement profil:', error); diff --git a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.html b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.html index d0faebe..14568c6 100644 --- a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.html +++ b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.html @@ -15,14 +15,41 @@ @if (!isLoading) {
- user-image + @if (user){ + @if (merchant){ + @if (merchant.logo && merchant.logo.trim() !== '') { + + } @else { + + } + }@else { + + } + }
{{ getDisplayName() || 'Utilisateur' }} diff --git a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts index 296ca02..49cdb5e 100644 --- a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts +++ b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts @@ -3,23 +3,45 @@ import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; import { userDropdownItems } from '@layouts/components/data'; import { AuthService } from '@/app/core/services/auth.service'; import { User, UserRole } from '@core/models/dcb-bo-hub-user.model'; -import { Subject, takeUntil, 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({ selector: 'app-user-profile', standalone: true, - imports: [NgbCollapseModule], + imports: [NgbCollapseModule,CommonModule], templateUrl: './user-profile.component.html', }) export class UserProfileComponent implements OnInit, OnDestroy { 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(); - user: User | null = null; + // Cache des URLs de logos + private logoUrlCache = new Map(); + // Ajouter un cache pour les logos non trouvés + private logoErrorCache = new Set(); + // 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; hasError = false; + hasSuccess = ''; currentProfileLoaded = false; ngOnInit(): void { @@ -45,9 +67,9 @@ export class UserProfileComponent implements OnInit, OnDestroy { // Le profil sera chargé via la subscription } else { console.log('🔐 User not authenticated'); - this.user = null; + this.user = undefined; this.isLoading = false; - this.cdr.detectChanges(); + this.cdRef.detectChanges(); } } @@ -76,22 +98,31 @@ export class UserProfileComponent implements OnInit, OnDestroy { if (profile) { console.log('📥 User profile updated:', profile.username); this.user = profile; + + this.currentUserRole = this.extractUserRole(profile); + this.isHubUser = this.checkIfHubUser(); + + if (!this.isHubUser) { + this.merchanPartnerId = profile?.merchantPartnerId; + this.loadMerchantProfile() + } + this.currentProfileLoaded = true; } else { console.log('📭 User profile cleared'); - this.user = null; + this.user = undefined; this.currentProfileLoaded = false; } this.isLoading = false; this.hasError = false; - this.cdr.detectChanges(); + this.cdRef.detectChanges(); }, error: (error) => { console.error('❌ Error in profile subscription:', error); this.hasError = true; this.isLoading = false; - this.cdr.detectChanges(); + this.cdRef.detectChanges(); } }); } @@ -115,10 +146,10 @@ export class UserProfileComponent implements OnInit, OnDestroy { } else { // Si l'utilisateur s'est déconnecté console.log('👋 User logged out'); - this.user = null; + this.user = undefined; this.currentProfileLoaded = false; this.isLoading = false; - this.cdr.detectChanges(); + this.cdRef.detectChanges(); } } }); @@ -130,21 +161,20 @@ export class UserProfileComponent implements OnInit, OnDestroy { loadUserProfile(): void { this.isLoading = true; this.hasError = false; - this.cdr.detectChanges(); + this.cdRef.detectChanges(); this.authService.loadUserProfile() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (profile) => { - // Note: le profil sera automatiquement mis à jour via la subscription getUserProfile() this.isLoading = false; - this.cdr.detectChanges(); + this.cdRef.detectChanges(); }, error: (error) => { console.error('❌ Failed to load user profile:', error); this.hasError = true; this.isLoading = false; - this.cdr.detectChanges(); + this.cdRef.detectChanges(); // Essayer de rafraîchir le token si erreur 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 { + + 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 */ @@ -234,19 +437,91 @@ export class UserProfileComponent implements OnInit, OnDestroy { 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 { - return `assets/images/users/user-2.jpg`; + 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ère les erreurs de chargement d'avatar + * Génère un fallback SVG en data URL */ - onAvatarError(event: Event): void { - const img = event.target as HTMLImageElement; - img.src = 'assets/images/users/user-2.jpg'; - img.onerror = null; + private generateFallbackDataUrl(): string { + const 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.'; } } \ No newline at end of file diff --git a/src/app/layouts/components/topbar/components/user-profile/user-profile.html b/src/app/layouts/components/topbar/components/user-profile/user-profile.html index b0c25dc..bfe4582 100644 --- a/src/app/layouts/components/topbar/components/user-profile/user-profile.html +++ b/src/app/layouts/components/topbar/components/user-profile/user-profile.html @@ -4,12 +4,41 @@ ngbDropdownToggle class="topbar-link dropdown-toggle drop-arrow-none px-2" > - user-image + @if (user){ + @if (merchant){ + @if (merchant.logo && merchant.logo.trim() !== '') { + + } @else { + + } + }@else { + + } + }