diff --git a/src/app/core/models/merchant-config.model.ts b/src/app/core/models/merchant-config.model.ts index 0d6f2b6..70eb162 100644 --- a/src/app/core/models/merchant-config.model.ts +++ b/src/app/core/models/merchant-config.model.ts @@ -116,13 +116,12 @@ export interface ApiMerchant { adresse: string; phone: string; configs: ApiMerchantConfig[]; - merchantUsers: ApiMerchantUser[]; + users: ApiMerchantUser[]; technicalContacts: ApiTechnicalContact[]; createdAt?: string; updatedAt?: string; } -// === DTOs CRUD === // === DTOs CRUD === export interface CreateMerchantDto { name: string; @@ -153,9 +152,6 @@ export interface UpdateUserRoleDto { role: UserRole; } -export interface UpdateUserRoleDto { - role: UserRole; // Utilisation de vos rôles existants -} // === RÉPONSES API === export interface ApiResponse { diff --git a/src/app/core/services/hub-users-roles-management.service.ts b/src/app/core/services/hub-users-roles-management.service.ts index 51c5ad9..575982b 100644 --- a/src/app/core/services/hub-users-roles-management.service.ts +++ b/src/app/core/services/hub-users-roles-management.service.ts @@ -43,10 +43,15 @@ export class RoleManagementService { })) })), tap(roles => this.availableRoles$.next(roles)), - catchError(error => this.getDefaultHubRoles(error)) + catchError(error => { + console.error('Error loading hub roles:', error); + // On renvoie un observable valide pour éviter le crash + return of({ roles: [] } as AvailableRolesWithPermissions); + }) ); } + /** * Charge les rôles Marchands disponibles */ @@ -59,56 +64,14 @@ export class RoleManagementService { })) })), tap(roles => this.availableRoles$.next(roles)), - catchError(error => this.getDefaultMerchantRoles(error)) + catchError(error => { + console.error('Error loading merchant roles:', error); + // On renvoie un observable valide pour éviter le crash + return of({ roles: [] } as AvailableRolesWithPermissions); + }) ); } - /** - * Rôles Hub par défaut en cas d'erreur - */ - private getDefaultHubRoles(error?: any): Observable { - console.error('Error loading hub roles:', error); - const defaultRoles: AvailableRolesWithPermissions = { - roles: [ - UserRole.DCB_ADMIN, - UserRole.DCB_SUPPORT, - UserRole.DCB_PARTNER - ].map(role => ({ - value: role, - label: this.getRoleLabel(role), - description: this.getRoleDescription(role), - allowedForCreation: true, - userType: UserType.HUB, - permissions: this.getPermissionsForRole(role) - })) - }; - this.availableRoles$.next(defaultRoles); - return of(defaultRoles); - } - - /** - * Rôles Marchands par défaut en cas d'erreur - */ - private getDefaultMerchantRoles(error?: any): Observable { - console.error('Error loading merchant roles:', error); - const defaultRoles: AvailableRolesWithPermissions = { - roles: [ - UserRole.DCB_PARTNER_ADMIN, - UserRole.DCB_PARTNER_MANAGER, - UserRole.DCB_PARTNER_SUPPORT - ].map(role => ({ - value: role, - label: this.getRoleLabel(role), - description: this.getRoleDescription(role), - allowedForCreation: true, - userType: UserType.MERCHANT_PARTNER, - permissions: this.getPermissionsForRole(role) - })) - }; - this.availableRoles$.next(defaultRoles); - return of(defaultRoles); - } - /** * Définit le rôle de l'utilisateur courant */ @@ -192,10 +155,10 @@ export class RoleManagementService { canDeleteUsers: true, canManageRoles: true, canViewStats: true, - canManageMerchants: true, + canManageMerchants: false, canAccessAdmin: false, canAccessSupport: false, - canAccessPartner: true, + canAccessPartner: false, assignableRoles: [UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT] }; diff --git a/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts b/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts index 72c294c..dbbeccc 100644 --- a/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts +++ b/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts @@ -51,9 +51,7 @@ export class MerchantConfigsList implements OnInit, OnDestroy { @Output() openCreateMerchantModal = new EventEmitter(); @Output() editMerchantRequested = new EventEmitter(); @Output() deleteMerchantRequested = new EventEmitter(); - @Output() activateMerchantRequested = new EventEmitter(); - @Output() deactivateMerchantRequested = new EventEmitter(); - + // Données allMerchants: Merchant[] = []; filteredMerchants: Merchant[] = []; @@ -259,14 +257,6 @@ export class MerchantConfigsList implements OnInit, OnDestroy { this.deleteMerchantRequested.emit(merchant); } - activateMerchant(merchant: Merchant) { - this.activateMerchantRequested.emit(merchant); - } - - deactivateMerchant(merchant: Merchant) { - this.deactivateMerchantRequested.emit(merchant); - } - // ==================== FILTRES ET RECHERCHE ==================== onSearch() { 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 7dc22ca..230fe44 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 @@ -4,7 +4,7 @@
-

{{ getProfileTitle() }}

+

Profil Marchand

