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 = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -645,4 +645,4 @@
|
|||||||
.small {
|
.small {
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,41 +35,36 @@ export class App implements OnInit {
|
|||||||
private async initializeAuth(): Promise<void> {
|
private async initializeAuth(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const isAuthenticated = await this.authService.initialize();
|
const isAuthenticated = await this.authService.initialize();
|
||||||
|
|
||||||
setTimeout(() => {
|
// Attendre la vraie route après bootstrap
|
||||||
this.handleInitialNavigation(isAuthenticated);
|
this.router.events
|
||||||
});
|
.pipe(filter(event => event instanceof NavigationEnd))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.handleInitialNavigation(isAuthenticated);
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during authentication initialization:', error);
|
console.error('Error during authentication initialization:', error);
|
||||||
setTimeout(() => {
|
this.router.navigate(['/auth/login']);
|
||||||
this.router.navigate(['/auth/login']);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private handleInitialNavigation(isAuthenticated: boolean): void {
|
private handleInitialNavigation(isAuthenticated: boolean): void {
|
||||||
const currentUrl = this.router.url;
|
const currentUrl = this.router.url;
|
||||||
|
|
||||||
if (!isAuthenticated && this.shouldRedirectToLogin(currentUrl)) {
|
// Non authentifié
|
||||||
this.router.navigate(['/auth/login']);
|
if (!isAuthenticated) {
|
||||||
} else if (isAuthenticated && this.shouldRedirectToDashboard(currentUrl)) {
|
if (!this.isPublicRoute(currentUrl)) {
|
||||||
this.router.navigate(['/dcb-dashboard']);
|
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 {
|
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));
|
return publicRoutes.some(route => url.startsWith(route));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,52 +1,44 @@
|
|||||||
// src/app/core/guards/auth.guard.ts
|
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
import { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '../services/auth.service';
|
||||||
import { RoleService } from '../services/role.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) => {
|
export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||||
const authService = inject(AuthService);
|
const authService = inject(AuthService);
|
||||||
const roleService = inject(RoleService);
|
const roleService = inject(RoleService);
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
|
|
||||||
// Attendre que l'initialisation du service Auth soit terminée
|
|
||||||
return authService.getInitializedState().pipe(
|
return authService.getInitializedState().pipe(
|
||||||
switchMap(initialized => {
|
filter(init => init === true),
|
||||||
if (!initialized) {
|
take(1),
|
||||||
return of(false);
|
switchMap(() => {
|
||||||
}
|
|
||||||
|
// 1) Si déjà authentifié
|
||||||
// 🔒 Étape 1 : Vérifier si déjà authentifié
|
|
||||||
if (authService.isAuthenticated()) {
|
if (authService.isAuthenticated()) {
|
||||||
return of(checkRoleAccess(route, roleService, router, state.url));
|
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();
|
const refreshToken = authService.getRefreshToken();
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
return authService.refreshAccessToken().pipe(
|
return authService.refreshAccessToken().pipe(
|
||||||
tap(() => {
|
tap(() => roleService.refreshRoles()),
|
||||||
// Recharger les rôles après un refresh réussi
|
|
||||||
roleService.refreshRoles();
|
|
||||||
}),
|
|
||||||
map(() => checkRoleAccess(route, roleService, router, state.url)),
|
map(() => checkRoleAccess(route, roleService, router, state.url)),
|
||||||
catchError(() => {
|
catchError(() => {
|
||||||
// En cas d’échec de refresh → déconnexion + redirection login
|
|
||||||
authService.logout().subscribe();
|
authService.logout().subscribe();
|
||||||
return of(redirectToLogin(router, state.url));
|
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));
|
return of(redirectToLogin(router, state.url));
|
||||||
}),
|
}),
|
||||||
catchError(() => {
|
catchError(() => of(redirectToLogin(router, state.url)))
|
||||||
return of(redirectToLogin(router, state.url));
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie l'accès basé sur les rôles requis
|
* Vérifie l'accès basé sur les rôles requis
|
||||||
@ -57,7 +49,7 @@ function checkRoleAccess(
|
|||||||
router: Router,
|
router: Router,
|
||||||
currentUrl: string
|
currentUrl: string
|
||||||
): boolean {
|
): boolean {
|
||||||
const requiredRoles = route.data?.['roles'] as string[];
|
const requiredRoles = route.data?.['requiredRoles'] as string[];
|
||||||
|
|
||||||
// Si aucun rôle requis → accès autorisé
|
// Si aucun rôle requis → accès autorisé
|
||||||
if (!requiredRoles || requiredRoles.length === 0) {
|
if (!requiredRoles || requiredRoles.length === 0) {
|
||||||
@ -67,12 +59,10 @@ function checkRoleAccess(
|
|||||||
const hasRequiredRole = roleService.hasAnyRole(requiredRoles);
|
const hasRequiredRole = roleService.hasAnyRole(requiredRoles);
|
||||||
const currentUserRoles = roleService.getCurrentUserRoles();
|
const currentUserRoles = roleService.getCurrentUserRoles();
|
||||||
|
|
||||||
// ✅ L’utilisateur possède un des rôles requis
|
|
||||||
if (hasRequiredRole) {
|
if (hasRequiredRole) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ❌ Sinon → rediriger vers une page "non autorisée"
|
|
||||||
router.navigate(['/unauthorized'], {
|
router.navigate(['/unauthorized'], {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
requiredRoles: requiredRoles.join(','),
|
requiredRoles: requiredRoles.join(','),
|
||||||
|
|||||||
@ -1,35 +1,29 @@
|
|||||||
// src/app/core/guards/public.guard.ts
|
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { CanActivateFn, Router } from '@angular/router';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '../services/auth.service';
|
||||||
import { map, catchError, of } from 'rxjs';
|
import { map, catchError, of } from 'rxjs';
|
||||||
|
|
||||||
export const publicGuard: CanActivateFn = () => {
|
export const publicGuard: CanActivateFn = (route, state) => {
|
||||||
const authService = inject(AuthService);
|
const authService = inject(AuthService);
|
||||||
const router = inject(Router);
|
|
||||||
|
|
||||||
// 🔒 Si l'utilisateur est déjà authentifié → redirection vers le tableau de bord
|
// Vérifier si un refresh token est disponible
|
||||||
if (authService.isAuthenticated()) {
|
|
||||||
router.navigate(['/dcb-dashboard'], { replaceUrl: true });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔄 Vérifier si un refresh token est disponible
|
|
||||||
const refreshToken = authService.getRefreshToken();
|
const refreshToken = authService.getRefreshToken();
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
return authService.refreshAccessToken().pipe(
|
return authService.refreshAccessToken().pipe(
|
||||||
map(() => {
|
map(() => {
|
||||||
// ✅ Rafraîchissement réussi → redirection vers le dashboard
|
// Rafraîchissement réussi → bloquer l'accès aux pages publiques
|
||||||
router.navigate(['/dcb-dashboard'], { replaceUrl: true });
|
|
||||||
return false;
|
return false;
|
||||||
}),
|
}),
|
||||||
catchError(() => {
|
catchError((error) => {
|
||||||
// ❌ Rafraîchissement échoué → autoriser l’accès à la page publique
|
// Rafraîchissement échoué → autoriser l'accès à la page publique
|
||||||
return of(true);
|
return of(true);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👤 Aucun token → accès autorisé à la page publique
|
if (authService.isAuthenticated()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@ -16,13 +16,6 @@ export enum UserRole {
|
|||||||
DCB_PARTNER_SUPPORT = 'dcb-partner-support'
|
DCB_PARTNER_SUPPORT = 'dcb-partner-support'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MerchantStatus {
|
|
||||||
ACTIVE = 'ACTIVE',
|
|
||||||
INACTIVE = 'INACTIVE',
|
|
||||||
PENDING = 'PENDING',
|
|
||||||
SUSPENDED = 'SUSPENDED'
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ConfigType {
|
export enum ConfigType {
|
||||||
API_KEY = 'API_KEY',
|
API_KEY = 'API_KEY',
|
||||||
SECRET_KEY = 'SECRET_KEY',
|
SECRET_KEY = 'SECRET_KEY',
|
||||||
@ -34,30 +27,27 @@ export enum ConfigType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum Operator {
|
export enum Operator {
|
||||||
ORANGE_CI = 1,
|
ORANGE_OSN = 1
|
||||||
MTN_CI = 2,
|
|
||||||
MOOV_CI = 3,
|
|
||||||
WAVE = 4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === MODÈLES PRINCIPAUX ===
|
// === MODÈLES PRINCIPAUX ===
|
||||||
export interface MerchantConfig {
|
export interface MerchantConfig {
|
||||||
id?: number;
|
id?: string;
|
||||||
name: ConfigType | string;
|
name: ConfigType | string;
|
||||||
value: string;
|
value: string;
|
||||||
operatorId: Operator;
|
operatorId: Operator | null;
|
||||||
merchantId?: number;
|
merchantPartnerId?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TechnicalContact {
|
export interface TechnicalContact {
|
||||||
id?: number;
|
id?: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
email: string;
|
email: string;
|
||||||
merchantId?: number;
|
merchantPartnerId?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
}
|
}
|
||||||
@ -69,26 +59,70 @@ export interface MerchantUser {
|
|||||||
email?: string;
|
email?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
merchantPartnerId?: number;
|
merchantPartnerId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Merchant {
|
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;
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
logo?: string;
|
logo?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
adresse: string;
|
adresse: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
status?: MerchantStatus;
|
configs: ApiMerchantConfig[];
|
||||||
configs: MerchantConfig[];
|
merchantUsers: ApiMerchantUser[];
|
||||||
users: MerchantUser[];
|
technicalContacts: ApiTechnicalContact[];
|
||||||
technicalContacts: TechnicalContact[];
|
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
createdBy?: string;
|
|
||||||
createdByUsername?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === DTOs CRUD ===
|
||||||
// === DTOs CRUD ===
|
// === DTOs CRUD ===
|
||||||
export interface CreateMerchantDto {
|
export interface CreateMerchantDto {
|
||||||
name: string;
|
name: string;
|
||||||
@ -96,18 +130,27 @@ export interface CreateMerchantDto {
|
|||||||
description?: string;
|
description?: string;
|
||||||
adresse: string;
|
adresse: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
configs: Omit<MerchantConfig, 'id' | 'merchantId' | 'createdAt' | 'updatedAt'>[];
|
configs: Omit<MerchantConfig, 'id' | 'merchantPartnerId' | 'createdAt' | 'updatedAt'>[];
|
||||||
technicalContacts: Omit<TechnicalContact, 'id' | 'merchantId' | 'createdAt' | 'updatedAt'>[];
|
technicalContacts: Omit<TechnicalContact, 'id' | 'merchantPartnerId' | 'createdAt' | 'updatedAt'>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateMerchantDto extends Partial<CreateMerchantDto> {
|
export interface UpdateMerchantDto extends Partial<CreateMerchantDto> {}
|
||||||
status?: MerchantStatus;
|
|
||||||
|
// DTO mise à jour d'une configuration
|
||||||
|
export interface UpdateMerchantConfigDto {
|
||||||
|
name?: string;
|
||||||
|
value?: string;
|
||||||
|
operatorId?: Operator | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddUserToMerchantDto {
|
export interface AddUserToMerchantDto {
|
||||||
userId: string;
|
userId: string;
|
||||||
role: UserRole; // Utilisation de vos rôles existants
|
role: UserRole;
|
||||||
merchantPartnerId: number;
|
merchantPartnerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateUserRoleDto {
|
||||||
|
role: UserRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateUserRoleDto {
|
export interface UpdateUserRoleDto {
|
||||||
@ -142,39 +185,15 @@ export interface MerchantStatsResponse {
|
|||||||
// === SEARCH ===
|
// === SEARCH ===
|
||||||
export interface SearchMerchantsParams {
|
export interface SearchMerchantsParams {
|
||||||
query?: string;
|
query?: string;
|
||||||
status?: MerchantStatus;
|
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === UTILITAIRES ===
|
// === UTILITAIRES ===
|
||||||
export class MerchantUtils {
|
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 {
|
static getOperatorName(operatorId: Operator): string {
|
||||||
const operatorNames = {
|
const operatorNames = {
|
||||||
[Operator.ORANGE_CI]: 'Orange CI',
|
[Operator.ORANGE_OSN]: 'Orange OSN'
|
||||||
[Operator.MTN_CI]: 'MTN CI',
|
|
||||||
[Operator.MOOV_CI]: 'Moov CI',
|
|
||||||
[Operator.WAVE]: 'Wave'
|
|
||||||
};
|
};
|
||||||
return operatorNames[operatorId] || 'Inconnu';
|
return operatorNames[operatorId] || 'Inconnu';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,42 +69,30 @@ export class AuthService {
|
|||||||
* Initialise l'authentification au démarrage de l'application
|
* Initialise l'authentification au démarrage de l'application
|
||||||
*/
|
*/
|
||||||
async initialize(): Promise<boolean> {
|
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 {
|
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());
|
await firstValueFrom(this.loadUserProfile());
|
||||||
|
this.authState$.next(true);
|
||||||
setTimeout(() => {
|
this.initialized$.next(true);
|
||||||
this.authState$.next(true);
|
|
||||||
this.initialized$.next(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
} catch {
|
||||||
} catch (error) {
|
|
||||||
this.clearAuthData();
|
this.clearAuthData();
|
||||||
setTimeout(() => {
|
this.initialized$.next(true);
|
||||||
this.initialized$.next(true);
|
|
||||||
});
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,7 +240,6 @@ export class AuthService {
|
|||||||
lastLogin: apiUser.lastLogin || apiUser.lastLoginAt || apiUser.lastConnection || null
|
lastLogin: apiUser.lastLogin || apiUser.lastLoginAt || apiUser.lastConnection || null
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('✅ Utilisateur mappé:', mappedUser);
|
|
||||||
return 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,
|
PaginatedResponse,
|
||||||
MerchantStatsResponse,
|
MerchantStatsResponse,
|
||||||
SearchMerchantsParams,
|
SearchMerchantsParams,
|
||||||
MerchantStatus
|
ApiMerchant,
|
||||||
|
ApiMerchantConfig,
|
||||||
|
ApiTechnicalContact,
|
||||||
|
ApiMerchantUser
|
||||||
} from '@core/models/merchant-config.model';
|
} from '@core/models/merchant-config.model';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@ -29,16 +32,16 @@ export class MerchantConfigService {
|
|||||||
|
|
||||||
// Merchant CRUD Operations
|
// Merchant CRUD Operations
|
||||||
createMerchant(createMerchantDto: CreateMerchantDto): Observable<Merchant> {
|
createMerchant(createMerchantDto: CreateMerchantDto): Observable<Merchant> {
|
||||||
return this.http.post<ApiResponse<Merchant>>(this.baseApiUrl, createMerchantDto).pipe(
|
return this.http.post<ApiMerchant>(this.baseApiUrl, createMerchantDto).pipe(
|
||||||
map(response => {
|
map(apiMerchant => {
|
||||||
if (!response.success) {
|
console.log('Merchant created successfully:', apiMerchant);
|
||||||
throw new Error(response.error || 'Failed to create merchant');
|
return this.mapApiMerchantToMerchant(apiMerchant);
|
||||||
}
|
|
||||||
return response.data!;
|
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error('Error creating merchant:', 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) {
|
if (params?.query) {
|
||||||
httpParams = httpParams.set('query', params.query);
|
httpParams = httpParams.set('query', params.query);
|
||||||
}
|
}
|
||||||
if (params?.status) {
|
// L'API retourne directement un tableau de merchants
|
||||||
httpParams = httpParams.set('status', params.status);
|
return this.http.get<ApiMerchant[]>(this.baseApiUrl, { params: httpParams }).pipe(
|
||||||
}
|
map(apiMerchants => {
|
||||||
|
const total = apiMerchants.length;
|
||||||
return this.http.get<ApiResponse<PaginatedResponse<Merchant>>>(this.baseApiUrl, { params: httpParams }).pipe(
|
const totalPages = Math.ceil(total / limit);
|
||||||
map(response => {
|
|
||||||
if (!response.success) {
|
// Appliquer la pagination côté client
|
||||||
throw new Error(response.error || 'Failed to load merchants');
|
const startIndex = (page - 1) * limit;
|
||||||
}
|
const endIndex = startIndex + limit;
|
||||||
return response.data!;
|
const paginatedItems = apiMerchants.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: paginatedItems.map(apiMerchant => this.mapApiMerchantToMerchant(apiMerchant)),
|
||||||
|
total: total,
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
totalPages: totalPages
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error('Error loading merchants:', error);
|
console.error('Error loading merchants:', error);
|
||||||
@ -69,13 +80,12 @@ export class MerchantConfigService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMerchantById(id: number): Observable<Merchant> {
|
getMerchantById(id: string): Observable<Merchant> {
|
||||||
return this.http.get<ApiResponse<Merchant>>(`${this.baseApiUrl}/${id}`).pipe(
|
const numericId = parseInt(id);
|
||||||
map(response => {
|
// L'API retourne directement l'objet merchant
|
||||||
if (!response.success) {
|
return this.http.get<ApiMerchant>(`${this.baseApiUrl}/${numericId}`).pipe(
|
||||||
throw new Error(response.error || 'Failed to load merchant');
|
map(apiMerchant => {
|
||||||
}
|
return this.mapApiMerchantToMerchant(apiMerchant);
|
||||||
return response.data!;
|
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(`Error loading merchant ${id}:`, error);
|
console.error(`Error loading merchant ${id}:`, error);
|
||||||
@ -84,13 +94,12 @@ export class MerchantConfigService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMerchant(id: number, updateMerchantDto: UpdateMerchantDto): Observable<Merchant> {
|
updateMerchant(id: string, updateMerchantDto: UpdateMerchantDto): Observable<Merchant> {
|
||||||
return this.http.patch<ApiResponse<Merchant>>(`${this.baseApiUrl}/${id}`, updateMerchantDto).pipe(
|
const numericId = parseInt(id);
|
||||||
map(response => {
|
// L'API retourne directement l'objet mis à jour
|
||||||
if (!response.success) {
|
return this.http.patch<ApiMerchant>(`${this.baseApiUrl}/${numericId}`, updateMerchantDto).pipe(
|
||||||
throw new Error(response.error || 'Failed to update merchant');
|
map(apiMerchant => {
|
||||||
}
|
return this.mapApiMerchantToMerchant(apiMerchant);
|
||||||
return response.data!;
|
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(`Error updating merchant ${id}:`, error);
|
console.error(`Error updating merchant ${id}:`, error);
|
||||||
@ -99,12 +108,11 @@ export class MerchantConfigService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteMerchant(id: number): Observable<void> {
|
deleteMerchant(id: string): Observable<void> {
|
||||||
return this.http.delete<ApiResponse<void>>(`${this.baseApiUrl}/${id}`).pipe(
|
const numericId = parseInt(id);
|
||||||
map(response => {
|
// L'API ne retourne probablement rien ou un simple message
|
||||||
if (!response.success) {
|
return this.http.delete<void>(`${this.baseApiUrl}/${numericId}`).pipe(
|
||||||
throw new Error(response.error || 'Failed to delete merchant');
|
map(() => {
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(`Error deleting merchant ${id}:`, error);
|
console.error(`Error deleting merchant ${id}:`, error);
|
||||||
@ -115,12 +123,16 @@ export class MerchantConfigService {
|
|||||||
|
|
||||||
// User Management
|
// User Management
|
||||||
addUserToMerchant(addUserDto: AddUserToMerchantDto): Observable<MerchantUser> {
|
addUserToMerchant(addUserDto: AddUserToMerchantDto): Observable<MerchantUser> {
|
||||||
return this.http.post<ApiResponse<MerchantUser>>(`${this.baseApiUrl}/users`, addUserDto).pipe(
|
// Convertir merchantPartnerId en number pour l'API
|
||||||
map(response => {
|
const apiDto = {
|
||||||
if (!response.success) {
|
...addUserDto,
|
||||||
throw new Error(response.error || 'Failed to add user to merchant');
|
merchantPartnerId: addUserDto.merchantPartnerId ? parseInt(addUserDto.merchantPartnerId) : undefined
|
||||||
}
|
};
|
||||||
return response.data!;
|
|
||||||
|
// 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 => {
|
catchError(error => {
|
||||||
console.error('Error adding user to merchant:', error);
|
console.error('Error adding user to merchant:', error);
|
||||||
@ -129,13 +141,16 @@ export class MerchantConfigService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMerchantUsers(merchantId: number): Observable<MerchantUser[]> {
|
getMerchantUsers(merchantId: string): Observable<MerchantUser[]> {
|
||||||
return this.http.get<ApiResponse<MerchantUser[]>>(`${this.baseApiUrl}/${merchantId}/users`).pipe(
|
const numericMerchantId = parseInt(merchantId);
|
||||||
map(response => {
|
// Option 1: Si vous avez un endpoint spécifique pour les users
|
||||||
if (!response.success) {
|
// return this.http.get<ApiMerchantUser[]>(`${this.baseApiUrl}/${numericMerchantId}/users`).pipe(
|
||||||
throw new Error(response.error || 'Failed to load merchant users');
|
|
||||||
}
|
// Option 2: Récupérer le merchant complet et extraire les users
|
||||||
return response.data!;
|
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 => {
|
catchError(error => {
|
||||||
console.error(`Error loading users for merchant ${merchantId}:`, 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> {
|
updateUserRole(merchantId: string, userId: string, updateRoleDto: UpdateUserRoleDto): Observable<MerchantUser> {
|
||||||
return this.http.patch<ApiResponse<MerchantUser>>(
|
const numericMerchantId = parseInt(merchantId);
|
||||||
`${this.baseApiUrl}/${merchantId}/users/${userId}/role`,
|
// L'API retourne directement l'utilisateur mis à jour
|
||||||
|
return this.http.patch<ApiMerchantUser>(
|
||||||
|
`${this.baseApiUrl}/${numericMerchantId}/users/${userId}/role`,
|
||||||
updateRoleDto
|
updateRoleDto
|
||||||
).pipe(
|
).pipe(
|
||||||
map(response => {
|
map(apiUser => {
|
||||||
if (!response.success) {
|
return this.mapApiUserToUser(apiUser);
|
||||||
throw new Error(response.error || 'Failed to update user role');
|
|
||||||
}
|
|
||||||
return response.data!;
|
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(`Error updating user role for merchant ${merchantId}, user ${userId}:`, 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> {
|
removeUserFromMerchant(merchantId: string, userId: string): Observable<void> {
|
||||||
return this.http.delete<ApiResponse<void>>(`${this.baseApiUrl}/${merchantId}/users/${userId}`).pipe(
|
const numericMerchantId = parseInt(merchantId);
|
||||||
map(response => {
|
// L'API ne retourne probablement rien
|
||||||
if (!response.success) {
|
return this.http.delete<void>(`${this.baseApiUrl}/${numericMerchantId}/users/${userId}`).pipe(
|
||||||
throw new Error(response.error || 'Failed to remove user from merchant');
|
map(() => {
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(`Error removing user ${userId} from merchant ${merchantId}:`, error);
|
console.error(`Error removing user ${userId} from merchant ${merchantId}:`, error);
|
||||||
@ -177,12 +190,10 @@ export class MerchantConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUserMerchants(userId: string): Observable<Merchant[]> {
|
getUserMerchants(userId: string): Observable<Merchant[]> {
|
||||||
return this.http.get<ApiResponse<Merchant[]>>(`${this.baseApiUrl}/user/${userId}`).pipe(
|
// L'API retourne directement un tableau de merchants
|
||||||
map(response => {
|
return this.http.get<ApiMerchant[]>(`${this.baseApiUrl}/user/${userId}`).pipe(
|
||||||
if (!response.success) {
|
map(apiMerchants => {
|
||||||
throw new Error(response.error || 'Failed to load user merchants');
|
return apiMerchants.map(merchant => this.mapApiMerchantToMerchant(merchant));
|
||||||
}
|
|
||||||
return response.data!;
|
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(`Error loading merchants for user ${userId}:`, error);
|
console.error(`Error loading merchants for user ${userId}:`, error);
|
||||||
@ -192,13 +203,18 @@ export class MerchantConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Config Management
|
// Config Management
|
||||||
addConfigToMerchant(merchantId: number, config: Omit<MerchantConfig, 'id' | 'merchantId'>): Observable<MerchantConfig> {
|
addConfigToMerchant(merchantId: string, config: Omit<MerchantConfig, 'id' | 'merchantPartnerId'>): Observable<MerchantConfig> {
|
||||||
return this.http.post<ApiResponse<MerchantConfig>>(`${this.baseApiUrl}/${merchantId}/configs`, config).pipe(
|
const numericMerchantId = parseInt(merchantId);
|
||||||
map(response => {
|
const apiConfig: Omit<ApiMerchantConfig, 'id'> = {
|
||||||
if (!response.success) {
|
...config,
|
||||||
throw new Error(response.error || 'Failed to add config to merchant');
|
operatorId: config.operatorId,
|
||||||
}
|
merchantPartnerId: numericMerchantId
|
||||||
return response.data!;
|
};
|
||||||
|
|
||||||
|
// 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 => {
|
catchError(error => {
|
||||||
console.error(`Error adding config to merchant ${merchantId}:`, 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> {
|
updateConfig(configId: string, config: Partial<MerchantConfig>): Observable<MerchantConfig> {
|
||||||
return this.http.patch<ApiResponse<MerchantConfig>>(`${this.baseApiUrl}/configs/${configId}`, config).pipe(
|
const numericConfigId = parseInt(configId);
|
||||||
map(response => {
|
|
||||||
if (!response.success) {
|
// Préparer l'objet de configuration pour l'API
|
||||||
throw new Error(response.error || 'Failed to update config');
|
const apiConfig: Partial<ApiMerchantConfig> = {
|
||||||
}
|
name: config.name,
|
||||||
return response.data!;
|
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 => {
|
catchError(error => {
|
||||||
console.error(`Error updating config ${configId}:`, error);
|
console.error(`Error updating config ${configId}:`, error);
|
||||||
@ -222,12 +249,12 @@ export class MerchantConfigService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteConfig(configId: number): Observable<void> {
|
deleteConfig(configId: string): Observable<void> {
|
||||||
return this.http.delete<ApiResponse<void>>(`${this.baseApiUrl}/configs/${configId}`).pipe(
|
const numericConfigId = parseInt(configId);
|
||||||
map(response => {
|
// L'API ne retourne probablement rien
|
||||||
if (!response.success) {
|
return this.http.delete<void>(`${this.baseApiUrl}/configs/${numericConfigId}`).pipe(
|
||||||
throw new Error(response.error || 'Failed to delete config');
|
map(() => {
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(`Error deleting config ${configId}:`, error);
|
console.error(`Error deleting config ${configId}:`, error);
|
||||||
@ -237,13 +264,17 @@ export class MerchantConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Technical Contacts Management
|
// Technical Contacts Management
|
||||||
addTechnicalContact(merchantId: number, contact: Omit<TechnicalContact, 'id' | 'merchantId'>): Observable<TechnicalContact> {
|
addTechnicalContactToMerchant(merchantId: string, contact: Omit<TechnicalContact, 'id' | 'merchantPartnerId'>): Observable<TechnicalContact> {
|
||||||
return this.http.post<ApiResponse<TechnicalContact>>(`${this.baseApiUrl}/${merchantId}/contacts`, contact).pipe(
|
const numericMerchantId = parseInt(merchantId);
|
||||||
map(response => {
|
const apiContact: Omit<ApiTechnicalContact, 'id'> = {
|
||||||
if (!response.success) {
|
...contact,
|
||||||
throw new Error(response.error || 'Failed to add technical contact');
|
merchantPartnerId: numericMerchantId
|
||||||
}
|
};
|
||||||
return response.data!;
|
|
||||||
|
// 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 => {
|
catchError(error => {
|
||||||
console.error(`Error adding technical contact to merchant ${merchantId}:`, 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> {
|
updateTechnicalContact(contactId: string, contact: Partial<TechnicalContact>): Observable<TechnicalContact> {
|
||||||
return this.http.patch<ApiResponse<TechnicalContact>>(`${this.baseApiUrl}/contacts/${contactId}`, contact).pipe(
|
const numericContactId = parseInt(contactId);
|
||||||
map(response => {
|
|
||||||
if (!response.success) {
|
const apiContact: Partial<ApiTechnicalContact> = {
|
||||||
throw new Error(response.error || 'Failed to update technical contact');
|
firstName: contact.firstName,
|
||||||
}
|
lastName: contact.lastName,
|
||||||
return response.data!;
|
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 => {
|
catchError(error => {
|
||||||
console.error(`Error updating technical contact ${contactId}:`, error);
|
console.error(`Error updating technical contact ${contactId}:`, error);
|
||||||
@ -267,12 +309,12 @@ export class MerchantConfigService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTechnicalContact(contactId: number): Observable<void> {
|
deleteTechnicalContact(contactId: string): Observable<void> {
|
||||||
return this.http.delete<ApiResponse<void>>(`${this.baseApiUrl}/contacts/${contactId}`).pipe(
|
const numericContactId = parseInt(contactId);
|
||||||
map(response => {
|
// L'API ne retourne probablement rien
|
||||||
if (!response.success) {
|
return this.http.delete<void>(`${this.baseApiUrl}/technical-contacts/${numericContactId}`).pipe(
|
||||||
throw new Error(response.error || 'Failed to delete technical contact');
|
map(() => {
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(`Error deleting technical contact ${contactId}:`, error);
|
console.error(`Error deleting technical contact ${contactId}:`, error);
|
||||||
@ -281,35 +323,57 @@ export class MerchantConfigService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Statistics
|
// Stats
|
||||||
getMerchantStats(): Observable<MerchantStatsResponse> {
|
getMerchantStats(): Observable<MerchantStatsResponse> {
|
||||||
return this.http.get<ApiResponse<MerchantStatsResponse>>(`${this.baseApiUrl}/stats`).pipe(
|
// L'API retourne directement les stats
|
||||||
map(response => {
|
return this.http.get<MerchantStatsResponse>(`${this.baseApiUrl}/stats`).pipe(
|
||||||
if (!response.success) {
|
map(stats => {
|
||||||
throw new Error(response.error || 'Failed to load merchant statistics');
|
return stats;
|
||||||
}
|
|
||||||
return response.data!;
|
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error('Error loading merchant statistics:', error);
|
console.error('Error loading merchant stats:', error);
|
||||||
return throwError(() => error);
|
return throwError(() => error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status Management
|
// ==================== MAPPING METHODS ====================
|
||||||
updateMerchantStatus(merchantId: number, status: MerchantStatus): Observable<Merchant> {
|
|
||||||
return this.http.patch<ApiResponse<Merchant>>(`${this.baseApiUrl}/${merchantId}/status`, { status }).pipe(
|
// Méthode principale de mapping Merchant
|
||||||
map(response => {
|
private mapApiMerchantToMerchant(apiMerchant: any): Merchant {
|
||||||
if (!response.success) {
|
|
||||||
throw new Error(response.error || 'Failed to update merchant status');
|
return {
|
||||||
}
|
...apiMerchant,
|
||||||
return response.data!;
|
id: apiMerchant.id?.toString() || '', // number → string
|
||||||
}),
|
configs: (apiMerchant.configs || []).map((config: any) => this.mapApiConfigToConfig(config)),
|
||||||
catchError(error => {
|
users: (apiMerchant.merchantUsers || []).map((user: any) => this.mapApiUserToUser(user)), // merchantUsers → users
|
||||||
console.error(`Error updating status for merchant ${merchantId}:`, error);
|
technicalContacts: (apiMerchant.technicalContacts || []).map((contact: any) => this.mapApiContactToContact(contact)),
|
||||||
return throwError(() => error);
|
};
|
||||||
})
|
}
|
||||||
);
|
|
||||||
|
// 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 { 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 { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { UiCard } from '@app/components/ui-card';
|
import { catchError, map, of, Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { MerchantConfigService } from './merchant-config.service';
|
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 { 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({
|
@Component({
|
||||||
selector: 'app-merchant-config',
|
selector: 'app-merchant-config',
|
||||||
@ -18,50 +31,73 @@ import { RoleManagementService } from '@core/services/hub-users-roles-management
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgIcon,
|
NgIcon,
|
||||||
NgbProgressbarModule,
|
NgbNavModule,
|
||||||
UiCard
|
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 fb = inject(FormBuilder);
|
||||||
|
private authService = inject(AuthService);
|
||||||
private merchantConfigService = inject(MerchantConfigService);
|
private merchantConfigService = inject(MerchantConfigService);
|
||||||
|
private hubUsersService = inject(HubUsersService);
|
||||||
protected roleService = inject(RoleManagementService);
|
protected roleService = inject(RoleManagementService);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
// Configuration wizard
|
// Configuration
|
||||||
currentStep = 0;
|
readonly UserRole = UserRole;
|
||||||
wizardSteps = [
|
readonly ConfigType = ConfigType;
|
||||||
{ id: 'basic-info', icon: 'lucideBuilding', title: 'Informations de Base', subtitle: 'Détails du merchant' },
|
readonly Operator = Operator;
|
||||||
{ 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' }
|
|
||||||
];
|
|
||||||
|
|
||||||
configLoading = false;
|
// Propriétés de configuration
|
||||||
configError = '';
|
pageTitle: string = 'Gestion des Marchands';
|
||||||
configSuccess = '';
|
pageSubtitle: string = 'Administrez les marchands et leurs configurations techniques';
|
||||||
|
badge: any = { icon: 'lucideSettings', text: 'Merchant Management' };
|
||||||
|
|
||||||
// Formulaires
|
// État de l'interface
|
||||||
merchantForm = this.fb.group({
|
activeTab: 'list' | 'profile' = 'list';
|
||||||
basicInfo: this.fb.group({
|
selectedMerchantId: string | null = null;
|
||||||
name: ['', [Validators.required, Validators.minLength(2)]],
|
selectedConfigId: string | null = null;
|
||||||
logo: [''],
|
|
||||||
description: [''],
|
// Gestion des permissions
|
||||||
adresse: ['', [Validators.required]],
|
currentUserRole: UserRole | null = null;
|
||||||
phone: ['', [Validators.required, Validators.pattern(/^\+?[0-9\s\-\(\)]+$/)]]
|
currentUserType: UserType | null = null;
|
||||||
}),
|
currentMerchantPartnerId: string = '';
|
||||||
technicalContacts: this.fb.array([]),
|
userPermissions: any = null;
|
||||||
configs: this.fb.array([])
|
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
|
// Opérateurs disponibles
|
||||||
operators = [
|
operators = [
|
||||||
{ id: Operator.ORANGE_CI, name: 'Orange CI' },
|
{ id: Operator.ORANGE_OSN, name: 'Orange' }
|
||||||
{ id: Operator.MTN_CI, name: 'MTN CI' },
|
|
||||||
{ id: Operator.MOOV_CI, name: 'Moov CI' },
|
|
||||||
{ id: Operator.WAVE, name: 'Wave' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Types de configuration
|
// Types de configuration
|
||||||
@ -75,220 +111,622 @@ export class MerchantConfig implements OnInit {
|
|||||||
{ name: ConfigType.CUSTOM, label: 'Personnalisé' }
|
{ name: ConfigType.CUSTOM, label: 'Personnalisé' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Rôles disponibles pour les merchants (utilisation de vos rôles existants)
|
// Liste des partenaires marchands (pour les admins)
|
||||||
availableMerchantRoles = MerchantUtils.getAvailableMerchantRoles();
|
merchantPartners: User[] = [];
|
||||||
|
loadingMerchantPartners = false;
|
||||||
|
merchantPartnersError = '';
|
||||||
|
selectedMerchantPartnerId: string = '';
|
||||||
|
|
||||||
|
// Cache des marchands
|
||||||
|
merchantProfiles: { [merchantId: string]: Merchant } = {};
|
||||||
|
loadingMerchants: { [merchantId: string]: boolean } = {};
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
// Ajouter un contact technique par défaut
|
this.activeTab = 'list';
|
||||||
this.addTechnicalContact();
|
this.loadCurrentUserPermissions();
|
||||||
// Ajouter une configuration par défaut
|
this.loadMerchantPartnersIfNeeded();
|
||||||
this.addConfig();
|
this.initializeMerchantPartnerContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation du wizard
|
// ==================== MÉTHODES MANQUANTES POUR LE TEMPLATE ====================
|
||||||
get progressValue(): number {
|
|
||||||
return ((this.currentStep + 1) / this.wizardSteps.length) * 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextStep() {
|
/**
|
||||||
if (this.currentStep < this.wizardSteps.length - 1 && this.isStepValid(this.currentStep)) {
|
* Méthodes pour la gestion des rôles (manquantes)
|
||||||
this.currentStep++;
|
*/
|
||||||
}
|
getRoleBadgeClass(role: UserRole): string {
|
||||||
}
|
return this.roleService.getRoleBadgeClass(role);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleLabel(role: UserRole): string {
|
getRoleLabel(role: UserRole): string {
|
||||||
return this.roleService.getRoleLabel(role);
|
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 { About } from '@modules/about/about';
|
||||||
import { SubscriptionsManagement } from './subscriptions/subscriptions';
|
import { SubscriptionsManagement } from './subscriptions/subscriptions';
|
||||||
import { SubscriptionPayments } from './subscriptions/subscription-payments/subscription-payments';
|
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 = [
|
const routes: Routes = [
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@ -191,7 +191,7 @@ const routes: Routes = [
|
|||||||
// ---------------------------
|
// ---------------------------
|
||||||
{
|
{
|
||||||
path: 'merchant-config',
|
path: 'merchant-config',
|
||||||
component: MerchantConfig,
|
component: MerchantConfigManagement,
|
||||||
canActivate: [authGuard, roleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
title: 'Merchant Config',
|
title: 'Merchant Config',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user