From a4834002dfa082c5d73b8c5f378a5edc760eda6a Mon Sep 17 00:00:00 2001 From: diallolatoile Date: Thu, 20 Nov 2025 17:53:38 +0000 Subject: [PATCH] feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature --- src/app/app.config.ts | 1 - src/app/app.scss | 2 +- src/app/app.ts | 41 +- src/app/core/guards/auth.guard.ts | 36 +- src/app/core/guards/public.guard.ts | 28 +- src/app/core/models/merchant-config.model.ts | 127 ++- src/app/core/services/auth.service.ts | 53 +- .../merchant-config/custom-validators.ts | 52 + .../merchant-config-list.html | 315 ++++++ .../merchant-config-list.ts | 440 +++++++ .../merchant-config-view.html | 410 +++++++ .../merchant-config-view.ts | 637 +++++++++++ .../merchant-config/merchant-config.html | 1008 +++++++++++------ .../merchant-config.service.ts | 330 +++--- .../merchant-config/merchant-config.ts | 926 +++++++++++---- src/app/modules/modules.routes.ts | 4 +- 16 files changed, 3554 insertions(+), 856 deletions(-) create mode 100644 src/app/modules/merchant-config/custom-validators.ts create mode 100644 src/app/modules/merchant-config/merchant-config-list/merchant-config-list.html create mode 100644 src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts create mode 100644 src/app/modules/merchant-config/merchant-config-view/merchant-config-view.html create mode 100644 src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 1ddab69..8c0dfba 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -17,4 +17,3 @@ export const appConfig: ApplicationConfig = { ] }; - diff --git a/src/app/app.scss b/src/app/app.scss index d1fd304..e0c0ebd 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -645,4 +645,4 @@ .small { font-size: 0.8125rem; } -} \ No newline at end of file +} diff --git a/src/app/app.ts b/src/app/app.ts index c2d2b33..c8d8d0c 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -35,41 +35,36 @@ export class App implements OnInit { private async initializeAuth(): Promise { try { const isAuthenticated = await this.authService.initialize(); - - setTimeout(() => { - this.handleInitialNavigation(isAuthenticated); - }); - + + // Attendre la vraie route après bootstrap + this.router.events + .pipe(filter(event => event instanceof NavigationEnd)) + .subscribe(() => { + this.handleInitialNavigation(isAuthenticated); + }); + } catch (error) { console.error('Error during authentication initialization:', error); - setTimeout(() => { - this.router.navigate(['/auth/login']); - }); + this.router.navigate(['/auth/login']); } } + private handleInitialNavigation(isAuthenticated: boolean): void { const currentUrl = this.router.url; - - if (!isAuthenticated && this.shouldRedirectToLogin(currentUrl)) { - this.router.navigate(['/auth/login']); - } else if (isAuthenticated && this.shouldRedirectToDashboard(currentUrl)) { - this.router.navigate(['/dcb-dashboard']); + + // Non authentifié + if (!isAuthenticated) { + if (!this.isPublicRoute(currentUrl)) { + this.router.navigate(['/auth/login']); + } + return; } - - this.cdr.detectChanges(); - } - private shouldRedirectToLogin(url: string): boolean { - return url === '/' || !this.isPublicRoute(url); - } - - private shouldRedirectToDashboard(url: string): boolean { - return url === '/' || this.isPublicRoute(url); } private isPublicRoute(url: string): boolean { - const publicRoutes = ['/auth/login', '/auth/reset-password', '/auth/forgot-password']; + const publicRoutes = ['/auth/login']; return publicRoutes.some(route => url.startsWith(route)); } diff --git a/src/app/core/guards/auth.guard.ts b/src/app/core/guards/auth.guard.ts index b4464c1..a5b499f 100644 --- a/src/app/core/guards/auth.guard.ts +++ b/src/app/core/guards/auth.guard.ts @@ -1,52 +1,44 @@ -// src/app/core/guards/auth.guard.ts import { inject } from '@angular/core'; import { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { AuthService } from '../services/auth.service'; import { RoleService } from '../services/role.service'; -import { map, catchError, of, tap, switchMap } from 'rxjs'; +import { map, catchError, of, switchMap, tap, filter, take } from 'rxjs'; export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { const authService = inject(AuthService); const roleService = inject(RoleService); const router = inject(Router); - // Attendre que l'initialisation du service Auth soit terminée return authService.getInitializedState().pipe( - switchMap(initialized => { - if (!initialized) { - return of(false); - } - - // 🔒 Étape 1 : Vérifier si déjà authentifié + filter(init => init === true), + take(1), + switchMap(() => { + + // 1) Si déjà authentifié if (authService.isAuthenticated()) { return of(checkRoleAccess(route, roleService, router, state.url)); } - // 🔄 Étape 2 : Tenter un rafraîchissement du token s’il existe + // 2) Token expiré → essayer refresh const refreshToken = authService.getRefreshToken(); if (refreshToken) { return authService.refreshAccessToken().pipe( - tap(() => { - // Recharger les rôles après un refresh réussi - roleService.refreshRoles(); - }), + tap(() => roleService.refreshRoles()), map(() => checkRoleAccess(route, roleService, router, state.url)), catchError(() => { - // En cas d’échec de refresh → déconnexion + redirection login authService.logout().subscribe(); return of(redirectToLogin(router, state.url)); }) ); } - // 🚫 Étape 3 : Aucun token → redirection vers login + // 3) Pas de token du tout return of(redirectToLogin(router, state.url)); }), - catchError(() => { - return of(redirectToLogin(router, state.url)); - }) + catchError(() => of(redirectToLogin(router, state.url))) ); -}; + +} /** * Vérifie l'accès basé sur les rôles requis @@ -57,7 +49,7 @@ function checkRoleAccess( router: Router, currentUrl: string ): boolean { - const requiredRoles = route.data?.['roles'] as string[]; + const requiredRoles = route.data?.['requiredRoles'] as string[]; // Si aucun rôle requis → accès autorisé if (!requiredRoles || requiredRoles.length === 0) { @@ -67,12 +59,10 @@ function checkRoleAccess( const hasRequiredRole = roleService.hasAnyRole(requiredRoles); const currentUserRoles = roleService.getCurrentUserRoles(); - // ✅ L’utilisateur possède un des rôles requis if (hasRequiredRole) { return true; } - // ❌ Sinon → rediriger vers une page "non autorisée" router.navigate(['/unauthorized'], { queryParams: { requiredRoles: requiredRoles.join(','), diff --git a/src/app/core/guards/public.guard.ts b/src/app/core/guards/public.guard.ts index 0cb3749..0f7b468 100644 --- a/src/app/core/guards/public.guard.ts +++ b/src/app/core/guards/public.guard.ts @@ -1,35 +1,29 @@ -// src/app/core/guards/public.guard.ts import { inject } from '@angular/core'; -import { CanActivateFn, Router } from '@angular/router'; +import { CanActivateFn } from '@angular/router'; import { AuthService } from '../services/auth.service'; import { map, catchError, of } from 'rxjs'; -export const publicGuard: CanActivateFn = () => { +export const publicGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); - const router = inject(Router); - // 🔒 Si l'utilisateur est déjà authentifié → redirection vers le tableau de bord - if (authService.isAuthenticated()) { - router.navigate(['/dcb-dashboard'], { replaceUrl: true }); - return false; - } - - // 🔄 Vérifier si un refresh token est disponible + // Vérifier si un refresh token est disponible const refreshToken = authService.getRefreshToken(); if (refreshToken) { return authService.refreshAccessToken().pipe( map(() => { - // ✅ Rafraîchissement réussi → redirection vers le dashboard - router.navigate(['/dcb-dashboard'], { replaceUrl: true }); + // Rafraîchissement réussi → bloquer l'accès aux pages publiques return false; }), - catchError(() => { - // ❌ Rafraîchissement échoué → autoriser l’accès à la page publique + catchError((error) => { + // Rafraîchissement échoué → autoriser l'accès à la page publique return of(true); }) ); } - // 👤 Aucun token → accès autorisé à la page publique + if (authService.isAuthenticated()) { + return false; + } + return true; -}; +}; \ No newline at end of file diff --git a/src/app/core/models/merchant-config.model.ts b/src/app/core/models/merchant-config.model.ts index edcc3eb..0d6f2b6 100644 --- a/src/app/core/models/merchant-config.model.ts +++ b/src/app/core/models/merchant-config.model.ts @@ -16,13 +16,6 @@ export enum UserRole { DCB_PARTNER_SUPPORT = 'dcb-partner-support' } -export enum MerchantStatus { - ACTIVE = 'ACTIVE', - INACTIVE = 'INACTIVE', - PENDING = 'PENDING', - SUSPENDED = 'SUSPENDED' -} - export enum ConfigType { API_KEY = 'API_KEY', SECRET_KEY = 'SECRET_KEY', @@ -34,30 +27,27 @@ export enum ConfigType { } export enum Operator { - ORANGE_CI = 1, - MTN_CI = 2, - MOOV_CI = 3, - WAVE = 4 + ORANGE_OSN = 1 } // === MODÈLES PRINCIPAUX === export interface MerchantConfig { - id?: number; + id?: string; name: ConfigType | string; value: string; - operatorId: Operator; - merchantId?: number; + operatorId: Operator | null; + merchantPartnerId?: string; createdAt?: string; updatedAt?: string; } export interface TechnicalContact { - id?: number; + id?: string; firstName: string; lastName: string; phone: string; email: string; - merchantId?: number; + merchantPartnerId?: string; createdAt?: string; updatedAt?: string; } @@ -69,26 +59,70 @@ export interface MerchantUser { email?: string; firstName?: string; lastName?: string; - merchantPartnerId?: number; + merchantPartnerId?: string; } export interface Merchant { + id?: string; + name: string; + logo?: string; + description?: string; + adresse: string; + phone: string; + configs: MerchantConfig[]; + users: MerchantUser[]; + technicalContacts: TechnicalContact[]; + createdAt?: string; + updatedAt?: string; +} + +// Interfaces pour la réponse API (backend - types number) +export interface ApiMerchantConfig { + id?: number; + name: ConfigType | string; + value: string; + operatorId: Operator | null; + merchantPartnerId?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface ApiTechnicalContact { + id?: number; + firstName: string; + lastName: string; + phone: string; + email: string; + merchantPartnerId?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface ApiMerchantUser { + userId: string; + role: UserRole; + username?: string; + email?: string; + firstName?: string; + lastName?: string; + merchantPartnerId?: number; +} + +export interface ApiMerchant { id?: number; name: string; logo?: string; description?: string; adresse: string; phone: string; - status?: MerchantStatus; - configs: MerchantConfig[]; - users: MerchantUser[]; - technicalContacts: TechnicalContact[]; + configs: ApiMerchantConfig[]; + merchantUsers: ApiMerchantUser[]; + technicalContacts: ApiTechnicalContact[]; createdAt?: string; updatedAt?: string; - createdBy?: string; - createdByUsername?: string; } +// === DTOs CRUD === // === DTOs CRUD === export interface CreateMerchantDto { name: string; @@ -96,18 +130,27 @@ export interface CreateMerchantDto { description?: string; adresse: string; phone: string; - configs: Omit[]; - technicalContacts: Omit[]; + configs: Omit[]; + technicalContacts: Omit[]; } -export interface UpdateMerchantDto extends Partial { - status?: MerchantStatus; +export interface UpdateMerchantDto extends Partial {} + +// DTO mise à jour d'une configuration +export interface UpdateMerchantConfigDto { + name?: string; + value?: string; + operatorId?: Operator | null; } export interface AddUserToMerchantDto { userId: string; - role: UserRole; // Utilisation de vos rôles existants - merchantPartnerId: number; + role: UserRole; + merchantPartnerId: string; +} + +export interface UpdateUserRoleDto { + role: UserRole; } export interface UpdateUserRoleDto { @@ -142,39 +185,15 @@ export interface MerchantStatsResponse { // === SEARCH === export interface SearchMerchantsParams { query?: string; - status?: MerchantStatus; page?: number; limit?: number; } // === UTILITAIRES === export class MerchantUtils { - static getStatusDisplayName(status: MerchantStatus): string { - const statusNames = { - [MerchantStatus.ACTIVE]: 'Actif', - [MerchantStatus.INACTIVE]: 'Inactif', - [MerchantStatus.PENDING]: 'En attente', - [MerchantStatus.SUSPENDED]: 'Suspendu' - }; - return statusNames[status] || status; - } - - static getStatusBadgeClass(status: MerchantStatus): string { - const statusClasses = { - [MerchantStatus.ACTIVE]: 'badge bg-success', - [MerchantStatus.INACTIVE]: 'badge bg-secondary', - [MerchantStatus.PENDING]: 'badge bg-warning', - [MerchantStatus.SUSPENDED]: 'badge bg-danger' - }; - return statusClasses[status] || 'badge bg-secondary'; - } - static getOperatorName(operatorId: Operator): string { const operatorNames = { - [Operator.ORANGE_CI]: 'Orange CI', - [Operator.MTN_CI]: 'MTN CI', - [Operator.MOOV_CI]: 'Moov CI', - [Operator.WAVE]: 'Wave' + [Operator.ORANGE_OSN]: 'Orange OSN' }; return operatorNames[operatorId] || 'Inconnu'; } diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts index 62da742..df2924a 100644 --- a/src/app/core/services/auth.service.ts +++ b/src/app/core/services/auth.service.ts @@ -69,42 +69,30 @@ export class AuthService { * Initialise l'authentification au démarrage de l'application */ async initialize(): Promise { - - await new Promise(resolve => setTimeout(resolve, 0)); - + const token = this.getAccessToken(); + + // Pas de token → pas authentifié + if (!token) { + this.initialized$.next(true); + return false; + } + + // Token expiré → tenter refresh + if (this.isTokenExpired(token)) { + const ok = await this.tryRefreshToken(); + this.initialized$.next(true); + return ok; + } + + // Token valide → charger profil try { - const token = this.getAccessToken(); - - if (!token) { - setTimeout(() => { - this.initialized$.next(true); - }); - return false; - } - - if (this.isTokenExpired(token)) { - const refreshSuccess = await this.tryRefreshToken(); - setTimeout(() => { - this.initialized$.next(true); - }); - return refreshSuccess; - } - - // Token valide : charger le profil utilisateur await firstValueFrom(this.loadUserProfile()); - - setTimeout(() => { - this.authState$.next(true); - this.initialized$.next(true); - }); - + this.authState$.next(true); + this.initialized$.next(true); return true; - - } catch (error) { + } catch { this.clearAuthData(); - setTimeout(() => { - this.initialized$.next(true); - }); + this.initialized$.next(true); return false; } } @@ -252,7 +240,6 @@ export class AuthService { lastLogin: apiUser.lastLogin || apiUser.lastLoginAt || apiUser.lastConnection || null }; - console.log('✅ Utilisateur mappé:', mappedUser); return mappedUser; } diff --git a/src/app/modules/merchant-config/custom-validators.ts b/src/app/modules/merchant-config/custom-validators.ts new file mode 100644 index 0000000..63a66a8 --- /dev/null +++ b/src/app/modules/merchant-config/custom-validators.ts @@ -0,0 +1,52 @@ +import { AbstractControl, ValidatorFn, ValidationErrors } from '@angular/forms'; + +export class CustomValidators { + static phoneNumber(control: AbstractControl): ValidationErrors | null { + if (!control.value) return null; + + // Format international E.164 + const phoneRegex = /^\+?[1-9]\d{1,14}$/; + const cleaned = control.value.replace(/[\s\-\(\)]/g, ''); + + return phoneRegex.test(cleaned) ? null : { invalidPhone: true }; + } + + static url(control: AbstractControl): ValidationErrors | null { + if (!control.value) return null; + + try { + new URL(control.value); + return null; + } catch { + return { invalidUrl: true }; + } + } + + static noSpecialCharacters(control: AbstractControl): ValidationErrors | null { + if (!control.value) return null; + + const regex = /^[a-zA-Z0-9À-ÿ\s\-_\.]*$/; + return regex.test(control.value) ? null : { specialCharacters: true }; + } + + static secureUrl(control: AbstractControl): ValidationErrors | null { + if (!control.value) return null; + + try { + const url = new URL(control.value); + return url.protocol === 'https:' ? null : { notSecureUrl: true }; + } catch { + return { invalidUrl: true }; + } + } + + static uniqueEmails(emails: string[]): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const email = control.value; + if (!email) return null; + + const duplicateCount = emails.filter(e => e === email).length; + return duplicateCount > 1 ? { duplicateEmail: true } : null; + }; + } +} \ No newline at end of file diff --git a/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.html b/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.html new file mode 100644 index 0000000..b237a40 --- /dev/null +++ b/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.html @@ -0,0 +1,315 @@ + + + + {{ getHelperText() }} + + +
+ + +
+
+
+ +
+ +
+
+
+ +
+
+ @if (showCreateButton && canCreateMerchants) { + + } + +
+
+
+ + +
+
+
+ + + + +
+
+ +
+ +
+ +
+ +
+
+ + + @if (loading) { +
+
+ Chargement... +
+

{{ getLoadingText() }}

+
+ } + + + @if (error && !loading) { + + } + + + @if (!loading && !error) { +
+ + + + + + + + + + + + + + @for (merchant of displayedMerchants; track merchant.id) { + + + + + + + + + + } + @empty { + + + + } + +
+
+ Marchand + +
+
DescriptionContactConfigurationsContacts Tech +
+ Créé le + +
+
Actions
+
+ @if (merchant.logo) { + Logo {{ merchant.name }} + } +
+ +
+
+ {{ merchant.name }} + {{ merchant.adresse }} +
+
+
+ @if (merchant.description) { + {{ merchant.description }} + } @else { + Aucune description + } + +
+ + + {{ merchant.phone }} + + @if (merchant.technicalContacts && merchant.technicalContacts.length > 0) { + + + {{ merchant.technicalContacts[0].firstName }} {{ merchant.technicalContacts[0].lastName }} + + } +
+
+
+ @if (merchant.configs && merchant.configs.length > 0) { +
+ @for (config of merchant.configs.slice(0, 2); track config.id) { + + {{ getConfigTypeLabel(config.name) }} + + } + @if (merchant.configs.length > 2) { + + +{{ merchant.configs.length - 2 }} + + } +
+ + {{ merchant.configs.length }} configuration(s) + + } @else { + + Aucune config + + } +
+
+ @if (merchant.technicalContacts && merchant.technicalContacts.length > 0) { +
+ + {{ merchant.technicalContacts.length }} contact(s) + + + {{ merchant.technicalContacts[0].email }} + +
+ } @else { + + Aucun contact + + } +
+ + {{ formatTimestamp(merchant.createdAt!) }} + + +
+ + + @if (showDeleteButton) { + + } +
+
+
+ +
{{ getEmptyStateTitle() }}
+

{{ getEmptyStateDescription() }}

+ @if (showCreateButton && canCreateMerchants) { + + } +
+
+
+ + + @if (totalPages > 1) { +
+
+ Affichage de {{ getStartIndex() }} à {{ getEndIndex() }} sur {{ totalItems }} marchands +
+ +
+ } + + + @if (displayedMerchants.length > 0) { +
+
+
+ + Total : {{ allMerchants.length }} marchands + +
+
+ + Configurations : {{ getTotalConfigsCount() }} + +
+
+ + Contacts : {{ getTotalContactsCount() }} + +
+
+
+ } + } +
+
\ No newline at end of file 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 new file mode 100644 index 0000000..72c294c --- /dev/null +++ b/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts @@ -0,0 +1,440 @@ +import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, Input, OnDestroy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgIcon } from '@ng-icons/core'; +import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; +import { Observable, Subject, map, of } from 'rxjs'; +import { catchError, takeUntil } from 'rxjs/operators'; + +import { + Merchant, + ConfigType, + Operator, + MerchantUtils, +} from '@core/models/merchant-config.model'; + +import { MerchantConfigService } from '../merchant-config.service'; +import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { AuthService } from '@core/services/auth.service'; +import { UiCard } from '@app/components/ui-card'; + +@Component({ + selector: 'app-merchant-config-list', + standalone: true, + imports: [ + CommonModule, + FormsModule, + NgIcon, + UiCard, + NgbPaginationModule + ], + templateUrl: './merchant-config-list.html', +}) +export class MerchantConfigsList implements OnInit, OnDestroy { + private authService = inject(AuthService); + private merchantConfigService = inject(MerchantConfigService); + protected roleService = inject(RoleManagementService); + private cdRef = inject(ChangeDetectorRef); + private destroy$ = new Subject(); + + // Configuration + readonly ConfigType = ConfigType; + readonly Operator = Operator; + readonly MerchantUtils = MerchantUtils; + + // Inputs + @Input() canCreateMerchants: boolean = false; + @Input() canDeleteMerchants: boolean = false; + + // Outputs + @Output() merchantSelected = new EventEmitter(); + @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[] = []; + displayedMerchants: Merchant[] = []; + + // États + loading = false; + error = ''; + + // Recherche et filtres + searchTerm = ''; + operatorFilter: Operator | 'all' = 'all'; + + // Pagination + currentPage = 1; + itemsPerPage = 10; + totalItems = 0; + totalPages = 0; + + // Tri + sortField: keyof Merchant = 'name'; + sortDirection: 'asc' | 'desc' = 'asc'; + + // Filtres disponibles + availableOperators: { value: Operator | 'all'; label: string }[] = []; + + // Permissions + currentUserRole: any = null; + canViewAllMerchants = false; + + // ==================== CONVERSION IDS ==================== + + private convertIdToNumber(id: string): number { + const numId = Number(id); + if (isNaN(numId)) { + throw new Error(`ID invalide pour la conversion en number: ${id}`); + } + return numId; + } + + private convertIdToString(id: number): string { + return id.toString(); + } + + private convertMerchantToFrontend(merchant: any): Merchant { + return { + ...merchant, + id: merchant.id ? this.convertIdToString(merchant.id) : undefined, + configs: merchant.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 ? 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 ? merchant.users.map((user: any) => ({ + ...user, + merchantPartnerId: user.merchantPartnerId ? this.convertIdToString(user.merchantPartnerId) : undefined + })) : [] + }; + } + + private convertMerchantsToFrontend(merchants: any[]): Merchant[] { + return merchants.map(merchant => this.convertMerchantToFrontend(merchant)); + } + + // Getters pour la logique conditionnelle + get showCreateButton(): boolean { + return this.canCreateMerchants; + } + + get showDeleteButton(): boolean { + return this.canDeleteMerchants; + } + + getColumnCount(): number { + return 8; // Nombre de colonnes dans le tableau + } + + ngOnInit() { + this.initializeAvailableFilters(); + } + + ngAfterViewInit() { + this.loadCurrentUserPermissions(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + private loadCurrentUserPermissions() { + this.authService.getUserProfile() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (user) => { + this.currentUserRole = this.extractUserRole(user); + this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole); + this.loadMerchants(); + }, + error: (error) => { + console.error('Error loading current user permissions:', error); + this.loadMerchants(); + } + }); + } + + private extractUserRole(user: any): any { + const userRoles = this.authService.getCurrentUserRoles(); + return userRoles && userRoles.length > 0 ? userRoles[0] : null; + } + + private canViewAllMerchantsCheck(role: any): boolean { + if (!role) return false; + + const canViewAllRoles = [ + 'DCB_ADMIN', + 'DCB_SUPPORT', + 'DCB_PARTNER_ADMIN' + ]; + + return canViewAllRoles.includes(role); + } + + private initializeAvailableFilters() { + this.availableOperators = [ + { value: 'all', label: 'Tous les opérateurs' }, + { value: Operator.ORANGE_OSN, label: 'Orange' } + ]; + } + + loadMerchants() { + this.loading = true; + this.error = ''; + + let merchantsObservable: Observable; + + if (this.canViewAllMerchants) { + merchantsObservable = this.getAllMerchants(); + } else { + merchantsObservable = this.getMyMerchants(); + } + + merchantsObservable + .pipe( + takeUntil(this.destroy$), + catchError(error => { + console.error('Error loading merchants:', error); + this.error = 'Erreur lors du chargement des marchands'; + return of([] as Merchant[]); + }) + ) + .subscribe({ + next: (merchants) => { + this.allMerchants = merchants || []; + this.applyFiltersAndPagination(); + this.loading = false; + this.cdRef.detectChanges(); + }, + error: () => { + this.error = 'Erreur lors du chargement des marchands'; + this.loading = false; + this.allMerchants = []; + this.filteredMerchants = []; + this.displayedMerchants = []; + this.cdRef.detectChanges(); + } + }); + } + + private getAllMerchants(): Observable { + return this.merchantConfigService.getMerchants(1, 1000).pipe( + map(response => { + return this.convertMerchantsToFrontend(response.items); + }), + catchError(error => { + console.error('Error getting all merchants:', error); + return of([]); + }) + ); + } + + private getMyMerchants(): Observable { + return this.getAllMerchants(); + } + + + // ==================== ACTIONS ==================== + + viewMerchantProfile(merchant: Merchant) { + this.merchantSelected.emit(merchant.id!); + } + + editMerchant(merchant: Merchant) { + this.editMerchantRequested.emit(merchant); + } + + deleteMerchant(merchant: Merchant) { + this.deleteMerchantRequested.emit(merchant); + } + + activateMerchant(merchant: Merchant) { + this.activateMerchantRequested.emit(merchant); + } + + deactivateMerchant(merchant: Merchant) { + this.deactivateMerchantRequested.emit(merchant); + } + + // ==================== FILTRES ET RECHERCHE ==================== + + onSearch() { + this.currentPage = 1; + this.applyFiltersAndPagination(); + } + + onClearFilters() { + this.searchTerm = ''; + this.operatorFilter = 'all'; + this.currentPage = 1; + this.applyFiltersAndPagination(); + } + + filterByOperator(operator: Operator | 'all') { + this.operatorFilter = operator; + this.currentPage = 1; + this.applyFiltersAndPagination(); + } + + applyFiltersAndPagination() { + if (!this.allMerchants) { + this.allMerchants = []; + } + + // Appliquer les filtres + this.filteredMerchants = this.allMerchants.filter(merchant => { + const matchesSearch = !this.searchTerm || + merchant.name.toLowerCase().includes(this.searchTerm.toLowerCase()) || + merchant.adresse.toLowerCase().includes(this.searchTerm.toLowerCase()) || + merchant.phone.toLowerCase().includes(this.searchTerm.toLowerCase()) || + (merchant.description && merchant.description.toLowerCase().includes(this.searchTerm.toLowerCase())); + // Filtrer par opérateur basé sur les configurations + const matchesOperator = this.operatorFilter === 'all' || + (merchant.configs && merchant.configs.some(config => config.operatorId === this.operatorFilter)); + + return matchesSearch && matchesOperator; + }); + + // Appliquer le tri + this.filteredMerchants.sort((a, b) => { + const aValue = a[this.sortField]; + const bValue = b[this.sortField]; + + if (aValue === bValue) return 0; + + let comparison = 0; + if (typeof aValue === 'string' && typeof bValue === 'string') { + comparison = aValue.localeCompare(bValue); + } else if (aValue instanceof Date && bValue instanceof Date) { + comparison = aValue.getTime() - bValue.getTime(); + } else if (typeof aValue === 'number' && typeof bValue === 'number') { + comparison = aValue - bValue; + } + + return this.sortDirection === 'asc' ? comparison : -comparison; + }); + + // Calculer la pagination + this.totalItems = this.filteredMerchants.length; + this.totalPages = Math.ceil(this.totalItems / this.itemsPerPage); + + // Appliquer la pagination + const startIndex = (this.currentPage - 1) * this.itemsPerPage; + const endIndex = startIndex + this.itemsPerPage; + this.displayedMerchants = this.filteredMerchants.slice(startIndex, endIndex); + } + + // ==================== TRI ==================== + + sort(field: keyof Merchant) { + if (this.sortField === field) { + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + this.sortField = field; + this.sortDirection = 'asc'; + } + this.applyFiltersAndPagination(); + } + + getSortIcon(field: string): string { + if (this.sortField !== field) return 'lucideArrowUpDown'; + return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown'; + } + + // ==================== PAGINATION ==================== + + onPageChange(page: number) { + this.currentPage = page; + this.applyFiltersAndPagination(); + } + + getStartIndex(): number { + return (this.currentPage - 1) * this.itemsPerPage + 1; + } + + getEndIndex(): number { + return Math.min(this.currentPage * this.itemsPerPage, this.totalItems); + } + + // ==================== MÉTHODES STATISTIQUES ==================== + + getTotalMerchantsCount(): number { + return this.allMerchants.length; + } + + getTotalConfigsCount(): number { + return this.allMerchants.reduce((total, merchant) => + total + (merchant.configs ? merchant.configs.length : 0), 0); + } + + getTotalContactsCount(): number { + return this.allMerchants.reduce((total, merchant) => + total + (merchant.technicalContacts ? merchant.technicalContacts.length : 0), 0); + } + + // ==================== MÉTHODES D'AFFICHAGE ==================== + + getConfigTypeLabel(configName: ConfigType | string): string { + return MerchantUtils.getConfigTypeName(configName); + } + + formatTimestamp(timestamp: string): string { + if (!timestamp) return 'Non disponible'; + return new Date(timestamp).toLocaleDateString('fr-FR', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } + + // ==================== MÉTHODES POUR LE TEMPLATE ==================== + + refreshData() { + this.loadMerchants(); + } + + getCardTitle(): string { + return this.canViewAllMerchants + ? 'Tous les Marchands' + : 'Mes Marchands'; + } + + getHelperText(): string { + return this.canViewAllMerchants + ? 'Vue administrative - Gestion de tous les marchands' + : 'Vos marchands partenaires'; + } + + getHelperIcon(): string { + return this.canViewAllMerchants ? 'lucideShield' : 'lucideStore'; + } + + getLoadingText(): string { + return 'Chargement des marchands...'; + } + + getEmptyStateTitle(): string { + return 'Aucun marchand trouvé'; + } + + getEmptyStateDescription(): string { + return 'Aucun marchand ne correspond à vos critères de recherche.'; + } + + getEmptyStateButtonText(): string { + return 'Créer le premier marchand'; + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..7dc22ca --- /dev/null +++ b/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.html @@ -0,0 +1,410 @@ +
+ +
+
+
+
+

{{ getProfileTitle() }}

+ +
+ +
+ + + + + +
+
+
+
+ + + @if (currentUserRole && !canManageAllConfigs()) { +
+
+
+
+ +
+ Permissions limitées : Vous ne pouvez que consulter vos configurations +
+
+
+
+
+ } + + + @if (error) { +
+
+ +
{{ error }}
+ +
+
+ } + + @if (success) { +
+
+ +
{{ success }}
+ +
+
+ } + +
+ + @if (loading) { +
+
+ Chargement... +
+

Chargement des configurations...

+
+ } + + + @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 (isSensitiveConfig(config)) { +
+ + + Cette valeur contient des informations sensibles. Soyez prudent lors de la modification. + +
+ } +
+
+
+
+ } +
+
+ } +
+ } + + + @if (totalPages > 1) { +
+ + +
+ } +
+ } + + + @if (configs.length === 0 && !loading) { +
+
+ +
Aucune configuration trouvée
+

Ce marchand ne possède aucune configuration pour le moment.

+ +
+
+ } +
+
\ 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 new file mode 100644 index 0000000..0138460 --- /dev/null +++ b/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts @@ -0,0 +1,637 @@ +import { Component, inject, OnInit, Input, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core'; +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 { Subject, takeUntil } from 'rxjs'; + +import { + MerchantConfig, + UpdateMerchantConfigDto, + ConfigType, + Operator, + MerchantUtils +} from '@core/models/merchant-config.model'; + +import { MerchantConfigService } from '../merchant-config.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'; + +@Component({ + selector: 'app-merchant-config-view', + standalone: true, + imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule, NgbPaginationModule], + templateUrl: './merchant-config-view.html', + styles: [` + .config-card { + border: 1px solid #e0e0e0; + 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); + } + .config-header { + background-color: #f8f9fa; + padding: 1rem; + border-bottom: 1px solid #e0e0e0; + cursor: pointer; + } + .config-content { + padding: 1rem; + } + .config-value { + font-family: 'Courier New', monospace; + background-color: #f8f9fa; + padding: 0.5rem; + 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; + } + .expanded .config-header { + background-color: #e9ecef; + } + `] +}) +export class MerchantConfigView implements OnInit, OnDestroy { + private merchantConfigService = inject(MerchantConfigService); + private roleService = inject(RoleManagementService); + private authService = inject(AuthService); + private cdRef = inject(ChangeDetectorRef); + private destroy$ = new Subject(); + + readonly ConfigType = ConfigType; + readonly Operator = Operator; + readonly MerchantUtils = MerchantUtils; + + @Input() merchantId!: string; + @Output() back = new EventEmitter(); + @Output() editConfigRequested = new EventEmitter(); + + // Liste de toutes les configurations + configs: MerchantConfig[] = []; + loading = false; + saving = false; + error = ''; + success = ''; + + // Gestion des permissions + currentUserRole: any = null; + + // Édition + editingConfigId: string | null = null; + editedConfig: UpdateMerchantConfigDto = {}; + + // Affichage des valeurs sensibles + showSensitiveValues: { [configId: string]: boolean } = {}; + + // Pagination et expansion + expandedConfigs: { [configId: string]: boolean } = {}; + page = 1; + pageSize = 10; + + // ==================== CONVERSION IDS ==================== + + /** + * 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}`); + } + return numId; + } + + /** + * Convertit un ID number en string pour Angular + */ + private convertIdToString(id: number): string { + return id.toString(); + } + + /** + * 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 + }; + } + + // Getters pour la logique conditionnelle + 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 '••••••••••••••••'; + } + + if (this.shouldTruncateValue(config) && !this.showSensitiveValues[config.id!]) { + return config.value.length > 50 + ? config.value.substring(0, 50) + '...' + : config.value; + } + + 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' + }); + } + + toggleSensitiveValue(configId: string) { + this.showSensitiveValues[configId] = !this.showSensitiveValues[configId]; + this.cdRef.detectChanges(); + } + + getValueIcon(config: MerchantConfig): string { + if (!this.isSensitiveConfig(config)) return 'lucideFileText'; + return this.showSensitiveValues[config.id!] ? 'lucideEyeOff' : 'lucideEye'; + } + + getValueTooltip(config: MerchantConfig): string { + if (!this.isSensitiveConfig(config)) return 'Valeur de configuration'; + 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.'; + } + + // ==================== MÉTHODES DE NAVIGATION ==================== + + goBack() { + this.back.emit(); + } + + requestEdit(configId: string) { + this.editConfigRequested.emit(configId); + } + + // ==================== MÉTHODES DE VALIDATION ==================== + + private validateConfigForm(): { isValid: boolean; error?: string } { + if (!this.editedConfig.name?.trim()) { + return { isValid: false, error: 'Le nom de la configuration est requis' }; + } + + if (!this.editedConfig.value?.trim()) { + return { isValid: false, error: 'La valeur de la configuration est requise' }; + } + + if (!this.editedConfig.operatorId) { + return { isValid: false, error: 'L\'opérateur est requis' }; + } + + // Validation spécifique selon le type + if (this.editedConfig.name === ConfigType.WEBHOOK_URL || + this.editedConfig.name === ConfigType.CALLBACK_URL) { + try { + new URL(this.editedConfig.value); + } catch { + return { isValid: false, error: 'URL invalide' }; + } + } + + 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 }; + } + + isFormValid(): boolean { + return this.validateConfigForm().isValid; + } + + // ==================== MÉTHODES UTILITAIRES ==================== + + 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'; + } + return ''; + } + + getConfigTypeIconSafe(configName: string): string { + const validConfigTypes = Object.values(ConfigType); + if (validConfigTypes.includes(configName as ConfigType)) { + return this.getConfigTypeIcon(configName as ConfigType); + } + return 'lucideSettings'; + } + + 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'; + } + + getOperatorIcon(operatorId: Operator): string { + const icons = { + [Operator.ORANGE_OSN]: 'lucideSignal' + }; + return icons[operatorId] || 'lucideSmartphone'; + } + + // 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); + } +} \ 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 e8896d6..adede0f 100644 --- a/src/app/modules/merchant-config/merchant-config.html +++ b/src/app/modules/merchant-config/merchant-config.html @@ -1,335 +1,693 @@ - - - Gestion des Merchants - - -
- - +
+ - - - - - @if (configError) { -
- - {{ configError }} -
- } - - @if (configSuccess) { -
- - {{ configSuccess }} -
- } - - -
- @for (step of wizardSteps; track $index; let i = $index) { -
-
- - @if (i === 0) { -
-
- -
- - @if (basicInfo.get('name')?.invalid && basicInfo.get('name')?.touched) { -
Le nom du merchant est requis
- } -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- - @if (basicInfo.get('adresse')?.invalid && basicInfo.get('adresse')?.touched) { -
L'adresse est requise
- } -
-
-
- -
- - @if (basicInfo.get('phone')?.invalid && basicInfo.get('phone')?.touched) { -
Le téléphone est requis et doit être valide
- } -
-
-
- } - - - @if (i === 1) { -
-
-
-
Contacts Techniques
- -
-
- - @for (contact of technicalContacts.controls; track $index; let idx = $index) { -
-
-
Contact #{{ idx + 1 }}
- @if (technicalContacts.length > 1) { - - } -
- -
-
- - - @if (getContactControl(contact, 'firstName').invalid && getContactControl(contact, 'firstName').touched) { -
Le prénom est requis
- } -
-
- - - @if (getContactControl(contact, 'lastName').invalid && getContactControl(contact, 'lastName').touched) { -
Le nom est requis
- } -
-
- - - @if (getContactControl(contact, 'phone').invalid && getContactControl(contact, 'phone').touched) { -
Le téléphone est requis
- } -
-
- - - @if (getContactControl(contact, 'email').invalid && getContactControl(contact, 'email').touched) { -
L'email est requis et doit être valide
- } -
-
-
+ @if (!canCreateMerchants) { + + + Permissions limitées + } -
- } - - - @if (i === 2) { -
-
-
-
Configurations Techniques
- -
-
- - @for (config of configs.controls; track $index; let idx = $index) { -
-
-
Configuration #{{ idx + 1 }}
- @if (configs.length > 1) { - - } -
- -
-
- - - @if (getConfigControl(config, 'name').invalid && getConfigControl(config, 'name').touched) { -
Le nom est requis
- } -
-
- - - @if (getConfigControl(config, 'value').invalid && getConfigControl(config, 'value').touched) { -
La valeur est requise
- } -
-
- - - @if (getConfigControl(config, 'operatorId').invalid && getConfigControl(config, 'operatorId').touched) { -
L'opérateur est requis
- } -
-
-
+ @if (selectedMerchantPartnerId) { + + Marchand : {{ selectedMerchantPartnerId }} + } -
- } - - - @if (i === 4) { -
-
-
- - Vérifiez les informations avant de créer le merchant -
- -
-
-
Récapitulatif
- -
-
- Informations de Base:
- Nom: {{ basicInfo.value.name || 'Non renseigné' }}
- Description: {{ basicInfo.value.description || 'Non renseigné' }}
- Adresse: {{ basicInfo.value.adresse || 'Non renseigné' }}
- Téléphone: {{ basicInfo.value.phone || 'Non renseigné' }} -
-
- -
-
- Contacts Techniques: - @for (contact of technicalContacts.value; track $index; let idx = $index) { -
- Contact #{{ idx + 1 }}: - {{ contact.firstName }} {{ contact.lastName }} - - {{ contact.phone }} - {{ contact.email }} -
- } -
-
- -
-
- Configurations: - @for (config of configs.value; track $index; let idx = $index) { -
- Configuration #{{ idx + 1 }}: - {{ getConfigTypeName(config.name) }} = {{ config.value }} - ({{ getOperatorName(config.operatorId) }}) -
- } -
-
-
-
-
-
- } -
- - -
- @if (i > 0) { - - } @else { -
- } - - @if (i < wizardSteps.length - 1) { - - } @else { -
+ @if (canCreateMerchants) { + }
- } +
+
+ } + + + @if (showMerchantPartnerField) { +
+
+
+
+
+
+ +
+
+ +
+
+ @if (loadingMerchantPartners) { +
+ Chargement... +
+ } +
+
+
+
+
+
+ } + + +
+
+ + +
-
\ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + \ 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 f7e0e79..c4ff298 100644 --- a/src/app/modules/merchant-config/merchant-config.service.ts +++ b/src/app/modules/merchant-config/merchant-config.service.ts @@ -19,7 +19,10 @@ import { PaginatedResponse, MerchantStatsResponse, SearchMerchantsParams, - MerchantStatus + ApiMerchant, + ApiMerchantConfig, + ApiTechnicalContact, + ApiMerchantUser } from '@core/models/merchant-config.model'; @Injectable({ providedIn: 'root' }) @@ -29,16 +32,16 @@ export class MerchantConfigService { // Merchant CRUD Operations createMerchant(createMerchantDto: CreateMerchantDto): Observable { - return this.http.post>(this.baseApiUrl, createMerchantDto).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to create merchant'); - } - return response.data!; + return this.http.post(this.baseApiUrl, createMerchantDto).pipe( + map(apiMerchant => { + console.log('Merchant created successfully:', apiMerchant); + return this.mapApiMerchantToMerchant(apiMerchant); }), catchError(error => { console.error('Error creating merchant:', error); - return throwError(() => error); + return throwError(() => new Error( + error.error?.message || error.message || 'An unexpected error occurred while creating merchant' + )); }) ); } @@ -51,16 +54,24 @@ export class MerchantConfigService { if (params?.query) { httpParams = httpParams.set('query', params.query); } - if (params?.status) { - httpParams = httpParams.set('status', params.status); - } - - return this.http.get>>(this.baseApiUrl, { params: httpParams }).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to load merchants'); - } - return response.data!; + // L'API retourne directement un tableau de merchants + return this.http.get(this.baseApiUrl, { params: httpParams }).pipe( + map(apiMerchants => { + 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)), + total: total, + page: page, + limit: limit, + totalPages: totalPages + }; }), catchError(error => { console.error('Error loading merchants:', error); @@ -69,13 +80,12 @@ export class MerchantConfigService { ); } - getMerchantById(id: number): Observable { - return this.http.get>(`${this.baseApiUrl}/${id}`).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to load merchant'); - } - return response.data!; + getMerchantById(id: string): Observable { + const numericId = parseInt(id); + // L'API retourne directement l'objet merchant + return this.http.get(`${this.baseApiUrl}/${numericId}`).pipe( + map(apiMerchant => { + return this.mapApiMerchantToMerchant(apiMerchant); }), catchError(error => { console.error(`Error loading merchant ${id}:`, error); @@ -84,13 +94,12 @@ export class MerchantConfigService { ); } - updateMerchant(id: number, updateMerchantDto: UpdateMerchantDto): Observable { - return this.http.patch>(`${this.baseApiUrl}/${id}`, updateMerchantDto).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to update merchant'); - } - return response.data!; + 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( + map(apiMerchant => { + return this.mapApiMerchantToMerchant(apiMerchant); }), catchError(error => { console.error(`Error updating merchant ${id}:`, error); @@ -99,12 +108,11 @@ export class MerchantConfigService { ); } - deleteMerchant(id: number): Observable { - return this.http.delete>(`${this.baseApiUrl}/${id}`).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to delete merchant'); - } + deleteMerchant(id: string): Observable { + const numericId = parseInt(id); + // L'API ne retourne probablement rien ou un simple message + return this.http.delete(`${this.baseApiUrl}/${numericId}`).pipe( + map(() => { }), catchError(error => { console.error(`Error deleting merchant ${id}:`, error); @@ -115,12 +123,16 @@ export class MerchantConfigService { // User Management addUserToMerchant(addUserDto: AddUserToMerchantDto): Observable { - return this.http.post>(`${this.baseApiUrl}/users`, addUserDto).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to add user to merchant'); - } - return response.data!; + // Convertir merchantPartnerId en number pour l'API + const apiDto = { + ...addUserDto, + merchantPartnerId: addUserDto.merchantPartnerId ? parseInt(addUserDto.merchantPartnerId) : undefined + }; + + // L'API retourne directement l'utilisateur ajouté + return this.http.post(`${this.baseApiUrl}/users`, apiDto).pipe( + map(apiUser => { + return this.mapApiUserToUser(apiUser); }), catchError(error => { console.error('Error adding user to merchant:', error); @@ -129,13 +141,16 @@ export class MerchantConfigService { ); } - getMerchantUsers(merchantId: number): Observable { - return this.http.get>(`${this.baseApiUrl}/${merchantId}/users`).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to load merchant users'); - } - return response.data!; + 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 + return this.http.get(`${this.baseApiUrl}/${numericMerchantId}`).pipe( + map(apiMerchant => { + // Retourner les users mappés depuis merchantUsers + return (apiMerchant.merchantUsers || []).map(user => this.mapApiUserToUser(user)); }), catchError(error => { console.error(`Error loading users for merchant ${merchantId}:`, error); @@ -144,16 +159,15 @@ export class MerchantConfigService { ); } - updateUserRole(merchantId: number, userId: string, updateRoleDto: UpdateUserRoleDto): Observable { - return this.http.patch>( - `${this.baseApiUrl}/${merchantId}/users/${userId}/role`, + updateUserRole(merchantId: string, userId: string, updateRoleDto: UpdateUserRoleDto): Observable { + const numericMerchantId = parseInt(merchantId); + // L'API retourne directement l'utilisateur mis à jour + return this.http.patch( + `${this.baseApiUrl}/${numericMerchantId}/users/${userId}/role`, updateRoleDto ).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to update user role'); - } - return response.data!; + map(apiUser => { + return this.mapApiUserToUser(apiUser); }), catchError(error => { console.error(`Error updating user role for merchant ${merchantId}, user ${userId}:`, error); @@ -162,12 +176,11 @@ export class MerchantConfigService { ); } - removeUserFromMerchant(merchantId: number, userId: string): Observable { - return this.http.delete>(`${this.baseApiUrl}/${merchantId}/users/${userId}`).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to remove user from merchant'); - } + removeUserFromMerchant(merchantId: string, userId: string): Observable { + const numericMerchantId = parseInt(merchantId); + // L'API ne retourne probablement rien + return this.http.delete(`${this.baseApiUrl}/${numericMerchantId}/users/${userId}`).pipe( + map(() => { }), catchError(error => { console.error(`Error removing user ${userId} from merchant ${merchantId}:`, error); @@ -177,12 +190,10 @@ export class MerchantConfigService { } getUserMerchants(userId: string): Observable { - return this.http.get>(`${this.baseApiUrl}/user/${userId}`).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to load user merchants'); - } - return response.data!; + // L'API retourne directement un tableau de merchants + return this.http.get(`${this.baseApiUrl}/user/${userId}`).pipe( + map(apiMerchants => { + return apiMerchants.map(merchant => this.mapApiMerchantToMerchant(merchant)); }), catchError(error => { console.error(`Error loading merchants for user ${userId}:`, error); @@ -192,13 +203,18 @@ export class MerchantConfigService { } // Config Management - addConfigToMerchant(merchantId: number, config: Omit): Observable { - return this.http.post>(`${this.baseApiUrl}/${merchantId}/configs`, config).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to add config to merchant'); - } - return response.data!; + addConfigToMerchant(merchantId: string, config: Omit): Observable { + const numericMerchantId = parseInt(merchantId); + const apiConfig: Omit = { + ...config, + operatorId: config.operatorId, + merchantPartnerId: numericMerchantId + }; + + // L'API retourne directement la configuration créée + return this.http.post(`${this.baseApiUrl}/${numericMerchantId}/configs`, apiConfig).pipe( + map(apiConfig => { + return this.mapApiConfigToConfig(apiConfig); }), catchError(error => { console.error(`Error adding config to merchant ${merchantId}:`, error); @@ -207,13 +223,24 @@ export class MerchantConfigService { ); } - updateConfig(configId: number, config: Partial): Observable { - return this.http.patch>(`${this.baseApiUrl}/configs/${configId}`, config).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to update config'); - } - return response.data!; + updateConfig(configId: string, config: Partial): Observable { + const numericConfigId = parseInt(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) + }) + }; + + // L'API retourne directement la configuration mise à jour + return this.http.patch(`${this.baseApiUrl}/configs/${numericConfigId}`, apiConfig).pipe( + map(apiConfig => { + return this.mapApiConfigToConfig(apiConfig); }), catchError(error => { console.error(`Error updating config ${configId}:`, error); @@ -222,12 +249,12 @@ export class MerchantConfigService { ); } - deleteConfig(configId: number): Observable { - return this.http.delete>(`${this.baseApiUrl}/configs/${configId}`).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to delete config'); - } + deleteConfig(configId: string): Observable { + const numericConfigId = parseInt(configId); + // L'API ne retourne probablement rien + return this.http.delete(`${this.baseApiUrl}/configs/${numericConfigId}`).pipe( + map(() => { + }), catchError(error => { console.error(`Error deleting config ${configId}:`, error); @@ -237,13 +264,17 @@ export class MerchantConfigService { } // Technical Contacts Management - addTechnicalContact(merchantId: number, contact: Omit): Observable { - return this.http.post>(`${this.baseApiUrl}/${merchantId}/contacts`, contact).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to add technical contact'); - } - return response.data!; + 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); @@ -252,13 +283,24 @@ export class MerchantConfigService { ); } - updateTechnicalContact(contactId: number, contact: Partial): Observable { - return this.http.patch>(`${this.baseApiUrl}/contacts/${contactId}`, contact).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to update technical contact'); - } - return response.data!; + updateTechnicalContact(contactId: string, contact: Partial): Observable { + const numericContactId = parseInt(contactId); + + 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) + }) + }; + + // 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); @@ -267,12 +309,12 @@ export class MerchantConfigService { ); } - deleteTechnicalContact(contactId: number): Observable { - return this.http.delete>(`${this.baseApiUrl}/contacts/${contactId}`).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to delete technical contact'); - } + 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); @@ -281,35 +323,57 @@ export class MerchantConfigService { ); } - // Statistics + // Stats getMerchantStats(): Observable { - return this.http.get>(`${this.baseApiUrl}/stats`).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to load merchant statistics'); - } - return response.data!; + // 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 statistics:', error); + console.error('Error loading merchant stats:', error); return throwError(() => error); }) ); } - // Status Management - updateMerchantStatus(merchantId: number, status: MerchantStatus): Observable { - return this.http.patch>(`${this.baseApiUrl}/${merchantId}/status`, { status }).pipe( - map(response => { - if (!response.success) { - throw new Error(response.error || 'Failed to update merchant status'); - } - return response.data!; - }), - catchError(error => { - console.error(`Error updating status for merchant ${merchantId}:`, 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 + }; } } \ 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 ccf6904..226721c 100644 --- a/src/app/modules/merchant-config/merchant-config.ts +++ b/src/app/modules/merchant-config/merchant-config.ts @@ -1,14 +1,27 @@ -import { ChangeDetectorRef, Component, inject, OnInit } from '@angular/core'; +import { Component, inject, OnInit, TemplateRef, ViewChild, ChangeDetectorRef, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormGroup, FormControl } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormGroup } from '@angular/forms'; import { NgIcon } from '@ng-icons/core'; -import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap'; -import { UiCard } from '@app/components/ui-card'; +import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; +import { catchError, map, of, Subject, takeUntil } from 'rxjs'; + import { MerchantConfigService } from './merchant-config.service'; -import { CreateMerchantDto, MerchantConfig as MerchantConfigModel, TechnicalContact, MerchantUtils, ConfigType, Operator } from '@core/models/merchant-config.model'; -import { UserRole } from '@core/models/dcb-bo-hub-user.model'; -import { firstValueFrom, Subject } from 'rxjs'; import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; +import { AuthService } from '@core/services/auth.service'; +import { PageTitle } from '@app/components/page-title/page-title'; +import { MerchantConfigsList } from './merchant-config-list/merchant-config-list'; +import { MerchantConfigView } from './merchant-config-view/merchant-config-view'; + +import { + CreateMerchantDto, + MerchantUtils, + ConfigType, + Operator, + UpdateMerchantDto, + Merchant, +} 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'; @Component({ selector: 'app-merchant-config', @@ -18,50 +31,73 @@ import { RoleManagementService } from '@core/services/hub-users-roles-management FormsModule, ReactiveFormsModule, NgIcon, - NgbProgressbarModule, - UiCard + NgbNavModule, + NgbModalModule, + PageTitle, + MerchantConfigsList, + MerchantConfigView ], - templateUrl: './merchant-config.html' + templateUrl: './merchant-config.html', }) -export class MerchantConfig implements OnInit { +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); protected roleService = inject(RoleManagementService); private cdRef = inject(ChangeDetectorRef); private destroy$ = new Subject(); - // Configuration wizard - currentStep = 0; - wizardSteps = [ - { id: 'basic-info', icon: 'lucideBuilding', title: 'Informations de Base', subtitle: 'Détails du merchant' }, - { id: 'technical-contacts', icon: 'lucideUsers', title: 'Contacts Techniques', subtitle: 'Personnes de contact' }, - { id: 'configurations', icon: 'lucideSettings', title: 'Configurations', subtitle: 'Paramètres techniques' }, - { id: 'review', icon: 'lucideCheckCircle', title: 'Validation', subtitle: 'Vérification finale' } - ]; + // Configuration + readonly UserRole = UserRole; + readonly ConfigType = ConfigType; + readonly Operator = Operator; - configLoading = false; - configError = ''; - configSuccess = ''; + // Propriétés de configuration + pageTitle: string = 'Gestion des Marchands'; + pageSubtitle: string = 'Administrez les marchands et leurs configurations techniques'; + badge: any = { icon: 'lucideSettings', text: 'Merchant Management' }; - // Formulaires - merchantForm = this.fb.group({ - basicInfo: this.fb.group({ - name: ['', [Validators.required, Validators.minLength(2)]], - logo: [''], - description: [''], - adresse: ['', [Validators.required]], - phone: ['', [Validators.required, Validators.pattern(/^\+?[0-9\s\-\(\)]+$/)]] - }), - technicalContacts: this.fb.array([]), - configs: this.fb.array([]) - }); + // État de l'interface + activeTab: 'list' | 'profile' = 'list'; + selectedMerchantId: string | null = null; + selectedConfigId: string | null = null; + + // Gestion des permissions + currentUserRole: UserRole | null = null; + currentUserType: UserType | null = null; + currentMerchantPartnerId: string = ''; + userPermissions: any = null; + canCreateMerchants = false; + canDeleteMerchants = false; + canManageMerchants = false; + + // Formulaire de création + newMerchant: CreateMerchantDto = this.getDefaultMerchantForm(); + + // États des opérations + creatingMerchant = false; + createMerchantError = ''; + updatingMerchant = false; + updateMerchantError = ''; + deletingMerchant = false; + deleteMerchantError = ''; + + selectedMerchantForEdit: Merchant | null = null; + selectedMerchantForDelete: Merchant | null = null; + + // Références aux templates de modals + @ViewChild('createMerchantModal') createMerchantModal!: TemplateRef; + @ViewChild('editMerchantModal') editMerchantModal!: TemplateRef; + @ViewChild('deleteMerchantModal') deleteMerchantModal!: TemplateRef; + + // Références aux composants enfants + @ViewChild(MerchantConfigsList) merchantConfigsList!: MerchantConfigsList; // Opérateurs disponibles operators = [ - { id: Operator.ORANGE_CI, name: 'Orange CI' }, - { id: Operator.MTN_CI, name: 'MTN CI' }, - { id: Operator.MOOV_CI, name: 'Moov CI' }, - { id: Operator.WAVE, name: 'Wave' } + { id: Operator.ORANGE_OSN, name: 'Orange' } ]; // Types de configuration @@ -75,220 +111,622 @@ export class MerchantConfig implements OnInit { { name: ConfigType.CUSTOM, label: 'Personnalisé' } ]; - // Rôles disponibles pour les merchants (utilisation de vos rôles existants) - availableMerchantRoles = MerchantUtils.getAvailableMerchantRoles(); + // Liste des partenaires marchands (pour les admins) + merchantPartners: User[] = []; + loadingMerchantPartners = false; + merchantPartnersError = ''; + selectedMerchantPartnerId: string = ''; + + // Cache des marchands + merchantProfiles: { [merchantId: string]: Merchant } = {}; + loadingMerchants: { [merchantId: string]: boolean } = {}; ngOnInit() { - // Ajouter un contact technique par défaut - this.addTechnicalContact(); - // Ajouter une configuration par défaut - this.addConfig(); + this.activeTab = 'list'; + this.loadCurrentUserPermissions(); + this.loadMerchantPartnersIfNeeded(); + this.initializeMerchantPartnerContext(); } - // Navigation du wizard - get progressValue(): number { - return ((this.currentStep + 1) / this.wizardSteps.length) * 100; - } + // ==================== MÉTHODES MANQUANTES POUR LE TEMPLATE ==================== - nextStep() { - if (this.currentStep < this.wizardSteps.length - 1 && this.isStepValid(this.currentStep)) { - this.currentStep++; - } - } - - previousStep() { - if (this.currentStep > 0) { - this.currentStep--; - } - } - - goToStep(index: number) { - if (this.isStepAccessible(index)) { - this.currentStep = index; - } - } - - isStepAccessible(index: number): boolean { - if (index === 0) return true; - - for (let i = 0; i < index; i++) { - if (!this.isStepValid(i)) { - return false; - } - } - return true; - } - - isStepValid(stepIndex: number): boolean { - switch (stepIndex) { - case 0: // Basic Info - return this.basicInfo.valid; - case 1: // Technical Contacts - return this.technicalContacts.valid && this.technicalContacts.length > 0; - case 2: // Configurations - return this.configs.valid; - case 3: // Review - return this.merchantForm.valid; - default: - return false; - } - } - - // Gestion des contacts techniques - get technicalContacts(): FormArray { - return this.merchantForm.get('technicalContacts') as FormArray; - } - - addTechnicalContact() { - const contactGroup = this.fb.group({ - firstName: ['', [Validators.required]], - lastName: ['', [Validators.required]], - phone: ['', [Validators.required, Validators.pattern(/^\+?[0-9\s\-\(\)]+$/)]], - email: ['', [Validators.required, Validators.email]] - }); - this.technicalContacts.push(contactGroup); - } - - removeTechnicalContact(index: number) { - if (this.technicalContacts.length > 1) { - this.technicalContacts.removeAt(index); - } - } - - getContactControl(contact: any, field: string): FormControl { - return contact.get(field) as FormControl; - } - - // Gestion des configurations - get configs(): FormArray { - return this.merchantForm.get('configs') as FormArray; - } - - addConfig() { - const configGroup = this.fb.group({ - name: ['', [Validators.required]], - value: ['', [Validators.required]], - operatorId: [Operator.ORANGE_CI, [Validators.required]] - }); - this.configs.push(configGroup); - } - - removeConfig(index: number) { - if (this.configs.length > 1) { - this.configs.removeAt(index); - } - } - - getConfigControl(config: any, field: string): FormControl { - return config.get(field) as FormControl; - } - - // Soumission du formulaire - async submitForm() { - if (this.merchantForm.valid) { - this.configLoading = true; - this.configError = ''; - - try { - const formData = this.merchantForm.value; - - const createMerchantDto: CreateMerchantDto = { - name: this.safeString(formData.basicInfo?.name), - logo: this.safeString(formData.basicInfo?.logo), - description: this.safeString(formData.basicInfo?.description), - adresse: this.safeString(formData.basicInfo?.adresse), - phone: this.safeString(formData.basicInfo?.phone), - technicalContacts: (formData.technicalContacts || []).map((contact: any) => ({ - firstName: this.safeString(contact.firstName), - lastName: this.safeString(contact.lastName), - phone: this.safeString(contact.phone), - email: this.safeString(contact.email) - })), - configs: (formData.configs || []).map((config: any) => ({ - name: this.safeString(config.name), - value: this.safeString(config.value), - operatorId: this.safeNumber(config.operatorId) - })) - }; - - // Validation avant envoi - const validationErrors = MerchantUtils.validateMerchantCreation(createMerchantDto); - if (validationErrors.length > 0) { - this.configError = validationErrors.join(', '); - return; - } - - const response = await firstValueFrom( - this.merchantConfigService.createMerchant(createMerchantDto) - ); - - this.configSuccess = `Merchant créé avec succès! ID: ${response.id}`; - this.merchantForm.reset(); - this.currentStep = 0; - - // Réinitialiser les tableaux - this.technicalContacts.clear(); - this.configs.clear(); - this.addTechnicalContact(); - this.addConfig(); - - } catch (error: any) { - this.configError = error.message || 'Erreur lors de la création du merchant'; - console.error('Error creating merchant:', error); - } finally { - this.configLoading = false; - } - } else { - this.configError = 'Veuillez corriger les erreurs dans le formulaire'; - this.markAllFieldsAsTouched(); - } - } - - // Méthodes utilitaires - private safeString(value: string | null | undefined): string { - return value || ''; - } - - private safeNumber(value: number | null | undefined): number { - return value || 0; - } - - private markAllFieldsAsTouched() { - Object.keys(this.merchantForm.controls).forEach(key => { - const control = this.merchantForm.get(key); - if (control instanceof FormGroup) { - Object.keys(control.controls).forEach(subKey => { - control.get(subKey)?.markAsTouched(); - }); - } else if (control instanceof FormArray) { - control.controls.forEach((arrayControl: any) => { - if (arrayControl instanceof FormGroup) { - Object.keys(arrayControl.controls).forEach(subKey => { - arrayControl.get(subKey)?.markAsTouched(); - }); - } - }); - } else { - control?.markAsTouched(); - } - }); - } - - // Getters pour les formulaires - get basicInfo() { - return this.merchantForm.get('basicInfo') as FormGroup; - } - - // Méthodes pour le template - getOperatorName(operatorId: Operator): string { - return MerchantUtils.getOperatorName(operatorId); - } - - getConfigTypeName(configName: string): string { - return MerchantUtils.getConfigTypeName(configName); + /** + * Méthodes pour la gestion des rôles (manquantes) + */ + getRoleBadgeClass(role: UserRole): string { + return this.roleService.getRoleBadgeClass(role); } getRoleLabel(role: UserRole): string { return this.roleService.getRoleLabel(role); } + + /** + * Méthodes pour la gestion des contacts techniques + */ + addTechnicalContact(): void { + if (!this.newMerchant.technicalContacts) { + this.newMerchant.technicalContacts = []; + } + + this.newMerchant.technicalContacts.push({ + firstName: '', + lastName: '', + phone: '', + email: '' + }); + } + + removeTechnicalContact(index: number): void { + if (this.newMerchant.technicalContacts && this.newMerchant.technicalContacts.length > 1) { + this.newMerchant.technicalContacts.splice(index, 1); + } + } + + /** + * Méthodes pour la gestion des configurations + */ + addConfig(): void { + if (!this.newMerchant.configs) { + this.newMerchant.configs = []; + } + + this.newMerchant.configs.push({ + name: ConfigType.API_KEY, + value: '', + operatorId: Operator.ORANGE_OSN + }); + } + + removeConfig(index: number): void { + if (this.newMerchant.configs && this.newMerchant.configs.length > 1) { + this.newMerchant.configs.splice(index, 1); + } + } + + // ==================== CONVERSION IDS ==================== + + /** + * Convertit un ID number en string pour Angular + */ + private convertIdToString(id: number): string { + return id.toString(); + } + + /** + * 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 + })) || [] + }; + } + + /** + * Convertit un DTO avec des IDs string en number pour l'API + */ + private convertMerchantToBackend(dto: any): any { + + 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 + })) || [] + }; + } + + // ==================== GESTION DES PERMISSIONS ==================== + + private loadCurrentUserPermissions(): void { + this.authService.getUserProfile() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (user) => { + this.currentUserRole = this.extractUserRole(user); + this.currentMerchantPartnerId = this.extractMerchantPartnerId(user); + this.currentUserType = this.extractUserType(user); + + if (this.currentUserRole) { + this.userPermissions = this.roleService.getPermissionsForRole(this.currentUserRole); + this.canCreateMerchants = this.canManageMerchant(); + this.canDeleteMerchants = this.canManageMerchant(); + this.canManageMerchants = this.canManageMerchant(); + } + + // Initialiser le contexte marchand + this.selectedMerchantPartnerId = this.currentMerchantPartnerId; + }, + error: (error) => { + console.error('Error loading user profile:', error); + } + }); + } + + private extractUserRole(user: any): UserRole | null { + const userRoles = this.authService.getCurrentUserRoles(); + return userRoles && userRoles.length > 0 ? userRoles[0] : null; + } + + private extractUserType(user: any): UserType | null { + return this.authService.getCurrentUserType(); + } + + private extractMerchantPartnerId(user: any): string { + return user?.merchantPartnerId || this.authService.getCurrentMerchantPartnerId() || ''; + } + + // ==================== PERMISSIONS SPÉCIFIQUES MARCHAND ==================== + + private canManageMerchant(): boolean { + return this.roleService.canManageMerchants(this.currentUserRole); + } + + /** + * Vérifie si l'utilisateur peut modifier un marchand spécifique + */ + canModifyMerchant(merchant: Merchant): boolean { + if (this.canManageMerchants) return true; + + // PARTNER ne peut modifier que ses propres marchands + // Vérifier via les configs ou users + return merchant.configs?.some(config => config.merchantPartnerId === this.currentMerchantPartnerId) || + merchant.users?.some(user => user.merchantPartnerId === this.currentMerchantPartnerId); + } + + // ==================== 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) && + this.currentMerchantPartnerId) { + this.selectedMerchantPartnerId = this.currentMerchantPartnerId; + } + } + + onMerchantSelectionChange(selectedMerchantId: string): void { + this.selectedMerchantPartnerId = selectedMerchantId; + + // Recharger les marchands pour le partenaire sélectionné + if (this.merchantConfigsList) { + this.merchantConfigsList.refreshData(); + } + } + + // ==================== GESTION DES ONGLETS ==================== + + showTab(tab: 'list' | 'profile', merchantId?: string): void { + console.log(`Switching to tab: ${tab}`, merchantId ? `for merchant ${merchantId}` : ''); + this.activeTab = tab; + + if (merchantId) { + this.selectedMerchantId = merchantId; + + // Charger le profil si pas déjà chargé + if (!this.merchantProfiles[merchantId]) { + this.loadMerchantProfile(merchantId); + } + } else { + this.selectedMerchantId = null; + } + } + + private loadMerchantProfile(merchantId: string): void { + if (this.loadingMerchants[merchantId]) return; + + this.loadingMerchants[merchantId] = true; + + this.merchantConfigService.getMerchantById(merchantId).subscribe({ + next: (merchant) => { + // Conversion pour Angular + const frontendMerchant = this.convertMerchantToFrontend(merchant); + this.merchantProfiles[merchantId] = frontendMerchant; + this.loadingMerchants[merchantId] = false; + }, + error: (error) => { + console.error(`Error loading merchant profile ${merchantId}:`, error); + this.loadingMerchants[merchantId] = false; + } + }); + } + + backToList(): void { + console.log('🔙 Returning to list view'); + this.activeTab = 'list'; + this.selectedMerchantId = null; + } + + // ==================== ÉVÉNEMENTS DES COMPOSANTS ENFANTS ==================== + + onMerchantSelected(merchantId: string): void { + this.showTab('profile', merchantId); + } + + onEditMerchantRequested(merchant: Merchant): void { + // Le composant enfant envoie un objet Merchant complet + this.openEditMerchantModal(merchant.id!); + } + + onDeleteMerchantRequested(merchant: Merchant): void { + // Le composant enfant envoie un objet Merchant complet + this.openDeleteMerchantModal(merchant.id!); + } + + onEditConfigRequested(configId: string): void { + this.openEditMerchantModal(configId); + } + // ==================== GESTION DES MODALS ==================== + + openModal(content: TemplateRef, size: 'sm' | 'lg' | 'xl' = 'lg'): void { + this.modalService.open(content, { + size: size, + centered: true, + scrollable: true + }); + } + + openCreateMerchantModal(): void { + if (!this.canCreateMerchants) { + console.warn('User does not have permission to create merchants'); + return; + } + + this.resetMerchantForm(); + this.createMerchantError = ''; + this.openModal(this.createMerchantModal); + } + + private openEditMerchantModal(merchantId: string): void { + + this.merchantConfigService.getMerchantById(merchantId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (merchant) => { + // Conversion pour Angular + const frontendMerchant = this.convertMerchantToFrontend(merchant); + + if (!this.canModifyMerchant(frontendMerchant)) { + this.updateMerchantError = 'Vous n\'avez pas la permission de modifier ce marchand'; + return; + } + + this.selectedMerchantForEdit = frontendMerchant; + this.updateMerchantError = ''; + this.populateEditForm(frontendMerchant); + this.openModal(this.editMerchantModal); + }, + error: (error) => { + console.error('❌ Error loading merchant for edit:', error); + this.updateMerchantError = 'Erreur lors du chargement du marchand'; + this.cdRef.detectChanges(); + } + }); + } + + private openDeleteMerchantModal(merchantId: string): void { + if (!this.canDeleteMerchants) { + console.warn('User does not have permission to delete merchants'); + return; + } + + this.merchantConfigService.getMerchantById(merchantId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (merchant) => { + // Conversion pour Angular + const frontendMerchant = this.convertMerchantToFrontend(merchant); + + if (!this.canModifyMerchant(frontendMerchant)) { + this.deleteMerchantError = 'Vous n\'avez pas la permission de supprimer ce marchand'; + return; + } + + this.selectedMerchantForDelete = frontendMerchant; + this.deleteMerchantError = ''; + this.openModal(this.deleteMerchantModal); + }, + error: (error) => { + console.error('❌ Error loading merchant for deletion:', error); + this.deleteMerchantError = 'Erreur lors du chargement du marchand'; + this.cdRef.detectChanges(); + } + }); + } + + // ==================== OPÉRATIONS CRUD ==================== + + createMerchant(): void { + if (!this.canCreateMerchants) { + this.createMerchantError = 'Vous n\'avez pas la permission de créer des marchands'; + return; + } + + const validation = MerchantUtils.validateMerchantCreation(this.newMerchant); + if (validation.length > 0) { + this.createMerchantError = validation.join(', '); + return; + } + + this.creatingMerchant = true; + this.createMerchantError = ''; + + // Conversion pour l'API + const createDto = this.convertMerchantToBackend(this.newMerchant); + + console.log('📤 Creating merchant:', createDto); + + this.merchantConfigService.createMerchant(createDto) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (createdMerchant) => { + // Conversion de la réponse pour Angular + const frontendMerchant = this.convertMerchantToFrontend(createdMerchant); + + console.log('✅ Merchant created successfully:', frontendMerchant); + this.creatingMerchant = false; + this.modalService.dismissAll(); + this.refreshMerchantsList(); + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('❌ Error creating merchant:', error); + this.creatingMerchant = false; + this.createMerchantError = this.getCreateErrorMessage(error); + this.cdRef.detectChanges(); + } + }); + } + + 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(', '); + return; + } + + this.updatingMerchant = true; + this.updateMerchantError = ''; + + // Conversion pour l'API + 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 + }; + + this.merchantConfigService.updateMerchant(merchantId, updateDto) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (updatedMerchant) => { + // Conversion pour Angular + const frontendMerchant = this.convertMerchantToFrontend(updatedMerchant); + + this.updatingMerchant = false; + this.modalService.dismissAll(); + this.refreshMerchantsList(); + + // Mettre à jour le cache + if (this.selectedMerchantId) { + this.merchantProfiles[this.selectedMerchantId] = frontendMerchant; + } + + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('❌ Error updating merchant:', error); + this.updatingMerchant = false; + this.updateMerchantError = this.getUpdateErrorMessage(error); + this.cdRef.detectChanges(); + } + }); + } + + confirmDeleteMerchant(): void { + if (!this.selectedMerchantForDelete) { + this.deleteMerchantError = 'Aucun marchand sélectionné pour suppression'; + return; + } + + this.deletingMerchant = true; + this.deleteMerchantError = ''; + + // Conversion pour l'API + const merchantId = this.selectedMerchantForDelete.id!; + + this.merchantConfigService.deleteMerchant(merchantId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => { + console.log('✅ Merchant deleted successfully'); + this.deletingMerchant = false; + this.modalService.dismissAll(); + this.refreshMerchantsList(); + + // Nettoyer le cache + if (this.selectedMerchantId === this.selectedMerchantForDelete?.id) { + this.backToList(); + } + + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('❌ Error deleting merchant:', error); + this.deletingMerchant = false; + this.deleteMerchantError = this.getDeleteErrorMessage(error); + this.cdRef.detectChanges(); + } + }); + } + + // ==================== MÉTHODES UTILITAIRES ==================== + + private getDefaultMerchantForm(): CreateMerchantDto { + return { + name: '', + logo: '', + description: '', + adresse: '', + phone: '', + configs: [{ + name: ConfigType.API_KEY, + value: '', + operatorId: Operator.ORANGE_OSN + }], + technicalContacts: [{ + firstName: '', + lastName: '', + phone: '', + email: '' + }] + }; + } + + private resetMerchantForm(): void { + this.newMerchant = this.getDefaultMerchantForm(); + console.log('🔄 Merchant form reset'); + } + + private populateEditForm(merchant: Merchant): void { + this.selectedMerchantForEdit = { ...merchant }; + } + + private refreshMerchantsList(): void { + if (this.merchantConfigsList && typeof this.merchantConfigsList.refreshData === 'function') { + console.log('🔄 Refreshing merchants list...'); + this.merchantConfigsList.refreshData(); + } else { + console.warn('❌ MerchantConfigsList component not available for refresh'); + this.showTab('list'); + } + } + + // ==================== GESTION DES ERREURS ==================== + + private getCreateErrorMessage(error: any): string { + if (error.error?.message) return error.error.message; + if (error.status === 400) return 'Données invalides. Vérifiez les champs.'; + if (error.status === 409) return 'Un marchand similaire existe déjà.'; + if (error.status === 403) return 'Permission refusée.'; + return 'Erreur lors de la création. Veuillez réessayer.'; + } + + private getUpdateErrorMessage(error: any): string { + if (error.error?.message) return error.error.message; + if (error.status === 400) return 'Données invalides.'; + if (error.status === 404) return 'Marchand non trouvé.'; + if (error.status === 403) return 'Permission refusée.'; + return 'Erreur lors de la modification. Veuillez réessayer.'; + } + + private getDeleteErrorMessage(error: any): string { + if (error.error?.message) return error.error.message; + if (error.status === 404) return 'Marchand non trouvé.'; + if (error.status === 403) return 'Permission refusée.'; + if (error.status === 409) return 'Marchand utilisé, impossible de supprimer.'; + return 'Erreur lors de la suppression. Veuillez réessayer.'; + } + + // ==================== MÉTHODES TEMPLATE ==================== + + getOperatorName(operatorId: Operator): string { + return MerchantUtils.getOperatorName(operatorId); + } + + getConfigTypeName(configType: ConfigType): string { + return MerchantUtils.getConfigTypeName(configType); + } + + // ==================== GETTERS TEMPLATE ==================== + + get currentMerchantProfile(): Merchant | null { + return this.selectedMerchantId ? this.merchantProfiles[this.selectedMerchantId] : null; + } + + get isLoadingMerchant(): boolean { + return this.selectedMerchantId ? this.loadingMerchants[this.selectedMerchantId] : false; + } + + get showMerchantPartnerField(): boolean { + return this.canManageMerchants; + } + + get requireMerchantPartnerSelection(): boolean { + return this.canManageMerchants; + } + + get isPartnerUser(): boolean { + return this.currentUserRole === UserRole.DCB_PARTNER || + this.currentUserRole === UserRole.DCB_PARTNER_ADMIN; + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } } \ No newline at end of file diff --git a/src/app/modules/modules.routes.ts b/src/app/modules/modules.routes.ts index 3c9f76e..41883e6 100644 --- a/src/app/modules/modules.routes.ts +++ b/src/app/modules/modules.routes.ts @@ -23,7 +23,7 @@ import { Help } from '@modules/help/help'; import { About } from '@modules/about/about'; import { SubscriptionsManagement } from './subscriptions/subscriptions'; import { SubscriptionPayments } from './subscriptions/subscription-payments/subscription-payments'; -import { MerchantConfig } from './merchant-config/merchant-config'; +import { MerchantConfigManagement } from './merchant-config/merchant-config'; const routes: Routes = [ // --------------------------- @@ -191,7 +191,7 @@ const routes: Routes = [ // --------------------------- { path: 'merchant-config', - component: MerchantConfig, + component: MerchantConfigManagement, canActivate: [authGuard, roleGuard], data: { title: 'Merchant Config',