diff --git a/src/app/app.scss b/src/app/app.scss index 66507b1..858fcad 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -1108,4 +1108,157 @@ $transition-base: all 0.3s ease; width: 80px; height: 80px; } +} + +/* ==================== STYLES DU LOGO ==================== */ + +/* Conteneur principal */ +.logo-card { + position: relative; + width: 220px; + height: 220px; + transition: all 0.3s ease; +} + +.logo-card:hover { + transform: translateY(-5px); +} + +/* Conteneur d'affichage du logo */ +.logo-display-container { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + border-radius: 16px; + background: linear-gradient(145deg, #ffffff, #f0f0f0); +} + +/* Logo marchand */ +.merchant-logo { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.5s ease; + opacity: 0; +} + +.merchant-logo.logo-loaded { + opacity: 1; +} + +.logo-display-container:hover .merchant-logo { + transform: scale(1.05); +} + +/* Placeholder par défaut */ +.default-logo-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.logo-initials { + font-size: 4rem; + font-weight: bold; + text-transform: uppercase; +} + +/* Overlay au hover */ +.logo-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s ease; +} + +.logo-display-container:hover .logo-overlay { + opacity: 1; +} + +.overlay-content { + text-align: center; + color: white; +} + +.overlay-icon { + font-size: 2rem; + margin-bottom: 0.5rem; +} + +.overlay-text { + font-size: 0.8rem; + opacity: 0.9; +} + +/* Badge de statut */ +.status-badge { + position: absolute; + top: 10px; + right: 10px; + padding: 0.25rem 0.5rem; + font-size: 0.7rem; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); +} + +/* Loader */ +.logo-loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 10; +} + +/* Informations sous le logo */ +.logo-info { + max-width: 220px; +} + +.merchant-name { + font-size: 1.1rem; + color: #333; +} + +/* Actions */ +.logo-actions .btn { + padding: 0.25rem 0.5rem; + border-radius: 20px; +} + +.logo-actions .btn:hover { + transform: scale(1.1); +} + +/* Animation de chargement */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.merchant-logo { + animation: fadeIn 0.5s ease forwards; +} + +/* États */ +.logo-loading .logo-display-container { + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: loading 1.5s infinite; +} + +@keyframes loading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } } \ No newline at end of file diff --git a/src/app/core/services/minio.service.ts b/src/app/core/services/minio.service.ts index aafc9d7..09e4267 100644 --- a/src/app/core/services/minio.service.ts +++ b/src/app/core/services/minio.service.ts @@ -220,16 +220,24 @@ export class MinioService { /** * Supprime un logo */ - deleteMerchantLogo(fileName: string): Observable { - if (!fileName || fileName.trim() === '') { - return throwError(() => new Error('Nom de fichier invalide')); + deleteMerchantLogo( + merchantId: string, + fileName: string + ): Observable { + if (!merchantId || !fileName || fileName.trim() === '') { + return throwError(() => new Error('Paramètres invalides ou Nom de fichier invalide')); } + const params: any = { + fileName + }; + return this.http.delete<{ success: boolean; message: string }>( - `${this.baseUrl}/${encodeURIComponent(fileName)}` + `${this.baseUrl}/merchants/${merchantId}/logos/url`, + { params } ).pipe( map(response => { if (response.success) { diff --git a/src/app/modules/hub-users-management/merchant-users.ts b/src/app/modules/hub-users-management/merchant-users.ts index 0a23d9b..55167a2 100644 --- a/src/app/modules/hub-users-management/merchant-users.ts +++ b/src/app/modules/hub-users-management/merchant-users.ts @@ -349,7 +349,7 @@ export class MerchantUsersManagement implements OnInit, OnDestroy { this.loadingMerchantPartners = true; this.merchantPartnersError = ''; - this.merchantConfigService.getAllMerchants() + this.merchantConfigService.fetchAllMerchants() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (merchants) => { @@ -549,7 +549,7 @@ export class MerchantUsersManagement implements OnInit, OnDestroy { } const mappedRole = this.mapToMerchantConfigRole(this.newUser.role); - + if (!mappedRole) { this.createUserError = `Impossible de mapper le rôle ${this.getRoleLabel(this.newUser.role)} vers un rôle MerchantConfig valide`; return; @@ -571,15 +571,19 @@ export class MerchantUsersManagement implements OnInit, OnDestroy { enabled: this.newUser.enabled, emailVerified: this.newUser.emailVerified, userType: this.newUser.userType, - merchantPartnerId: this.newUser.merchantPartnerId // Passer l'ID du merchant + merchantPartnerId: this.newUser.merchantPartnerId }; + // Variable pour stocker l'ID de l'utilisateur créé en cas de rollback + let createdUserId: string | null = null; + this.merchantUsersService.createMerchantUser(userDto) .pipe( switchMap((createdKeycloakUser) => { console.log('✅ Keycloak user created successfully:', createdKeycloakUser); + createdUserId = createdKeycloakUser.id; - // 2. Ajouter l'utilisateur au merchant dans MerchantConfig + // 2. Si c'est un utilisateur Merchant, l'ajouter au merchant dans MerchantConfig if (this.isMerchantRole(this.newUser.role) && this.newUser.merchantPartnerId) { const merchantPartnerId = Number(this.newUser.merchantPartnerId); @@ -594,33 +598,89 @@ export class MerchantUsersManagement implements OnInit, OnDestroy { map((merchantConfigUser) => { return { keycloakUser: createdKeycloakUser, - merchantConfigUser + merchantConfigUser, + success: true }; + }), + catchError((merchantError) => { + console.error('❌ Failed to add user to merchant config:', merchantError); + + // ROLLBACK: Supprimer l'utilisateur Keycloak créé + if (createdUserId) { + console.log(`🔄 Rollback: Deleting Keycloak user ${createdUserId} because merchant association failed`); + return this.merchantUsersService.deleteMerchantUser(createdUserId).pipe( + map(() => { + console.log(`✅ Keycloak user ${createdUserId} deleted as part of rollback`); + throw new Error(`Failed to associate user with merchant: ${merchantError.message}. User creation rolled back.`); + }), + catchError((deleteError) => { + console.error(`❌ Failed to delete Keycloak user during rollback:`, deleteError); + // Même si le rollback échoue, on propage l'erreur originale avec info supplémentaire + throw new Error(`Failed to associate user with merchant: ${merchantError.message}. AND failed to rollback user creation: ${deleteError.message}`); + }) + ); + } + + // Si createdUserId est null, on ne peut pas rollback + throw merchantError; }) ); } - return of({ keycloakUser: createdKeycloakUser }); + // Si pas d'association nécessaire (non-Merchant user), retourner directement + return of({ + keycloakUser: createdKeycloakUser, + merchantConfigUser: null, + success: true + }); }), takeUntil(this.destroy$) ) .subscribe({ next: (result) => { - console.log('✅ Complete user creation successful:', result); - this.creatingUser = false; - this.modalService.dismissAll(); - this.refreshUsersList(); - this.cdRef.detectChanges(); + if (result.success) { + console.log('✅ Complete user creation successful:', result); + this.creatingUser = false; + this.modalService.dismissAll(); + this.refreshUsersList(); + this.cdRef.detectChanges(); + } }, error: (error) => { console.error('❌ Error in user creation process:', error); this.creatingUser = false; - this.createUserError = this.getErrorMessage(error); - this.cdRef.detectChanges(); + + // Déterminer le message d'erreur approprié + if (error.message.includes('rolled back')) { + // Erreur avec rollback réussi + this.createUserError = error.message; + console.log('⚠️ User creation rolled back successfully'); + } else if (createdUserId && !error.message.includes('failed to rollback')) { + // L'utilisateur a été créé mais une autre erreur est survenue + // Tentative de nettoyage de l'utilisateur orphelin + console.log(`⚠️ Attempting to clean up orphaned user: ${createdUserId}`); + this.merchantUsersService.deleteMerchantUser(createdUserId).subscribe({ + next: () => { + console.log(`✅ Orphaned user cleaned up: ${createdUserId}`); + this.createUserError = `User creation failed after user was created. Orphaned user ${createdUserId} has been cleaned up. Error: ${error.message}`; + this.cdRef.detectChanges(); + }, + error: (cleanupError) => { + console.error(`❌ Failed to cleanup orphaned user:`, cleanupError); + this.createUserError = `User creation failed and cleanup of orphaned user ${createdUserId} also failed. Please contact admin. Original error: ${error.message}`; + this.cdRef.detectChanges(); + } + }); + } else { + // Autre erreur + this.createUserError = this.getErrorMessage(error); + this.cdRef.detectChanges(); + } } }); } + // Méthode pour trouver le merchantPartnerId de l'utilisateur connecté private findMerchantPartnerIdForCurrentUser(): void { const currentUserId = this.authService.getCurrentUserId(); @@ -791,16 +851,28 @@ export class MerchantUsersManagement implements OnInit, OnDestroy { if (error.error?.message) { return error.error.message; } - if (error.status === 400) { - return 'Données invalides. Vérifiez les champs du formulaire.'; - } + if (error.status === 409) { return 'Un utilisateur avec ce nom d\'utilisateur ou email existe déjà.'; } + if (error.status === 403) { return 'Vous n\'avez pas les permissions nécessaires pour cette action.'; } - return 'Erreur lors de la création de l\'utilisateur. Veuillez réessayer.'; + + if (error.status === 400) { + return 'Données invalides. Vérifiez les informations saisies'; + } + + if (error.status === 404) { + return 'Le merchant spécifié n\'existe pas'; + } + + if (error.status === 0 || error.status === 503) { + return 'Service temporairement indisponible. Veuillez réessayer plus tard'; + } + + return 'Une erreur inattendue est survenue lors de l\'Operation'; } private getResetPasswordErrorMessage(error: any): string { diff --git a/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.html b/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.html index ff44387..952ddfc 100644 --- a/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.html +++ b/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.html @@ -112,28 +112,37 @@
-
+
+ -
- @if (merchant.logo && merchant.logo.trim() !== '') { - - } @else { - - } +
+
+ @if (merchant.logo && merchant.logo.trim() !== '') { + + } @else { + + } +
+ +
+
+ {{ merchant.logo ? 'Logo personnalisé' : 'Logo par défaut' }} +
+
diff --git a/src/app/modules/merchant-config/merchant-config.service.ts b/src/app/modules/merchant-config/merchant-config.service.ts index e9e4dad..9c156bc 100644 --- a/src/app/modules/merchant-config/merchant-config.service.ts +++ b/src/app/modules/merchant-config/merchant-config.service.ts @@ -138,7 +138,7 @@ export class MerchantConfigService { return Math.ceil(value / 10000) * 10000; } - private fetchAllMerchants(params?: SearchMerchantsParams): Observable { + fetchAllMerchants(params?: SearchMerchantsParams): Observable { // Commencer avec un take raisonnable const initialTake = 500; @@ -190,7 +190,7 @@ export class MerchantConfigService { const skip = this.merchantsCache.length; let httpParams = new HttpParams() .set('take', take.toString()) - .set('skip', skip.toString()); // Si votre API supporte skip + .set('skip', skip.toString()); if (params?.query) { httpParams = httpParams.set('query', params.query.trim()); diff --git a/src/app/modules/merchant-config/merchant-config.ts b/src/app/modules/merchant-config/merchant-config.ts index 5ad8e8a..48bdae9 100644 --- a/src/app/modules/merchant-config/merchant-config.ts +++ b/src/app/modules/merchant-config/merchant-config.ts @@ -927,18 +927,21 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { console.log('✅ New logo uploaded:', uploadResponse); this.uploadingLogo = false; - // Mettre à jour le nom du logo dans l'objet merchant - this.selectedMerchantForEdit!.logo = uploadResponse.data.fileName; - // Supprimer l'ancien logo si différent const oldLogo = this.selectedMerchantForEdit!.logo; if (oldLogo && oldLogo !== uploadResponse.data.fileName) { - this.minioService.deleteMerchantLogo(oldLogo).subscribe({ + + this.logoUrlCache.delete(oldLogo); + + this.minioService.deleteMerchantLogo(String(this.selectedMerchantForEdit?.id), oldLogo).subscribe({ next: () => console.log('🗑️ Old logo deleted'), error: (err) => console.warn('⚠️ Could not delete old logo:', err) }); } + // Mettre à jour le nom du logo dans l'objet merchant + this.selectedMerchantForEdit!.logo = uploadResponse.data.fileName; + console.log('Logo : ' + this.selectedMerchantForEdit!.logo) // Retourner l'observable pour la mise à jour du marchand @@ -972,10 +975,12 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { // Mettre à jour le cache if (merchantId) { this.merchantProfiles[merchantId] = frontendMerchant; + + const cacheKey = `${merchantId}_${frontendMerchant.name}`; // Invalider le cache de l'URL du logo if (frontendMerchant.logo) { - this.logoUrlCache.delete(frontendMerchant.logo); + this.logoUrlCache.set(cacheKey, frontendMerchant.logo); } } @@ -1011,7 +1016,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { logoFileName: string, ): Observable { - const cacheKey = `${merchantId}_${logoFileName}`; + const cacheKey = `${merchantId}_${merchantName}`; // Vérifier si le logo est en cache d'erreur if (this.logoErrorCache.has(cacheKey)) {