feat: Add Health Check Endpoint
This commit is contained in:
parent
de4c725554
commit
326b9c8ec1
@ -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<any>(
|
||||
`${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);
|
||||
|
||||
@ -15,14 +15,41 @@
|
||||
<!-- États normal et erreur avec @if -->
|
||||
@if (!isLoading) {
|
||||
<div class="d-flex align-items-center">
|
||||
<img
|
||||
[src]="getUserAvatar()"
|
||||
class="rounded-circle me-2"
|
||||
width="36"
|
||||
height="36"
|
||||
alt="user-image"
|
||||
(error)="onAvatarError($event)"
|
||||
/>
|
||||
@if (user){
|
||||
@if (merchant){
|
||||
@if (merchant.logo && merchant.logo.trim() !== '') {
|
||||
<img
|
||||
[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)"
|
||||
/>
|
||||
}
|
||||
}
|
||||
<div>
|
||||
<h5 class="my-0 fw-semibold">
|
||||
{{ getDisplayName() || 'Utilisateur' }}
|
||||
|
||||
@ -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<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;
|
||||
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<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
|
||||
*/
|
||||
@ -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 = `<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.';
|
||||
}
|
||||
}
|
||||
@ -4,12 +4,41 @@
|
||||
ngbDropdownToggle
|
||||
class="topbar-link dropdown-toggle drop-arrow-none px-2"
|
||||
>
|
||||
<img
|
||||
src="assets/images/users/user-2.jpg"
|
||||
width="32"
|
||||
class="rounded-circle d-flex"
|
||||
alt="user-image"
|
||||
/>
|
||||
@if (user){
|
||||
@if (merchant){
|
||||
@if (merchant.logo && merchant.logo.trim() !== '') {
|
||||
<img
|
||||
[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>
|
||||
<div ngbDropdownMenu class="dropdown-menu dropdown-menu-end">
|
||||
@for (item of menuItems; track $index; let i = $index) {
|
||||
|
||||
@ -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 { MenuService } from '@core/services/menu.service'
|
||||
import {
|
||||
@ -9,7 +9,12 @@ import {
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { NgIcon } from '@ng-icons/core'
|
||||
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({
|
||||
selector: 'app-user-profile-topbar',
|
||||
@ -19,17 +24,46 @@ import { Subscription } from 'rxjs'
|
||||
NgbDropdownToggle,
|
||||
RouterLink,
|
||||
NgIcon,
|
||||
CommonModule,
|
||||
],
|
||||
templateUrl: './user-profile.html',
|
||||
})
|
||||
export class UserProfile implements OnInit, OnDestroy {
|
||||
private authService = inject(AuthService)
|
||||
private menuService = inject(MenuService)
|
||||
private subscription?: Subscription
|
||||
private authService = inject(AuthService);
|
||||
private merchantConfigService = inject(MerchantConfigService);
|
||||
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[] = []
|
||||
merchanPartnerId: string | undefined
|
||||
|
||||
ngOnInit() {
|
||||
this.loadUserProfile()
|
||||
this.loadDropdownItems()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// ==================== 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() {
|
||||
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.';
|
||||
}
|
||||
|
||||
}
|
||||
@ -307,7 +307,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
|
||||
constructor(
|
||||
private accessService: DashboardAccessService,
|
||||
private cdr: ChangeDetectorRef
|
||||
private cdRef: ChangeDetectorRef
|
||||
) {
|
||||
Chart.register(...registerables);
|
||||
}
|
||||
@ -326,12 +326,14 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
console.log('✅ Dashboard: waitForReady() a émis - Initialisation...');
|
||||
this.dashboardInitialized = true;
|
||||
this.initializeDashboard();
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('❌ Dashboard: Erreur dans waitForReady():', err);
|
||||
// Gérer l'erreur - peut-être rediriger vers une page d'erreur
|
||||
this.addAlert('danger', 'Erreur d\'initialisation',
|
||||
'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({
|
||||
next: (merchants) => {
|
||||
this.allowedMerchants = merchants;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: (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');
|
||||
this.loading.globalData = false;
|
||||
this.calculateStats();
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
setTimeout(() => this.updateAllCharts(), 100);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Erreur lors du chargement des données globales:', err);
|
||||
this.loading.globalData = false;
|
||||
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`);
|
||||
this.loading.merchantData = false;
|
||||
this.calculateStats();
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
setTimeout(() => this.updateAllCharts(), 100);
|
||||
},
|
||||
error: (err) => {
|
||||
@ -493,7 +495,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
this.loading.merchantData = false;
|
||||
this.addAlert('danger', 'Erreur de chargement',
|
||||
`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;
|
||||
|
||||
this.loading.chart = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création du graphique principal:', error);
|
||||
this.loading.chart = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1109,13 +1111,13 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
this.updateOverallHealth();
|
||||
this.generateHealthAlerts();
|
||||
this.loading.healthCheck = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
}),
|
||||
catchError(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.loading.healthCheck = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
return of(null);
|
||||
})
|
||||
).subscribe()
|
||||
@ -1274,7 +1276,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
this.metricDropdown.close();
|
||||
}
|
||||
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
this.updateMainChart();
|
||||
@ -1284,7 +1286,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
changePeriod(period: ReportPeriod): void {
|
||||
this.dataSelection.period = period;
|
||||
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
this.updateMainChart();
|
||||
@ -1294,7 +1296,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
changeChartType(type: ChartType): void {
|
||||
this.dataSelection.chartType = type;
|
||||
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
this.updateMainChart();
|
||||
@ -1302,7 +1304,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
refreshChartData(): void {
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
setTimeout(() => {
|
||||
this.updateAllCharts();
|
||||
}, 50);
|
||||
|
||||
@ -183,13 +183,18 @@ export class MerchantConfigView implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private minioService = inject(MinioService);
|
||||
private sanitizer = inject(DomSanitizer);
|
||||
|
||||
// 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>();
|
||||
|
||||
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 Operator = Operator;
|
||||
readonly MerchantUtils = MerchantUtils;
|
||||
@ -218,8 +223,7 @@ export class MerchantConfigView implements OnInit, OnDestroy {
|
||||
editingConfigId: number | null = null;
|
||||
editedConfig: UpdateMerchantConfigDto = {};
|
||||
configToDelete: MerchantConfig | null = null;
|
||||
private deleteModalRef: any = null;
|
||||
|
||||
|
||||
// Affichage des valeurs sensibles
|
||||
showSensitiveValues: { [configId: number]: boolean } = {};
|
||||
|
||||
@ -232,10 +236,6 @@ export class MerchantConfigView implements OnInit, OnDestroy {
|
||||
page = 1;
|
||||
pageSize = 5;
|
||||
|
||||
// Cache
|
||||
private merchantCache: { data: Merchant, timestamp: number } | null = null;
|
||||
private readonly CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
ngOnInit() {
|
||||
if (this.merchantId) {
|
||||
this.loadCurrentUserPermissions();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user