feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
7091f1665d
commit
a4834002df
@ -17,4 +17,3 @@ export const appConfig: ApplicationConfig = {
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -36,40 +36,35 @@ export class App implements OnInit {
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
filter(init => init === true),
|
||||
take(1),
|
||||
switchMap(() => {
|
||||
|
||||
// 🔒 Étape 1 : Vérifier si déjà authentifié
|
||||
// 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(','),
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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<MerchantConfig, 'id' | 'merchantId' | 'createdAt' | 'updatedAt'>[];
|
||||
technicalContacts: Omit<TechnicalContact, 'id' | 'merchantId' | 'createdAt' | 'updatedAt'>[];
|
||||
configs: Omit<MerchantConfig, 'id' | 'merchantPartnerId' | 'createdAt' | 'updatedAt'>[];
|
||||
technicalContacts: Omit<TechnicalContact, 'id' | 'merchantPartnerId' | 'createdAt' | 'updatedAt'>[];
|
||||
}
|
||||
|
||||
export interface UpdateMerchantDto extends Partial<CreateMerchantDto> {
|
||||
status?: MerchantStatus;
|
||||
export interface UpdateMerchantDto extends Partial<CreateMerchantDto> {}
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
@ -69,42 +69,30 @@ export class AuthService {
|
||||
* Initialise l'authentification au démarrage de l'application
|
||||
*/
|
||||
async initialize(): Promise<boolean> {
|
||||
const token = this.getAccessToken();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
52
src/app/modules/merchant-config/custom-validators.ts
Normal file
52
src/app/modules/merchant-config/custom-validators.ts
Normal file
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,315 @@
|
||||
<app-ui-card [title]="getCardTitle()">
|
||||
<a
|
||||
helper-text
|
||||
href="javascript:void(0);"
|
||||
class="icon-link icon-link-hover link-primary fw-semibold"
|
||||
>
|
||||
<ng-icon [name]="getHelperIcon()" class="me-1"></ng-icon>
|
||||
{{ getHelperText() }}
|
||||
</a>
|
||||
|
||||
<div card-body>
|
||||
|
||||
<!-- Barre d'actions supérieure -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<!-- Statistiques rapides par statut -->
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary"
|
||||
>
|
||||
Tous ({{ getTotalMerchantsCount() }})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
@if (showCreateButton && canCreateMerchants) {
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
(click)="openCreateMerchantModal.emit()"
|
||||
>
|
||||
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
|
||||
Nouveau Marchand
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
(click)="refreshData()"
|
||||
[disabled]="loading"
|
||||
>
|
||||
<ng-icon name="lucideRefreshCw" class="me-1" [class.spin]="loading"></ng-icon>
|
||||
Actualiser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Barre de recherche et filtres avancés -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<ng-icon name="lucideSearch"></ng-icon>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Rechercher par nom, adresse, téléphone..."
|
||||
[(ngModel)]="searchTerm"
|
||||
(input)="onSearch()"
|
||||
[disabled]="loading"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" [(ngModel)]="operatorFilter" (change)="applyFiltersAndPagination()">
|
||||
@for (operator of availableOperators; track operator.value) {
|
||||
<option [value]="operator.value">{{ operator.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-outline-secondary w-100" (click)="onClearFilters()" [disabled]="loading">
|
||||
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||
Effacer les filtres
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
@if (loading) {
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
<p class="mt-2 text-muted">{{ getLoadingText() }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Error State -->
|
||||
@if (error && !loading) {
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||
<div>{{ error }}</div>
|
||||
<button class="btn-close ms-auto" (click)="error = ''"></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Merchants Table -->
|
||||
@if (!loading && !error) {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th (click)="sort('name')" class="cursor-pointer">
|
||||
<div class="d-flex align-items-center">
|
||||
<span>Marchand</span>
|
||||
<ng-icon [name]="getSortIcon('name')" class="ms-1 fs-12"></ng-icon>
|
||||
</div>
|
||||
</th>
|
||||
<th>Description</th>
|
||||
<th>Contact</th>
|
||||
<th>Configurations</th>
|
||||
<th>Contacts Tech</th>
|
||||
<th (click)="sort('createdAt')" class="cursor-pointer">
|
||||
<div class="d-flex align-items-center">
|
||||
<span>Créé le</span>
|
||||
<ng-icon [name]="getSortIcon('createdAt')" class="ms-1 fs-12"></ng-icon>
|
||||
</div>
|
||||
</th>
|
||||
<th width="180">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (merchant of displayedMerchants; track merchant.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
@if (merchant.logo) {
|
||||
<img
|
||||
[src]="merchant.logo"
|
||||
alt="Logo {{ merchant.name }}"
|
||||
class="avatar-sm rounded-circle me-2"
|
||||
onerror="this.style.display='none'"
|
||||
>
|
||||
}
|
||||
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||
<ng-icon name="lucideStore" class="text-primary fs-12"></ng-icon>
|
||||
</div>
|
||||
<div>
|
||||
<strong class="d-block">{{ merchant.name }}</strong>
|
||||
<small class="text-muted">{{ merchant.adresse }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@if (merchant.description) {
|
||||
<span class="text-muted">{{ merchant.description }}</span>
|
||||
} @else {
|
||||
<span class="text-muted fst-italic">Aucune description</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<small class="text-muted">
|
||||
<ng-icon name="lucidePhone" class="me-1" size="12"></ng-icon>
|
||||
{{ merchant.phone }}
|
||||
</small>
|
||||
@if (merchant.technicalContacts && merchant.technicalContacts.length > 0) {
|
||||
<small class="text-muted">
|
||||
<ng-icon name="lucideUser" class="me-1" size="12"></ng-icon>
|
||||
{{ merchant.technicalContacts[0].firstName }} {{ merchant.technicalContacts[0].lastName }}
|
||||
</small>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column gap-1">
|
||||
@if (merchant.configs && merchant.configs.length > 0) {
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
@for (config of merchant.configs.slice(0, 2); track config.id) {
|
||||
<span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-10">
|
||||
{{ getConfigTypeLabel(config.name) }}
|
||||
</span>
|
||||
}
|
||||
@if (merchant.configs.length > 2) {
|
||||
<span class="badge bg-secondary">
|
||||
+{{ merchant.configs.length - 2 }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
{{ merchant.configs.length }} configuration(s)
|
||||
</small>
|
||||
} @else {
|
||||
<span class="badge bg-warning bg-opacity-10 text-warning">
|
||||
Aucune config
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@if (merchant.technicalContacts && merchant.technicalContacts.length > 0) {
|
||||
<div class="d-flex flex-column">
|
||||
<span class="badge bg-info bg-opacity-10 text-info">
|
||||
{{ merchant.technicalContacts.length }} contact(s)
|
||||
</span>
|
||||
<small class="text-muted">
|
||||
{{ merchant.technicalContacts[0].email }}
|
||||
</small>
|
||||
</div>
|
||||
} @else {
|
||||
<span class="badge bg-warning bg-opacity-10 text-warning">
|
||||
Aucun contact
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
{{ formatTimestamp(merchant.createdAt!) }}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
(click)="viewMerchantProfile(merchant)"
|
||||
title="Voir les détails"
|
||||
>
|
||||
<ng-icon name="lucideEye"></ng-icon>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline-warning btn-sm"
|
||||
(click)="editMerchant(merchant)"
|
||||
title="Modifier le marchand"
|
||||
>
|
||||
<ng-icon name="lucideEdit"></ng-icon>
|
||||
</button>
|
||||
@if (showDeleteButton) {
|
||||
<button
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
(click)="deleteMerchant(merchant)"
|
||||
title="Supprimer le marchand"
|
||||
>
|
||||
<ng-icon name="lucideTrash2"></ng-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@empty {
|
||||
<tr>
|
||||
<td [attr.colspan]="getColumnCount()" class="text-center py-4">
|
||||
<div class="text-muted">
|
||||
<ng-icon name="lucideStore" class="fs-1 mb-3 opacity-50"></ng-icon>
|
||||
<h5 class="mb-2">{{ getEmptyStateTitle() }}</h5>
|
||||
<p class="mb-3">{{ getEmptyStateDescription() }}</p>
|
||||
@if (showCreateButton && canCreateMerchants) {
|
||||
<button class="btn btn-primary" (click)="openCreateMerchantModal.emit()">
|
||||
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
|
||||
{{ getEmptyStateButtonText() }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if (totalPages > 1) {
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<div class="text-muted">
|
||||
Affichage de {{ getStartIndex() }} à {{ getEndIndex() }} sur {{ totalItems }} marchands
|
||||
</div>
|
||||
<nav>
|
||||
<ngb-pagination
|
||||
[collectionSize]="totalItems"
|
||||
[page]="currentPage"
|
||||
[pageSize]="itemsPerPage"
|
||||
[maxSize]="5"
|
||||
[rotate]="true"
|
||||
[boundaryLinks]="true"
|
||||
(pageChange)="onPageChange($event)"
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Résumé des résultats -->
|
||||
@if (displayedMerchants.length > 0) {
|
||||
<div class="mt-3 pt-3 border-top">
|
||||
<div class="row text-center">
|
||||
<div class="col">
|
||||
<small class="text-muted">
|
||||
<strong>Total :</strong> {{ allMerchants.length }} marchands
|
||||
</small>
|
||||
</div>
|
||||
<div class="col">
|
||||
<small class="text-muted">
|
||||
<strong>Configurations :</strong> {{ getTotalConfigsCount() }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="col">
|
||||
<small class="text-muted">
|
||||
<strong>Contacts :</strong> {{ getTotalContactsCount() }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</app-ui-card>
|
||||
@ -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<void>();
|
||||
|
||||
// Configuration
|
||||
readonly ConfigType = ConfigType;
|
||||
readonly Operator = Operator;
|
||||
readonly MerchantUtils = MerchantUtils;
|
||||
|
||||
// Inputs
|
||||
@Input() canCreateMerchants: boolean = false;
|
||||
@Input() canDeleteMerchants: boolean = false;
|
||||
|
||||
// Outputs
|
||||
@Output() merchantSelected = new EventEmitter<string>();
|
||||
@Output() openCreateMerchantModal = new EventEmitter<void>();
|
||||
@Output() editMerchantRequested = new EventEmitter<Merchant>();
|
||||
@Output() deleteMerchantRequested = new EventEmitter<Merchant>();
|
||||
@Output() activateMerchantRequested = new EventEmitter<Merchant>();
|
||||
@Output() deactivateMerchantRequested = new EventEmitter<Merchant>();
|
||||
|
||||
// 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<Merchant[]>;
|
||||
|
||||
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<Merchant[]> {
|
||||
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<Merchant[]> {
|
||||
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';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,410 @@
|
||||
<div class="container-fluid">
|
||||
<!-- En-tête avec navigation -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h4 class="mb-1">{{ getProfileTitle() }}</h4>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="javascript:void(0)" (click)="goBack()" class="text-decoration-none cursor-pointer">
|
||||
Marchands
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
Configurations
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<!-- Bouton rafraîchissement -->
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
(click)="refresh()"
|
||||
[disabled]="loading"
|
||||
>
|
||||
<ng-icon name="lucideRefreshCw" class="me-1" [class.spin]="loading"></ng-icon>
|
||||
Actualiser
|
||||
</button>
|
||||
|
||||
<!-- Bouton retour -->
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
(click)="goBack()"
|
||||
>
|
||||
<ng-icon name="lucideArrowLeft" class="me-1"></ng-icon>
|
||||
Retour
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Indicateur de permissions -->
|
||||
@if (currentUserRole && !canManageAllConfigs()) {
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideShield" class="me-2"></ng-icon>
|
||||
<div>
|
||||
<strong>Permissions limitées :</strong> Vous ne pouvez que consulter vos configurations
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Messages d'alerte -->
|
||||
@if (error) {
|
||||
<div class="alert alert-danger">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||
<div>{{ error }}</div>
|
||||
<button class="btn-close ms-auto" (click)="clearMessages()"></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (success) {
|
||||
<div class="alert alert-success">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||
<div>{{ success }}</div>
|
||||
<button class="btn-close ms-auto" (click)="clearMessages()"></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<!-- Loading State -->
|
||||
@if (loading) {
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
<p class="mt-2 text-muted">Chargement des configurations...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Liste des configurations -->
|
||||
@if (configs.length > 0 && !loading) {
|
||||
<div class="col-12">
|
||||
<!-- En-tête de liste -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<h5 class="card-title mb-0">
|
||||
<ng-icon name="lucideSettings" class="me-2"></ng-icon>
|
||||
{{ configs.length }} configuration(s) trouvée(s)
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<div class="text-muted small">
|
||||
Page {{ page }} sur {{ totalPages }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liste des configurations -->
|
||||
@for (config of paginatedConfigs; track config.id) {
|
||||
<div class="config-card card mb-3" [class.expanded]="isConfigExpanded(config.id!)">
|
||||
<!-- En-tête de configuration -->
|
||||
<div class="config-header" (click)="toggleConfigExpansion(config.id!)">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon
|
||||
[name]="getConfigTypeIconSafe(config.name)"
|
||||
class="me-3 text-primary"
|
||||
></ng-icon>
|
||||
<div>
|
||||
<h6 class="mb-1">{{ config.name }}</h6>
|
||||
<div class="d-flex gap-2">
|
||||
<span [ngClass]="getTypeBadgeClass(config)" class="badge">
|
||||
{{ getTypeLabel(config) }}
|
||||
</span>
|
||||
<span [ngClass]="getOperatorBadgeClass(config)" class="badge">
|
||||
{{ getOperatorLabel(config) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
<!-- Indicateur sensible -->
|
||||
@if (isSensitiveConfig(config)) {
|
||||
<ng-icon name="lucideShield" class="me-2 text-warning" size="16"></ng-icon>
|
||||
}
|
||||
|
||||
<!-- Date de modification -->
|
||||
<small class="text-muted me-3">
|
||||
Modifié le {{ getLastUpdateDate(config) }}
|
||||
</small>
|
||||
|
||||
<!-- Icône expansion -->
|
||||
<ng-icon
|
||||
[name]="isConfigExpanded(config.id!) ? 'lucideChevronUp' : 'lucideChevronDown'"
|
||||
class="text-muted"
|
||||
></ng-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenu détaillé (expandable) -->
|
||||
@if (isConfigExpanded(config.id!)) {
|
||||
<div class="config-content">
|
||||
<div class="row g-3">
|
||||
<!-- Informations de base -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small text-muted">Nom</label>
|
||||
<div class="fw-semibold">{{ config.name }}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small text-muted">Type</label>
|
||||
<div>
|
||||
<span [ngClass]="getTypeBadgeClass(config)" class="badge">
|
||||
{{ getTypeLabel(config) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small text-muted">Opérateur</label>
|
||||
<div>
|
||||
<span [ngClass]="getOperatorBadgeClass(config)" class="badge">
|
||||
{{ getOperatorLabel(config) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small text-muted">ID Configuration</label>
|
||||
<div class="font-monospace small text-truncate">{{ config.id }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Valeur -->
|
||||
<div class="col-12">
|
||||
<label class="form-label small text-muted">
|
||||
Valeur
|
||||
@if (isSensitiveConfig(config)) {
|
||||
<span class="text-warning ms-1">
|
||||
<ng-icon name="lucideShield" size="12"></ng-icon>
|
||||
Sensible
|
||||
</span>
|
||||
}
|
||||
</label>
|
||||
<div class="config-value" [ngClass]="getValueDisplayClass(config)">
|
||||
<span class="font-monospace">
|
||||
{{ getDisplayValue(config) }}
|
||||
</span>
|
||||
@if (canShowFullValue(config)) {
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary ms-2"
|
||||
(click)="toggleSensitiveValue(config.id!); $event.stopPropagation()"
|
||||
[title]="getValueTooltip(config)"
|
||||
>
|
||||
<ng-icon [name]="getValueIcon(config)" class="me-1"></ng-icon>
|
||||
{{ showSensitiveValues[config.id!] ? 'Masquer' : 'Afficher' }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@if (isSensitiveConfig(config) && !showSensitiveValues[config.id!]) {
|
||||
<div class="form-text text-warning">
|
||||
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||
Valeur masquée pour des raisons de sécurité
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Informations système -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small text-muted">Date de création</label>
|
||||
<div>{{ getCreationDate(config) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small text-muted">Dernière modification</label>
|
||||
<div>{{ getLastUpdateDate(config) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="col-12">
|
||||
<label class="form-label small text-muted">Description</label>
|
||||
<div class="alert alert-info mb-0">
|
||||
<small>{{ getConfigUsageInfo(config) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="config-actions">
|
||||
<div class="row g-2">
|
||||
<div class="col-auto">
|
||||
@if (isEditingConfig(config.id!)) {
|
||||
<!-- Mode édition -->
|
||||
<div class="d-flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
(click)="cancelEditing(); $event.stopPropagation()"
|
||||
[disabled]="saving"
|
||||
>
|
||||
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-sm"
|
||||
(click)="saveConfig(config); $event.stopPropagation()"
|
||||
[disabled]="saving || !isFormValid()"
|
||||
>
|
||||
@if (saving) {
|
||||
<div class="spinner-border spinner-border-sm me-1" role="status">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
}
|
||||
<ng-icon name="lucideCheck" class="me-1"></ng-icon>
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Mode consultation -->
|
||||
<div class="d-flex gap-2">
|
||||
@if (canEditConfig(config)) {
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
(click)="startEditing(config); $event.stopPropagation()"
|
||||
>
|
||||
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
|
||||
Modifier
|
||||
</button>
|
||||
}
|
||||
|
||||
<button
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
(click)="requestEdit(config.id!); $event.stopPropagation()"
|
||||
>
|
||||
<ng-icon name="lucideExternalLink" class="me-1"></ng-icon>
|
||||
Détails
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Formulaire d'édition -->
|
||||
@if (isEditingConfig(config.id!)) {
|
||||
<div class="col-12 mt-3">
|
||||
<div class="border-top pt-3">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Nom</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control form-control-sm"
|
||||
[(ngModel)]="editedConfig.name"
|
||||
placeholder="Nom de la configuration"
|
||||
[disabled]="saving"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Type</label>
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
[(ngModel)]="editedConfig.name"
|
||||
[disabled]="saving"
|
||||
>
|
||||
<option value="" disabled>Sélectionnez un type</option>
|
||||
@for (type of getAvailableConfigTypes(); track type.value) {
|
||||
<option [value]="type.value">{{ type.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Opérateur</label>
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
[(ngModel)]="editedConfig.operatorId"
|
||||
[disabled]="saving"
|
||||
>
|
||||
<option value="" disabled>Sélectionnez un opérateur</option>
|
||||
@for (operator of getAvailableOperators(); track operator.value) {
|
||||
<option [value]="operator.value">{{ operator.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label small">Valeur</label>
|
||||
<textarea
|
||||
class="form-control form-control-sm font-monospace"
|
||||
[(ngModel)]="editedConfig.value"
|
||||
placeholder="Valeur de la configuration"
|
||||
[disabled]="saving"
|
||||
rows="3"
|
||||
></textarea>
|
||||
@if (isSensitiveConfig(config)) {
|
||||
<div class="alert alert-warning mt-2 mb-0">
|
||||
<small>
|
||||
<ng-icon name="lucideAlertTriangle" class="me-1"></ng-icon>
|
||||
Cette valeur contient des informations sensibles. Soyez prudent lors de la modification.
|
||||
</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Pagination -->
|
||||
@if (totalPages > 1) {
|
||||
<div class="d-flex justify-content-center mt-4">
|
||||
<ngb-pagination
|
||||
[collectionSize]="configs.length"
|
||||
[page]="page"
|
||||
[pageSize]="pageSize"
|
||||
(pageChange)="page = $event"
|
||||
[maxSize]="5"
|
||||
[boundaryLinks]="true"
|
||||
>
|
||||
</ngb-pagination>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Empty State -->
|
||||
@if (configs.length === 0 && !loading) {
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="text-muted">
|
||||
<ng-icon name="lucideSettings" class="fs-1 mb-3 opacity-50"></ng-icon>
|
||||
<h5 class="mb-2">Aucune configuration trouvée</h5>
|
||||
<p class="mb-3">Ce marchand ne possède aucune configuration pour le moment.</p>
|
||||
<button class="btn btn-primary" (click)="refresh()">
|
||||
<ng-icon name="lucideRefreshCw" class="me-1"></ng-icon>
|
||||
Actualiser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -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<void>();
|
||||
|
||||
readonly ConfigType = ConfigType;
|
||||
readonly Operator = Operator;
|
||||
readonly MerchantUtils = MerchantUtils;
|
||||
|
||||
@Input() merchantId!: string;
|
||||
@Output() back = new EventEmitter<void>();
|
||||
@Output() editConfigRequested = new EventEmitter<string>();
|
||||
|
||||
// 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, string> = {
|
||||
[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, string> = {
|
||||
[ConfigType.API_KEY]: 'lucideKey',
|
||||
[ConfigType.SECRET_KEY]: 'lucideShield',
|
||||
[ConfigType.WEBHOOK_URL]: 'lucideGlobe',
|
||||
[ConfigType.CALLBACK_URL]: 'lucideRefreshCw',
|
||||
[ConfigType.TIMEOUT]: 'lucideClock',
|
||||
[ConfigType.RETRY_COUNT]: 'lucideRepeat',
|
||||
[ConfigType.CUSTOM]: 'lucideSettings'
|
||||
};
|
||||
return icons[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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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<Merchant> {
|
||||
return this.http.post<ApiResponse<Merchant>>(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<ApiMerchant>(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);
|
||||
}
|
||||
// L'API retourne directement un tableau de merchants
|
||||
return this.http.get<ApiMerchant[]>(this.baseApiUrl, { params: httpParams }).pipe(
|
||||
map(apiMerchants => {
|
||||
const total = apiMerchants.length;
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return this.http.get<ApiResponse<PaginatedResponse<Merchant>>>(this.baseApiUrl, { params: httpParams }).pipe(
|
||||
map(response => {
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || 'Failed to load merchants');
|
||||
}
|
||||
return response.data!;
|
||||
// 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<Merchant> {
|
||||
return this.http.get<ApiResponse<Merchant>>(`${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<Merchant> {
|
||||
const numericId = parseInt(id);
|
||||
// L'API retourne directement l'objet merchant
|
||||
return this.http.get<ApiMerchant>(`${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<Merchant> {
|
||||
return this.http.patch<ApiResponse<Merchant>>(`${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<Merchant> {
|
||||
const numericId = parseInt(id);
|
||||
// L'API retourne directement l'objet mis à jour
|
||||
return this.http.patch<ApiMerchant>(`${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<void> {
|
||||
return this.http.delete<ApiResponse<void>>(`${this.baseApiUrl}/${id}`).pipe(
|
||||
map(response => {
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || 'Failed to delete merchant');
|
||||
}
|
||||
deleteMerchant(id: string): Observable<void> {
|
||||
const numericId = parseInt(id);
|
||||
// L'API ne retourne probablement rien ou un simple message
|
||||
return this.http.delete<void>(`${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<MerchantUser> {
|
||||
return this.http.post<ApiResponse<MerchantUser>>(`${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<ApiMerchantUser>(`${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<MerchantUser[]> {
|
||||
return this.http.get<ApiResponse<MerchantUser[]>>(`${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<MerchantUser[]> {
|
||||
const numericMerchantId = parseInt(merchantId);
|
||||
// Option 1: Si vous avez un endpoint spécifique pour les users
|
||||
// return this.http.get<ApiMerchantUser[]>(`${this.baseApiUrl}/${numericMerchantId}/users`).pipe(
|
||||
|
||||
// Option 2: Récupérer le merchant complet et extraire les users
|
||||
return this.http.get<ApiMerchant>(`${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<MerchantUser> {
|
||||
return this.http.patch<ApiResponse<MerchantUser>>(
|
||||
`${this.baseApiUrl}/${merchantId}/users/${userId}/role`,
|
||||
updateUserRole(merchantId: string, userId: string, updateRoleDto: UpdateUserRoleDto): Observable<MerchantUser> {
|
||||
const numericMerchantId = parseInt(merchantId);
|
||||
// L'API retourne directement l'utilisateur mis à jour
|
||||
return this.http.patch<ApiMerchantUser>(
|
||||
`${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<void> {
|
||||
return this.http.delete<ApiResponse<void>>(`${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<void> {
|
||||
const numericMerchantId = parseInt(merchantId);
|
||||
// L'API ne retourne probablement rien
|
||||
return this.http.delete<void>(`${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<Merchant[]> {
|
||||
return this.http.get<ApiResponse<Merchant[]>>(`${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<ApiMerchant[]>(`${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<MerchantConfig, 'id' | 'merchantId'>): Observable<MerchantConfig> {
|
||||
return this.http.post<ApiResponse<MerchantConfig>>(`${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<MerchantConfig, 'id' | 'merchantPartnerId'>): Observable<MerchantConfig> {
|
||||
const numericMerchantId = parseInt(merchantId);
|
||||
const apiConfig: Omit<ApiMerchantConfig, 'id'> = {
|
||||
...config,
|
||||
operatorId: config.operatorId,
|
||||
merchantPartnerId: numericMerchantId
|
||||
};
|
||||
|
||||
// L'API retourne directement la configuration créée
|
||||
return this.http.post<ApiMerchantConfig>(`${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<MerchantConfig>): Observable<MerchantConfig> {
|
||||
return this.http.patch<ApiResponse<MerchantConfig>>(`${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<MerchantConfig>): Observable<MerchantConfig> {
|
||||
const numericConfigId = parseInt(configId);
|
||||
|
||||
// Préparer l'objet de configuration pour l'API
|
||||
const apiConfig: Partial<ApiMerchantConfig> = {
|
||||
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<ApiMerchantConfig>(`${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<void> {
|
||||
return this.http.delete<ApiResponse<void>>(`${this.baseApiUrl}/configs/${configId}`).pipe(
|
||||
map(response => {
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || 'Failed to delete config');
|
||||
}
|
||||
deleteConfig(configId: string): Observable<void> {
|
||||
const numericConfigId = parseInt(configId);
|
||||
// L'API ne retourne probablement rien
|
||||
return this.http.delete<void>(`${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<TechnicalContact, 'id' | 'merchantId'>): Observable<TechnicalContact> {
|
||||
return this.http.post<ApiResponse<TechnicalContact>>(`${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<TechnicalContact, 'id' | 'merchantPartnerId'>): Observable<TechnicalContact> {
|
||||
const numericMerchantId = parseInt(merchantId);
|
||||
const apiContact: Omit<ApiTechnicalContact, 'id'> = {
|
||||
...contact,
|
||||
merchantPartnerId: numericMerchantId
|
||||
};
|
||||
|
||||
// L'API retourne directement le contact créé
|
||||
return this.http.post<ApiTechnicalContact>(`${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<TechnicalContact>): Observable<TechnicalContact> {
|
||||
return this.http.patch<ApiResponse<TechnicalContact>>(`${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<TechnicalContact>): Observable<TechnicalContact> {
|
||||
const numericContactId = parseInt(contactId);
|
||||
|
||||
const apiContact: Partial<ApiTechnicalContact> = {
|
||||
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<ApiTechnicalContact>(`${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<void> {
|
||||
return this.http.delete<ApiResponse<void>>(`${this.baseApiUrl}/contacts/${contactId}`).pipe(
|
||||
map(response => {
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || 'Failed to delete technical contact');
|
||||
}
|
||||
deleteTechnicalContact(contactId: string): Observable<void> {
|
||||
const numericContactId = parseInt(contactId);
|
||||
// L'API ne retourne probablement rien
|
||||
return this.http.delete<void>(`${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<MerchantStatsResponse> {
|
||||
return this.http.get<ApiResponse<MerchantStatsResponse>>(`${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<MerchantStatsResponse>(`${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<Merchant> {
|
||||
return this.http.patch<ApiResponse<Merchant>>(`${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
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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<void>();
|
||||
|
||||
// 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<any>;
|
||||
@ViewChild('editMerchantModal') editMerchantModal!: TemplateRef<any>;
|
||||
@ViewChild('deleteMerchantModal') deleteMerchantModal!: TemplateRef<any>;
|
||||
|
||||
// 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<any>, 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();
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user