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 { 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);
|
||||||
|
|||||||
@ -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' }}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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.';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user