feat: Add Health Check Endpoint

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

View File

@ -1,7 +1,7 @@
import { Injectable, inject, EventEmitter } from '@angular/core';
import { 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);

View File

@ -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' }}

View File

@ -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';
}
/**
* Obtient l'URL de l'avatar de l'utilisateur
*/
getUserAvatar(): string {
return `assets/images/users/user-2.jpg`;
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;
}
/**
* Gère les erreurs de chargement d'avatar
* Extrait les initiales de manière intelligente
*/
onAvatarError(event: Event): void {
const img = event.target as HTMLImageElement;
img.src = 'assets/images/users/user-2.jpg';
img.onerror = null;
private extractInitials(name: string): string {
if (!name || name.trim() === '') {
return '??';
}
// Nettoyer le nom
const cleanedName = name.trim().toUpperCase();
// Extraire les mots
const words = cleanedName.split(/\s+/);
// Si un seul mot, prendre les deux premières lettres
if (words.length === 1) {
return words[0].substring(0, 2) || '??';
}
// Prendre la première lettre des deux premiers mots
const initials = words
.slice(0, 2) // Prendre les 2 premiers mots
.map(word => word[0] || '')
.join('');
return initials || name.substring(0, 2).toUpperCase() || '??';
}
/**
* Génère un fallback SVG en data URL
*/
private generateFallbackDataUrl(): string {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
<rect width="40" height="40" fill="#667eea" rx="20"/>
<text x="20" y="22" text-anchor="middle" fill="white" font-family="Arial" font-size="14" font-weight="bold">?</text>
</svg>`;
return 'data:image/svg+xml;base64,' + btoa(svg);
}
// ==================== GESTION DES ERREURS ====================
private getErrorMessage(error: any): string {
if (error.error?.message) {
return error.error.message;
}
if (error.status === 400) {
return 'Données invalides. Vérifiez les informations saisies.';
}
if (error.status === 403) {
return 'Vous n\'avez pas les permissions nécessaires pour cette action';
}
if (error.status === 404) {
return 'Utilisateur non trouvé';
}
if (error.status === 409) {
return 'Cet email est déjà utilisé par un autre utilisateur';
}
return 'Une erreur est survenue. Veuillez réessayer.';
}
}

View File

@ -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) {

View File

@ -1,4 +1,4 @@
import { Component, inject, OnInit, OnDestroy } from '@angular/core'
import { Component, inject, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'
import { AuthService } from '@core/services/auth.service'
import { 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.';
}
}

View File

@ -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);

View File

@ -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,7 +223,6 @@ 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();