@@ -44,14 +44,14 @@
- @if (currentUserRole && !canManageAllConfigs()) { + @if (currentUserRole && !canManageAllMerchants()) {
- Permissions limitées : Vous ne pouvez que consulter vos configurations + Permissions limitées : Vous ne pouvez modifier que vos propres configurations
@@ -80,331 +80,855 @@
} -
- - @if (loading) { -
-
- Chargement... -
-

Chargement des configurations...

+ + @if (loading) { +
+
+ Chargement...
- } +

Chargement du profil marchand...

+
+ } - - @if (configs.length > 0 && !loading) { -
- -
-
-
-
-
- - {{ configs.length }} configuration(s) trouvée(s) -
-
-
-
- Page {{ page }} sur {{ totalPages }} -
-
-
-
-
- - - @for (config of paginatedConfigs; track config.id) { -
- -
-
-
-
- -
-
{{ config.name }}
-
- - {{ getTypeLabel(config) }} - - - {{ getOperatorLabel(config) }} - -
-
-
-
-
-
- - @if (isSensitiveConfig(config)) { - - } - - - - Modifié le {{ getLastUpdateDate(config) }} - - - - -
-
-
-
- - - @if (isConfigExpanded(config.id!)) { -
-
- -
- -
{{ config.name }}
-
- -
- -
- - {{ getTypeLabel(config) }} - -
-
- -
- -
- - {{ getOperatorLabel(config) }} - -
-
- -
- -
{{ config.id }}
-
- - -
- -
- - {{ getDisplayValue(config) }} - - @if (canShowFullValue(config)) { - - } -
- @if (isSensitiveConfig(config) && !showSensitiveValues[config.id!]) { -
- - Valeur masquée pour des raisons de sécurité -
- } -
- - -
- -
{{ getCreationDate(config) }}
-
- -
- -
{{ getLastUpdateDate(config) }}
-
- - -
- -
- {{ getConfigUsageInfo(config) }} -
-
-
-
- - -
-
-
- @if (isEditingConfig(config.id!)) { - -
- - -
- } @else { - -
- @if (canEditConfig(config)) { - - } - - -
- } -
- - - @if (isEditingConfig(config.id!)) { -
-
-
-
- - + + @if (merchant && !loading) { +
+ +
+
+
+
-
+ + + +
+ +
+
+ + {{ merchant.configs.length }} configuration(s) +
+ +
+
+ Page {{ page }} sur {{ totalPages }} +
+ + @if (canManageAllMerchants() || canEditMerchant()) { + + } +
+
+ + + @if (merchant.configs.length > 0) { +
+ @for (config of paginatedConfigs; track trackByConfigId($index, config)) { +
+
+
+
+
+
+ +
+
{{ config.name }}
+
+ + {{ getTypeLabel(config) }} + + + {{ getOperatorLabel(config) }} + + @if (isSensitiveConfig(config)) { + + + Sensible + + } +
+
+
+
+
+
+ + Modifié {{ formatTimestamp(config.updatedAt) }} + + @if (canEditConfig(config)) { + + } + @if (canDeleteConfig(config) && !isEditingConfig(config.id!)) { + + } +
+ +
+
+
+ +
+
+ +
+ +
+ + {{ getDisplayValue(config) }} + + @if (shouldTruncateValue(config)) { + + } +
+
+ + +
+ +
{{ config.id }}
+
+ +
+ +
{{ config.merchantPartnerId || 'Non spécifié' }}
+
+ +
+ +
{{ formatTimestamp(config.createdAt) }}
+
+ +
+ +
{{ formatTimestamp(config.updatedAt) }}
+
+
+ + + @if (addingConfig) { +
+
+
+ + Nouvelle configuration +
+
+
+
+
+ + +
+ +
+ + +
+ +
+ + + @if (isNewConfigSensitive()) { +
+ + + Cette valeur contient des informations sensibles. Soyez prudent. + +
+ } +
+ +
+
+ + +
+
+
+
+
+ } + + + @if (isEditingConfig(config.id!)) { +
+
Modifier la configuration
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+
+
+ } +
+
+
+ } +
+ + + @if (totalPages > 1) { +
+ +
+ } + } @else { +
+
+ +
Aucune configuration
+

Ce marchand ne possède aucune configuration.

+
+
+ } +
+
+ + +
  • + + + Contacts + @if (merchant.technicalContacts.length > 0) { + {{ merchant.technicalContacts.length }} + } + + + +
    +
    + + Contacts techniques +
    + + @if (merchant.technicalContacts.length > 0) { +
    + @for (contact of paginatedContacts; track trackByContactId($index, contact)) { +
    +
    +
    +
    +
    + {{ getContactInitials(contact) }} +
    +
    +
    {{ getContactFullName(contact) }}
    +
    + + {{ contact.email }} +
    + + @if (contact.phone) { +
    + + {{ contact.phone }} +
    + } + +
    + + Créé le {{ contact.createdAt }} + +
    +
    +
    +
    +
    +
    + } +
    + + + @if (totalPages > 1) { +
    + +
    + } + } @else { +
    +
    + +
    Aucun contact technique
    +

    Aucun contact technique n'est associé à ce marchand.

    +
    +
    + } +
    +
    +
  • + +
  • + + + Utilisateurs + @if (merchant.users.length > 0) { + {{ merchant.users.length }} + } + + + +
    +
    + + Utilisateurs associés +
    + + @if (merchant.users.length > 0) { +
    + @for (user of paginatedUsers; track trackByUserId($index, user)) { +
    +
    +
    +
    +
    +
    +
    + + {{ user.username }} +
    +
    +
    {{ user.email }}
    + @if (isEditingUserRole(user.userId)) { + + } @else { + + {{ getUserRoleLabel(user.role) }} + + } +
    +
    +
    +
    + + Partner ID: {{ user.merchantPartnerId }} + + + @if (canEditUserRole()) { + @if (isEditingUserRole(user.userId)) { +
    + + +
    + } @else { + + } + } +
    +
    +
    +
    +
    + } +
    + + + @if (totalPages > 1) { +
    + +
    + } + } @else { +
    +
    + +
    Aucun utilisateur
    +

    Aucun utilisateur n'est associé à ce marchand.

    +
    +
    + } +
    +
    +
  • + + +
    +
    +
    +
    +
    + } + + + @if (!merchant && !loading) { +
    +
    + +
    Aucun marchand trouvé
    +

    Impossible de charger les informations du marchand.

    + +
    +
    + } +
    + + + + + + \ No newline at end of file + + + \ No newline at end of file diff --git a/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts b/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts index 0138460..72d3200 100644 --- a/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts +++ b/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts @@ -2,18 +2,23 @@ import { Component, inject, OnInit, Input, Output, EventEmitter, ChangeDetectorR import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { NgIcon } from '@ng-icons/core'; -import { NgbAlertModule, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbAlertModule, NgbPaginationModule, NgbNavModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { Subject, takeUntil } from 'rxjs'; import { + Merchant, MerchantConfig, + TechnicalContact, + MerchantUser, UpdateMerchantConfigDto, ConfigType, Operator, - MerchantUtils + MerchantUtils, + UpdateUserRoleDto } from '@core/models/merchant-config.model'; import { MerchantConfigService } from '../merchant-config.service'; +import { MerchantDataAdapter } from '../merchant-data-adapter.service'; import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; import { AuthService } from '@core/services/auth.service'; import { UserRole } from '@core/models/dcb-bo-hub-user.model'; @@ -21,27 +26,52 @@ import { UserRole } from '@core/models/dcb-bo-hub-user.model'; @Component({ selector: 'app-merchant-config-view', standalone: true, - imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule, NgbPaginationModule], + imports: [ + CommonModule, + FormsModule, + NgIcon, + NgbAlertModule, + NgbPaginationModule, + NgbNavModule + ], templateUrl: './merchant-config-view.html', styles: [` - .config-card { - border: 1px solid #e0e0e0; + .profile-section { + background: white; + border-radius: 0.5rem; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + margin-bottom: 1.5rem; + overflow: hidden; + } + + .profile-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 2rem; + } + + .section-card { + border: 1px solid #e9ecef; border-radius: 0.5rem; margin-bottom: 1rem; transition: box-shadow 0.3s ease; } - .config-card:hover { - box-shadow: 0 4px 8px rgba(0,0,0,0.1); + + .section-card:hover { + box-shadow: 0 4px 12px rgba(0,0,0,0.1); } - .config-header { + + .section-header { background-color: #f8f9fa; - padding: 1rem; - border-bottom: 1px solid #e0e0e0; + padding: 1rem 1.5rem; + border-bottom: 1px solid #e9ecef; cursor: pointer; } - .config-content { - padding: 1rem; + + .section-content { + padding: 1.5rem; } + .config-value { font-family: 'Courier New', monospace; background-color: #f8f9fa; @@ -49,28 +79,105 @@ import { UserRole } from '@core/models/dcb-bo-hub-user.model'; border-radius: 0.25rem; word-break: break-all; } + .sensitive-value { filter: blur(0.3rem); transition: filter 0.3s ease; } + .sensitive-value:hover { filter: none; } - .config-actions { - border-top: 1px solid #e0e0e0; - padding: 1rem; - background-color: #fafafa; + + .contact-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + font-size: 1.2rem; } - .expanded .config-header { - background-color: #e9ecef; + + .user-badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + border-radius: 2rem; + background: #f8f9fa; + border: 1px solid #e9ecef; } + + .stats-card { + text-align: center; + padding: 1.5rem; + border-radius: 0.5rem; + background: white; + border: 1px solid #e9ecef; + } + + .stats-number { + font-size: 2rem; + font-weight: bold; + color: #495057; + } + + .stats-label { + color: #6c757d; + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.5px; + } + .delete-user-modal .modal-content { + border: none; + border-radius: 0.5rem; + box-shadow: 0 10px 25px rgba(0,0,0,0.2); + } + + .delete-user-modal .modal-header { + background: linear-gradient(135deg, #fff5f5 0%, #fed7d7 100%); + border-bottom: 2px solid #feb2b2; + border-radius: 0.5rem 0.5rem 0 0; + } + + .avatar-lg { + width: 80px; + height: 80px; + } + + /* Styles pour les boutons d'action */ + .btn-group .btn { + border-radius: 0.25rem; + } + + .btn-group .btn:not(:first-child) { + margin-left: 0.25rem; + } + + /* Responsive */ + @media (max-width: 768px) { + .btn-group { + flex-direction: column; + gap: 0.25rem; + } + + .btn-group .btn { + margin-left: 0 !important; + } + } + `] }) export class MerchantConfigView implements OnInit, OnDestroy { private merchantConfigService = inject(MerchantConfigService); - private roleService = inject(RoleManagementService); private authService = inject(AuthService); + private roleService = inject(RoleManagementService); private cdRef = inject(ChangeDetectorRef); + private modalService = inject(NgbModal); private destroy$ = new Subject(); readonly ConfigType = ConfigType; @@ -80,70 +187,582 @@ export class MerchantConfigView implements OnInit, OnDestroy { @Input() merchantId!: string; @Output() back = new EventEmitter(); @Output() editConfigRequested = new EventEmitter(); + @Output() editMerchantRequested = new EventEmitter(); - // Liste de toutes les configurations - configs: MerchantConfig[] = []; + merchant: Merchant | null = null; + + // États loading = false; saving = false; error = ''; success = ''; + // Navigation par onglets + activeTab: 'overview' | 'configs' | 'contacts' | 'users' = 'overview'; + // Gestion des permissions - currentUserRole: any = null; - - // Édition + currentUserRole: UserRole | null = null; + currentMerchantPartnerId: string = ''; + + // Édition des configurations editingConfigId: string | null = null; editedConfig: UpdateMerchantConfigDto = {}; + configToDelete: MerchantConfig | null = null; + private deleteModalRef: any = null; // Affichage des valeurs sensibles showSensitiveValues: { [configId: string]: boolean } = {}; - // Pagination et expansion - expandedConfigs: { [configId: string]: boolean } = {}; + // Expansion des sections + expandedSections: { [sectionId: string]: boolean } = {}; + + // Pagination page = 1; - pageSize = 10; + pageSize = 5; - // ==================== CONVERSION IDS ==================== + // Cache + private merchantCache: { data: Merchant, timestamp: number } | null = null; + private readonly CACHE_TTL = 2 * 60 * 1000; // 2 minutes - /** - * Convertit un ID string en number pour l'API - */ - private convertIdToNumber(id: string): number { - const numId = Number(id); - if (isNaN(numId)) { - throw new Error(`ID invalide pour la conversion en number: ${id}`); + ngOnInit() { + if (this.merchantId) { + this.loadCurrentUserPermissions(); + this.loadMerchantProfile(); } - return numId; + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + // ==================== CHARGEMENT DES DONNÉES ==================== + + /** + * 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.merchantId); + + this.merchantConfigService.getMerchantById(this.merchantId) + .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(); + } + }); } /** - * Convertit un ID number en string pour Angular + * Charge les permissions de l'utilisateur courant */ - private convertIdToString(id: number): string { - return id.toString(); + private loadCurrentUserPermissions(): void { + this.authService.getUserProfile() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (profile) => { + this.currentUserRole = this.authService.getCurrentUserRole(); + this.currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId() || ''; + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('Error loading user permissions:', error); + } + }); } - /** - * Convertit une config avec des IDs number en string - */ - private convertConfigToFrontend(config: any): MerchantConfig { - return { - ...config, - id: config.id ? this.convertIdToString(config.id) : undefined, - merchantPartnerId: config.merchantPartnerId ? this.convertIdToString(config.merchantPartnerId) : undefined + // ==================== GESTION DU CACHE ==================== + + 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; + } + + // ==================== GESTION DES ONGLETS ==================== + + setActiveTab(tab: 'overview' | 'configs' | 'contacts' | 'users') { + this.activeTab = tab; + } + + // ==================== GESTION DE L'EXPANSION ==================== + + toggleSection(sectionId: string) { + this.expandedSections[sectionId] = !this.expandedSections[sectionId]; + } + + isSectionExpanded(sectionId: string): boolean { + return !!this.expandedSections[sectionId]; + } + + // ==================== ÉDITION DES CONFIGURATIONS ==================== + + startEditingConfig(config: MerchantConfig) { + if (!this.canEditConfig(config)) { + this.error = 'Vous n\'avez pas la permission de modifier cette configuration'; + return; + } + + this.editingConfigId = config.id!; + this.editedConfig = { + name: config.name, + value: config.value, + operatorId: config.operatorId }; } - // Getters pour la logique conditionnelle + cancelEditingConfig() { + this.editingConfigId = null; + this.editedConfig = {}; + this.error = ''; + this.success = ''; + } + + saveConfig(config: MerchantConfig) { + if (!this.canEditConfig(config)) return; + + const validation = this.validateConfigForm(); + if (!validation.isValid) { + this.error = validation.error!; + return; + } + + this.saving = true; + this.error = ''; + this.success = ''; + + // Sauvegarde de l'état original pour rollback + const originalConfig = { ...config }; + const configIndex = this.merchant!.configs.findIndex(c => c.id === config.id); + + // Mise à jour optimiste + if (configIndex !== -1) { + this.merchant!.configs[configIndex] = { + ...this.merchant!.configs[configIndex], + ...this.editedConfig, + updatedAt: new Date().toISOString() + }; + } + + this.merchantConfigService.updateConfig(config.id!, this.editedConfig) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (updatedConfig) => { + if (configIndex !== -1) { + this.merchant!.configs[configIndex] = updatedConfig; + } + + this.editingConfigId = null; + this.saving = false; + this.success = 'Configuration mise à jour avec succès'; + this.editedConfig = {}; + this.clearCache(); + this.cdRef.detectChanges(); + }, + error: (error) => { + // Rollback en cas d'erreur + if (configIndex !== -1) { + this.merchant!.configs[configIndex] = originalConfig; + } + + this.error = this.getErrorMessage(error); + this.saving = false; + this.cdRef.detectChanges(); + } + }); + } + + // États pour l'ajout de configuration + addingConfig = false; + newConfig: Omit = { + name: ConfigType.CUSTOM, + value: '', + operatorId: null + }; + + // États pour la modification de rôle + editingUserId: string | null = null; + newUserRole: UserRole | null = null; + + // ==================== GESTION DES CONFIGURATIONS ==================== + + startAddingConfig() { + this.addingConfig = true; + this.newConfig = { + name: ConfigType.CUSTOM, + value: '', + operatorId: null + }; + } + + cancelAddingConfig() { + this.addingConfig = false; + this.newConfig = { + name: ConfigType.CUSTOM, + value: '', + operatorId: null + }; + this.error = ''; + } + + addConfig() { + const validation = this.validateNewConfig(); + if (!validation.isValid) { + this.error = validation.error!; + return; + } + + this.saving = true; + this.error = ''; + + this.merchantConfigService.addConfigToMerchant(this.merchantId, this.newConfig) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (newConfig) => { + if (this.merchant) { + this.merchant.configs = [...this.merchant.configs, newConfig]; + } + this.addingConfig = false; + this.saving = false; + this.success = 'Configuration ajoutée avec succès'; + this.newConfig = { + name: ConfigType.CUSTOM, + value: '', + operatorId: null + }; + this.clearCache(); + this.cdRef.detectChanges(); + }, + error: (error) => { + this.error = this.getErrorMessage(error); + this.saving = false; + this.cdRef.detectChanges(); + } + }); + } + + /** + * Valide le nouveau formulaire de configuration + */ + validateNewConfig(): { isValid: boolean; error?: string } { + if (!this.newConfig.name?.trim()) { + return { isValid: false, error: 'Le nom de la configuration est requis' }; + } + + if (!this.newConfig.value?.trim()) { + return { isValid: false, error: 'La valeur de la configuration est requise' }; + } + + // Validation des URLs + if (this.newConfig.name === ConfigType.WEBHOOK_URL || + this.newConfig.name === ConfigType.CALLBACK_URL) { + try { + new URL(this.newConfig.value); + } catch { + return { isValid: false, error: 'URL invalide' }; + } + } + + return { isValid: true }; + } + + /** + * Vérifie si la nouvelle configuration est sensible + */ + isNewConfigSensitive(): boolean { + return this.isSensitiveConfig({ + name: this.newConfig.name, + value: '', + operatorId: null + } as MerchantConfig); + } + + // ==================== GESTION DE LA SUPPRESSION ==================== + + /** + * Ouvre le modal de suppression pour une configuration + */ + openDeleteConfigModal(config: MerchantConfig, modalTemplate: any) { + if (!this.canDeleteConfig(config)) { + this.error = 'Vous n\'avez pas la permission de supprimer cette configuration'; + return; + } + + this.configToDelete = config; + this.error = ''; // Clear any previous errors + + // Ouvrir le modal + this.deleteModalRef = this.modalService.open(modalTemplate, { + centered: true, + backdrop: 'static', + keyboard: false, + windowClass: 'delete-config-modal' + }); + } + + /** + * Confirme la suppression de la configuration + */ + confirmDeleteConfig() { + if (!this.configToDelete) { + this.error = 'Aucune configuration à supprimer'; + return; + } + + this.saving = true; + this.error = ''; + + // Sauvegarde pour rollback + const originalConfigs = [...this.merchant!.configs]; + const configIndex = this.merchant!.configs.findIndex(c => c.id === this.configToDelete!.id); + + // Mise à jour optimiste + if (configIndex !== -1) { + this.merchant!.configs = this.merchant!.configs.filter(c => c.id !== this.configToDelete!.id); + } + + this.merchantConfigService.deleteConfig(this.configToDelete.id!) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => { + this.saving = false; + this.success = 'Configuration supprimée avec succès'; + this.closeDeleteModal(); + this.clearCache(); + this.cdRef.detectChanges(); + }, + error: (error) => { + // Rollback en cas d'erreur + this.merchant!.configs = originalConfigs; + this.error = this.getErrorMessage(error); + this.saving = false; + this.cdRef.detectChanges(); + } + }); + } + + /** + * Ferme le modal de suppression + */ + closeDeleteModal() { + if (this.deleteModalRef) { + this.deleteModalRef.close(); + this.deleteModalRef = null; + } + this.configToDelete = null; + this.error = ''; + } + + /** + * Annule la suppression et ferme le modal + */ + dismissDeleteModal(modal: any) { + modal.dismiss(); + this.configToDelete = null; + this.error = ''; + } + + // ==================== GESTION DES RÔLES UTILISATEURS ==================== + + startEditingUserRole(user: MerchantUser) { + if (!this.canManageAllMerchants()) { + this.error = 'Vous n\'avez pas la permission de modifier les rôles'; + return; + } + + this.editingUserId = user.userId; + this.newUserRole = user.role; + } + + cancelEditingUserRole() { + this.editingUserId = null; + this.newUserRole = null; + this.error = ''; + } + + updateUserRole(user: MerchantUser) { + if (!this.newUserRole || !this.editingUserId) { + return; + } + + this.saving = true; + this.error = ''; + + // Sauvegarde pour rollback + const originalRole = user.role; + const userIndex = this.merchant!.users.findIndex(u => u.userId === user.userId); + + // Mise à jour optimiste + if (userIndex !== -1) { + this.merchant!.users[userIndex] = { + ...this.merchant!.users[userIndex], + role: this.newUserRole + }; + } + + const updateRoleDto: UpdateUserRoleDto = { + role: this.newUserRole + }; + + this.merchantConfigService.updateUserRole(this.merchantId, user.userId, updateRoleDto) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (updatedUser) => { + if (userIndex !== -1) { + this.merchant!.users[userIndex] = updatedUser; + } + this.editingUserId = null; + this.newUserRole = null; + this.saving = false; + this.success = 'Rôle utilisateur mis à jour avec succès'; + this.clearCache(); + this.cdRef.detectChanges(); + }, + error: (error) => { + // Rollback + if (userIndex !== -1) { + this.merchant!.users[userIndex].role = originalRole; + } + this.error = this.getErrorMessage(error); + this.saving = false; + this.cdRef.detectChanges(); + } + }); + } + + isEditingUserRole(userId: string): boolean { + return this.editingUserId === userId; + } + + // ==================== UTILITAIRES D'AFFICHAGE ==================== + + getAvailableConfigTypes(): { value: ConfigType, label: string }[] { + return [ + { value: ConfigType.API_KEY, label: 'Clé API' }, + { value: ConfigType.SECRET_KEY, label: 'Clé secrète' }, + { value: ConfigType.WEBHOOK_URL, label: 'URL Webhook' }, + { value: ConfigType.CALLBACK_URL, label: 'URL Callback' }, + { value: ConfigType.TIMEOUT, label: 'Timeout' }, + { value: ConfigType.RETRY_COUNT, label: 'Nombre de tentatives' }, + { value: ConfigType.CUSTOM, label: 'Personnalisé' } + ]; + } + + getAvailableOperators(): { value: Operator; label: string }[] { + const operators: Operator[] = [ + Operator.ORANGE_OSN + ]; + + return operators.map(operator => ({ + value: operator, + label: MerchantUtils.getOperatorName(operator) + })); + } + getAvailableUserRoles(): { value: UserRole, label: string }[] { + return [ + { value: UserRole.DCB_PARTNER, label: 'Partenaire' }, + { value: UserRole.DCB_PARTNER_ADMIN, label: 'Admin Partenaire' }, + { value: UserRole.DCB_ADMIN, label: 'Administrateur' }, + { value: UserRole.DCB_SUPPORT, label: 'Support' } + ].filter(role => + // Filtrer selon les permissions de l'utilisateur courant + this.canAssignRole(role.value) + ); + } + + canAssignRole(role: UserRole): boolean { + if (!this.currentUserRole) return false; + + // Un admin peut assigner tous les rôles + if (this.currentUserRole === UserRole.DCB_ADMIN) return true; + + // Un admin partenaire peut assigner des rôles partenaire + if (this.currentUserRole === UserRole.DCB_PARTNER_ADMIN) { + return [UserRole.DCB_PARTNER, UserRole.DCB_PARTNER_ADMIN].includes(role); + } + + return false; + } + + canDeleteConfig(config: MerchantConfig): boolean { + return this.canEditConfig(config); + } + + canEditUserRole(): boolean { + return this.canManageAllMerchants(); + } + + isEditingConfig(configId: string): boolean { + return this.editingConfigId === configId; + } + + // ==================== VÉRIFICATIONS DE PERMISSIONS ==================== + + canEditConfig(config: MerchantConfig): boolean { + if (!config || !this.currentUserRole) return false; + + if (this.canManageAllMerchants()) return true; + + return config.merchantPartnerId === this.currentMerchantPartnerId; + } + + canManageAllMerchants(): boolean { + const adminRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER_ADMIN]; + return this.currentUserRole ? adminRoles.includes(this.currentUserRole) : false; + } + + canEditMerchant(): boolean { + if (!this.merchant || !this.currentUserRole) return false; + + if (this.canManageAllMerchants()) return true; + + // PARTNER ne peut modifier que ses propres marchands + return this.merchant.configs?.some(config => + config.merchantPartnerId === this.currentMerchantPartnerId + ) || this.merchant.users?.some(user => + user.merchantPartnerId === this.currentMerchantPartnerId + ); + } + + // ==================== UTILITAIRES D'AFFICHAGE ==================== + + // Configuration utilities isSensitiveConfig(config: MerchantConfig): boolean { return config.name === ConfigType.API_KEY || config.name === ConfigType.SECRET_KEY; } - shouldTruncateValue(config: MerchantConfig): boolean { - return this.isSensitiveConfig(config) || config.value.length > 100; - } - getDisplayValue(config: MerchantConfig): string { if (this.isSensitiveConfig(config) && !this.showSensitiveValues[config.id!]) { return '••••••••••••••••'; @@ -158,265 +777,12 @@ export class MerchantConfigView implements OnInit, OnDestroy { return config.value; } - ngOnInit() { - if (this.merchantId) { - this.loadCurrentUserPermissions(); - this.loadAllConfigs(); - } - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - /** - * Charge les permissions de l'utilisateur courant - */ - private loadCurrentUserPermissions(): void { - this.authService.getUserProfile() - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (profile) => { - this.currentUserRole = this.authService.getCurrentUserRole(); - this.cdRef.detectChanges(); - }, - error: (error) => { - console.error('Error loading user permissions:', error); - } - }); - } - - /** - * Charge toutes les configurations du merchant - */ - loadAllConfigs() { - this.loading = true; - this.error = ''; - - console.log("Chargement des configurations pour le merchant ID:", this.merchantId); - - this.merchantConfigService.getMerchantById(this.merchantId) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (merchant) => { - // Conversion pour Angular - const frontendMerchant = this.convertMerchantToFrontend(merchant); - - console.log("Merchant chargé:", frontendMerchant); - console.log("Configurations trouvées:", frontendMerchant.configs?.length || 0); - - // Récupérer toutes les configurations du marchand - this.configs = frontendMerchant.configs || []; - - if (this.configs.length === 0) { - this.error = 'Aucune configuration trouvée pour ce marchand'; - } - - this.loading = false; - this.cdRef.detectChanges(); - }, - error: (error) => { - this.error = 'Erreur lors du chargement des configurations du marchand'; - this.loading = false; - this.cdRef.detectChanges(); - console.error('Error loading merchant configs:', error); - } - }); - } - - /** - * Convertit un marchand avec ses configs pour Angular - */ - private convertMerchantToFrontend(merchant: any): any { - return { - ...merchant, - id: merchant.id ? this.convertIdToString(merchant.id) : undefined, - configs: merchant.configs?.map((config: any) => this.convertConfigToFrontend(config)) || [], - technicalContacts: merchant.technicalContacts?.map((contact: any) => ({ - ...contact, - id: contact.id ? this.convertIdToString(contact.id) : undefined, - merchantPartnerId: contact.merchantPartnerId ? this.convertIdToString(contact.merchantPartnerId) : undefined - })) || [], - users: merchant.users?.map((user: any) => ({ - ...user, - merchantPartnerId: user.merchantPartnerId ? this.convertIdToString(user.merchantPartnerId) : undefined - })) || [] - }; - } - - // ==================== GESTION DE L'EXPANSION ==================== - - toggleConfigExpansion(configId: string) { - this.expandedConfigs[configId] = !this.expandedConfigs[configId]; - this.cdRef.detectChanges(); - } - - isConfigExpanded(configId: string): boolean { - return !!this.expandedConfigs[configId]; - } - - // ==================== GESTION DE L'ÉDITION ==================== - - startEditing(config: MerchantConfig) { - if (!this.canEditConfig(config)) { - this.error = 'Vous n\'avez pas la permission de modifier cette configuration'; - return; - } - - this.editingConfigId = config.id!; - this.editedConfig = { - name: config.name, - value: config.value, - operatorId: config.operatorId - }; - this.cdRef.detectChanges(); - } - - cancelEditing() { - this.editingConfigId = null; - this.editedConfig = {}; - this.error = ''; - this.success = ''; - this.cdRef.detectChanges(); - } - - saveConfig(config: MerchantConfig) { - if (!this.canEditConfig(config)) return; - - // Validation du formulaire - const validation = this.validateConfigForm(); - if (!validation.isValid) { - this.error = validation.error!; - return; - } - - this.saving = true; - this.error = ''; - this.success = ''; - - this.merchantConfigService.updateConfig(config.id!, this.editedConfig) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (updatedConfig) => { - // Mettre à jour la configuration dans la liste - const index = this.configs.findIndex(c => c.id === config.id); - if (index !== -1) { - this.configs[index] = this.convertConfigToFrontend(updatedConfig); - } - - this.editingConfigId = null; - this.saving = false; - this.success = 'Configuration mise à jour avec succès'; - this.editedConfig = {}; - this.cdRef.detectChanges(); - }, - error: (error) => { - this.error = this.getErrorMessage(error); - this.saving = false; - this.cdRef.detectChanges(); - console.error('Error updating merchant config:', error); - } - }); - } - - isEditingConfig(configId: string): boolean { - return this.editingConfigId === configId; - } - - // ==================== VÉRIFICATIONS DE PERMISSIONS ==================== - - /** - * Vérifie si l'utilisateur peut éditer cette configuration - */ - canEditConfig(config: MerchantConfig): boolean { - if (!config) return false; - - // ADMIN peut éditer toutes les configs - if (this.canManageAllConfigs()) { - return true; - } - - // PARTNER ne peut éditer que ses propres configs - return config.merchantPartnerId === this.authService.getCurrentMerchantPartnerId(); - } - - /** - * Vérifie si l'utilisateur peut activer/désactiver cette configuration - */ - canToggleStatus(config: MerchantConfig): boolean { - return this.canEditConfig(config); - } - - /** - * Vérifie si l'utilisateur peut supprimer cette configuration - */ - canDeleteConfig(config: MerchantConfig): boolean { - return this.canEditConfig(config); - } - - /** - * Vérifie si l'utilisateur peut gérer toutes les configurations - */ - canManageAllConfigs(): boolean { - const adminRoles = [UserRole.DCB_ADMIN, UserRole.DCB_PARTNER, UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_SUPPORT]; - return adminRoles.includes(this.currentUserRole); - } - - // ==================== UTILITAIRES D'AFFICHAGE ==================== - - getOperatorBadgeClass(config: MerchantConfig): string { - if (!config || config.operatorId === null) return 'badge bg-secondary'; - - const classes = { - [Operator.ORANGE_OSN]: 'badge bg-warning' - } as const; - - const operatorId = config.operatorId; - return operatorId in classes ? classes[operatorId as Operator] : 'badge bg-secondary'; - } - - getTypeBadgeClass(config: MerchantConfig): string { - if (!config) return 'badge bg-secondary'; - - const classes = { - [ConfigType.API_KEY]: 'badge bg-primary', - [ConfigType.SECRET_KEY]: 'badge bg-danger', - [ConfigType.WEBHOOK_URL]: 'badge bg-success', - [ConfigType.CALLBACK_URL]: 'badge bg-info', - [ConfigType.TIMEOUT]: 'badge bg-warning', - [ConfigType.RETRY_COUNT]: 'badge bg-secondary', - [ConfigType.CUSTOM]: 'badge bg-dark' - } as const; - - const configName = config.name; - return configName in classes ? classes[configName as ConfigType] : 'badge bg-secondary'; - } - - getOperatorLabel(config: MerchantConfig): string { - if (!config || config.operatorId === null) return 'Inconnu'; - return MerchantUtils.getOperatorName(config.operatorId); - } - - getTypeLabel(config: MerchantConfig): string { - if (!config) return 'Inconnu'; - return MerchantUtils.getConfigTypeName(config.name as ConfigType); - } - - formatTimestamp(timestamp: number): string { - if (!timestamp) return 'Non disponible'; - return new Date(timestamp).toLocaleDateString('fr-FR', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); + shouldTruncateValue(config: MerchantConfig): boolean { + return this.isSensitiveConfig(config) || config.value.length > 100; } toggleSensitiveValue(configId: string) { this.showSensitiveValues[configId] = !this.showSensitiveValues[configId]; - this.cdRef.detectChanges(); } getValueIcon(config: MerchantConfig): string { @@ -429,38 +795,185 @@ export class MerchantConfigView implements OnInit, OnDestroy { return this.showSensitiveValues[config.id!] ? 'Masquer la valeur' : 'Afficher la valeur'; } - // ==================== 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 pour effectuer cette action.'; - } - if (error.status === 404) { - return 'Configuration non trouvée.'; - } - if (error.status === 409) { - return 'Conflit de données. Cette configuration existe peut-être déjà.'; - } - return 'Erreur lors de l\'opération. Veuillez réessayer.'; + // Badges et labels + getOperatorBadgeClass(config: MerchantConfig): string { + if (!config || config.operatorId === null) return 'badge bg-secondary'; + + const classes = { + [Operator.ORANGE_OSN]: 'badge bg-warning text-dark' + }; + + return classes[config.operatorId as Operator] || 'badge bg-secondary'; } - // ==================== MÉTHODES DE NAVIGATION ==================== + getTypeBadgeClass(config: MerchantConfig): string { + if (!config) return 'badge bg-secondary'; + + const classes = { + [ConfigType.API_KEY]: 'badge bg-primary', + [ConfigType.SECRET_KEY]: 'badge bg-danger', + [ConfigType.WEBHOOK_URL]: 'badge bg-success', + [ConfigType.CALLBACK_URL]: 'badge bg-info', + [ConfigType.TIMEOUT]: 'badge bg-warning text-dark', + [ConfigType.RETRY_COUNT]: 'badge bg-secondary', + [ConfigType.CUSTOM]: 'badge bg-dark' + }; + + return classes[config.name as ConfigType] || 'badge bg-secondary'; + } + + getOperatorLabel(config: MerchantConfig): string { + if (!config || config.operatorId === null) return 'Inconnu'; + return MerchantUtils.getOperatorName(config.operatorId); + } + + getTypeLabel(config: MerchantConfig): string { + if (!config) return 'Inconnu'; + return MerchantUtils.getConfigTypeName(config.name as ConfigType); + } + + // Contact utilities + getContactInitials(contact: TechnicalContact): string { + return `${contact.firstName.charAt(0)}${contact.lastName.charAt(0)}`.toUpperCase(); + } + + getContactFullName(contact: TechnicalContact): string { + return `${contact.firstName} ${contact.lastName}`; + } + + // User utilities + getUserRoleBadgeClass(role: UserRole): string { + return this.roleService.getRoleBadgeClass(role); + } + + getUserRoleLabel(role: UserRole): string { + return this.roleService.getRoleLabel(role); + } + + // Date formatting + + /** + * Formatte un timestamp + */ + formatTimestamp(timestamp: string | number | undefined | null): string { + if (!timestamp) return 'Non disponible'; + + try { + const date = new Date(timestamp); + return isNaN(date.getTime()) + ? 'Date invalide' + : date.toLocaleDateString('fr-FR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } catch { + return 'Date invalide'; + } + } + // Icônes + /** + * Obtient l'icône pour un type de configuration + */ + getConfigTypeIcon(configType: string): string { + // Conversion sécurisée vers ConfigType + const validConfigType = Object.values(ConfigType).includes(configType as ConfigType) + ? configType as ConfigType + : ConfigType.CUSTOM; + + const icons: Record = { + [ConfigType.API_KEY]: 'lucideKey', + [ConfigType.SECRET_KEY]: 'lucideShield', + [ConfigType.WEBHOOK_URL]: 'lucideGlobe', + [ConfigType.CALLBACK_URL]: 'lucideRefreshCw', + [ConfigType.TIMEOUT]: 'lucideClock', + [ConfigType.RETRY_COUNT]: 'lucideRepeat', + [ConfigType.CUSTOM]: 'lucideSettings' + }; + return icons[validConfigType] || 'lucideSettings'; + } + + // ==================== STATISTIQUES ==================== + + getMerchantStats() { + if (!this.merchant) return null; + + return { + configs: { + total: this.merchant.configs.length, + sensitive: this.merchant.configs.filter(c => this.isSensitiveConfig(c)).length, + custom: this.merchant.configs.filter(c => c.name === ConfigType.CUSTOM).length + }, + contacts: { + total: this.merchant.technicalContacts.length + }, + users: { + total: this.merchant.users.length, + admins: this.merchant.users.filter(u => + [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_ADMIN].includes(u.role) + ).length + } + }; + } + + // ==================== PAGINATION ==================== + + get paginatedConfigs(): MerchantConfig[] { + const startIndex = (this.page - 1) * this.pageSize; + return this.merchant?.configs.slice(startIndex, startIndex + this.pageSize) || []; + } + + get paginatedContacts(): TechnicalContact[] { + const startIndex = (this.page - 1) * this.pageSize; + return this.merchant?.technicalContacts.slice(startIndex, startIndex + this.pageSize) || []; + } + + get paginatedUsers(): MerchantUser[] { + const startIndex = (this.page - 1) * this.pageSize; + return this.merchant?.users.slice(startIndex, startIndex + this.pageSize) || []; + } + + get totalPages(): number { + const items = this.getCurrentTabItems(); + return Math.ceil(items.length / this.pageSize); + } + + private getCurrentTabItems(): any[] { + switch (this.activeTab) { + case 'configs': return this.merchant?.configs || []; + case 'contacts': return this.merchant?.technicalContacts || []; + case 'users': return this.merchant?.users || []; + default: return []; + } + } + + // ==================== ACTIONS ==================== + + refresh() { + this.clearCache(); + this.loadMerchantProfile(); + } + + clearMessages() { + this.error = ''; + this.success = ''; + } goBack() { this.back.emit(); } - requestEdit(configId: string) { + requestEditConfig(configId: string) { this.editConfigRequested.emit(configId); } - // ==================== MÉTHODES DE VALIDATION ==================== + editMerchant(merchant: Merchant) { + this.editMerchantRequested.emit(merchant); + } + + // ==================== VALIDATION ==================== private validateConfigForm(): { isValid: boolean; error?: string } { if (!this.editedConfig.name?.trim()) { @@ -485,20 +998,6 @@ export class MerchantConfigView implements OnInit, OnDestroy { } } - if (this.editedConfig.name === ConfigType.TIMEOUT) { - const timeout = parseInt(this.editedConfig.value, 10); - if (isNaN(timeout) || timeout < 0) { - return { isValid: false, error: 'Timeout doit être un nombre positif' }; - } - } - - if (this.editedConfig.name === ConfigType.RETRY_COUNT) { - const retryCount = parseInt(this.editedConfig.value, 10); - if (isNaN(retryCount) || retryCount < 0) { - return { isValid: false, error: 'Le nombre de tentatives doit être un nombre positif' }; - } - } - return { isValid: true }; } @@ -506,132 +1005,38 @@ export class MerchantConfigView implements OnInit, OnDestroy { return this.validateConfigForm().isValid; } - // ==================== MÉTHODES UTILITAIRES ==================== + // ==================== GESTION DES ERREURS ==================== - getCreationDate(config: MerchantConfig): string { - if (!config?.createdAt) return 'Non disponible'; - return this.formatTimestamp(new Date(config.createdAt).getTime()); - } - - getLastUpdateDate(config: MerchantConfig): string { - if (!config?.updatedAt) return 'Non disponible'; - return this.formatTimestamp(new Date(config.updatedAt).getTime()); - } - - refresh() { - this.loadAllConfigs(); - } - - clearMessages() { - this.error = ''; - this.success = ''; - this.cdRef.detectChanges(); - } - - // Méthodes pour le template - getProfileTitle(): string { - return `Configurations du Marchand (${this.configs.length})`; - } - - getContextDescription(): string { - return 'Gestion des configurations techniques des marchands'; - } - - showMerchantPartnerInfo(config: MerchantConfig): boolean { - return this.canManageAllConfigs() && Boolean(config?.merchantPartnerId); - } - - getConfigUsageInfo(config: MerchantConfig): string { - if (!config) return ''; - - const usageInfo: Record = { - [ConfigType.API_KEY]: 'Utilisée pour l\'authentification aux APIs', - [ConfigType.SECRET_KEY]: 'Clé secrète pour la signature des requêtes', - [ConfigType.WEBHOOK_URL]: 'URL pour recevoir les notifications', - [ConfigType.CALLBACK_URL]: 'URL pour les retours de paiement', - [ConfigType.TIMEOUT]: 'Délai d\'expiration des requêtes en millisecondes', - [ConfigType.RETRY_COUNT]: 'Nombre de tentatives en cas d\'échec', - [ConfigType.CUSTOM]: 'Configuration personnalisée' - }; - - const configType = config.name as ConfigType; - return usageInfo[configType] || 'Configuration technique'; - } - - getSecurityRecommendation(config: MerchantConfig): string { - if (!this.isSensitiveConfig(config)) return ''; - - return 'Cette configuration contient des informations sensibles. ' + - 'Manipulez-la avec précaution et ne la partagez pas.'; - } - - // Opérateurs disponibles pour l'édition - getAvailableOperators(): { value: Operator; label: string }[] { - return [ - { value: Operator.ORANGE_OSN, label: 'Orange' } - ]; - } - - // Types de configuration disponibles pour l'édition - getAvailableConfigTypes(): { value: ConfigType; label: string }[] { - return [ - { value: ConfigType.API_KEY, label: 'Clé API' }, - { value: ConfigType.SECRET_KEY, label: 'Clé Secrète' }, - { value: ConfigType.WEBHOOK_URL, label: 'URL Webhook' }, - { value: ConfigType.CALLBACK_URL, label: 'URL Callback' }, - { value: ConfigType.TIMEOUT, label: 'Timeout (ms)' }, - { value: ConfigType.RETRY_COUNT, label: 'Nombre de tentatives' }, - { value: ConfigType.CUSTOM, label: 'Personnalisé' } - ]; - } - - // Méthodes pour les actions spécifiques - canShowFullValue(config: MerchantConfig): boolean { - return this.isSensitiveConfig(config) || this.shouldTruncateValue(config); - } - - getValueDisplayClass(config: MerchantConfig): string { - if (this.isSensitiveConfig(config) && !this.showSensitiveValues[config.id!]) { - return 'sensitive-value'; + private getErrorMessage(error: any): string { + if (error.error?.message) { + return error.error.message; } - return ''; - } - - getConfigTypeIconSafe(configName: string): string { - const validConfigTypes = Object.values(ConfigType); - if (validConfigTypes.includes(configName as ConfigType)) { - return this.getConfigTypeIcon(configName as ConfigType); + if (error.status === 400) { + return 'Données invalides. Vérifiez les informations saisies.'; } - return 'lucideSettings'; + if (error.status === 403) { + return 'Vous n\'avez pas les permissions pour effectuer cette action.'; + } + if (error.status === 404) { + return 'Ressource non trouvée.'; + } + if (error.status === 409) { + return 'Conflit de données. Cette ressource existe peut-être déjà.'; + } + return 'Erreur lors de l\'opération. Veuillez réessayer.'; } - getConfigTypeIcon(configType: ConfigType): string { - const icons: Record = { - [ConfigType.API_KEY]: 'lucideKey', - [ConfigType.SECRET_KEY]: 'lucideShield', - [ConfigType.WEBHOOK_URL]: 'lucideGlobe', - [ConfigType.CALLBACK_URL]: 'lucideRefreshCw', - [ConfigType.TIMEOUT]: 'lucideClock', - [ConfigType.RETRY_COUNT]: 'lucideRepeat', - [ConfigType.CUSTOM]: 'lucideSettings' - }; - return icons[configType] || 'lucideSettings'; + // ==================== TRACKBY FUNCTIONS ==================== + + trackByConfigId(index: number, config: MerchantConfig): string { + return config.id || `config-${index}`; } - getOperatorIcon(operatorId: Operator): string { - const icons = { - [Operator.ORANGE_OSN]: 'lucideSignal' - }; - return icons[operatorId] || 'lucideSmartphone'; + trackByContactId(index: number, contact: TechnicalContact): string { + return contact.id || `contact-${index}`; } - // Pagination - get paginatedConfigs(): MerchantConfig[] { - const startIndex = (this.page - 1) * this.pageSize; - return this.configs.slice(startIndex, startIndex + this.pageSize); - } - - get totalPages(): number { - return Math.ceil(this.configs.length / this.pageSize); + trackByUserId(index: number, user: MerchantUser): string { + return user.userId || `user-${index}`; } } \ No newline at end of file diff --git a/src/app/modules/merchant-config/merchant-config.html b/src/app/modules/merchant-config/merchant-config.html index adede0f..ba509aa 100644 --- a/src/app/modules/merchant-config/merchant-config.html +++ b/src/app/modules/merchant-config/merchant-config.html @@ -31,61 +31,21 @@ }
    - @if (canCreateMerchants) { - - }
    } - - @if (showMerchantPartnerField) { + + @if (successMessage) {
    -
    -
    -
    -
    - -
    -
    - -
    -
    - @if (loadingMerchantPartners) { -
    - Chargement... -
    - } -
    -
    +
    +
    + +
    {{ successMessage }}
    +
    @@ -107,8 +67,7 @@ Marchands - - + @@ -511,7 +472,16 @@ @if (selectedMerchantForEdit) {
    -
    + + +
    +
    +
    + + Informations Générales +
    +
    +
    @@ -532,6 +503,7 @@ [(ngModel)]="selectedMerchantForEdit.logo" name="logo" [disabled]="updatingMerchant" + placeholder="https://exemple.com/logo.png" >
    @@ -543,6 +515,7 @@ name="description" [disabled]="updatingMerchant" rows="2" + placeholder="Description du marchand" >
    @@ -555,6 +528,7 @@ name="adresse" required [disabled]="updatingMerchant" + placeholder="Adresse complète" >
    @@ -567,12 +541,226 @@ name="phone" required [disabled]="updatingMerchant" + placeholder="+XX X XX XX XX XX" >
    -
    - - \ No newline at end of file + diff --git a/src/app/modules/merchant-config/merchant-config.service.ts b/src/app/modules/merchant-config/merchant-config.service.ts index c4ff298..e4382c9 100644 --- a/src/app/modules/merchant-config/merchant-config.service.ts +++ b/src/app/modules/merchant-config/merchant-config.service.ts @@ -1,7 +1,7 @@ import { Injectable, inject } from '@angular/core'; -import { HttpClient, HttpParams } from '@angular/common/http'; +import { HttpClient, HttpParams, HttpErrorResponse } from '@angular/common/http'; import { environment } from '@environments/environment'; -import { Observable, map, catchError, throwError } from 'rxjs'; +import { Observable, map, catchError, throwError, retry, timeout } from 'rxjs'; // Import de vos modèles existants import { UserRole } from '@core/models/dcb-bo-hub-user.model'; @@ -25,24 +25,33 @@ import { ApiMerchantUser } from '@core/models/merchant-config.model'; +// SERVICE DE CONVERSION +import { MerchantDataAdapter } from './merchant-data-adapter.service'; + @Injectable({ providedIn: 'root' }) export class MerchantConfigService { private http = inject(HttpClient); + private dataAdapter = inject(MerchantDataAdapter); private baseApiUrl = `${environment.configApiUrl}/merchants`; - // Merchant CRUD Operations + private readonly REQUEST_TIMEOUT = 30000; + private readonly MAX_RETRIES = 2; + + // ==================== MERCHANT CRUD OPERATIONS ==================== + createMerchant(createMerchantDto: CreateMerchantDto): Observable { - return this.http.post(this.baseApiUrl, createMerchantDto).pipe( + const apiDto = this.dataAdapter.convertCreateMerchantToApi(createMerchantDto); + + console.log('📤 Creating merchant:', apiDto); + + return this.http.post(this.baseApiUrl, apiDto).pipe( + timeout(this.REQUEST_TIMEOUT), + retry(this.MAX_RETRIES), map(apiMerchant => { - console.log('Merchant created successfully:', apiMerchant); - return this.mapApiMerchantToMerchant(apiMerchant); + console.log('✅ Merchant created successfully:', apiMerchant); + return this.dataAdapter.convertApiMerchantToFrontend(apiMerchant); }), - catchError(error => { - console.error('Error creating merchant:', error); - return throwError(() => new Error( - error.error?.message || error.message || 'An unexpected error occurred while creating merchant' - )); - }) + catchError(error => this.handleError('createMerchant', error)) ); } @@ -52,328 +61,313 @@ export class MerchantConfigService { .set('limit', limit.toString()); if (params?.query) { - httpParams = httpParams.set('query', params.query); + httpParams = httpParams.set('query', params.query.trim()); } - // L'API retourne directement un tableau de merchants - return this.http.get(this.baseApiUrl, { params: httpParams }).pipe( + + console.log(`📥 Loading merchants page ${page}, limit ${limit}`, params); + + return this.http.get(this.baseApiUrl, { + params: httpParams + }).pipe( + timeout(this.REQUEST_TIMEOUT), + retry(this.MAX_RETRIES), map(apiMerchants => { - const total = apiMerchants.length; + const total = apiMerchants.length; const totalPages = Math.ceil(total / limit); - // Appliquer la pagination côté client const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; const paginatedItems = apiMerchants.slice(startIndex, endIndex); - return { - items: paginatedItems.map(apiMerchant => this.mapApiMerchantToMerchant(apiMerchant)), + const response: PaginatedResponse = { + items: paginatedItems.map(apiMerchant => + this.dataAdapter.convertApiMerchantToFrontend(apiMerchant) + ), total: total, page: page, limit: limit, totalPages: totalPages }; + + console.log(`✅ Loaded ${response.items.length} merchants`); + return response; }), - catchError(error => { - console.error('Error loading merchants:', error); - return throwError(() => error); - }) + catchError(error => this.handleError('getMerchants', error)) + ); + } + + getAllMerchants(params?: SearchMerchantsParams): Observable { + let httpParams = new HttpParams(); + + if (params?.query) { + httpParams = httpParams.set('query', params.query.trim()); + } + + return this.http.get(this.baseApiUrl, { params: httpParams }).pipe( + timeout(this.REQUEST_TIMEOUT), + map(apiMerchants => + apiMerchants.map(merchant => + this.dataAdapter.convertApiMerchantToFrontend(merchant) + ) + ), + catchError(error => this.handleError('getAllMerchants', error)) ); } getMerchantById(id: string): Observable { - const numericId = parseInt(id); - // L'API retourne directement l'objet merchant + const numericId = this.convertIdToNumber(id); + + console.log(`📥 Loading merchant ${id}`); + return this.http.get(`${this.baseApiUrl}/${numericId}`).pipe( + timeout(this.REQUEST_TIMEOUT), + retry(this.MAX_RETRIES), map(apiMerchant => { - return this.mapApiMerchantToMerchant(apiMerchant); + console.log(`✅ Merchant ${id} loaded successfully`); + return this.dataAdapter.convertApiMerchantToFrontend(apiMerchant); }), - catchError(error => { - console.error(`Error loading merchant ${id}:`, error); - return throwError(() => error); - }) + catchError(error => this.handleError('getMerchantById', error, { merchantId: id })) ); } updateMerchant(id: string, updateMerchantDto: UpdateMerchantDto): Observable { - const numericId = parseInt(id); - // L'API retourne directement l'objet mis à jour - return this.http.patch(`${this.baseApiUrl}/${numericId}`, updateMerchantDto).pipe( + const numericId = this.convertIdToNumber(id); + + const apiDto = this.dataAdapter.convertUpdateMerchantToApi(updateMerchantDto); + + console.log(`📤 Updating merchant ${id}:`, apiDto); + + return this.http.patch(`${this.baseApiUrl}/${numericId}`, apiDto).pipe( + timeout(this.REQUEST_TIMEOUT), map(apiMerchant => { - return this.mapApiMerchantToMerchant(apiMerchant); + console.log(`✅ Merchant ${id} updated successfully`); + return this.dataAdapter.convertApiMerchantToFrontend(apiMerchant); }), - catchError(error => { - console.error(`Error updating merchant ${id}:`, error); - return throwError(() => error); - }) + catchError(error => this.handleError('updateMerchant', error, { merchantId: id })) ); } deleteMerchant(id: string): Observable { - const numericId = parseInt(id); - // L'API ne retourne probablement rien ou un simple message + const numericId = this.convertIdToNumber(id); + + console.log(`🗑️ Deleting merchant ${id}`); + return this.http.delete(`${this.baseApiUrl}/${numericId}`).pipe( + timeout(this.REQUEST_TIMEOUT), map(() => { + console.log(`✅ Merchant ${id} deleted successfully`); }), - catchError(error => { - console.error(`Error deleting merchant ${id}:`, error); - return throwError(() => error); - }) + catchError(error => this.handleError('deleteMerchant', error, { merchantId: id })) ); } - // User Management + // ==================== USER MANAGEMENT ==================== + addUserToMerchant(addUserDto: AddUserToMerchantDto): Observable { - // Convertir merchantPartnerId en number pour l'API + // CONVERSION AVEC L'ADAPTER const apiDto = { ...addUserDto, - merchantPartnerId: addUserDto.merchantPartnerId ? parseInt(addUserDto.merchantPartnerId) : undefined + merchantPartnerId: addUserDto.merchantPartnerId ? + this.convertIdToNumber(addUserDto.merchantPartnerId) : undefined }; - // L'API retourne directement l'utilisateur ajouté + console.log('📤 Adding user to merchant:', apiDto); + return this.http.post(`${this.baseApiUrl}/users`, apiDto).pipe( + timeout(this.REQUEST_TIMEOUT), map(apiUser => { - return this.mapApiUserToUser(apiUser); + console.log('✅ User added to merchant successfully'); + // ✅ UTILISATION DE L'ADAPTER + return this.dataAdapter.convertApiUserToFrontend(apiUser); }), - catchError(error => { - console.error('Error adding user to merchant:', error); - return throwError(() => error); - }) + catchError(error => this.handleError('addUserToMerchant', error)) ); } getMerchantUsers(merchantId: string): Observable { - const numericMerchantId = parseInt(merchantId); - // Option 1: Si vous avez un endpoint spécifique pour les users - // return this.http.get(`${this.baseApiUrl}/${numericMerchantId}/users`).pipe( - - // Option 2: Récupérer le merchant complet et extraire les users + const numericMerchantId = this.convertIdToNumber(merchantId); + return this.http.get(`${this.baseApiUrl}/${numericMerchantId}`).pipe( + timeout(this.REQUEST_TIMEOUT), map(apiMerchant => { - // Retourner les users mappés depuis merchantUsers - return (apiMerchant.merchantUsers || []).map(user => this.mapApiUserToUser(user)); + return (apiMerchant.users || []).map(user => + // ✅ UTILISATION DE L'ADAPTER + this.dataAdapter.convertApiUserToFrontend(user) + ); }), - catchError(error => { - console.error(`Error loading users for merchant ${merchantId}:`, error); - return throwError(() => error); - }) + catchError(error => this.handleError('getMerchantUsers', error, { merchantId })) ); } updateUserRole(merchantId: string, userId: string, updateRoleDto: UpdateUserRoleDto): Observable { - const numericMerchantId = parseInt(merchantId); - // L'API retourne directement l'utilisateur mis à jour + const numericMerchantId = this.convertIdToNumber(merchantId); + return this.http.patch( `${this.baseApiUrl}/${numericMerchantId}/users/${userId}/role`, updateRoleDto ).pipe( + timeout(this.REQUEST_TIMEOUT), map(apiUser => { - return this.mapApiUserToUser(apiUser); + console.log(`✅ User ${userId} role updated successfully`); + // ✅ UTILISATION DE L'ADAPTER + return this.dataAdapter.convertApiUserToFrontend(apiUser); }), - catchError(error => { - console.error(`Error updating user role for merchant ${merchantId}, user ${userId}:`, error); - return throwError(() => error); - }) + catchError(error => this.handleError('updateUserRole', error, { merchantId, userId })) ); } removeUserFromMerchant(merchantId: string, userId: string): Observable { - const numericMerchantId = parseInt(merchantId); - // L'API ne retourne probablement rien + const numericMerchantId = this.convertIdToNumber(merchantId); + return this.http.delete(`${this.baseApiUrl}/${numericMerchantId}/users/${userId}`).pipe( + timeout(this.REQUEST_TIMEOUT), map(() => { + console.log(`✅ User ${userId} removed from merchant ${merchantId} successfully`); }), - catchError(error => { - console.error(`Error removing user ${userId} from merchant ${merchantId}:`, error); - return throwError(() => error); - }) + catchError(error => this.handleError('removeUserFromMerchant', error, { merchantId, userId })) ); } getUserMerchants(userId: string): Observable { - // L'API retourne directement un tableau de merchants return this.http.get(`${this.baseApiUrl}/user/${userId}`).pipe( + timeout(this.REQUEST_TIMEOUT), map(apiMerchants => { - return apiMerchants.map(merchant => this.mapApiMerchantToMerchant(merchant)); + return apiMerchants.map(merchant => + this.dataAdapter.convertApiMerchantToFrontend(merchant) + ); }), - catchError(error => { - console.error(`Error loading merchants for user ${userId}:`, error); - return throwError(() => error); - }) + catchError(error => this.handleError('getUserMerchants', error, { userId })) ); } - // Config Management + // ==================== CONFIG MANAGEMENT ==================== + addConfigToMerchant(merchantId: string, config: Omit): Observable { - const numericMerchantId = parseInt(merchantId); + const numericMerchantId = this.convertIdToNumber(merchantId); + + // CONVERSION AVEC L'ADAPTER const apiConfig: Omit = { ...config, operatorId: config.operatorId, merchantPartnerId: numericMerchantId }; - // L'API retourne directement la configuration créée + console.log(`📤 Adding config to merchant ${merchantId}:`, apiConfig); + return this.http.post(`${this.baseApiUrl}/${numericMerchantId}/configs`, apiConfig).pipe( + timeout(this.REQUEST_TIMEOUT), map(apiConfig => { - return this.mapApiConfigToConfig(apiConfig); + console.log('✅ Config added to merchant successfully'); + // ✅ UTILISATION DE L'ADAPTER + return this.dataAdapter.convertApiConfigToFrontend(apiConfig); }), - catchError(error => { - console.error(`Error adding config to merchant ${merchantId}:`, error); - return throwError(() => error); - }) + catchError(error => this.handleError('addConfigToMerchant', error, { merchantId })) ); } updateConfig(configId: string, config: Partial): Observable { - const numericConfigId = parseInt(configId); + const numericConfigId = this.convertIdToNumber(configId); - // Préparer l'objet de configuration pour l'API - const apiConfig: Partial = { - name: config.name, - value: config.value, - operatorId: config.operatorId, - // Si merchantPartnerId est présent, le convertir en number - ...(config.merchantPartnerId && { - merchantPartnerId: parseInt(config.merchantPartnerId) - }) - }; + const apiConfig = this.dataAdapter.convertConfigUpdateToApi(config); + + console.log(`📤 Updating config ${configId}:`, apiConfig); - // L'API retourne directement la configuration mise à jour return this.http.patch(`${this.baseApiUrl}/configs/${numericConfigId}`, apiConfig).pipe( + timeout(this.REQUEST_TIMEOUT), map(apiConfig => { - return this.mapApiConfigToConfig(apiConfig); + console.log(`✅ Config ${configId} updated successfully`); + // ✅ UTILISATION DE L'ADAPTER + return this.dataAdapter.convertApiConfigToFrontend(apiConfig); }), - catchError(error => { - console.error(`Error updating config ${configId}:`, error); - return throwError(() => error); - }) + catchError(error => this.handleError('updateConfig', error, { configId })) ); } deleteConfig(configId: string): Observable { - const numericConfigId = parseInt(configId); - // L'API ne retourne probablement rien + const numericConfigId = this.convertIdToNumber(configId); + + console.log(`🗑️ Deleting config ${configId}`); + return this.http.delete(`${this.baseApiUrl}/configs/${numericConfigId}`).pipe( + timeout(this.REQUEST_TIMEOUT), map(() => { - + console.log(`✅ Config ${configId} deleted successfully`); }), - catchError(error => { - console.error(`Error deleting config ${configId}:`, error); - return throwError(() => error); - }) + catchError(error => this.handleError('deleteConfig', error, { configId })) ); } - // Technical Contacts Management - addTechnicalContactToMerchant(merchantId: string, contact: Omit): Observable { - const numericMerchantId = parseInt(merchantId); - const apiContact: Omit = { - ...contact, - merchantPartnerId: numericMerchantId - }; - - // L'API retourne directement le contact créé - return this.http.post(`${this.baseApiUrl}/${numericMerchantId}/technical-contacts`, apiContact).pipe( - map(apiContact => { - return this.mapApiContactToContact(apiContact); - }), - catchError(error => { - console.error(`Error adding technical contact to merchant ${merchantId}:`, error); - return throwError(() => error); - }) + getMerchantConfigs(merchantId: string): Observable { + return this.getMerchantById(merchantId).pipe( + map(merchant => merchant?.configs ?? []) ); } - updateTechnicalContact(contactId: string, contact: Partial): Observable { - const numericContactId = parseInt(contactId); + // ==================== UTILITY METHODS ==================== + + private handleError(operation: string, error: any, context?: any): Observable { + console.error(`❌ Error in ${operation}:`, error, context); + + let userMessage: string; - const apiContact: Partial = { - firstName: contact.firstName, - lastName: contact.lastName, - phone: contact.phone, - email: contact.email, - // Si merchantPartnerId est présent, le convertir en number - ...(contact.merchantPartnerId && { - merchantPartnerId: parseInt(contact.merchantPartnerId) - }) + if (error instanceof HttpErrorResponse) { + switch (error.status) { + case 0: + userMessage = 'Erreur de connexion. Vérifiez votre connexion internet.'; + break; + case 400: + userMessage = 'Données invalides. Vérifiez les informations saisies.'; + break; + case 401: + userMessage = 'Authentification requise. Veuillez vous reconnecter.'; + break; + case 403: + userMessage = 'Vous n\'avez pas les permissions pour effectuer cette action.'; + break; + case 404: + userMessage = 'Ressource non trouvée.'; + break; + case 409: + userMessage = 'Conflit de données. Cette ressource existe peut-être déjà.'; + break; + case 500: + userMessage = 'Erreur serveur. Veuillez réessayer plus tard.'; + break; + case 503: + userMessage = 'Service temporairement indisponible. Veuillez réessayer plus tard.'; + break; + default: + userMessage = 'Une erreur inattendue est survenue.'; + } + } else if (error.name === 'TimeoutError') { + userMessage = 'La requête a expiré. Veuillez réessayer.'; + } else { + userMessage = 'Une erreur inattendue est survenue.'; + } + + const errorDetails = { + operation, + context, + status: error.status, + message: error.message, + url: error.url }; + + console.error('Error details:', errorDetails); - // L'API retourne directement le contact mis à jour - return this.http.patch(`${this.baseApiUrl}/technical-contacts/${numericContactId}`, apiContact).pipe( - map(apiContact => { - return this.mapApiContactToContact(apiContact); - }), - catchError(error => { - console.error(`Error updating technical contact ${contactId}:`, error); - return throwError(() => error); - }) - ); + return throwError(() => new Error(userMessage)); } - deleteTechnicalContact(contactId: string): Observable { - const numericContactId = parseInt(contactId); - // L'API ne retourne probablement rien - return this.http.delete(`${this.baseApiUrl}/technical-contacts/${numericContactId}`).pipe( - map(() => { - - }), - catchError(error => { - console.error(`Error deleting technical contact ${contactId}:`, error); - return throwError(() => error); - }) - ); - } - - // Stats - getMerchantStats(): Observable { - // L'API retourne directement les stats - return this.http.get(`${this.baseApiUrl}/stats`).pipe( - map(stats => { - return stats; - }), - catchError(error => { - console.error('Error loading merchant stats:', error); - return throwError(() => error); - }) - ); - } - - // ==================== MAPPING METHODS ==================== - - // Méthode principale de mapping Merchant - private mapApiMerchantToMerchant(apiMerchant: any): Merchant { - - return { - ...apiMerchant, - id: apiMerchant.id?.toString() || '', // number → string - configs: (apiMerchant.configs || []).map((config: any) => this.mapApiConfigToConfig(config)), - users: (apiMerchant.merchantUsers || []).map((user: any) => this.mapApiUserToUser(user)), // merchantUsers → users - technicalContacts: (apiMerchant.technicalContacts || []).map((contact: any) => this.mapApiContactToContact(contact)), - }; - } - - // Méthode de mapping pour les configurations - private mapApiConfigToConfig(apiConfig: any): MerchantConfig { - return { - ...apiConfig, - id: apiConfig.id?.toString(), // number → string - merchantPartnerId: apiConfig.merchantPartnerId?.toString() // number → string - }; - } - - // Méthode de mapping pour les contacts techniques - private mapApiContactToContact(apiContact: any): TechnicalContact { - return { - ...apiContact, - id: apiContact.id?.toString(), // number → string - merchantPartnerId: apiContact.merchantPartnerId?.toString() // number → string - }; - } - - // Méthode de mapping pour les utilisateurs - private mapApiUserToUser(apiUser: any): MerchantUser { - return { - ...apiUser, - merchantPartnerId: apiUser.merchantPartnerId?.toString() // number → string - }; + private convertIdToNumber(id: string): number { + if (!id) { + throw new Error('ID cannot be null or undefined'); + } + + const numericId = Number(id); + if (isNaN(numericId)) { + throw new Error(`Invalid ID format: ${id}`); + } + + return numericId; } } \ No newline at end of file diff --git a/src/app/modules/merchant-config/merchant-config.ts b/src/app/modules/merchant-config/merchant-config.ts index 226721c..3ec1451 100644 --- a/src/app/modules/merchant-config/merchant-config.ts +++ b/src/app/modules/merchant-config/merchant-config.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormGroup } from '@angular/forms'; import { NgIcon } from '@ng-icons/core'; import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { catchError, map, of, Subject, takeUntil } from 'rxjs'; +import { catchError, finalize, map, of, Subject, takeUntil } from 'rxjs'; import { MerchantConfigService } from './merchant-config.service'; import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; @@ -19,10 +19,14 @@ import { Operator, UpdateMerchantDto, Merchant, + MerchantConfig, + TechnicalContact } from '@core/models/merchant-config.model'; import { UserRole, UserType, PaginatedUserResponse, User } from '@core/models/dcb-bo-hub-user.model'; import { HubUsersService } from '../hub-users-management/hub-users.service'; +import { MerchantDataAdapter } from './merchant-data-adapter.service'; + @Component({ selector: 'app-merchant-config', standalone: true, @@ -41,10 +45,9 @@ import { HubUsersService } from '../hub-users-management/hub-users.service'; }) export class MerchantConfigManagement implements OnInit, OnDestroy { private modalService = inject(NgbModal); - private fb = inject(FormBuilder); private authService = inject(AuthService); private merchantConfigService = inject(MerchantConfigService); - private hubUsersService = inject(HubUsersService); + private dataAdapter = inject(MerchantDataAdapter); protected roleService = inject(RoleManagementService); private cdRef = inject(ChangeDetectorRef); private destroy$ = new Subject(); @@ -87,6 +90,10 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { selectedMerchantForEdit: Merchant | null = null; selectedMerchantForDelete: Merchant | null = null; + // États pour la gestion des modifications + editingConfigs: { [configId: string]: boolean } = {}; + editingContacts: { [contactId: string]: boolean } = {}; + // Références aux templates de modals @ViewChild('createMerchantModal') createMerchantModal!: TemplateRef; @ViewChild('editMerchantModal') editMerchantModal!: TemplateRef; @@ -94,6 +101,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { // Références aux composants enfants @ViewChild(MerchantConfigsList) merchantConfigsList!: MerchantConfigsList; + @ViewChild(MerchantConfigView) merchantConfigView!: MerchantConfigView; // Opérateurs disponibles operators = [ @@ -124,7 +132,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { ngOnInit() { this.activeTab = 'list'; this.loadCurrentUserPermissions(); - this.loadMerchantPartnersIfNeeded(); this.initializeMerchantPartnerContext(); } @@ -163,6 +170,27 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { } } + // Gestion des contacts dans l'édition + addTechnicalContactInEdit(): void { + if (!this.selectedMerchantForEdit?.technicalContacts) { + this.selectedMerchantForEdit!.technicalContacts = []; + } + + this.selectedMerchantForEdit?.technicalContacts.push({ + firstName: '', + lastName: '', + phone: '', + email: '' + }); + } + + removeTechnicalContactInEdit(index: number): void { + if (this.selectedMerchantForEdit?.technicalContacts && + this.selectedMerchantForEdit.technicalContacts.length > 1) { + this.selectedMerchantForEdit.technicalContacts.splice(index, 1); + } + } + /** * Méthodes pour la gestion des configurations */ @@ -184,60 +212,44 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { } } - // ==================== CONVERSION IDS ==================== - - /** - * Convertit un ID number en string pour Angular - */ - private convertIdToString(id: number): string { - return id.toString(); + //Gestion des configs dans l'édition + addConfigInEdit(): void { + if (!this.selectedMerchantForEdit?.configs) { + this.selectedMerchantForEdit!.configs = []; + } + + this.selectedMerchantForEdit?.configs.push({ + name: ConfigType.API_KEY, + value: '', + operatorId: Operator.ORANGE_OSN + }); } + removeConfigInEdit(index: number): void { + if (this.selectedMerchantForEdit?.configs && this.selectedMerchantForEdit.configs.length > 1) { + this.selectedMerchantForEdit.configs.splice(index, 1); + } + } + + // ==================== CONVERSION IDS ==================== + /** * Convertit un marchand avec des IDs number en string */ private convertMerchantToFrontend(merchant: any): Merchant { - return { - ...merchant, - id: merchant.id ? this.convertIdToString(merchant.id) : undefined, - configs: merchant.configs?.map((config: any) => ({ - ...config, - id: config.id ? this.convertIdToString(config.id) : undefined, - merchantPartnerId: config.merchantPartnerId ? this.convertIdToString(config.merchantPartnerId) : undefined - })) || [], - technicalContacts: merchant.technicalContacts?.map((contact: any) => ({ - ...contact, - id: contact.id ? this.convertIdToString(contact.id) : undefined, - merchantPartnerId: contact.merchantPartnerId ? this.convertIdToString(contact.merchantPartnerId) : undefined - })) || [], - users: merchant.users?.map((user: any) => ({ - ...user, - merchantPartnerId: user.merchantPartnerId ? this.convertIdToString(user.merchantPartnerId) : undefined - })) || [] - }; + return this.dataAdapter.convertApiMerchantToFrontend(merchant); } /** * Convertit un DTO avec des IDs string en number pour l'API */ private convertMerchantToBackend(dto: any): any { + return this.dataAdapter.convertCreateMerchantToApi(dto); + } - console.log("convertMerchantToBackend") - return { - ...dto, - // Pas de conversion pour l'ID du marchand en création (généré par le backend) - configs: dto.configs?.map((config: any) => ({ - ...config, - // Pas de conversion pour l'ID de config en création - operatorId: parseInt(config.operatorId), - merchantPartnerId: config.merchantPartnerId - })) || [], - technicalContacts: dto.technicalContacts?.map((contact: any) => ({ - ...contact, - // Pas de conversion pour l'ID de contact en création - merchantPartnerId: contact.merchantPartnerId - })) || [] - }; + // Conversion pour la mise à jour + private convertUpdateMerchantToBackend(dto: UpdateMerchantDto, existingMerchant?: Merchant): any { + return this.dataAdapter.convertUpdateMerchantToApi(dto, existingMerchant); } // ==================== GESTION DES PERMISSIONS ==================== @@ -300,40 +312,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { // ==================== GESTION DES MARCHANDS ==================== - private loadMerchantPartnersIfNeeded(): void { - if (this.canManageMerchants) { - this.loadMerchantPartners(); - } - } - - private loadMerchantPartners(): void { - this.loadingMerchantPartners = true; - this.merchantPartnersError = ''; - - this.hubUsersService.getAllDcbPartners() - .pipe( - map((response: PaginatedUserResponse) => response.users), - takeUntil(this.destroy$), - catchError(error => { - console.error('Error loading merchant partners:', error); - this.loadingMerchantPartners = false; - this.merchantPartnersError = 'Impossible de charger la liste des marchands'; - return of([] as User[]); - }) - ) - .subscribe({ - next: (partners) => { - this.merchantPartners = partners; - this.loadingMerchantPartners = false; - }, - error: (error) => { - console.error('❌ Erreur lors du chargement des marchands:', error); - this.loadingMerchantPartners = false; - this.merchantPartnersError = 'Impossible de charger la liste des marchands'; - } - }); - } - private initializeMerchantPartnerContext(): void { if ((this.currentUserRole === UserRole.DCB_PARTNER || this.currentUserRole === UserRole.DCB_PARTNER_ADMIN) && @@ -413,6 +391,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { onEditConfigRequested(configId: string): void { this.openEditMerchantModal(configId); } + // ==================== GESTION DES MODALS ==================== openModal(content: TemplateRef, size: 'sm' | 'lg' | 'xl' = 'lg'): void { @@ -535,30 +514,28 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { }); } + // Mise à jour COMPLÈTE du merchant updateMerchant(): void { if (!this.selectedMerchantForEdit) { this.updateMerchantError = 'Aucun marchand sélectionné pour modification'; return; } - const validation = MerchantUtils.validateMerchantCreation(this.selectedMerchantForEdit as CreateMerchantDto); - if (validation.length > 0) { - this.updateMerchantError = validation.join(', '); + // Validation des données complètes + const validation = this.validateMerchantUpdate(this.selectedMerchantForEdit); + if (!validation.isValid) { + this.updateMerchantError = validation.errors.join(', '); return; } this.updatingMerchant = true; this.updateMerchantError = ''; - // Conversion pour l'API + // Conversion pour l'API avec TOUTES les données const merchantId = this.selectedMerchantForEdit.id!; - const updateDto: UpdateMerchantDto = { - name: this.selectedMerchantForEdit.name, - logo: this.selectedMerchantForEdit.logo, - description: this.selectedMerchantForEdit.description, - adresse: this.selectedMerchantForEdit.adresse, - phone: this.selectedMerchantForEdit.phone - }; + const updateDto = this.convertUpdateMerchantToBackend(this.selectedMerchantForEdit, this.selectedMerchantForEdit); + + console.log('📤 Updating merchant with full data:', updateDto); this.merchantConfigService.updateMerchant(merchantId, updateDto) .pipe(takeUntil(this.destroy$)) @@ -569,6 +546,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { this.updatingMerchant = false; this.modalService.dismissAll(); + this.refreshMerchantsConfigsView(); this.refreshMerchantsList(); // Mettre à jour le cache @@ -576,6 +554,8 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { this.merchantProfiles[this.selectedMerchantId] = frontendMerchant; } + this.successMessage = 'Marchand modifié avec succès'; + this.cdRef.detectChanges(); }, error: (error) => { @@ -587,6 +567,74 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { }); } + // Validation complète pour la mise à jour + validateMerchantUpdate(merchant: UpdateMerchantDto): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + // Validation des champs de base + if (!merchant.name?.trim()) { + errors.push('Le nom du merchant est requis'); + } + + if (!merchant.adresse?.trim()) { + errors.push('L\'adresse est requise'); + } + + if (!merchant.phone?.trim()) { + errors.push('Le téléphone est requis'); + } + + // Validation des configurations + if (!merchant.configs || merchant.configs.length === 0) { + errors.push('Au moins une configuration est requise'); + } else { + merchant.configs.forEach((config, index) => { + if (!config.name?.trim()) { + errors.push(`Le type de configuration ${index + 1} est requis`); + } + if (!config.value?.trim()) { + errors.push(`La valeur de configuration ${index + 1} est requise`); + } + if (!config.operatorId) { + errors.push(`L'opérateur de configuration ${index + 1} est requis`); + } + }); + } + + // Validation des contacts techniques + if (!merchant.technicalContacts || merchant.technicalContacts.length === 0) { + errors.push('Au moins un contact technique est requis'); + } else { + merchant.technicalContacts.forEach((contact, index) => { + if (!contact.firstName?.trim()) { + errors.push(`Le prénom du contact ${index + 1} est requis`); + } + if (!contact.lastName?.trim()) { + errors.push(`Le nom du contact ${index + 1} est requis`); + } + if (!contact.phone?.trim()) { + errors.push(`Le téléphone du contact ${index + 1} est requis`); + } + if (!contact.email?.trim()) { + errors.push(`L'email du contact ${index + 1} est requis`); + } else if (!this.isValidEmail(contact.email)) { + errors.push(`L'email du contact ${index + 1} est invalide`); + } + }); + } + + return { + isValid: errors.length === 0, + errors: errors + }; + } + + // Validation d'email + private isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } + confirmDeleteMerchant(): void { if (!this.selectedMerchantForDelete) { this.deleteMerchantError = 'Aucun marchand sélectionné pour suppression'; @@ -653,7 +701,11 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { } private populateEditForm(merchant: Merchant): void { - this.selectedMerchantForEdit = { ...merchant }; + this.selectedMerchantForEdit = { + ...merchant, + configs: merchant.configs.map(config => ({ ...config })), + technicalContacts: merchant.technicalContacts.map(contact => ({ ...contact })) + }; } private refreshMerchantsList(): void { @@ -666,6 +718,16 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { } } + private refreshMerchantsConfigsView(): void { + if (this.merchantConfigView && typeof this.merchantConfigView.refresh === 'function') { + console.log('🔄 Refreshing merchants config view...'); + this.merchantConfigView.refresh(); + } else { + console.warn('❌ MerchantConfigsView component not available for refresh'); + this.showTab('list'); + } + } + // ==================== GESTION DES ERREURS ==================== private getCreateErrorMessage(error: any): string { @@ -702,6 +764,38 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { return MerchantUtils.getConfigTypeName(configType); } + // Méthodes utilitaires pour le template d'édition + trackByConfigId(index: number, config: MerchantConfig): string { + return config.id || `new-${index}`; + } + + trackByContactId(index: number, contact: TechnicalContact): string { + return contact.id || `new-${index}`; + } + + // Méthode pour détecter les configs sensibles + isSensitiveConfig(config: MerchantConfig): boolean { + return config.name === ConfigType.API_KEY || + config.name === ConfigType.SECRET_KEY; + } + + // Méthode pour obtenir l'icône d'un type de config + getConfigTypeIconSafe(configName: string): string { + const icons: { [key: string]: string } = { + [ConfigType.API_KEY]: 'lucideKey', + [ConfigType.SECRET_KEY]: 'lucideShield', + [ConfigType.WEBHOOK_URL]: 'lucideGlobe', + [ConfigType.CALLBACK_URL]: 'lucideRefreshCw', + [ConfigType.TIMEOUT]: 'lucideClock', + [ConfigType.RETRY_COUNT]: 'lucideRepeat', + [ConfigType.CUSTOM]: 'lucideSettings' + }; + return icons[configName] || 'lucideSettings'; + } + + // Propriété pour le message de succès + successMessage: string = ''; + // ==================== GETTERS TEMPLATE ==================== get currentMerchantProfile(): Merchant | null { diff --git a/src/app/modules/merchant-config/merchant-data-adapter.service.ts b/src/app/modules/merchant-config/merchant-data-adapter.service.ts new file mode 100644 index 0000000..f4bd035 --- /dev/null +++ b/src/app/modules/merchant-config/merchant-data-adapter.service.ts @@ -0,0 +1,447 @@ +import { Injectable } from '@angular/core'; +import { + Merchant, + CreateMerchantDto, + UpdateMerchantDto, + MerchantConfig, + TechnicalContact, + ApiMerchant, + ApiMerchantConfig, + ApiTechnicalContact, + ConfigType, + Operator +} from '@core/models/merchant-config.model'; + +@Injectable({ + providedIn: 'root' +}) +export class MerchantDataAdapter { + + // ==================== CONVERSION API → FRONTEND ==================== + + /** + * Convertit un merchant de l'API vers le format Frontend + */ + convertApiMerchantToFrontend(apiMerchant: ApiMerchant): Merchant { + if (!apiMerchant) { + throw new Error('Merchant data is required for conversion'); + } + + return { + ...apiMerchant, + id: this.convertIdToString(apiMerchant.id), + configs: (apiMerchant.configs || []).map(config => + this.convertApiConfigToFrontend(config) + ), + technicalContacts: (apiMerchant.technicalContacts || []).map(contact => + this.convertApiContactToFrontend(contact) + ), + users: (apiMerchant.users || []).map(user => + this.convertApiUserToFrontend(user) + ) + }; + } + + /** + * Convertit une configuration de l'API vers le format Frontend + */ + convertApiConfigToFrontend(apiConfig: ApiMerchantConfig): MerchantConfig { + return { + ...apiConfig, + id: this.convertIdToString(apiConfig.id), + merchantPartnerId: this.convertIdToString(apiConfig.merchantPartnerId) + }; + } + + /** + * Convertit un contact technique de l'API vers le format Frontend + */ + convertApiContactToFrontend(apiContact: ApiTechnicalContact): TechnicalContact { + return { + ...apiContact, + id: this.convertIdToString(apiContact.id), + merchantPartnerId: this.convertIdToString(apiContact.merchantPartnerId) + }; + } + + /** + * Convertit un utilisateur merchant de l'API vers le format Frontend + */ + convertApiUserToFrontend(apiUser: any): any { + return { + ...apiUser, + merchantPartnerId: this.convertIdToString(apiUser.merchantPartnerId) + }; + } + + // ==================== CONVERSION FRONTEND → API ==================== + + /** + * Convertit un DTO de création pour l'API + */ + convertCreateMerchantToApi(dto: CreateMerchantDto): any { + this.validateCreateMerchantDto(dto); + + return { + name: dto.name?.trim(), + logo: dto.logo?.trim() || undefined, + description: dto.description?.trim() || undefined, + adresse: dto.adresse?.trim(), + phone: dto.phone?.trim(), + configs: (dto.configs || []).map(config => ({ + name: config.name, + value: config.value?.trim(), + operatorId: this.validateOperatorId(config.operatorId) + })), + technicalContacts: (dto.technicalContacts || []).map(contact => ({ + firstName: contact.firstName?.trim(), + lastName: contact.lastName?.trim(), + phone: contact.phone?.trim(), + email: contact.email?.trim() + })) + }; + } + + /** + * Convertit un DTO de mise à jour pour l'API + */ + convertUpdateMerchantToApi(dto: UpdateMerchantDto, existingMerchant?: Merchant): any { + this.validateUpdateMerchantDto(dto); + + const updateData: any = {}; + + // Champs de base + if (dto.name !== undefined) updateData.name = dto.name?.trim(); + if (dto.logo !== undefined) updateData.logo = dto.logo?.trim() || null; + if (dto.description !== undefined) updateData.description = dto.description?.trim() || null; + if (dto.adresse !== undefined) updateData.adresse = dto.adresse?.trim(); + if (dto.phone !== undefined) updateData.phone = dto.phone?.trim(); + + // Configurations - seulement si présentes dans le DTO + if (dto.configs !== undefined) { + updateData.configs = (dto.configs || []).map(config => { + const apiConfig: any = { + name: config.name, + value: config.value?.trim(), + operatorId: this.validateOperatorId(config.operatorId) + }; + + return apiConfig; + }); + } + + // Contacts techniques - seulement si présents dans le DTO + if (dto.technicalContacts !== undefined) { + updateData.technicalContacts = (dto.technicalContacts || []).map(contact => { + const apiContact: any = { + firstName: contact.firstName?.trim(), + lastName: contact.lastName?.trim(), + phone: contact.phone?.trim(), + email: contact.email?.trim() + }; + + return apiContact; + }); + } + + return updateData; + } + + /** + * Convertit une configuration pour la mise à jour via endpoint spécifique + */ + convertConfigUpdateToApi(config: Partial): any { + const updateData: any = {}; + + if (config.name !== undefined) updateData.name = config.name; + if (config.value !== undefined) updateData.value = config.value?.trim(); + if (config.operatorId !== undefined) { + updateData.operatorId = this.validateOperatorId(config.operatorId); + } + + if (config.merchantPartnerId !== undefined) { + updateData.merchantPartnerId = this.convertIdToNumber(config.merchantPartnerId); + } + + return updateData; + } + + // ==================== VALIDATION ==================== + + /** + * Valide le DTO de création + */ + private validateCreateMerchantDto(dto: CreateMerchantDto): void { + const errors: string[] = []; + + if (!dto.name?.trim()) { + errors.push('Le nom du merchant est requis'); + } + + if (!dto.adresse?.trim()) { + errors.push('L\'adresse est requise'); + } + + if (!dto.phone?.trim()) { + errors.push('Le téléphone est requis'); + } + + if (!dto.technicalContacts || dto.technicalContacts.length === 0) { + errors.push('Au moins un contact technique est requis'); + } else { + dto.technicalContacts.forEach((contact, index) => { + if (!contact.firstName?.trim()) { + errors.push(`Le prénom du contact ${index + 1} est requis`); + } + if (!contact.lastName?.trim()) { + errors.push(`Le nom du contact ${index + 1} est requis`); + } + if (!contact.phone?.trim()) { + errors.push(`Le téléphone du contact ${index + 1} est requis`); + } + if (!contact.email?.trim()) { + errors.push(`L'email du contact ${index + 1} est requis`); + } else if (!this.isValidEmail(contact.email)) { + errors.push(`L'email du contact ${index + 1} est invalide`); + } + }); + } + + if (!dto.configs || dto.configs.length === 0) { + errors.push('Au moins une configuration est requise'); + } else { + dto.configs.forEach((config, index) => { + if (!config.name?.trim()) { + errors.push(`Le type de configuration ${index + 1} est requis`); + } + if (!config.value?.trim()) { + errors.push(`La valeur de configuration ${index + 1} est requise`); + } + if (!config.operatorId) { + errors.push(`L'opérateur de configuration ${index + 1} est requis`); + } + }); + } + + if (errors.length > 0) { + throw new Error(`Validation failed: ${errors.join(', ')}`); + } + } + + /** + * Valide le DTO de mise à jour + */ + private validateUpdateMerchantDto(dto: UpdateMerchantDto): void { + const errors: string[] = []; + + // Validation des champs de base si présents + if (dto.name !== undefined && !dto.name.trim()) { + errors.push('Le nom du merchant est requis'); + } + + if (dto.adresse !== undefined && !dto.adresse.trim()) { + errors.push('L\'adresse est requise'); + } + + if (dto.phone !== undefined && !dto.phone.trim()) { + errors.push('Le téléphone est requis'); + } + + // Validation des configurations si présentes + if (dto.configs !== undefined) { + if (dto.configs.length === 0) { + errors.push('Au moins une configuration est requise'); + } else { + dto.configs.forEach((config, index) => { + if (!config.name?.trim()) { + errors.push(`Le type de configuration ${index + 1} est requis`); + } + if (!config.value?.trim()) { + errors.push(`La valeur de configuration ${index + 1} est requise`); + } + if (!config.operatorId) { + errors.push(`L'opérateur de configuration ${index + 1} est requis`); + } + }); + } + } + + // Validation des contacts si présents + if (dto.technicalContacts !== undefined) { + if (dto.technicalContacts.length === 0) { + errors.push('Au moins un contact technique est requis'); + } else { + dto.technicalContacts.forEach((contact, index) => { + if (!contact.firstName?.trim()) { + errors.push(`Le prénom du contact ${index + 1} est requis`); + } + if (!contact.lastName?.trim()) { + errors.push(`Le nom du contact ${index + 1} est requis`); + } + if (!contact.phone?.trim()) { + errors.push(`Le téléphone du contact ${index + 1} est requis`); + } + if (!contact.email?.trim()) { + errors.push(`L'email du contact ${index + 1} est requis`); + } else if (!this.isValidEmail(contact.email)) { + errors.push(`L'email du contact ${index + 1} est invalide`); + } + }); + } + } + + if (errors.length > 0) { + throw new Error(`Validation failed: ${errors.join(', ')}`); + } + } + + + /** + * Valide un operatorId + */ + private validateOperatorId(operatorId: Operator | number | string | null): number { + if (operatorId === null || operatorId === undefined) { + throw new Error('Operator ID is required'); + } + + let numericId: number; + + if (typeof operatorId === 'number') { + numericId = operatorId; + } else if (typeof operatorId === 'string') { + numericId = parseInt(operatorId, 10); + } else { + // Cas "sécurité" : si TS voit encore "never" + numericId = parseInt(String(operatorId), 10); + } + + if (isNaN(numericId)) { + throw new Error(`Invalid operator ID: ${operatorId}`); + } + + // Vérifier que l'opérateur fait partie des valeurs enum valides + const validOperators = Object.values(Operator).filter(val => typeof val === 'number'); + if (!validOperators.includes(numericId)) { + throw new Error(`Unsupported operator ID: ${operatorId}`); + } + + return numericId; + } + + // ==================== UTILITAIRES DE CONVERSION ==================== + + /** + * Convertit un ID number en string + */ + private convertIdToString(id: number | undefined): string | undefined { + if (id === undefined || id === null) return undefined; + return id.toString(); + } + + /** + * Convertit un ID string en number + */ + private convertIdToNumber(id: string | undefined): number | undefined { + if (!id) return undefined; + + const numericId = parseInt(id, 10); + if (isNaN(numericId)) { + throw new Error(`Invalid ID for conversion to number: ${id}`); + } + + return numericId; + } + + /** + * Valide un email + */ + private isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } + + // ==================== MÉTHODES UTILITAIRES ==================== + + /** + * Crée un nouveau merchant vide pour l'édition + */ + createEmptyMerchant(): Merchant { + return { + name: '', + adresse: '', + phone: '', + configs: [this.createEmptyConfig()], + technicalContacts: [this.createEmptyContact()], + users: [] + }; + } + + /** + * Crée une configuration vide + */ + createEmptyConfig(): MerchantConfig { + return { + name: ConfigType.API_KEY, + value: '', + operatorId: Operator.ORANGE_OSN + }; + } + + /** + * Crée un contact technique vide + */ + createEmptyContact(): TechnicalContact { + return { + firstName: '', + lastName: '', + phone: '', + email: '' + }; + } + + /** + * Clone un merchant profondément (pour éviter les mutations) + */ + cloneMerchant(merchant: Merchant): Merchant { + return { + ...merchant, + configs: merchant.configs.map(config => ({ ...config })), + technicalContacts: merchant.technicalContacts.map(contact => ({ ...contact })), + users: merchant.users.map(user => ({ ...user })) + }; + } + + /** + * Vérifie si un merchant a été modifié + */ + hasMerchantChanged(original: Merchant, updated: Merchant): boolean { + return JSON.stringify(original) !== JSON.stringify(updated); + } + + /** + * Extrait seulement les champs modifiés pour l'update + */ + getChangedFields(original: Merchant, updated: Merchant): Partial { + const changes: Partial = {}; + + if (original.name !== updated.name) changes.name = updated.name; + if (original.logo !== updated.logo) changes.logo = updated.logo; + if (original.description !== updated.description) changes.description = updated.description; + if (original.adresse !== updated.adresse) changes.adresse = updated.adresse; + if (original.phone !== updated.phone) changes.phone = updated.phone; + + // Vérifier les changements dans les configurations + const configsChanged = JSON.stringify(original.configs) !== JSON.stringify(updated.configs); + if (configsChanged) { + changes.configs = updated.configs; + } + + // Vérifier les changements dans les contacts + const contactsChanged = JSON.stringify(original.technicalContacts) !== JSON.stringify(updated.technicalContacts); + if (contactsChanged) { + changes.technicalContacts = updated.technicalContacts; + } + + return changes; + } +} \ No newline at end of file