feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
7f26a4bdea
commit
02d58ba4fa
@ -7,6 +7,7 @@ import { provideIcons } from '@ng-icons/core';
|
|||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { filter, map, mergeMap } from 'rxjs';
|
import { filter, map, mergeMap } from 'rxjs';
|
||||||
import { AuthService } from './core/services/auth.service';
|
import { AuthService } from './core/services/auth.service';
|
||||||
|
import { RoleSyncService } from './core/services/role-sync.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@ -22,6 +23,7 @@ export class App implements OnInit {
|
|||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private activatedRoute = inject(ActivatedRoute);
|
private activatedRoute = inject(ActivatedRoute);
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
|
private roleSyncService = inject(RoleSyncService);
|
||||||
private cdr = inject(ChangeDetectorRef);
|
private cdr = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
|
|||||||
149
src/app/core/models/dcb-bo-hub-transaction.model.ts
Normal file
149
src/app/core/models/dcb-bo-hub-transaction.model.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
// [file name]: transactions/models/transaction.ts
|
||||||
|
import { Currency } from '@core/models/dcb-bo-hub-subscription.model';
|
||||||
|
|
||||||
|
// Types de transaction basés sur les abonnements
|
||||||
|
export enum TransactionType {
|
||||||
|
SUBSCRIPTION_PAYMENT = 'SUBSCRIPTION_PAYMENT',
|
||||||
|
SUBSCRIPTION_RENEWAL = 'SUBSCRIPTION_RENEWAL',
|
||||||
|
ONE_TIME_PAYMENT = 'ONE_TIME_PAYMENT'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Transaction {
|
||||||
|
// Identifiants
|
||||||
|
id: string;
|
||||||
|
externalReference?: string;
|
||||||
|
subscriptionId?: number;
|
||||||
|
|
||||||
|
// Informations financières
|
||||||
|
amount: number;
|
||||||
|
currency: Currency;
|
||||||
|
|
||||||
|
// Statut et type
|
||||||
|
status: TransactionStatus;
|
||||||
|
type: TransactionType;
|
||||||
|
|
||||||
|
// Informations produit/abonnement
|
||||||
|
productId: string;
|
||||||
|
productName: string;
|
||||||
|
periodicity?: string;
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
transactionDate: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
nextPaymentDate?: Date;
|
||||||
|
|
||||||
|
// Informations marchand
|
||||||
|
merchantPartnerId?: number;
|
||||||
|
|
||||||
|
// Métadonnées
|
||||||
|
metadata?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionQuery {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
status?: TransactionStatus;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
merchantPartnerId?: number;
|
||||||
|
periodicity?: string;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionStats {
|
||||||
|
total: number;
|
||||||
|
totalAmount: number;
|
||||||
|
successCount: number;
|
||||||
|
failedCount: number;
|
||||||
|
pendingCount: number;
|
||||||
|
averageAmount: number;
|
||||||
|
byPeriodicity?: {
|
||||||
|
DAILY: number;
|
||||||
|
WEEKLY: number;
|
||||||
|
MONTHLY: number;
|
||||||
|
YEARLY: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedTransactions {
|
||||||
|
data: Transaction[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
stats: TransactionStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TransactionStatus =
|
||||||
|
| 'PENDING'
|
||||||
|
| 'SUCCESS'
|
||||||
|
| 'FAILED'
|
||||||
|
| 'CANCELLED'
|
||||||
|
| 'EXPIRED';
|
||||||
|
|
||||||
|
export interface RefundRequest {
|
||||||
|
transactionId: string;
|
||||||
|
subscriptionId?: number;
|
||||||
|
reason?: string;
|
||||||
|
amount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionExportRequest {
|
||||||
|
format: 'csv' | 'excel' | 'pdf';
|
||||||
|
query: TransactionQuery;
|
||||||
|
columns?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilitaires pour les transactions
|
||||||
|
export class TransactionUtils {
|
||||||
|
static getStatusDisplayName(status: TransactionStatus): string {
|
||||||
|
const statusNames = {
|
||||||
|
'PENDING': 'En attente',
|
||||||
|
'SUCCESS': 'Réussi',
|
||||||
|
'FAILED': 'Échoué',
|
||||||
|
'CANCELLED': 'Annulé',
|
||||||
|
'EXPIRED': 'Expiré'
|
||||||
|
};
|
||||||
|
return statusNames[status] || status;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getTypeDisplayName(type: TransactionType): string {
|
||||||
|
const typeNames = {
|
||||||
|
[TransactionType.SUBSCRIPTION_PAYMENT]: 'Paiement d\'abonnement',
|
||||||
|
[TransactionType.SUBSCRIPTION_RENEWAL]: 'Renouvellement d\'abonnement',
|
||||||
|
[TransactionType.ONE_TIME_PAYMENT]: 'Paiement unique'
|
||||||
|
};
|
||||||
|
return typeNames[type] || type;
|
||||||
|
}
|
||||||
|
|
||||||
|
static formatAmount(amount: number, currency: Currency): string {
|
||||||
|
return new Intl.NumberFormat('fr-FR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: currency
|
||||||
|
}).format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getPeriodicityDisplayName(periodicity: string): string {
|
||||||
|
const periodicityNames: Record<string, string> = {
|
||||||
|
'Daily': 'Quotidien',
|
||||||
|
'Weekly': 'Hebdomadaire',
|
||||||
|
'Monthly': 'Mensuel',
|
||||||
|
'Yearly': 'Annuel'
|
||||||
|
};
|
||||||
|
return periodicityNames[periodicity] || periodicity;
|
||||||
|
}
|
||||||
|
|
||||||
|
static mapSubscriptionStatus(status: string): TransactionStatus {
|
||||||
|
const statusMap: Record<string, TransactionStatus> = {
|
||||||
|
'ACTIVE': 'SUCCESS',
|
||||||
|
'PENDING': 'PENDING',
|
||||||
|
'SUSPENDED': 'FAILED',
|
||||||
|
'CANCELLED': 'CANCELLED',
|
||||||
|
'EXPIRED': 'EXPIRED'
|
||||||
|
};
|
||||||
|
return statusMap[status] || 'PENDING';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -46,24 +46,36 @@ export interface UsersStatistics {
|
|||||||
totalUsers: number;
|
totalUsers: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// dcb-bo-hub-user.model.ts - MIS À JOUR
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string; // UUID Keycloak
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName?: string;
|
||||||
lastName: string;
|
lastName?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
userType: UserType; // HUB ou MERCHANT_PARTNER
|
userType: UserType; // HUB ou MERCHANT_PARTNER
|
||||||
|
role: UserRole; // Rôle(s)
|
||||||
|
|
||||||
role: UserRole;
|
// Champ critique pour la logique
|
||||||
|
merchantPartnerId?: string; // ID INT dans Merchant Config pour CE merchant
|
||||||
|
|
||||||
|
// Métadonnées
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
createdByUsername?: string;
|
createdByUsername?: string;
|
||||||
createdTimestamp: number;
|
createdTimestamp?: number;
|
||||||
lastLogin?: number;
|
lastLogin?: number;
|
||||||
profileImage?: string | null
|
}
|
||||||
|
|
||||||
|
export interface AssociateUserToMerchantDto {
|
||||||
|
userId: string;
|
||||||
|
merchantPartnerId: string;
|
||||||
|
role: UserRole; // Rôle à assigner dans le contexte du merchant
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DissociateUserFromMerchantDto {
|
||||||
|
userId: string;
|
||||||
|
merchantPartnerId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SyncResult {
|
export interface SyncResult {
|
||||||
@ -84,6 +96,7 @@ export interface CreateUserDto {
|
|||||||
role: UserRole;
|
role: UserRole;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
emailVerified?: boolean;
|
emailVerified?: boolean;
|
||||||
|
merchantPartnerId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateUserDto {
|
export interface UpdateUserDto {
|
||||||
@ -142,6 +155,7 @@ export interface SearchUsersParams {
|
|||||||
role?: UserRole;
|
role?: UserRole;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
userType?: UserType;
|
userType?: UserType;
|
||||||
|
merchantPartnerId?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,9 +63,7 @@ export interface MerchantUser {
|
|||||||
username?: string;
|
username?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string; // Référence au merchant dans MerchantConfig
|
||||||
merchantPartnerId?: number
|
|
||||||
merchantConfigId?: string; // Référence au merchant dans MerchantConfig
|
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
}
|
}
|
||||||
@ -113,8 +111,6 @@ export interface ApiMerchantUser {
|
|||||||
email?: string;
|
email?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
merchantPartnerId?: number;
|
|
||||||
merchantConfigId?: string;
|
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { environment } from '@environments/environment';
|
import { environment } from '@environments/environment';
|
||||||
import { BehaviorSubject, Observable, throwError, tap, catchError } from 'rxjs';
|
import { BehaviorSubject, Observable, throwError, tap, catchError, finalize, of } from 'rxjs';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
import { DashboardAccessService } from '@modules/dcb-dashboard/services/dashboard-access.service';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
UserType,
|
UserType,
|
||||||
UserRole,
|
UserRole,
|
||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
import { TransactionAccessService } from '@modules/transactions/services/transaction-access.service';
|
||||||
|
|
||||||
// === INTERFACES DTO AUTH ===
|
// === INTERFACES DTO AUTH ===
|
||||||
export interface LoginDto {
|
export interface LoginDto {
|
||||||
@ -59,6 +62,9 @@ export class AuthService {
|
|||||||
private readonly tokenKey = 'access_token';
|
private readonly tokenKey = 'access_token';
|
||||||
private readonly refreshTokenKey = 'refresh_token';
|
private readonly refreshTokenKey = 'refresh_token';
|
||||||
|
|
||||||
|
private readonly dashboardAccessService = inject(DashboardAccessService);
|
||||||
|
private readonly transactionAccessService = inject(TransactionAccessService);
|
||||||
|
|
||||||
private authState$ = new BehaviorSubject<boolean>(this.isAuthenticated());
|
private authState$ = new BehaviorSubject<boolean>(this.isAuthenticated());
|
||||||
private userProfile$ = new BehaviorSubject<User | null>(null);
|
private userProfile$ = new BehaviorSubject<User | null>(null);
|
||||||
private initialized$ = new BehaviorSubject<boolean>(false);
|
private initialized$ = new BehaviorSubject<boolean>(false);
|
||||||
@ -168,19 +174,113 @@ export class AuthService {
|
|||||||
/**
|
/**
|
||||||
* Déconnexion utilisateur
|
* Déconnexion utilisateur
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Déconnexion utilisateur avec nettoyage complet
|
||||||
|
*/
|
||||||
logout(): Observable<LogoutResponseDto> {
|
logout(): Observable<LogoutResponseDto> {
|
||||||
|
const token = this.getAccessToken();
|
||||||
|
|
||||||
|
// Si pas de token, nettoyer et retourner un observable complet
|
||||||
|
if (!token) {
|
||||||
|
this.clearAuthData();
|
||||||
|
return of({ message: 'Already logged out' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter le token dans le header si nécessaire
|
||||||
|
const headers = new HttpHeaders({
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
});
|
||||||
|
|
||||||
return this.http.post<LogoutResponseDto>(
|
return this.http.post<LogoutResponseDto>(
|
||||||
`${environment.iamApiUrl}/auth/logout`,
|
`${environment.iamApiUrl}/auth/logout`,
|
||||||
{}
|
{},
|
||||||
|
{ headers }
|
||||||
).pipe(
|
).pipe(
|
||||||
tap(() => this.clearAuthData()),
|
tap(() => {
|
||||||
catchError(error => {
|
|
||||||
this.clearAuthData();
|
this.clearAuthData();
|
||||||
return throwError(() => error);
|
this.dashboardAccessService.clearCache();
|
||||||
|
this.transactionAccessService.clearCache();
|
||||||
|
this.clearAllStorage(); // Nettoyer tout le storage
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
// Même en cas d'erreur, nettoyer tout
|
||||||
|
this.clearAuthData();
|
||||||
|
this.dashboardAccessService.clearCache();
|
||||||
|
this.transactionAccessService.clearCache();
|
||||||
|
this.clearAllStorage();
|
||||||
|
console.warn('Logout API error, but local data cleared:', error);
|
||||||
|
// Retourner un succès simulé pour permettre la navigation
|
||||||
|
return of({ message: 'Local session cleared' });
|
||||||
|
}),
|
||||||
|
finalize(() => {
|
||||||
|
// Garantir le nettoyage dans tous les cas
|
||||||
|
this.clearAuthData();
|
||||||
|
this.dashboardAccessService.clearCache();
|
||||||
|
this.transactionAccessService.clearCache();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déconnexion forcée sans appel API
|
||||||
|
*/
|
||||||
|
forceLogout(): void {
|
||||||
|
this.clearAuthData();
|
||||||
|
this.dashboardAccessService.clearCache();
|
||||||
|
this.transactionAccessService.clearCache();
|
||||||
|
this.clearAllStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoyer toutes les données d'authentification
|
||||||
|
*/
|
||||||
|
private clearAuthData(): void {
|
||||||
|
|
||||||
|
this.dashboardAccessService.clearCache();
|
||||||
|
this.transactionAccessService.clearCache();
|
||||||
|
|
||||||
|
// Supprimer tous les tokens et données utilisateur
|
||||||
|
localStorage.removeItem(this.tokenKey);
|
||||||
|
localStorage.removeItem(this.refreshTokenKey);
|
||||||
|
localStorage.removeItem('user_profile');
|
||||||
|
localStorage.removeItem('auth_state');
|
||||||
|
|
||||||
|
// Nettoyer sessionStorage également
|
||||||
|
sessionStorage.clear();
|
||||||
|
|
||||||
|
// Réinitialiser les BehaviorSubjects
|
||||||
|
this.authState$.next(false);
|
||||||
|
this.userProfile$.next(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoyer tout le stockage local
|
||||||
|
*/
|
||||||
|
private clearAllStorage(): void {
|
||||||
|
// Supprimer toutes les clés liées à l'authentification
|
||||||
|
Object.keys(localStorage).forEach(key => {
|
||||||
|
if (key.includes('token') || key.includes('auth') || key.includes('user')) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nettoyer sessionStorage
|
||||||
|
sessionStorage.clear();
|
||||||
|
|
||||||
|
// Nettoyer les cookies
|
||||||
|
this.clearCookies();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoyer les cookies
|
||||||
|
*/
|
||||||
|
private clearCookies(): void {
|
||||||
|
document.cookie.split(';').forEach(cookie => {
|
||||||
|
const [name] = cookie.split('=');
|
||||||
|
document.cookie = `${name.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chargement du profil utilisateur
|
* Chargement du profil utilisateur
|
||||||
*/
|
*/
|
||||||
@ -231,6 +331,7 @@ export class AuthService {
|
|||||||
lastName: apiUser.lastName || apiUser.lastname || apiUser.family_name || '',
|
lastName: apiUser.lastName || apiUser.lastname || apiUser.family_name || '',
|
||||||
enabled: apiUser.enabled ?? apiUser.active ?? true,
|
enabled: apiUser.enabled ?? apiUser.active ?? true,
|
||||||
emailVerified: apiUser.emailVerified ?? apiUser.email_verified ?? false,
|
emailVerified: apiUser.emailVerified ?? apiUser.email_verified ?? false,
|
||||||
|
merchantPartnerId: apiUser.merchantPartnerId || apiUser.partnerId || apiUser.merchantId || null,
|
||||||
userType: userType,
|
userType: userType,
|
||||||
role: apiUser.clientRoles || apiUser.clientRoles?.[0] || '', // Gérer rôle unique ou tableau
|
role: apiUser.clientRoles || apiUser.clientRoles?.[0] || '', // Gérer rôle unique ou tableau
|
||||||
createdBy: apiUser.createdBy || apiUser.creatorId || null,
|
createdBy: apiUser.createdBy || apiUser.creatorId || null,
|
||||||
@ -256,13 +357,6 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearAuthData(): void {
|
|
||||||
localStorage.removeItem(this.tokenKey);
|
|
||||||
localStorage.removeItem(this.refreshTokenKey);
|
|
||||||
this.authState$.next(false);
|
|
||||||
this.userProfile$.next(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// === VALIDATION DU TOKEN ===
|
// === VALIDATION DU TOKEN ===
|
||||||
|
|
||||||
validateToken(): Observable<TokenValidationResponseDto> {
|
validateToken(): Observable<TokenValidationResponseDto> {
|
||||||
@ -449,6 +543,14 @@ export class AuthService {
|
|||||||
return profile?.id || null;
|
return profile?.id || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le merchantPartnerId de l'utilisateur courant (si marchand)
|
||||||
|
*/
|
||||||
|
getCurrentMerchantPartnerId(): string | null {
|
||||||
|
const profile = this.userProfile$.value;
|
||||||
|
return profile?.merchantPartnerId || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si le profil fourni est celui de l'utilisateur courant
|
* Vérifie si le profil fourni est celui de l'utilisateur courant
|
||||||
*/
|
*/
|
||||||
|
|||||||
445
src/app/core/services/hub-users-roles-management-old.service.ts
Normal file
445
src/app/core/services/hub-users-roles-management-old.service.ts
Normal file
@ -0,0 +1,445 @@
|
|||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { HubUsersService } from '@modules/hub-users-management/hub-users.service';
|
||||||
|
import { MerchantUsersService } from '@modules/hub-users-management/merchant-users.service';
|
||||||
|
import { BehaviorSubject, Observable, map, tap, of, catchError } from 'rxjs';
|
||||||
|
import { UserRole, UserType, AvailableRole } from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
|
// Interfaces
|
||||||
|
export interface RolePermission {
|
||||||
|
canCreateUsers: boolean;
|
||||||
|
canEditUsers: boolean;
|
||||||
|
canDeleteUsers: boolean;
|
||||||
|
canManageRoles: boolean;
|
||||||
|
canViewStats: boolean;
|
||||||
|
canManageMerchants: boolean;
|
||||||
|
canAccessAdmin: boolean;
|
||||||
|
canAccessSupport: boolean;
|
||||||
|
canAccessPartner: boolean;
|
||||||
|
assignableRoles: UserRole[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailableRolesWithPermissions {
|
||||||
|
roles: (AvailableRole & { permissions: RolePermission })[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoleConfig {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
badgeClass: string;
|
||||||
|
icon: string;
|
||||||
|
permissions: RolePermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permissions par défaut
|
||||||
|
const DEFAULT_PERMISSIONS: RolePermission = {
|
||||||
|
canCreateUsers: false,
|
||||||
|
canEditUsers: false,
|
||||||
|
canDeleteUsers: false,
|
||||||
|
canManageRoles: false,
|
||||||
|
canViewStats: false,
|
||||||
|
canManageMerchants: false,
|
||||||
|
canAccessAdmin: false,
|
||||||
|
canAccessSupport: false,
|
||||||
|
canAccessPartner: false,
|
||||||
|
assignableRoles: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configuration des rôles
|
||||||
|
const ROLE_CONFIG: Record<UserRole, RoleConfig> = {
|
||||||
|
[UserRole.DCB_ADMIN]: {
|
||||||
|
label: 'Administrateur DCB',
|
||||||
|
description: 'Administrateur système avec tous les accès',
|
||||||
|
badgeClass: 'bg-danger',
|
||||||
|
icon: 'lucideShield',
|
||||||
|
permissions: {
|
||||||
|
canCreateUsers: true,
|
||||||
|
canEditUsers: true,
|
||||||
|
canDeleteUsers: true,
|
||||||
|
canManageRoles: true,
|
||||||
|
canViewStats: true,
|
||||||
|
canManageMerchants: true,
|
||||||
|
canAccessAdmin: true,
|
||||||
|
canAccessSupport: true,
|
||||||
|
canAccessPartner: true,
|
||||||
|
assignableRoles: Object.values(UserRole)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[UserRole.DCB_SUPPORT]: {
|
||||||
|
label: 'Support DCB',
|
||||||
|
description: 'Support technique avec accès étendus',
|
||||||
|
badgeClass: 'bg-info',
|
||||||
|
icon: 'lucideHeadphones',
|
||||||
|
permissions: {
|
||||||
|
canCreateUsers: true,
|
||||||
|
canEditUsers: true,
|
||||||
|
canDeleteUsers: false,
|
||||||
|
canManageRoles: true,
|
||||||
|
canViewStats: true,
|
||||||
|
canManageMerchants: true,
|
||||||
|
canAccessAdmin: false,
|
||||||
|
canAccessSupport: true,
|
||||||
|
canAccessPartner: true,
|
||||||
|
assignableRoles: [
|
||||||
|
UserRole.DCB_SUPPORT,
|
||||||
|
UserRole.DCB_PARTNER_ADMIN,
|
||||||
|
UserRole.DCB_PARTNER_MANAGER,
|
||||||
|
UserRole.DCB_PARTNER_SUPPORT
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[UserRole.DCB_PARTNER_ADMIN]: {
|
||||||
|
label: 'Admin Partenaire',
|
||||||
|
description: 'Administrateur de partenaire marchand',
|
||||||
|
badgeClass: 'bg-warning',
|
||||||
|
icon: 'lucideShieldCheck',
|
||||||
|
permissions: {
|
||||||
|
canCreateUsers: true,
|
||||||
|
canEditUsers: true,
|
||||||
|
canDeleteUsers: true,
|
||||||
|
canManageRoles: true,
|
||||||
|
canViewStats: true,
|
||||||
|
canManageMerchants: false,
|
||||||
|
canAccessAdmin: false,
|
||||||
|
canAccessSupport: false,
|
||||||
|
canAccessPartner: false,
|
||||||
|
assignableRoles: [UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[UserRole.DCB_PARTNER_MANAGER]: {
|
||||||
|
label: 'Manager Partenaire',
|
||||||
|
description: 'Manager opérationnel partenaire',
|
||||||
|
badgeClass: 'bg-success',
|
||||||
|
icon: 'lucideUserCog',
|
||||||
|
permissions: {
|
||||||
|
canCreateUsers: false,
|
||||||
|
canEditUsers: false,
|
||||||
|
canDeleteUsers: false,
|
||||||
|
canManageRoles: false,
|
||||||
|
canViewStats: true,
|
||||||
|
canManageMerchants: true,
|
||||||
|
canAccessAdmin: false,
|
||||||
|
canAccessSupport: false,
|
||||||
|
canAccessPartner: true,
|
||||||
|
assignableRoles: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[UserRole.DCB_PARTNER_SUPPORT]: {
|
||||||
|
label: 'Support Partenaire',
|
||||||
|
description: 'Support technique partenaire',
|
||||||
|
badgeClass: 'bg-secondary',
|
||||||
|
icon: 'lucideHeadphones',
|
||||||
|
permissions: {
|
||||||
|
canCreateUsers: false,
|
||||||
|
canEditUsers: false,
|
||||||
|
canDeleteUsers: false,
|
||||||
|
canManageRoles: false,
|
||||||
|
canViewStats: true,
|
||||||
|
canManageMerchants: false,
|
||||||
|
canAccessAdmin: false,
|
||||||
|
canAccessSupport: false,
|
||||||
|
canAccessPartner: true,
|
||||||
|
assignableRoles: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[UserRole.MERCHANT_CONFIG_ADMIN]: {
|
||||||
|
label: 'Admin Marchand',
|
||||||
|
description: 'Administrateur de configuration marchand',
|
||||||
|
badgeClass: 'bg-warning',
|
||||||
|
icon: 'lucideSettings',
|
||||||
|
permissions: DEFAULT_PERMISSIONS
|
||||||
|
},
|
||||||
|
[UserRole.MERCHANT_CONFIG_MANAGER]: {
|
||||||
|
label: 'Manager Marchand',
|
||||||
|
description: 'Manager de configuration marchand',
|
||||||
|
badgeClass: 'bg-success',
|
||||||
|
icon: 'lucideUserCog',
|
||||||
|
permissions: DEFAULT_PERMISSIONS
|
||||||
|
},
|
||||||
|
[UserRole.MERCHANT_CONFIG_TECHNICAL]: {
|
||||||
|
label: 'Technique Marchand',
|
||||||
|
description: 'Support technique configuration marchand',
|
||||||
|
badgeClass: 'bg-secondary',
|
||||||
|
icon: 'lucideWrench',
|
||||||
|
permissions: DEFAULT_PERMISSIONS
|
||||||
|
},
|
||||||
|
[UserRole.MERCHANT_CONFIG_VIEWER]: {
|
||||||
|
label: 'Visualiseur Marchand',
|
||||||
|
description: 'Visualiseur de configuration marchand',
|
||||||
|
badgeClass: 'bg-light',
|
||||||
|
icon: 'lucideEye',
|
||||||
|
permissions: DEFAULT_PERMISSIONS
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Rôles Hub (pour les filtres)
|
||||||
|
const HUB_ROLES = [
|
||||||
|
UserRole.DCB_ADMIN,
|
||||||
|
UserRole.DCB_SUPPORT,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Rôles Marchands (pour les filtres)
|
||||||
|
const MERCHANT_ROLES = [
|
||||||
|
UserRole.DCB_PARTNER_ADMIN,
|
||||||
|
UserRole.DCB_PARTNER_MANAGER,
|
||||||
|
UserRole.DCB_PARTNER_SUPPORT,
|
||||||
|
UserRole.MERCHANT_CONFIG_ADMIN,
|
||||||
|
UserRole.MERCHANT_CONFIG_MANAGER,
|
||||||
|
UserRole.MERCHANT_CONFIG_TECHNICAL,
|
||||||
|
UserRole.MERCHANT_CONFIG_VIEWER
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class RoleManagementService {
|
||||||
|
private hubUsersService = inject(HubUsersService);
|
||||||
|
private merchantUsersService = inject(MerchantUsersService);
|
||||||
|
|
||||||
|
private availableRoles$ = new BehaviorSubject<AvailableRolesWithPermissions | null>(null);
|
||||||
|
private currentUserRole$ = new BehaviorSubject<UserRole | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge les rôles Hub disponibles
|
||||||
|
*/
|
||||||
|
loadAvailableHubRoles(): Observable<AvailableRolesWithPermissions> {
|
||||||
|
return this.loadRoles(
|
||||||
|
() => this.hubUsersService.getAvailableHubRoles(),
|
||||||
|
'hub'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge les rôles Marchands disponibles
|
||||||
|
*/
|
||||||
|
loadAvailableMerchantRoles(): Observable<AvailableRolesWithPermissions> {
|
||||||
|
return this.loadRoles(
|
||||||
|
() => this.merchantUsersService.getAvailableMerchantRoles(),
|
||||||
|
'merchant'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode générique pour charger les rôles
|
||||||
|
*/
|
||||||
|
private loadRoles(
|
||||||
|
fetchFn: () => Observable<{ roles: AvailableRole[] }>,
|
||||||
|
type: 'hub' | 'merchant'
|
||||||
|
): Observable<AvailableRolesWithPermissions> {
|
||||||
|
return fetchFn().pipe(
|
||||||
|
map(apiResponse => ({
|
||||||
|
roles: apiResponse.roles.map(role => ({
|
||||||
|
...role,
|
||||||
|
permissions: this.getPermissionsForRole(role.value)
|
||||||
|
}))
|
||||||
|
})),
|
||||||
|
tap(roles => this.availableRoles$.next(roles)),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error loading ${type} roles:`, error);
|
||||||
|
return of({ roles: [] } as AvailableRolesWithPermissions);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Définit le rôle de l'utilisateur courant
|
||||||
|
*/
|
||||||
|
setCurrentUserRole(role: UserRole): void {
|
||||||
|
this.currentUserRole$.next(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le rôle de l'utilisateur courant
|
||||||
|
*/
|
||||||
|
getCurrentUserRole(): Observable<UserRole | null> {
|
||||||
|
return this.currentUserRole$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la valeur actuelle du rôle utilisateur (synchrone)
|
||||||
|
*/
|
||||||
|
getCurrentUserRoleValue(): UserRole | null {
|
||||||
|
return this.currentUserRole$.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les permissions détaillées selon le rôle
|
||||||
|
*/
|
||||||
|
getPermissionsForRole(role: UserRole | null): RolePermission {
|
||||||
|
if (!role) {
|
||||||
|
return DEFAULT_PERMISSIONS;
|
||||||
|
}
|
||||||
|
return ROLE_CONFIG[role]?.permissions || DEFAULT_PERMISSIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un rôle peut être attribué par l'utilisateur courant
|
||||||
|
*/
|
||||||
|
canAssignRole(currentUserRole: UserRole | null, targetRole: UserRole): boolean {
|
||||||
|
if (!currentUserRole) return false;
|
||||||
|
|
||||||
|
const fullPermissionRoles = [
|
||||||
|
UserRole.DCB_ADMIN,
|
||||||
|
UserRole.DCB_SUPPORT
|
||||||
|
];
|
||||||
|
|
||||||
|
if (fullPermissionRoles.includes(currentUserRole)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = this.getPermissionsForRole(currentUserRole);
|
||||||
|
return permissions.assignableRoles.includes(targetRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes d'utilité pour les permissions
|
||||||
|
canCreateUsers(currentUserRole: UserRole | null): boolean {
|
||||||
|
return this.getPermission(currentUserRole, 'canCreateUsers');
|
||||||
|
}
|
||||||
|
|
||||||
|
canEditUsers(currentUserRole: UserRole | null): boolean {
|
||||||
|
return this.getPermission(currentUserRole, 'canEditUsers');
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeleteUsers(currentUserRole: UserRole | null): boolean {
|
||||||
|
return this.getPermission(currentUserRole, 'canDeleteUsers');
|
||||||
|
}
|
||||||
|
|
||||||
|
canManageRoles(currentUserRole: UserRole | null): boolean {
|
||||||
|
return this.getPermission(currentUserRole, 'canManageRoles');
|
||||||
|
}
|
||||||
|
|
||||||
|
canViewStats(currentUserRole: UserRole | null): boolean {
|
||||||
|
return this.getPermission(currentUserRole, 'canViewStats');
|
||||||
|
}
|
||||||
|
|
||||||
|
canManageMerchants(currentUserRole: UserRole | null): boolean {
|
||||||
|
return this.getPermission(currentUserRole, 'canManageMerchants');
|
||||||
|
}
|
||||||
|
|
||||||
|
canAccessAdmin(currentUserRole: UserRole | null): boolean {
|
||||||
|
return this.getPermission(currentUserRole, 'canAccessAdmin');
|
||||||
|
}
|
||||||
|
|
||||||
|
canAccessSupport(currentUserRole: UserRole | null): boolean {
|
||||||
|
return this.getPermission(currentUserRole, 'canAccessSupport');
|
||||||
|
}
|
||||||
|
|
||||||
|
canAccessPartner(currentUserRole: UserRole | null): boolean {
|
||||||
|
return this.getPermission(currentUserRole, 'canAccessPartner');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode helper générique pour les permissions
|
||||||
|
*/
|
||||||
|
private getPermission(
|
||||||
|
role: UserRole | null,
|
||||||
|
permissionKey: keyof RolePermission
|
||||||
|
): boolean {
|
||||||
|
if (!role) return false;
|
||||||
|
const permissions = this.getPermissionsForRole(role);
|
||||||
|
return Boolean(permissions[permissionKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthodes d'utilité pour les rôles
|
||||||
|
*/
|
||||||
|
getRoleLabel(role: string): string {
|
||||||
|
const userRole = role as UserRole;
|
||||||
|
return ROLE_CONFIG[userRole]?.label || role;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleDescription(role: string | UserRole): string {
|
||||||
|
const userRole = role as UserRole;
|
||||||
|
return ROLE_CONFIG[userRole]?.description || 'Description non disponible';
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleBadgeClass(role: string): string {
|
||||||
|
const userRole = role as UserRole;
|
||||||
|
return ROLE_CONFIG[userRole]?.badgeClass || 'bg-secondary';
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleIcon(role: string): string {
|
||||||
|
const userRole = role as UserRole;
|
||||||
|
return ROLE_CONFIG[userRole]?.icon || 'lucideUser';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifications de type de rôle
|
||||||
|
*/
|
||||||
|
isAdminRole(role: UserRole): boolean {
|
||||||
|
return role === UserRole.DCB_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSupportRole(role: UserRole): boolean {
|
||||||
|
return role === UserRole.DCB_SUPPORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMerchantUserRole(role: UserRole): boolean {
|
||||||
|
return role === UserRole.DCB_PARTNER_ADMIN
|
||||||
|
|| role === UserRole.DCB_PARTNER_MANAGER
|
||||||
|
|| role === UserRole.DCB_PARTNER_SUPPORT
|
||||||
|
|| role === UserRole.MERCHANT_CONFIG_ADMIN
|
||||||
|
|| role === UserRole.MERCHANT_CONFIG_MANAGER
|
||||||
|
|| role === UserRole.MERCHANT_CONFIG_TECHNICAL
|
||||||
|
|| role === UserRole.MERCHANT_CONFIG_VIEWER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestion des listes de rôles
|
||||||
|
*/
|
||||||
|
getAllRoles(): UserRole[] {
|
||||||
|
return Object.values(UserRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHubRoles(): UserRole[] {
|
||||||
|
return [...HUB_ROLES];
|
||||||
|
}
|
||||||
|
|
||||||
|
getMerchantRoles(): UserRole[] {
|
||||||
|
return [...MERCHANT_ROLES];
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssignableRoles(currentUserRole: UserRole | null): UserRole[] {
|
||||||
|
if (!currentUserRole) return [];
|
||||||
|
return this.getPermissionsForRole(currentUserRole).assignableRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssignableHubRoles(currentUserRole: UserRole | null): UserRole[] {
|
||||||
|
return this.filterAssignableRoles(currentUserRole, HUB_ROLES);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssignableMerchantRoles(currentUserRole: UserRole | null): UserRole[] {
|
||||||
|
return this.filterAssignableRoles(currentUserRole, MERCHANT_ROLES);
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterAssignableRoles(
|
||||||
|
currentUserRole: UserRole | null,
|
||||||
|
roleList: readonly UserRole[]
|
||||||
|
): UserRole[] {
|
||||||
|
if (!currentUserRole) return [];
|
||||||
|
const permissions = this.getPermissionsForRole(currentUserRole);
|
||||||
|
return roleList.filter(role => permissions.assignableRoles.includes(role));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifications de rôles
|
||||||
|
*/
|
||||||
|
hasRole(userRole: UserRole | null, targetRole: UserRole): boolean {
|
||||||
|
return userRole === targetRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAnyRole(userRole: UserRole | null, targetRoles: UserRole[]): boolean {
|
||||||
|
return userRole ? targetRoles.includes(userRole) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestion du cache
|
||||||
|
*/
|
||||||
|
clearCache(): void {
|
||||||
|
this.availableRoles$.next(null);
|
||||||
|
this.currentUserRole$.next(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableRoles(): Observable<AvailableRolesWithPermissions | null> {
|
||||||
|
return this.availableRoles$.asObservable();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,445 +1,231 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HubUsersService } from '@modules/hub-users-management/hub-users.service';
|
|
||||||
import { MerchantUsersService } from '@modules/hub-users-management/merchant-users.service';
|
|
||||||
import { BehaviorSubject, Observable, map, tap, of, catchError } from 'rxjs';
|
|
||||||
import { UserRole, UserType, AvailableRole } from '@core/models/dcb-bo-hub-user.model';
|
|
||||||
|
|
||||||
// Interfaces
|
export enum UserRole {
|
||||||
export interface RolePermission {
|
// Rôles Hub
|
||||||
canCreateUsers: boolean;
|
DCB_ADMIN = 'dcb-admin',
|
||||||
canEditUsers: boolean;
|
DCB_SUPPORT = 'dcb-support',
|
||||||
canDeleteUsers: boolean;
|
|
||||||
canManageRoles: boolean;
|
// Rôles Merchant User
|
||||||
canViewStats: boolean;
|
// Rôles Partenaires (Business)
|
||||||
canManageMerchants: boolean;
|
DCB_PARTNER_ADMIN = 'dcb-partner-admin',
|
||||||
canAccessAdmin: boolean;
|
DCB_PARTNER_MANAGER = 'dcb-partner-manager',
|
||||||
canAccessSupport: boolean;
|
DCB_PARTNER_SUPPORT = 'dcb-partner-support',
|
||||||
canAccessPartner: boolean;
|
|
||||||
assignableRoles: UserRole[];
|
// Rôles Configuration Marchands (Technique)
|
||||||
|
MERCHANT_CONFIG_ADMIN = 'ADMIN',
|
||||||
|
MERCHANT_CONFIG_MANAGER = 'MANAGER',
|
||||||
|
MERCHANT_CONFIG_TECHNICAL = 'TECHNICAL',
|
||||||
|
MERCHANT_CONFIG_VIEWER = 'VIEWER',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AvailableRolesWithPermissions {
|
type RoleCategory = 'hub' | 'partner' | 'config';
|
||||||
roles: (AvailableRole & { permissions: RolePermission })[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RoleConfig {
|
@Injectable({ providedIn: 'root' })
|
||||||
label: string;
|
export class RoleManagementService {
|
||||||
description: string;
|
private currentRole: UserRole | null = null;
|
||||||
badgeClass: string;
|
|
||||||
icon: string;
|
|
||||||
permissions: RolePermission;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permissions par défaut
|
// Mapping des rôles équivalents
|
||||||
const DEFAULT_PERMISSIONS: RolePermission = {
|
private readonly roleEquivalents = new Map<UserRole, UserRole[]>([
|
||||||
canCreateUsers: false,
|
[UserRole.DCB_PARTNER_ADMIN, [UserRole.MERCHANT_CONFIG_ADMIN]],
|
||||||
canEditUsers: false,
|
[UserRole.DCB_PARTNER_MANAGER, [UserRole.MERCHANT_CONFIG_MANAGER]],
|
||||||
canDeleteUsers: false,
|
[UserRole.DCB_PARTNER_SUPPORT, [UserRole.MERCHANT_CONFIG_TECHNICAL, UserRole.MERCHANT_CONFIG_VIEWER]],
|
||||||
canManageRoles: false,
|
[UserRole.MERCHANT_CONFIG_ADMIN, [UserRole.DCB_PARTNER_ADMIN]],
|
||||||
canViewStats: false,
|
[UserRole.MERCHANT_CONFIG_MANAGER, [UserRole.DCB_PARTNER_MANAGER]],
|
||||||
canManageMerchants: false,
|
[UserRole.MERCHANT_CONFIG_TECHNICAL, [UserRole.DCB_PARTNER_SUPPORT]],
|
||||||
canAccessAdmin: false,
|
[UserRole.MERCHANT_CONFIG_VIEWER, [UserRole.DCB_PARTNER_SUPPORT]]
|
||||||
canAccessSupport: false,
|
]);
|
||||||
canAccessPartner: false,
|
|
||||||
assignableRoles: []
|
// Catégories des rôles
|
||||||
|
private readonly roleCategories: Record<UserRole, RoleCategory> = {
|
||||||
|
[UserRole.DCB_ADMIN]: 'hub',
|
||||||
|
[UserRole.DCB_SUPPORT]: 'hub',
|
||||||
|
[UserRole.DCB_PARTNER_ADMIN]: 'partner',
|
||||||
|
[UserRole.DCB_PARTNER_MANAGER]: 'partner',
|
||||||
|
[UserRole.DCB_PARTNER_SUPPORT]: 'partner',
|
||||||
|
[UserRole.MERCHANT_CONFIG_ADMIN]: 'config',
|
||||||
|
[UserRole.MERCHANT_CONFIG_MANAGER]: 'config',
|
||||||
|
[UserRole.MERCHANT_CONFIG_TECHNICAL]: 'config',
|
||||||
|
[UserRole.MERCHANT_CONFIG_VIEWER]: 'config'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Configuration des rôles
|
// Labels des rôles
|
||||||
const ROLE_CONFIG: Record<UserRole, RoleConfig> = {
|
private readonly roleLabels: Record<UserRole, string> = {
|
||||||
[UserRole.DCB_ADMIN]: {
|
[UserRole.DCB_ADMIN]: 'Administrateur DCB',
|
||||||
label: 'Administrateur DCB',
|
[UserRole.DCB_SUPPORT]: 'Support DCB',
|
||||||
description: 'Administrateur système avec tous les accès',
|
[UserRole.DCB_PARTNER_ADMIN]: 'Admin Partenaire',
|
||||||
badgeClass: 'bg-danger',
|
[UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire',
|
||||||
icon: 'lucideShield',
|
[UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire',
|
||||||
permissions: {
|
[UserRole.MERCHANT_CONFIG_ADMIN]: 'Admin Configuration',
|
||||||
canCreateUsers: true,
|
[UserRole.MERCHANT_CONFIG_MANAGER]: 'Manager Configuration',
|
||||||
canEditUsers: true,
|
[UserRole.MERCHANT_CONFIG_TECHNICAL]: 'Technique Configuration',
|
||||||
canDeleteUsers: true,
|
[UserRole.MERCHANT_CONFIG_VIEWER]: 'Visualiseur Configuration'
|
||||||
canManageRoles: true,
|
};
|
||||||
canViewStats: true,
|
|
||||||
canManageMerchants: true,
|
|
||||||
canAccessAdmin: true,
|
|
||||||
canAccessSupport: true,
|
|
||||||
canAccessPartner: true,
|
|
||||||
assignableRoles: Object.values(UserRole)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[UserRole.DCB_SUPPORT]: {
|
|
||||||
label: 'Support DCB',
|
|
||||||
description: 'Support technique avec accès étendus',
|
|
||||||
badgeClass: 'bg-info',
|
|
||||||
icon: 'lucideHeadphones',
|
|
||||||
permissions: {
|
|
||||||
canCreateUsers: true,
|
|
||||||
canEditUsers: true,
|
|
||||||
canDeleteUsers: false,
|
|
||||||
canManageRoles: true,
|
|
||||||
canViewStats: true,
|
|
||||||
canManageMerchants: true,
|
|
||||||
canAccessAdmin: false,
|
|
||||||
canAccessSupport: true,
|
|
||||||
canAccessPartner: true,
|
|
||||||
assignableRoles: [
|
|
||||||
UserRole.DCB_SUPPORT,
|
|
||||||
UserRole.DCB_PARTNER_ADMIN,
|
|
||||||
UserRole.DCB_PARTNER_MANAGER,
|
|
||||||
UserRole.DCB_PARTNER_SUPPORT
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[UserRole.DCB_PARTNER_ADMIN]: {
|
|
||||||
label: 'Admin Partenaire',
|
|
||||||
description: 'Administrateur de partenaire marchand',
|
|
||||||
badgeClass: 'bg-warning',
|
|
||||||
icon: 'lucideShieldCheck',
|
|
||||||
permissions: {
|
|
||||||
canCreateUsers: true,
|
|
||||||
canEditUsers: true,
|
|
||||||
canDeleteUsers: true,
|
|
||||||
canManageRoles: true,
|
|
||||||
canViewStats: true,
|
|
||||||
canManageMerchants: false,
|
|
||||||
canAccessAdmin: false,
|
|
||||||
canAccessSupport: false,
|
|
||||||
canAccessPartner: false,
|
|
||||||
assignableRoles: [UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[UserRole.DCB_PARTNER_MANAGER]: {
|
|
||||||
label: 'Manager Partenaire',
|
|
||||||
description: 'Manager opérationnel partenaire',
|
|
||||||
badgeClass: 'bg-success',
|
|
||||||
icon: 'lucideUserCog',
|
|
||||||
permissions: {
|
|
||||||
canCreateUsers: false,
|
|
||||||
canEditUsers: false,
|
|
||||||
canDeleteUsers: false,
|
|
||||||
canManageRoles: false,
|
|
||||||
canViewStats: true,
|
|
||||||
canManageMerchants: true,
|
|
||||||
canAccessAdmin: false,
|
|
||||||
canAccessSupport: false,
|
|
||||||
canAccessPartner: true,
|
|
||||||
assignableRoles: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[UserRole.DCB_PARTNER_SUPPORT]: {
|
|
||||||
label: 'Support Partenaire',
|
|
||||||
description: 'Support technique partenaire',
|
|
||||||
badgeClass: 'bg-secondary',
|
|
||||||
icon: 'lucideHeadphones',
|
|
||||||
permissions: {
|
|
||||||
canCreateUsers: false,
|
|
||||||
canEditUsers: false,
|
|
||||||
canDeleteUsers: false,
|
|
||||||
canManageRoles: false,
|
|
||||||
canViewStats: true,
|
|
||||||
canManageMerchants: false,
|
|
||||||
canAccessAdmin: false,
|
|
||||||
canAccessSupport: false,
|
|
||||||
canAccessPartner: true,
|
|
||||||
assignableRoles: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[UserRole.MERCHANT_CONFIG_ADMIN]: {
|
|
||||||
label: 'Admin Marchand',
|
|
||||||
description: 'Administrateur de configuration marchand',
|
|
||||||
badgeClass: 'bg-warning',
|
|
||||||
icon: 'lucideSettings',
|
|
||||||
permissions: DEFAULT_PERMISSIONS
|
|
||||||
},
|
|
||||||
[UserRole.MERCHANT_CONFIG_MANAGER]: {
|
|
||||||
label: 'Manager Marchand',
|
|
||||||
description: 'Manager de configuration marchand',
|
|
||||||
badgeClass: 'bg-success',
|
|
||||||
icon: 'lucideUserCog',
|
|
||||||
permissions: DEFAULT_PERMISSIONS
|
|
||||||
},
|
|
||||||
[UserRole.MERCHANT_CONFIG_TECHNICAL]: {
|
|
||||||
label: 'Technique Marchand',
|
|
||||||
description: 'Support technique configuration marchand',
|
|
||||||
badgeClass: 'bg-secondary',
|
|
||||||
icon: 'lucideWrench',
|
|
||||||
permissions: DEFAULT_PERMISSIONS
|
|
||||||
},
|
|
||||||
[UserRole.MERCHANT_CONFIG_VIEWER]: {
|
|
||||||
label: 'Visualiseur Marchand',
|
|
||||||
description: 'Visualiseur de configuration marchand',
|
|
||||||
badgeClass: 'bg-light',
|
|
||||||
icon: 'lucideEye',
|
|
||||||
permissions: DEFAULT_PERMISSIONS
|
|
||||||
}
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// Rôles Hub (pour les filtres)
|
// Icônes des rôles
|
||||||
const HUB_ROLES = [
|
private readonly roleIcons: Record<UserRole, string> = {
|
||||||
UserRole.DCB_ADMIN,
|
[UserRole.DCB_ADMIN]: 'lucideShield',
|
||||||
UserRole.DCB_SUPPORT,
|
[UserRole.DCB_SUPPORT]: 'lucideHeadphones',
|
||||||
] as const;
|
[UserRole.DCB_PARTNER_ADMIN]: 'lucideShieldCheck',
|
||||||
|
[UserRole.DCB_PARTNER_MANAGER]: 'lucideUserCog',
|
||||||
|
[UserRole.DCB_PARTNER_SUPPORT]: 'user-headset',
|
||||||
|
[UserRole.MERCHANT_CONFIG_ADMIN]: 'lucideSettings',
|
||||||
|
[UserRole.MERCHANT_CONFIG_MANAGER]: 'lucideSliders',
|
||||||
|
[UserRole.MERCHANT_CONFIG_TECHNICAL]: 'lucideWrench',
|
||||||
|
[UserRole.MERCHANT_CONFIG_VIEWER]: 'lucideEye'
|
||||||
|
};
|
||||||
|
|
||||||
// Rôles Marchands (pour les filtres)
|
// === GESTION DU RÔLE COURANT ===
|
||||||
const MERCHANT_ROLES = [
|
setCurrentRole(role: UserRole | null): void {
|
||||||
UserRole.DCB_PARTNER_ADMIN,
|
this.currentRole = role;
|
||||||
UserRole.DCB_PARTNER_MANAGER,
|
|
||||||
UserRole.DCB_PARTNER_SUPPORT,
|
|
||||||
UserRole.MERCHANT_CONFIG_ADMIN,
|
|
||||||
UserRole.MERCHANT_CONFIG_MANAGER,
|
|
||||||
UserRole.MERCHANT_CONFIG_TECHNICAL,
|
|
||||||
UserRole.MERCHANT_CONFIG_VIEWER
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class RoleManagementService {
|
|
||||||
private hubUsersService = inject(HubUsersService);
|
|
||||||
private merchantUsersService = inject(MerchantUsersService);
|
|
||||||
|
|
||||||
private availableRoles$ = new BehaviorSubject<AvailableRolesWithPermissions | null>(null);
|
|
||||||
private currentUserRole$ = new BehaviorSubject<UserRole | null>(null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Charge les rôles Hub disponibles
|
|
||||||
*/
|
|
||||||
loadAvailableHubRoles(): Observable<AvailableRolesWithPermissions> {
|
|
||||||
return this.loadRoles(
|
|
||||||
() => this.hubUsersService.getAvailableHubRoles(),
|
|
||||||
'hub'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getCurrentRole(): UserRole | null {
|
||||||
* Charge les rôles Marchands disponibles
|
return this.currentRole;
|
||||||
*/
|
|
||||||
loadAvailableMerchantRoles(): Observable<AvailableRolesWithPermissions> {
|
|
||||||
return this.loadRoles(
|
|
||||||
() => this.merchantUsersService.getAvailableMerchantRoles(),
|
|
||||||
'merchant'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// === VÉRIFICATIONS DE RÔLES INDIVIDUELS ===
|
||||||
* Méthode générique pour charger les rôles
|
|
||||||
*/
|
isAdmin(): boolean {
|
||||||
private loadRoles(
|
return this.currentRole === UserRole.DCB_ADMIN;
|
||||||
fetchFn: () => Observable<{ roles: AvailableRole[] }>,
|
|
||||||
type: 'hub' | 'merchant'
|
|
||||||
): Observable<AvailableRolesWithPermissions> {
|
|
||||||
return fetchFn().pipe(
|
|
||||||
map(apiResponse => ({
|
|
||||||
roles: apiResponse.roles.map(role => ({
|
|
||||||
...role,
|
|
||||||
permissions: this.getPermissionsForRole(role.value)
|
|
||||||
}))
|
|
||||||
})),
|
|
||||||
tap(roles => this.availableRoles$.next(roles)),
|
|
||||||
catchError(error => {
|
|
||||||
console.error(`Error loading ${type} roles:`, error);
|
|
||||||
return of({ roles: [] } as AvailableRolesWithPermissions);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
isSupport(): boolean {
|
||||||
* Définit le rôle de l'utilisateur courant
|
return this.currentRole === UserRole.DCB_SUPPORT;
|
||||||
*/
|
|
||||||
setCurrentUserRole(role: UserRole): void {
|
|
||||||
this.currentUserRole$.next(role);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
isPartnerAdmin(): boolean {
|
||||||
* Récupère le rôle de l'utilisateur courant
|
return this.currentRole === UserRole.DCB_PARTNER_ADMIN;
|
||||||
*/
|
|
||||||
getCurrentUserRole(): Observable<UserRole | null> {
|
|
||||||
return this.currentUserRole$.asObservable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
isPartnerManager(): boolean {
|
||||||
* Récupère la valeur actuelle du rôle utilisateur (synchrone)
|
return this.currentRole === UserRole.DCB_PARTNER_MANAGER;
|
||||||
*/
|
|
||||||
getCurrentUserRoleValue(): UserRole | null {
|
|
||||||
return this.currentUserRole$.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
isPartnerSupport(): boolean {
|
||||||
* Récupère les permissions détaillées selon le rôle
|
return this.currentRole === UserRole.DCB_PARTNER_SUPPORT;
|
||||||
*/
|
|
||||||
getPermissionsForRole(role: UserRole | null): RolePermission {
|
|
||||||
if (!role) {
|
|
||||||
return DEFAULT_PERMISSIONS;
|
|
||||||
}
|
|
||||||
return ROLE_CONFIG[role]?.permissions || DEFAULT_PERMISSIONS;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
isConfigAdmin(): boolean {
|
||||||
* Vérifie si un rôle peut être attribué par l'utilisateur courant
|
return this.currentRole === UserRole.MERCHANT_CONFIG_ADMIN;
|
||||||
*/
|
|
||||||
canAssignRole(currentUserRole: UserRole | null, targetRole: UserRole): boolean {
|
|
||||||
if (!currentUserRole) return false;
|
|
||||||
|
|
||||||
const fullPermissionRoles = [
|
|
||||||
UserRole.DCB_ADMIN,
|
|
||||||
UserRole.DCB_SUPPORT
|
|
||||||
];
|
|
||||||
|
|
||||||
if (fullPermissionRoles.includes(currentUserRole)) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissions = this.getPermissionsForRole(currentUserRole);
|
isConfigManager(): boolean {
|
||||||
return permissions.assignableRoles.includes(targetRole);
|
return this.currentRole === UserRole.MERCHANT_CONFIG_MANAGER;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthodes d'utilité pour les permissions
|
isConfigTechnical(): boolean {
|
||||||
canCreateUsers(currentUserRole: UserRole | null): boolean {
|
return this.currentRole === UserRole.MERCHANT_CONFIG_TECHNICAL;
|
||||||
return this.getPermission(currentUserRole, 'canCreateUsers');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canEditUsers(currentUserRole: UserRole | null): boolean {
|
isConfigViewer(): boolean {
|
||||||
return this.getPermission(currentUserRole, 'canEditUsers');
|
return this.currentRole === UserRole.MERCHANT_CONFIG_VIEWER;
|
||||||
}
|
}
|
||||||
|
|
||||||
canDeleteUsers(currentUserRole: UserRole | null): boolean {
|
// === VÉRIFICATIONS AVEC MAPPING ===
|
||||||
return this.getPermission(currentUserRole, 'canDeleteUsers');
|
|
||||||
|
isAnyAdmin(): boolean {
|
||||||
|
return this.isAdmin() || this.isPartnerAdmin() || this.isConfigAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
canManageRoles(currentUserRole: UserRole | null): boolean {
|
isAnyManager(): boolean {
|
||||||
return this.getPermission(currentUserRole, 'canManageRoles');
|
return this.isPartnerManager() || this.isConfigManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
canViewStats(currentUserRole: UserRole | null): boolean {
|
isAnySupport(): boolean {
|
||||||
return this.getPermission(currentUserRole, 'canViewStats');
|
return this.isSupport() || this.isPartnerSupport() || this.isConfigTechnical() || this.isConfigViewer();
|
||||||
}
|
}
|
||||||
|
|
||||||
canManageMerchants(currentUserRole: UserRole | null): boolean {
|
// === VÉRIFICATIONS DE CATÉGORIES ===
|
||||||
return this.getPermission(currentUserRole, 'canManageMerchants');
|
|
||||||
|
isHubUser(): boolean {
|
||||||
|
return this.isAdmin() || this.isSupport();
|
||||||
}
|
}
|
||||||
|
|
||||||
canAccessAdmin(currentUserRole: UserRole | null): boolean {
|
isPartnerUser(): boolean {
|
||||||
return this.getPermission(currentUserRole, 'canAccessAdmin');
|
return this.isPartnerAdmin() || this.isPartnerManager() || this.isPartnerSupport();
|
||||||
}
|
}
|
||||||
|
|
||||||
canAccessSupport(currentUserRole: UserRole | null): boolean {
|
isConfigUser(): boolean {
|
||||||
return this.getPermission(currentUserRole, 'canAccessSupport');
|
return this.isConfigAdmin() || this.isConfigManager() || this.isConfigTechnical() || this.isConfigViewer();
|
||||||
}
|
}
|
||||||
|
|
||||||
canAccessPartner(currentUserRole: UserRole | null): boolean {
|
getRoleCategory(): RoleCategory | null {
|
||||||
return this.getPermission(currentUserRole, 'canAccessPartner');
|
if (!this.currentRole) return null;
|
||||||
|
return this.roleCategories[this.currentRole];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// === MAPPING ET ÉQUIVALENTS ===
|
||||||
* Méthode helper générique pour les permissions
|
|
||||||
*/
|
getEquivalentRoles(): UserRole[] {
|
||||||
private getPermission(
|
if (!this.currentRole) return [];
|
||||||
role: UserRole | null,
|
|
||||||
permissionKey: keyof RolePermission
|
const equivalents = [this.currentRole];
|
||||||
): boolean {
|
const mappedRoles = this.roleEquivalents.get(this.currentRole);
|
||||||
if (!role) return false;
|
|
||||||
const permissions = this.getPermissionsForRole(role);
|
if (mappedRoles) {
|
||||||
return Boolean(permissions[permissionKey]);
|
equivalents.push(...mappedRoles);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return [...new Set(equivalents)];
|
||||||
* Méthodes d'utilité pour les rôles
|
|
||||||
*/
|
|
||||||
getRoleLabel(role: string): string {
|
|
||||||
const userRole = role as UserRole;
|
|
||||||
return ROLE_CONFIG[userRole]?.label || role;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleDescription(role: string | UserRole): string {
|
hasEquivalentRole(targetRole: UserRole): boolean {
|
||||||
const userRole = role as UserRole;
|
if (!this.currentRole) return false;
|
||||||
return ROLE_CONFIG[userRole]?.description || 'Description non disponible';
|
|
||||||
|
if (this.currentRole === targetRole) return true;
|
||||||
|
|
||||||
|
const equivalents = this.roleEquivalents.get(this.currentRole);
|
||||||
|
return equivalents ? equivalents.includes(targetRole) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleBadgeClass(role: string): string {
|
getMappedRole(): UserRole | null {
|
||||||
const userRole = role as UserRole;
|
if (!this.currentRole) return null;
|
||||||
return ROLE_CONFIG[userRole]?.badgeClass || 'bg-secondary';
|
|
||||||
|
const equivalents = this.roleEquivalents.get(this.currentRole);
|
||||||
|
return equivalents && equivalents.length > 0 ? equivalents[0] : this.currentRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleIcon(role: string): string {
|
// === VÉRIFICATIONS GÉNÉRIQUES ===
|
||||||
const userRole = role as UserRole;
|
|
||||||
return ROLE_CONFIG[userRole]?.icon || 'lucideUser';
|
hasRole(role: UserRole): boolean {
|
||||||
|
return this.currentRole === role;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
hasAnyRole(...roles: UserRole[]): boolean {
|
||||||
* Vérifications de type de rôle
|
if (!this.currentRole) return false;
|
||||||
*/
|
return roles.includes(this.currentRole);
|
||||||
isAdminRole(role: UserRole): boolean {
|
|
||||||
return role === UserRole.DCB_ADMIN;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isSupportRole(role: UserRole): boolean {
|
// === UTILITAIRES ===
|
||||||
return role === UserRole.DCB_SUPPORT;
|
|
||||||
|
getRoleLabel(role?: UserRole): string {
|
||||||
|
const targetRole = role || this.currentRole;
|
||||||
|
return targetRole ? this.roleLabels[targetRole] || targetRole : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
isMerchantUserRole(role: UserRole): boolean {
|
getRoleIcon(role?: UserRole): string {
|
||||||
return role === UserRole.DCB_PARTNER_ADMIN
|
const targetRole = role || this.currentRole;
|
||||||
|| role === UserRole.DCB_PARTNER_MANAGER
|
return targetRole ? this.roleIcons[targetRole] || 'user' : 'user';
|
||||||
|| role === UserRole.DCB_PARTNER_SUPPORT
|
|
||||||
|| role === UserRole.MERCHANT_CONFIG_ADMIN
|
|
||||||
|| role === UserRole.MERCHANT_CONFIG_MANAGER
|
|
||||||
|| role === UserRole.MERCHANT_CONFIG_TECHNICAL
|
|
||||||
|| role === UserRole.MERCHANT_CONFIG_VIEWER;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gestion des listes de rôles
|
|
||||||
*/
|
|
||||||
getAllRoles(): UserRole[] {
|
getAllRoles(): UserRole[] {
|
||||||
return Object.values(UserRole);
|
return Object.values(UserRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
getHubRoles(): UserRole[] {
|
getHubRoles(): UserRole[] {
|
||||||
return [...HUB_ROLES];
|
return [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
|
||||||
}
|
}
|
||||||
|
|
||||||
getMerchantRoles(): UserRole[] {
|
getPartnerRoles(): UserRole[] {
|
||||||
return [...MERCHANT_ROLES];
|
return [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
|
||||||
}
|
}
|
||||||
|
|
||||||
getAssignableRoles(currentUserRole: UserRole | null): UserRole[] {
|
getConfigRoles(): UserRole[] {
|
||||||
if (!currentUserRole) return [];
|
return [
|
||||||
return this.getPermissionsForRole(currentUserRole).assignableRoles;
|
UserRole.MERCHANT_CONFIG_ADMIN,
|
||||||
}
|
UserRole.MERCHANT_CONFIG_MANAGER,
|
||||||
|
UserRole.MERCHANT_CONFIG_TECHNICAL,
|
||||||
getAssignableHubRoles(currentUserRole: UserRole | null): UserRole[] {
|
UserRole.MERCHANT_CONFIG_VIEWER
|
||||||
return this.filterAssignableRoles(currentUserRole, HUB_ROLES);
|
];
|
||||||
}
|
|
||||||
|
|
||||||
getAssignableMerchantRoles(currentUserRole: UserRole | null): UserRole[] {
|
|
||||||
return this.filterAssignableRoles(currentUserRole, MERCHANT_ROLES);
|
|
||||||
}
|
|
||||||
|
|
||||||
private filterAssignableRoles(
|
|
||||||
currentUserRole: UserRole | null,
|
|
||||||
roleList: readonly UserRole[]
|
|
||||||
): UserRole[] {
|
|
||||||
if (!currentUserRole) return [];
|
|
||||||
const permissions = this.getPermissionsForRole(currentUserRole);
|
|
||||||
return roleList.filter(role => permissions.assignableRoles.includes(role));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifications de rôles
|
|
||||||
*/
|
|
||||||
hasRole(userRole: UserRole | null, targetRole: UserRole): boolean {
|
|
||||||
return userRole === targetRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasAnyRole(userRole: UserRole | null, targetRoles: UserRole[]): boolean {
|
|
||||||
return userRole ? targetRoles.includes(userRole) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gestion du cache
|
|
||||||
*/
|
|
||||||
clearCache(): void {
|
|
||||||
this.availableRoles$.next(null);
|
|
||||||
this.currentUserRole$.next(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAvailableRoles(): Observable<AvailableRolesWithPermissions | null> {
|
|
||||||
return this.availableRoles$.asObservable();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,6 +2,7 @@ import { Injectable, inject } from '@angular/core';
|
|||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { PermissionsService } from './permissions.service';
|
import { PermissionsService } from './permissions.service';
|
||||||
import { MenuItemType, UserDropdownItemType } from '@/app/types/layout';
|
import { MenuItemType, UserDropdownItemType } from '@/app/types/layout';
|
||||||
|
import { UserRole } from './hub-users-roles-management.service';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class MenuService {
|
export class MenuService {
|
||||||
@ -23,7 +24,7 @@ export class MenuService {
|
|||||||
return this.permissionsService.canAccessModule(modulePath, userRoles);
|
return this.permissionsService.canAccessModule(modulePath, userRoles);
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterMenuItems(items: MenuItemType[], userRoles: string[]): MenuItemType[] {
|
private filterMenuItems(items: MenuItemType[], userRoles: UserRole[]): MenuItemType[] {
|
||||||
return items
|
return items
|
||||||
.filter(item => this.shouldDisplayMenuItem(item, userRoles))
|
.filter(item => this.shouldDisplayMenuItem(item, userRoles))
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
@ -32,7 +33,7 @@ export class MenuService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldDisplayMenuItem(item: MenuItemType, userRoles: string[]): boolean {
|
private shouldDisplayMenuItem(item: MenuItemType, userRoles: UserRole[]): boolean {
|
||||||
if (item.isTitle) return true;
|
if (item.isTitle) return true;
|
||||||
|
|
||||||
if (item.url && item.url !== '#') {
|
if (item.url && item.url !== '#') {
|
||||||
@ -47,7 +48,7 @@ export class MenuService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterUserDropdownItems(items: UserDropdownItemType[], userRoles: string[]): UserDropdownItemType[] {
|
private filterUserDropdownItems(items: UserDropdownItemType[], userRoles: UserRole[]): UserDropdownItemType[] {
|
||||||
return items.filter(item => {
|
return items.filter(item => {
|
||||||
if (item.isDivider || item.isHeader || !item.url || item.url === '#') {
|
if (item.isDivider || item.isHeader || !item.url || item.url === '#') {
|
||||||
return true;
|
return true;
|
||||||
@ -132,7 +133,6 @@ export class MenuService {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mettez à jour votre méthode
|
|
||||||
private getFullUserDropdown(): UserDropdownItemType[] {
|
private getFullUserDropdown(): UserDropdownItemType[] {
|
||||||
return [
|
return [
|
||||||
{ label: 'Welcome back!', isHeader: true },
|
{ label: 'Welcome back!', isHeader: true },
|
||||||
|
|||||||
49
src/app/core/services/role-sync.service.ts
Normal file
49
src/app/core/services/role-sync.service.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { RoleManagementService } from './hub-users-roles-management.service';
|
||||||
|
import { filter } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class RoleSyncService {
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
private roleService = inject(RoleManagementService);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.setupRoleSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupRoleSync(): void {
|
||||||
|
// Synchroniser le rôle lorsque l'état d'authentification change
|
||||||
|
this.authService.getAuthState().subscribe(isAuthenticated => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
this.syncRoleFromAuth();
|
||||||
|
} else {
|
||||||
|
// Utilisateur déconnecté - réinitialiser le rôle
|
||||||
|
this.roleService.setCurrentRole(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Synchroniser également lorsque le profil utilisateur change
|
||||||
|
this.authService.getUserProfile()
|
||||||
|
.pipe(filter(profile => !!profile))
|
||||||
|
.subscribe(() => {
|
||||||
|
if (this.authService.isAuthenticated()) {
|
||||||
|
this.syncRoleFromAuth();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncRoleFromAuth(): void {
|
||||||
|
try {
|
||||||
|
const userRole = this.authService.getCurrentUserRole();
|
||||||
|
if (userRole) {
|
||||||
|
this.roleService.setCurrentRole(userRole);
|
||||||
|
console.log(`Rôle synchronisé: ${userRole}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la synchronisation du rôle:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,18 +1,47 @@
|
|||||||
<div class="sidenav-user d-flex align-items-center">
|
<div class="sidenav-user d-flex align-items-center">
|
||||||
|
<!-- État de chargement avec @if -->
|
||||||
|
@if (isLoading) {
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="spinner-border spinner-border-sm text-primary me-2" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="my-0 fw-semibold">Chargement...</h5>
|
||||||
|
<h6 class="my-0 text-muted">Profil utilisateur</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- États normal et erreur avec @if -->
|
||||||
|
@if (!isLoading) {
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
<img
|
<img
|
||||||
src="assets/images/users/user-2.jpg"
|
[src]="getUserAvatar()"
|
||||||
class="rounded-circle me-2"
|
class="rounded-circle me-2"
|
||||||
width="36"
|
width="36"
|
||||||
height="36"
|
height="36"
|
||||||
alt="user-image"
|
alt="user-image"
|
||||||
onerror="this.src='assets/images/users/user-default.jpg'"
|
(error)="onAvatarError($event)"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h5 class="my-0 fw-semibold">
|
<h5 class="my-0 fw-semibold">
|
||||||
{{ getUserInitials() }} | {{ getDisplayName() }}
|
{{ getDisplayName() || 'Utilisateur' }}
|
||||||
</h5>
|
</h5>
|
||||||
<h6 class="my-0 text-muted">
|
<h6 class="my-0 text-muted">
|
||||||
{{ getUserRole(user) }}
|
<!-- Afficher le rôle ou un message d'erreur -->
|
||||||
|
@if (!hasError) {
|
||||||
|
<span>{{ getUserRole() }}</span>
|
||||||
|
}
|
||||||
|
@if (hasError) {
|
||||||
|
<span class="text-danger">
|
||||||
|
Erreur -
|
||||||
|
<button class="btn btn-link btn-sm p-0" (click)="retryLoadProfile()">
|
||||||
|
Réessayer
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@ -3,7 +3,7 @@ import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
import { userDropdownItems } from '@layouts/components/data';
|
import { userDropdownItems } from '@layouts/components/data';
|
||||||
import { AuthService } from '@/app/core/services/auth.service';
|
import { AuthService } from '@/app/core/services/auth.service';
|
||||||
import { User, UserRole } from '@core/models/dcb-bo-hub-user.model';
|
import { User, UserRole } from '@core/models/dcb-bo-hub-user.model';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil, distinctUntilChanged, filter, startWith } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-profile',
|
selector: 'app-user-profile',
|
||||||
@ -19,42 +19,158 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
user: User | null = null;
|
user: User | null = null;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
hasError = false;
|
||||||
|
currentProfileLoaded = false;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadUser();
|
console.log('🔄 UserProfileComponent initializing...');
|
||||||
|
|
||||||
// Subscribe to auth state changes
|
// S'abonner aux changements du profil utilisateur
|
||||||
|
this.setupProfileSubscription();
|
||||||
|
|
||||||
|
// S'abonner aux changements d'état d'authentification
|
||||||
|
this.setupAuthStateSubscription();
|
||||||
|
|
||||||
|
// Vérifier l'état initial
|
||||||
|
this.checkInitialState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie l'état initial sans accéder à la propriété privée
|
||||||
|
*/
|
||||||
|
private checkInitialState(): void {
|
||||||
|
// Utiliser la méthode publique isAuthenticated()
|
||||||
|
if (this.authService.isAuthenticated()) {
|
||||||
|
console.log('🔍 User is authenticated, checking profile...');
|
||||||
|
// Le profil sera chargé via la subscription
|
||||||
|
} else {
|
||||||
|
console.log('🔐 User not authenticated');
|
||||||
|
this.user = null;
|
||||||
|
this.isLoading = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S'abonne aux changements du profil utilisateur via la méthode publique
|
||||||
|
*/
|
||||||
|
private setupProfileSubscription(): void {
|
||||||
|
this.authService.getUserProfile()
|
||||||
|
.pipe(
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
startWith(null), // Émettre null au début
|
||||||
|
filter(profile => {
|
||||||
|
// Filtrer les valeurs null si on a déjà un profil
|
||||||
|
if (this.currentProfileLoaded && profile === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
distinctUntilChanged((prev, curr) => {
|
||||||
|
// Comparer les profils par leur ID
|
||||||
|
return prev?.id === curr?.id;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (profile) => {
|
||||||
|
if (profile) {
|
||||||
|
console.log('📥 User profile updated:', profile.username);
|
||||||
|
this.user = profile;
|
||||||
|
this.currentProfileLoaded = true;
|
||||||
|
} else {
|
||||||
|
console.log('📭 User profile cleared');
|
||||||
|
this.user = null;
|
||||||
|
this.currentProfileLoaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
this.hasError = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('❌ Error in profile subscription:', error);
|
||||||
|
this.hasError = true;
|
||||||
|
this.isLoading = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S'abonne aux changements d'état d'authentification
|
||||||
|
*/
|
||||||
|
private setupAuthStateSubscription(): void {
|
||||||
this.authService.getAuthState()
|
this.authService.getAuthState()
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (isAuthenticated) => {
|
next: (isAuthenticated) => {
|
||||||
|
console.log('🔐 Auth state changed:', isAuthenticated);
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
this.loadUser();
|
// Si l'utilisateur vient de se connecter, forcer le chargement du profil
|
||||||
|
if (!this.currentProfileLoaded) {
|
||||||
|
console.log('🔄 User just logged in, loading profile...');
|
||||||
|
this.loadUserProfile();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Si l'utilisateur s'est déconnecté
|
||||||
|
console.log('👋 User logged out');
|
||||||
this.user = null;
|
this.user = null;
|
||||||
|
this.currentProfileLoaded = false;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadUser(): void {
|
/**
|
||||||
|
* Charge le profil utilisateur explicitement
|
||||||
|
*/
|
||||||
|
loadUserProfile(): void {
|
||||||
|
console.log('🚀 Loading user profile...');
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
this.hasError = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
|
||||||
this.authService.getProfile()
|
this.authService.loadUserProfile()
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (profile) => {
|
next: (profile) => {
|
||||||
this.user = profile;
|
console.log('✅ Profile loaded successfully:', profile.username);
|
||||||
|
// Note: le profil sera automatiquement mis à jour via la subscription getUserProfile()
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Failed to load user profile:', error);
|
console.error('❌ Failed to load user profile:', error);
|
||||||
this.user = null;
|
this.hasError = true;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
|
||||||
|
// Essayer de rafraîchir le token si erreur 401
|
||||||
|
if (error.status === 401) {
|
||||||
|
this.authService.refreshAccessToken().subscribe({
|
||||||
|
next: () => {
|
||||||
|
// Recharger le profil après rafraîchissement
|
||||||
|
this.loadUserProfile();
|
||||||
|
},
|
||||||
|
error: (refreshError) => {
|
||||||
|
console.error('❌ Refresh token failed:', refreshError);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode pour réessayer le chargement en cas d'erreur
|
||||||
|
*/
|
||||||
|
retryLoadProfile(): void {
|
||||||
|
console.log('🔄 Retrying profile load...');
|
||||||
|
this.loadUserProfile();
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
@ -64,7 +180,7 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
|||||||
// Helper methods for template
|
// Helper methods for template
|
||||||
getUserInitials(): string {
|
getUserInitials(): string {
|
||||||
if (!this.user?.firstName || !this.user?.lastName) {
|
if (!this.user?.firstName || !this.user?.lastName) {
|
||||||
return 'UU'; // User Unknown
|
return this.user?.username?.substring(0, 2).toUpperCase() || 'UU';
|
||||||
}
|
}
|
||||||
return `${this.user.firstName.charAt(0)}${this.user.lastName.charAt(0)}`.toUpperCase();
|
return `${this.user.firstName.charAt(0)}${this.user.lastName.charAt(0)}`.toUpperCase();
|
||||||
}
|
}
|
||||||
@ -75,26 +191,70 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get user role with proper mapping
|
// Get user role with proper mapping
|
||||||
getUserRole(user: User | null): string {
|
getUserRole(): string {
|
||||||
if (!user) return 'Utilisateur';
|
if (!this.user) return 'Utilisateur';
|
||||||
|
|
||||||
// Use role from profile or fallback to token roles
|
// Utiliser le rôle du profil ou la méthode publique du service
|
||||||
const role = user.role || this.authService.getCurrentUserRoles();
|
const role = this.user.role || this.authService.getCurrentUserPrimaryRole();
|
||||||
|
|
||||||
|
if (!role) return 'Utilisateur';
|
||||||
|
|
||||||
// Map role to display name
|
// Map role to display name
|
||||||
const roleDisplayNames: { [key in UserRole]: string } = {
|
const roleDisplayNames: { [key: string]: string } = {
|
||||||
[UserRole.DCB_ADMIN]: 'Administrateur système avec tous les accès',
|
[UserRole.DCB_ADMIN]: 'Administrateur système',
|
||||||
[UserRole.DCB_SUPPORT]: 'Support technique avec accès étendus',
|
[UserRole.DCB_SUPPORT]: 'Support technique',
|
||||||
[UserRole.DCB_PARTNER_ADMIN]: 'Administrateur de partenaire marchand',
|
[UserRole.DCB_PARTNER_ADMIN]: 'Administrateur partenaire',
|
||||||
[UserRole.DCB_PARTNER_MANAGER]: 'Manager opérationnel partenaire',
|
[UserRole.DCB_PARTNER_MANAGER]: 'Manager opérationnel',
|
||||||
[UserRole.DCB_PARTNER_SUPPORT]: 'Support technique partenaire',
|
[UserRole.DCB_PARTNER_SUPPORT]: 'Support technique',
|
||||||
[UserRole.MERCHANT_CONFIG_ADMIN]: 'Administrateur de partenaire marchand',
|
[UserRole.MERCHANT_CONFIG_ADMIN]: 'Administrateur configuration',
|
||||||
[UserRole.MERCHANT_CONFIG_MANAGER]: 'Manager opérationnel partenaire',
|
[UserRole.MERCHANT_CONFIG_MANAGER]: 'Manager configuration',
|
||||||
[UserRole.MERCHANT_CONFIG_TECHNICAL]: 'Support technique partenaire',
|
[UserRole.MERCHANT_CONFIG_TECHNICAL]: 'Technicien',
|
||||||
[UserRole.MERCHANT_CONFIG_VIEWER]: 'Support technique partenaire'
|
[UserRole.MERCHANT_CONFIG_VIEWER]: 'Consultant'
|
||||||
};
|
};
|
||||||
|
|
||||||
const primaryRole = role;
|
return roleDisplayNames[role] || 'Utilisateur';
|
||||||
return roleDisplayNames[primaryRole] || 'Utilisateur';
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode pour obtenir la classe CSS du badge de rôle
|
||||||
|
*/
|
||||||
|
getRoleBadgeClass(): string {
|
||||||
|
if (!this.user?.role) return 'badge bg-secondary';
|
||||||
|
|
||||||
|
const roleClassMap: { [key: string]: string } = {
|
||||||
|
[UserRole.DCB_ADMIN]: 'badge bg-danger',
|
||||||
|
[UserRole.DCB_SUPPORT]: 'badge bg-info',
|
||||||
|
[UserRole.DCB_PARTNER_ADMIN]: 'badge bg-primary',
|
||||||
|
[UserRole.DCB_PARTNER_MANAGER]: 'badge bg-success',
|
||||||
|
[UserRole.DCB_PARTNER_SUPPORT]: 'badge bg-warning',
|
||||||
|
[UserRole.MERCHANT_CONFIG_ADMIN]: 'badge bg-primary',
|
||||||
|
[UserRole.MERCHANT_CONFIG_MANAGER]: 'badge bg-success',
|
||||||
|
[UserRole.MERCHANT_CONFIG_TECHNICAL]: 'badge bg-warning',
|
||||||
|
[UserRole.MERCHANT_CONFIG_VIEWER]: 'badge bg-secondary'
|
||||||
|
};
|
||||||
|
|
||||||
|
return roleClassMap[this.user.role] || 'badge bg-secondary';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient l'URL de l'avatar de l'utilisateur
|
||||||
|
*/
|
||||||
|
getUserAvatar(): string {
|
||||||
|
if (!this.user) {
|
||||||
|
return 'assets/images/users/user-default.jpg';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vous pouvez implémenter une logique pour générer un avatar personnalisé
|
||||||
|
// ou utiliser une image par défaut basée sur l'email/nom
|
||||||
|
return `assets/images/users/user-${(this.user.id?.charCodeAt(0) % 5) + 1}.jpg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les erreurs de chargement d'avatar
|
||||||
|
*/
|
||||||
|
onAvatarError(event: Event): void {
|
||||||
|
const img = event.target as HTMLImageElement;
|
||||||
|
img.src = 'assets/images/users/user-default.jpg';
|
||||||
|
img.onerror = null; // Éviter les boucles infinies
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// app/modules/auth/logout/logout.component.ts
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Router } from "@angular/router";
|
||||||
import { Router } from '@angular/router';
|
import { AuthService } from "@core/services/auth.service";
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { Subject, takeUntil, finalize } from "rxjs";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@ -22,7 +22,9 @@ import { AuthService } from '@core/services/auth.service';
|
|||||||
}
|
}
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class Logout implements OnInit {
|
export class Logout implements OnInit, OnDestroy {
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private isLoggingOut = false; // Flag pour éviter les doubles déconnexions
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
@ -30,22 +32,59 @@ export class Logout implements OnInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
// Vérifier si nous sommes déjà en train de déconnecter
|
||||||
|
if (this.isLoggingOut) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoggingOut = true;
|
||||||
this.logout();
|
this.logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
private logout(): void {
|
private logout(): void {
|
||||||
this.authService.logout().subscribe({
|
|
||||||
|
// Attendre un peu pour laisser le temps à l'UI de s'afficher
|
||||||
|
setTimeout(() => {
|
||||||
|
this.authService.logout().pipe(
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
finalize(() => {
|
||||||
|
this.navigateToLogin();
|
||||||
|
})
|
||||||
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
// Redirection vers la page de login après déconnexion
|
// Succès - navigation gérée dans finalize
|
||||||
this.router.navigate(['/auth/login'], {
|
|
||||||
queryParams: { logout: 'success' }
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Erreur lors de la déconnexion:', error);
|
console.warn('Erreur lors de la déconnexion:', error);
|
||||||
// Rediriger même en cas d'erreur
|
|
||||||
this.router.navigate(['/auth/login']);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
private navigateToLogin(): void {
|
||||||
|
// S'assurer que nous ne naviguons qu'une seule fois
|
||||||
|
if (this.router.url.includes('/auth/login')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Naviguer UNE seule fois vers le login
|
||||||
|
this.router.navigate(['/auth/login'], {
|
||||||
|
queryParams: {
|
||||||
|
logout: 'success',
|
||||||
|
timestamp: Date.now() // Pour éviter le cache
|
||||||
|
},
|
||||||
|
replaceUrl: true // Remplacer l'historique actuel
|
||||||
|
}).then(() => {
|
||||||
|
// Marquer comme terminé
|
||||||
|
this.isLoggingOut = false;
|
||||||
|
}).catch(() => {
|
||||||
|
this.isLoggingOut = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
this.isLoggingOut = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,19 +6,36 @@
|
|||||||
<h1 class="h3 mb-0 text-primary">
|
<h1 class="h3 mb-0 text-primary">
|
||||||
<ng-icon name="lucideLayoutDashboard" class="me-2"></ng-icon>
|
<ng-icon name="lucideLayoutDashboard" class="me-2"></ng-icon>
|
||||||
Dashboard FinTech Reporting
|
Dashboard FinTech Reporting
|
||||||
|
<span *ngIf="access.isMerchantUser" class="badge bg-success ms-2">
|
||||||
|
<ng-icon name="lucideStore" class="me-1"></ng-icon>
|
||||||
|
Merchant {{ merchantId }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="access.isHubUser" class="badge bg-primary ms-2">
|
||||||
|
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||||
|
Hub Admin
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-muted mb-0">Surveillance en temps réel des transactions et abonnements</p>
|
<p class="text-muted mb-0">
|
||||||
|
<ng-icon [name]="currentRoleIcon" class="me-1"></ng-icon>
|
||||||
|
{{ currentRoleLabel }} - {{ getCurrentMerchantName() }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2 align-items-center">
|
<div class="d-flex gap-2 align-items-center">
|
||||||
<!-- Contrôles rapides -->
|
<!-- Contrôles rapides -->
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<!-- Bouton Actualiser selon le type -->
|
||||||
<button class="btn btn-outline-primary btn-sm"
|
<button class="btn btn-outline-primary btn-sm"
|
||||||
(click)="loadAllData()"
|
(click)="refreshData()"
|
||||||
[disabled]="loading.all">
|
[disabled]="loading.globalData || loading.merchantData">
|
||||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading.all" class="me-1"></ng-icon>
|
<ng-icon name="lucideRefreshCw"
|
||||||
{{ loading.all ? 'Chargement...' : 'Actualiser' }}
|
[class.spin]="loading.globalData || loading.merchantData"
|
||||||
|
class="me-1"></ng-icon>
|
||||||
|
{{ (loading.globalData || loading.merchantData) ? 'Chargement...' : 'Actualiser' }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-danger btn-sm"
|
|
||||||
|
<!-- Bouton Sync seulement si autorisé -->
|
||||||
|
<button *ngIf="canTriggerSync()"
|
||||||
|
class="btn btn-outline-danger btn-sm"
|
||||||
(click)="triggerSync()"
|
(click)="triggerSync()"
|
||||||
[disabled]="loading.sync">
|
[disabled]="loading.sync">
|
||||||
<ng-icon name="lucideRefreshCcw" [class.spin]="loading.sync" class="me-1"></ng-icon>
|
<ng-icon name="lucideRefreshCcw" [class.spin]="loading.sync" class="me-1"></ng-icon>
|
||||||
@ -26,56 +43,58 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filtres -->
|
<!-- Filtres selon le type -->
|
||||||
<div class="filters-card">
|
<div *ngIf="canSelectMerchant() && shouldShowMerchantSelector()" class="filters-card">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<!-- Filtre Merchant pour hub users -->
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<span class="input-group-text bg-light">
|
<span class="input-group-text bg-light">
|
||||||
<ng-icon name="lucideStore"></ng-icon>
|
<ng-icon name="lucideStore"></ng-icon>
|
||||||
</span>
|
</span>
|
||||||
<input type="number" class="form-control form-control-sm"
|
|
||||||
[(ngModel)]="merchantId" min="1"
|
<select class="form-control form-control-sm"
|
||||||
placeholder="Merchant ID"
|
[ngModel]="merchantId"
|
||||||
(change)="refreshMerchantData()"
|
(ngModelChange)="selectMerchant($event)"
|
||||||
style="width: 100px;">
|
style="width: 180px;">
|
||||||
|
<option [value]="undefined">
|
||||||
|
<ng-icon name="lucideGlobe" class="me-1"></ng-icon>
|
||||||
|
Données globales
|
||||||
|
</option>
|
||||||
|
<option *ngFor="let merchant of allowedMerchants" [value]="merchant.id">
|
||||||
|
{{ merchant.name }} (ID: {{ merchant.id }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group input-group-sm">
|
<!-- Badge de contexte -->
|
||||||
<span class="input-group-text bg-light">
|
<div class="badge" [ngClass]="isViewingGlobal() ? 'bg-info' : 'bg-success'">
|
||||||
<ng-icon name="lucideCalendar"></ng-icon>
|
<ng-icon [name]="isViewingGlobal() ? 'lucideGlobe' : 'lucideStore'" class="me-1"></ng-icon>
|
||||||
</span>
|
{{ getCurrentMerchantName() }}
|
||||||
<input type="date" class="form-control form-control-sm"
|
</div>
|
||||||
[(ngModel)]="startDate"
|
</div>
|
||||||
style="width: 110px;">
|
|
||||||
<input type="date" class="form-control form-control-sm"
|
|
||||||
[(ngModel)]="endDate"
|
|
||||||
style="width: 110px;">
|
|
||||||
<button class="btn btn-outline-secondary btn-sm"
|
|
||||||
(click)="refreshWithDates()"
|
|
||||||
title="Appliquer les dates">
|
|
||||||
<ng-icon name="lucideFilter"></ng-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dropdown" ngbDropdown>
|
<!-- Options dropdown seulement pour hub users admin -->
|
||||||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
<div *ngIf="access.isHubUser && canManageMerchants()" ngbDropdown class="dropdown">
|
||||||
type="button"
|
<!-- Bouton déclencheur -->
|
||||||
ngbDropdownToggle>
|
<button class="btn btn-outline-secondary dropdown-toggle"
|
||||||
<ng-icon name="lucideSettings" class="me-1"></ng-icon>
|
ngbDropdownToggle
|
||||||
|
type="button">
|
||||||
|
<ng-icon name="lucideSettings" class="me-2"></ng-icon>
|
||||||
Options
|
Options
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Menu déroulant -->
|
||||||
<div class="dropdown-menu dropdown-menu-end" ngbDropdownMenu>
|
<div class="dropdown-menu dropdown-menu-end" ngbDropdownMenu>
|
||||||
<a class="dropdown-item" href="javascript:void(0)" (click)="checkSystemHealth()">
|
<button ngbDropdownItem (click)="checkSystemHealth()">
|
||||||
<ng-icon name="lucideHeartPulse" class="me-2"></ng-icon>
|
<ng-icon name="lucideHeartPulse" class="me-2"></ng-icon>
|
||||||
Vérifier la santé
|
Vérifier la santé
|
||||||
</a>
|
</button>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a class="dropdown-item" href="javascript:void(0)" (click)="loadAllData()">
|
<button ngbDropdownItem (click)="refreshData()">
|
||||||
<ng-icon name="lucideRefreshCw" class="me-2"></ng-icon>
|
<ng-icon name="lucideRefreshCw" class="me-2"></ng-icon>
|
||||||
Rafraîchir tout
|
Rafraîchir tout
|
||||||
</a>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -85,31 +104,72 @@
|
|||||||
<!-- Barre d'état rapide -->
|
<!-- Barre d'état rapide -->
|
||||||
<div class="status-bar mb-4">
|
<div class="status-bar mb-4">
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
|
<!-- Info rôle -->
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||||
|
<ng-icon [name]="getRoleStatusIcon()" [class]="getRoleStatusColor()"></ng-icon>
|
||||||
|
<small>{{ currentRoleLabel }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mode d'affichage -->
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||||
|
<ng-icon [name]="isViewingGlobal() ? 'lucideGlobe' : 'lucideStore'"
|
||||||
|
[class]="isViewingGlobal() ? 'text-info' : 'text-success'"></ng-icon>
|
||||||
|
<small>{{ getCurrentMerchantName() }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||||
<ng-icon name="lucideClock" class="text-primary"></ng-icon>
|
<ng-icon name="lucideClock" class="text-primary"></ng-icon>
|
||||||
<small>Mis à jour: {{ lastUpdated | date:'HH:mm:ss' }}</small>
|
<small>Mis à jour: {{ lastUpdated | date:'HH:mm:ss' }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
|
||||||
|
<!-- Services en ligne seulement pour hub users -->
|
||||||
|
<div *ngIf="shouldShowSystemHealth()" class="col-auto">
|
||||||
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||||
<ng-icon name="lucideCpu" class="text-success"></ng-icon>
|
<ng-icon name="lucideCpu" class="text-success"></ng-icon>
|
||||||
<small>Services: {{ stats.onlineServices }}/{{ stats.totalServices }} en ligne</small>
|
<small>Services: {{ stats.onlineServices }}/{{ stats.totalServices }} en ligne</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
|
||||||
|
<!-- Info merchant pour merchant users -->
|
||||||
|
<div *ngIf="access.isMerchantUser" class="col-auto">
|
||||||
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||||
<ng-icon name="lucidePhone" class="text-warning"></ng-icon>
|
<ng-icon name="lucideStore" class="text-success"></ng-icon>
|
||||||
<small>Opérateur: Orange</small>
|
<small>Merchant ID: {{ merchantId }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Merchant sélectionné pour hub users -->
|
||||||
|
<div *ngIf="access.isHubUser && isViewingMerchant()" class="col-auto">
|
||||||
|
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||||
|
<ng-icon name="lucideStore" class="text-info"></ng-icon>
|
||||||
|
<small>Merchant ID: {{ merchantId }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Message de sync -->
|
<!-- Message d'erreur si pas de permissions -->
|
||||||
<div *ngIf="syncResponse" class="alert alert-success alert-dismissible fade show mb-4">
|
<div *ngIf="!shouldShowTransactions()"
|
||||||
|
class="alert alert-warning mb-4">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon name="lucideCheckCircle" class="me-2 fs-5"></ng-icon>
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<strong>Permissions insuffisantes</strong>
|
||||||
|
<div class="text-muted small">Vous n'avez pas les permissions nécessaires pour voir les données.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message de sync seulement si autorisé à voir les alertes -->
|
||||||
|
<div *ngIf="syncResponse && shouldShowAlerts()" class="alert alert-success alert-dismissible fade show mb-4">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideCheckCircle2" class="me-2 fs-5"></ng-icon>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<strong>{{ syncResponse.message }}</strong>
|
<strong>{{ syncResponse.message }}</strong>
|
||||||
<div class="text-muted small">Synchronisée à {{ formatDate(syncResponse.timestamp) }}</div>
|
<div class="text-muted small">Synchronisée à {{ formatDate(syncResponse.timestamp) }}</div>
|
||||||
@ -119,7 +179,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ==================== SECTION DES KPIs HORIZONTAUX ==================== -->
|
<!-- ==================== SECTION DES KPIs HORIZONTAUX ==================== -->
|
||||||
<div class="kpi-section mb-4">
|
<div *ngIf="shouldShowKPIs()" class="kpi-section mb-4">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<!-- Transactions Journalières -->
|
<!-- Transactions Journalières -->
|
||||||
<div class="col-xl-2 col-md-4 col-sm-6">
|
<div class="col-xl-2 col-md-4 col-sm-6">
|
||||||
@ -171,9 +231,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Transactions Mensuelles -->
|
<!-- Transactions Mensuel -->
|
||||||
<div class="col-xl-2 col-md-4 col-sm-6">
|
<div class="col-xl-2 col-md-4 col-sm-6">
|
||||||
<div class="card kpi-card border-start border-success border-4">
|
<div class="card kpi-card border-start border-info border-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
<div>
|
<div>
|
||||||
@ -181,13 +241,13 @@
|
|||||||
<h4 class="fw-bold mb-0">{{ formatNumber(getPaymentStats().monthly.transactions) }}</h4>
|
<h4 class="fw-bold mb-0">{{ formatNumber(getPaymentStats().monthly.transactions) }}</h4>
|
||||||
<small class="text-muted">Mensuel</small>
|
<small class="text-muted">Mensuel</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="avatar-sm bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
<div class="avatar-sm bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
||||||
<ng-icon name="lucideCalendar" class="text-success fs-5"></ng-icon>
|
<ng-icon name="lucideCalendar" class="text-info fs-5"></ng-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<span class="text-muted small">{{ formatCurrency(getPaymentStats().monthly.revenue) }}</span>
|
<span class="text-muted small">{{ formatCurrency(getPaymentStats().monthly.revenue) }}</span>
|
||||||
<span class="badge bg-success bg-opacity-25 text-success">
|
<span class="badge bg-info bg-opacity-25 text-info">
|
||||||
<ng-icon name="lucideArrowUpRight" class="me-1"></ng-icon>
|
<ng-icon name="lucideArrowUpRight" class="me-1"></ng-icon>
|
||||||
{{ getPaymentStats().monthly.successRate | number:'1.0-0' }}%
|
{{ getPaymentStats().monthly.successRate | number:'1.0-0' }}%
|
||||||
</span>
|
</span>
|
||||||
@ -257,7 +317,7 @@
|
|||||||
<small class="text-muted">Global</small>
|
<small class="text-muted">Global</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="avatar-sm bg-danger bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
<div class="avatar-sm bg-danger bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
||||||
<ng-icon name="lucideCheckCircle" class="text-danger fs-5"></ng-icon>
|
<ng-icon name="lucideCheckCircle2" class="text-danger fs-5"></ng-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
@ -274,7 +334,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ==================== SECTION DES GRAPHIQUES FLEXIBLES ==================== -->
|
<!-- ==================== SECTION DES GRAPHIQUES FLEXIBLES ==================== -->
|
||||||
<div class="charts-section mb-4" *ngIf="!loading.all">
|
<div *ngIf="shouldShowCharts() && !loading.globalData && !loading.merchantData" class="charts-section mb-4">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Graphique principal dynamique -->
|
<!-- Graphique principal dynamique -->
|
||||||
<div class="col-xl-8">
|
<div class="col-xl-8">
|
||||||
@ -285,6 +345,14 @@
|
|||||||
<h5 class="card-title mb-0">
|
<h5 class="card-title mb-0">
|
||||||
<ng-icon [name]="getMetricIcon(dataSelection.metric)" class="text-primary me-2"></ng-icon>
|
<ng-icon [name]="getMetricIcon(dataSelection.metric)" class="text-primary me-2"></ng-icon>
|
||||||
{{ getChartTitle(dataSelection.metric) }}
|
{{ getChartTitle(dataSelection.metric) }}
|
||||||
|
<span *ngIf="isViewingMerchant()" class="badge bg-success ms-2">
|
||||||
|
<ng-icon name="lucideStore" class="me-1"></ng-icon>
|
||||||
|
Merchant {{ merchantId }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="isViewingGlobal()" class="badge bg-info ms-2">
|
||||||
|
<ng-icon name="lucideGlobe" class="me-1"></ng-icon>
|
||||||
|
Données globales
|
||||||
|
</span>
|
||||||
</h5>
|
</h5>
|
||||||
<p class="text-muted small mb-0">Visualisation en temps réel</p>
|
<p class="text-muted small mb-0">Visualisation en temps réel</p>
|
||||||
</div>
|
</div>
|
||||||
@ -334,15 +402,16 @@
|
|||||||
<div class="spinner-border text-primary"></div>
|
<div class="spinner-border text-primary"></div>
|
||||||
<p class="mt-2 text-muted">Chargement du graphique...</p>
|
<p class="mt-2 text-muted">Chargement du graphique...</p>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!loading.chart && !dailyTransactions"
|
<div *ngIf="!loading.chart && !getCurrentTransactionData()"
|
||||||
class="text-center py-5 text-muted">
|
class="text-center py-5 text-muted">
|
||||||
<ng-icon name="lucideBarChart3" class="fs-1 opacity-25"></ng-icon>
|
<ng-icon name="lucideBarChart3" class="fs-1 opacity-25"></ng-icon>
|
||||||
<p class="mt-2">Aucune donnée disponible</p>
|
<p class="mt-2">Aucune donnée disponible</p>
|
||||||
<button class="btn btn-sm btn-outline-primary mt-2" (click)="loadDailyTransactions()">
|
<button class="btn btn-sm btn-outline-primary mt-2"
|
||||||
|
(click)="refreshData()">
|
||||||
Charger les données
|
Charger les données
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!loading.chart && dailyTransactions"
|
<div *ngIf="!loading.chart && getCurrentTransactionData()"
|
||||||
class="position-relative" style="height: 300px;">
|
class="position-relative" style="height: 300px;">
|
||||||
<canvas #mainChartCanvas></canvas>
|
<canvas #mainChartCanvas></canvas>
|
||||||
</div>
|
</div>
|
||||||
@ -353,8 +422,8 @@
|
|||||||
<!-- Panneau droit avec 2 mini-graphiques -->
|
<!-- Panneau droit avec 2 mini-graphiques -->
|
||||||
<div class="col-xl-4">
|
<div class="col-xl-4">
|
||||||
<div class="row h-100 g-4">
|
<div class="row h-100 g-4">
|
||||||
<!-- Graphique de comparaison -->
|
<!-- Graphique de comparaison seulement pour hub users en mode global -->
|
||||||
<div class="col-12">
|
<div *ngIf="shouldShowChart('comparison')" class="col-12">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header bg-transparent border-bottom-0">
|
<div class="card-header bg-transparent border-bottom-0">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
@ -380,18 +449,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Taux de succès circulaire -->
|
<!-- Taux de succès circulaire -->
|
||||||
<div class="col-12">
|
<div *ngIf="shouldShowChart('successRate')" class="col-12">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header bg-transparent border-bottom-0">
|
<div class="card-header bg-transparent border-bottom-0">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title mb-0">
|
<h5 class="card-title mb-0">
|
||||||
<ng-icon name="lucideActivity" class="text-success me-2"></ng-icon>
|
<ng-icon name="lucideActivity" class="text-success me-2"></ng-icon>
|
||||||
Performance Globale
|
Performance
|
||||||
</h5>
|
</h5>
|
||||||
<button class="btn btn-sm btn-outline-success btn-sm"
|
<button class="btn btn-sm btn-outline-success btn-sm"
|
||||||
(click)="refreshDailyTransactions()"
|
(click)="refreshData()"
|
||||||
[disabled]="loading.dailyTransactions">
|
[disabled]="loading.globalData || loading.merchantData">
|
||||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading.dailyTransactions"></ng-icon>
|
<ng-icon name="lucideRefreshCw"
|
||||||
|
[class.spin]="loading.globalData || loading.merchantData"></ng-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -412,7 +482,7 @@
|
|||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div class="p-2 border rounded bg-success bg-opacity-10">
|
<div class="p-2 border rounded bg-success bg-opacity-10">
|
||||||
<div class="text-success fw-bold">
|
<div class="text-success fw-bold">
|
||||||
{{ formatNumber(dailyTransactions?.items?.[0]?.successCount || 0) }}
|
{{ formatNumber(getCurrentTransactionData()?.items?.[0]?.successCount || 0) }}
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">Réussies</small>
|
<small class="text-muted">Réussies</small>
|
||||||
</div>
|
</div>
|
||||||
@ -420,7 +490,7 @@
|
|||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div class="p-2 border rounded bg-danger bg-opacity-10">
|
<div class="p-2 border rounded bg-danger bg-opacity-10">
|
||||||
<div class="text-danger fw-bold">
|
<div class="text-danger fw-bold">
|
||||||
{{ formatNumber(dailyTransactions?.items?.[0]?.failedCount || 0) }}
|
{{ formatNumber(getCurrentTransactionData()?.items?.[0]?.failedCount || 0) }}
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">Échouées</small>
|
<small class="text-muted">Échouées</small>
|
||||||
</div>
|
</div>
|
||||||
@ -428,7 +498,7 @@
|
|||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div class="p-2 border rounded bg-info bg-opacity-10">
|
<div class="p-2 border rounded bg-info bg-opacity-10">
|
||||||
<div class="text-info fw-bold">
|
<div class="text-info fw-bold">
|
||||||
{{ formatNumber(dailyTransactions?.items?.[0]?.pendingCount || 0) }}
|
{{ formatNumber(getCurrentTransactionData()?.items?.[0]?.pendingCount || 0) }}
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">En attente</small>
|
<small class="text-muted">En attente</small>
|
||||||
</div>
|
</div>
|
||||||
@ -443,7 +513,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ==================== SECTION SANTÉ DES APIS ==================== -->
|
<!-- ==================== SECTION SANTÉ DES APIS ==================== -->
|
||||||
<div class="health-section mb-4">
|
<div *ngIf="shouldShowSystemHealth()" class="health-section mb-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-transparent border-bottom-0">
|
<div class="card-header bg-transparent border-bottom-0">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
@ -462,7 +532,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-outline-danger"
|
<button class="btn btn-sm btn-outline-danger"
|
||||||
(click)="refreshHealthCheck()"
|
(click)="checkSystemHealth()"
|
||||||
[disabled]="loading.healthCheck">
|
[disabled]="loading.healthCheck">
|
||||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading.healthCheck"></ng-icon>
|
<ng-icon name="lucideRefreshCw" [class.spin]="loading.healthCheck"></ng-icon>
|
||||||
</button>
|
</button>
|
||||||
@ -522,7 +592,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ==================== SECTION DES TABLEAUX ==================== -->
|
<!-- ==================== SECTION DES TABLEAUX ==================== -->
|
||||||
<div class="tables-section">
|
<div *ngIf="shouldShowTransactions()" class="tables-section">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Transactions récentes -->
|
<!-- Transactions récentes -->
|
||||||
<div class="col-xl-6">
|
<div class="col-xl-6">
|
||||||
@ -533,14 +603,19 @@
|
|||||||
<h5 class="card-title mb-0">
|
<h5 class="card-title mb-0">
|
||||||
<ng-icon name="lucideListChecks" class="text-primary me-2"></ng-icon>
|
<ng-icon name="lucideListChecks" class="text-primary me-2"></ng-icon>
|
||||||
Transactions récentes
|
Transactions récentes
|
||||||
|
<span class="badge ms-2"
|
||||||
|
[ngClass]="isViewingGlobal() ? 'bg-info' : 'bg-success'">
|
||||||
|
{{ getCurrentMerchantName() }}
|
||||||
|
</span>
|
||||||
</h5>
|
</h5>
|
||||||
<p class="text-muted small mb-0">Dernières 24 heures</p>
|
<p class="text-muted small mb-0">Dernières 24 heures</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-sm btn-outline-primary"
|
<button class="btn btn-sm btn-outline-primary"
|
||||||
(click)="refreshDailyTransactions()"
|
(click)="refreshData()"
|
||||||
[disabled]="loading.dailyTransactions">
|
[disabled]="loading.globalData || loading.merchantData">
|
||||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading.dailyTransactions"></ng-icon>
|
<ng-icon name="lucideRefreshCw"
|
||||||
|
[class.spin]="loading.globalData || loading.merchantData"></ng-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -557,10 +632,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let item of (dailyTransactions?.items?.slice(0, 5) || [])">
|
<tr *ngFor="let item of (getCurrentTransactionData()?.items?.slice(0, 5) || [])">
|
||||||
<td class="ps-3">
|
<td class="ps-3">
|
||||||
<div>{{ item.period }}</div>
|
<div>{{ item.period }}</div>
|
||||||
<small class="text-muted">{{ item.merchantPartnerId ? 'Merchant ' + item.merchantPartnerId : 'Tous' }}</small>
|
<small class="text-muted">{{ isViewingGlobal() ? 'Tous merchants' : 'Merchant ' + merchantId }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<div class="fw-medium">{{ formatCurrency(item.totalAmount) }}</div>
|
<div class="fw-medium">{{ formatCurrency(item.totalAmount) }}</div>
|
||||||
@ -574,7 +649,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="!(dailyTransactions?.items?.length)">
|
|
||||||
|
<tr *ngIf="!getCurrentTransactionData()?.items?.length">
|
||||||
<td colspan="4" class="text-center text-muted py-5">
|
<td colspan="4" class="text-center text-muted py-5">
|
||||||
<ng-icon name="lucideDatabase" class="fs-1 opacity-25 mb-3 d-block"></ng-icon>
|
<ng-icon name="lucideDatabase" class="fs-1 opacity-25 mb-3 d-block"></ng-icon>
|
||||||
<p class="mb-0">Aucune transaction disponible</p>
|
<p class="mb-0">Aucune transaction disponible</p>
|
||||||
@ -588,7 +664,7 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<ng-icon name="lucideInfo" class="me-1"></ng-icon>
|
<ng-icon name="lucideInfo" class="me-1"></ng-icon>
|
||||||
{{ (dailyTransactions?.items?.length || 0) }} périodes au total
|
{{ getCurrentTransactionData()?.items?.length || 0 }} périodes au total
|
||||||
</small>
|
</small>
|
||||||
<a href="#" class="btn btn-sm btn-outline-primary">Voir tout</a>
|
<a href="#" class="btn btn-sm btn-outline-primary">Voir tout</a>
|
||||||
</div>
|
</div>
|
||||||
@ -596,8 +672,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alertes système -->
|
<!-- Alertes système - Masqué si pas autorisé -->
|
||||||
<div class="col-xl-6">
|
<div *ngIf="shouldShowAlerts()" class="col-xl-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header bg-transparent border-bottom-0">
|
<div class="card-header bg-transparent border-bottom-0">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
@ -627,7 +703,7 @@
|
|||||||
name="lucideInfo"
|
name="lucideInfo"
|
||||||
class="text-info fs-5"></ng-icon>
|
class="text-info fs-5"></ng-icon>
|
||||||
<ng-icon *ngIf="alert.type === 'success'"
|
<ng-icon *ngIf="alert.type === 'success'"
|
||||||
name="lucideCheckCircle"
|
name="lucideCheckCircle2"
|
||||||
class="text-success fs-5"></ng-icon>
|
class="text-success fs-5"></ng-icon>
|
||||||
<ng-icon *ngIf="alert.type === 'danger'"
|
<ng-icon *ngIf="alert.type === 'danger'"
|
||||||
name="lucideAlertCircle"
|
name="lucideAlertCircle"
|
||||||
@ -643,7 +719,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="alerts.length === 0" class="text-center text-muted py-5">
|
<div *ngIf="alerts.length === 0" class="text-center text-muted py-5">
|
||||||
<ng-icon name="lucideCheckCircle" class="text-success fs-1 opacity-25 mb-3 d-block"></ng-icon>
|
<ng-icon name="lucideCheckCircle2" class="text-success fs-1 opacity-25 mb-3 d-block"></ng-icon>
|
||||||
<p class="mb-0">Aucune alerte active</p>
|
<p class="mb-0">Aucune alerte active</p>
|
||||||
<small class="text-muted">Tous les systèmes fonctionnent normalement</small>
|
<small class="text-muted">Tous les systèmes fonctionnent normalement</small>
|
||||||
</div>
|
</div>
|
||||||
@ -673,11 +749,13 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div class="text-muted small">
|
<div class="text-muted small">
|
||||||
<ng-icon name="lucideCode" class="me-1"></ng-icon>
|
<ng-icon name="lucideCode" class="me-1"></ng-icon>
|
||||||
Dashboard FinTech v2.0
|
Dashboard FinTech v2.0 •
|
||||||
|
<ng-icon [name]="currentRoleIcon" class="me-1 ms-2"></ng-icon>
|
||||||
|
{{ currentRoleLabel }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted small">
|
<div class="text-muted small">
|
||||||
<ng-icon name="lucideDatabase" class="me-1"></ng-icon>
|
<ng-icon [name]="isViewingGlobal() ? 'lucideGlobe' : 'lucideStore'" class="me-1"></ng-icon>
|
||||||
APIs: IAM, Config, Core, Reporting
|
{{ getCurrentMerchantName() }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted small">
|
<div class="text-muted small">
|
||||||
<ng-icon name="lucideClock" class="me-1"></ng-icon>
|
<ng-icon name="lucideClock" class="me-1"></ng-icon>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,233 @@
|
|||||||
|
import { inject, Injectable, Injector } from '@angular/core';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { map, catchError } from 'rxjs/operators';
|
||||||
|
import { UserRole, RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
|
import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service';
|
||||||
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
|
||||||
|
// Interface minimaliste
|
||||||
|
export interface DashboardAccess {
|
||||||
|
// Type d'utilisateur - CORE
|
||||||
|
isHubUser: boolean;
|
||||||
|
isMerchantUser: boolean;
|
||||||
|
|
||||||
|
// Info du rôle
|
||||||
|
roleLabel: string;
|
||||||
|
roleIcon: string;
|
||||||
|
userRole: UserRole;
|
||||||
|
|
||||||
|
// Merchant info (seulement pour merchant users)
|
||||||
|
merchantId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllowedMerchant {
|
||||||
|
id: number | undefined;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class DashboardAccessService {
|
||||||
|
private accessCache: DashboardAccess | null = null;
|
||||||
|
private merchantsCache: AllowedMerchant[] | null = null;
|
||||||
|
private currentMerchantId: number | null = null;
|
||||||
|
|
||||||
|
private readonly injector = inject(Injector);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private roleService: RoleManagementService,
|
||||||
|
private merchantService: MerchantConfigService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient l'accès simplifié
|
||||||
|
*/
|
||||||
|
getDashboardAccess(): DashboardAccess {
|
||||||
|
if (this.accessCache) {
|
||||||
|
return this.accessCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRole = this.roleService.getCurrentRole();
|
||||||
|
const isHubUser = this.roleService.isHubUser();
|
||||||
|
|
||||||
|
const access: DashboardAccess = {
|
||||||
|
isHubUser,
|
||||||
|
isMerchantUser: !isHubUser,
|
||||||
|
roleLabel: this.roleService.getRoleLabel(),
|
||||||
|
roleIcon: this.roleService.getRoleIcon(),
|
||||||
|
userRole: userRole || UserRole.DCB_SUPPORT,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pour les merchant users, définir leur merchant ID
|
||||||
|
if (!isHubUser) {
|
||||||
|
access.merchantId = this.getMerchantIdForUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.accessCache = access;
|
||||||
|
return access;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient le merchant ID pour un merchant user
|
||||||
|
*/
|
||||||
|
private getMerchantIdForUser(): number | undefined {
|
||||||
|
// Récupérer le merchant ID de l'utilisateur courant
|
||||||
|
const authService = this.injector.get(AuthService);
|
||||||
|
|
||||||
|
const merchantPartnerId = authService.getCurrentMerchantPartnerId();
|
||||||
|
|
||||||
|
if (!merchantPartnerId) return undefined;
|
||||||
|
|
||||||
|
const merchantId = parseInt(merchantPartnerId, 10);
|
||||||
|
|
||||||
|
return isNaN(merchantId) ? undefined : merchantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient les merchants disponibles
|
||||||
|
* - Hub users: tous les merchants
|
||||||
|
* - Merchant users: seulement leur merchant
|
||||||
|
*/
|
||||||
|
getAvailableMerchants(): Observable<AllowedMerchant[]> {
|
||||||
|
if (this.merchantsCache) {
|
||||||
|
return of(this.merchantsCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
const access = this.getDashboardAccess();
|
||||||
|
|
||||||
|
if (access.isHubUser) {
|
||||||
|
// Hub users voient tous les merchants
|
||||||
|
return this.merchantService.getAllMerchants().pipe(
|
||||||
|
map(merchants => {
|
||||||
|
const availableMerchants: any[] = merchants.map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
name: m.name
|
||||||
|
}));
|
||||||
|
this.merchantsCache = availableMerchants;
|
||||||
|
return availableMerchants;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Erreur chargement merchants:', error);
|
||||||
|
return of([]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Merchant users: seulement leur merchant
|
||||||
|
const merchantId = access.merchantId || this.getMerchantIdForUser();
|
||||||
|
return of([{
|
||||||
|
id: merchantId,
|
||||||
|
name: `Merchant ${merchantId}`
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Définit le merchant sélectionné
|
||||||
|
*/
|
||||||
|
setSelectedMerchantId(merchantId: number): void {
|
||||||
|
this.currentMerchantId = merchantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient le merchant sélectionné
|
||||||
|
*/
|
||||||
|
getSelectedMerchantId(): number | null {
|
||||||
|
const access = this.getDashboardAccess();
|
||||||
|
|
||||||
|
if (access.isMerchantUser) {
|
||||||
|
// Merchant users: toujours leur merchant
|
||||||
|
return access.merchantId || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hub users: le merchant sélectionné ou le premier
|
||||||
|
return this.currentMerchantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un merchant est accessible
|
||||||
|
*/
|
||||||
|
canAccessMerchant(merchantId: number): Observable<boolean> {
|
||||||
|
const access = this.getDashboardAccess();
|
||||||
|
|
||||||
|
// Hub users: tous les merchants sont accessibles
|
||||||
|
if (access.isHubUser) {
|
||||||
|
return of(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merchant users: seulement leur merchant
|
||||||
|
return of(access.merchantId === merchantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie le cache
|
||||||
|
*/
|
||||||
|
clearCache(): void {
|
||||||
|
this.accessCache = null;
|
||||||
|
this.merchantsCache = null;
|
||||||
|
this.currentMerchantId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthodes utilitaires pour le template
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Pour les Hub Users
|
||||||
|
shouldShowSystemHealth(): boolean {
|
||||||
|
return this.getDashboardAccess().isHubUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldShowAllTransactions(): boolean {
|
||||||
|
return this.getDashboardAccess().isHubUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
canTriggerSync(): boolean {
|
||||||
|
const access = this.getDashboardAccess();
|
||||||
|
return access.isHubUser && access.userRole === UserRole.DCB_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
canManageMerchants(): boolean {
|
||||||
|
const access = this.getDashboardAccess();
|
||||||
|
return access.isHubUser && access.userRole === UserRole.DCB_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour tous les utilisateurs
|
||||||
|
shouldShowTransactions(): boolean {
|
||||||
|
return true; // Tous peuvent voir les transactions (mais scope différent)
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldShowCharts(): boolean {
|
||||||
|
return true; // Tous peuvent voir les transactions (mais scope différent)
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldShowKPIs(): boolean {
|
||||||
|
return true; // Tous peuvent voir les transactions (mais scope différent)
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldShowAlerts(): boolean {
|
||||||
|
return true; // Tous peuvent voir les transactions (mais scope différent)
|
||||||
|
}
|
||||||
|
|
||||||
|
canRefreshData(): boolean {
|
||||||
|
return true; // Tous peuvent rafraîchir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour la sélection de merchant
|
||||||
|
canSelectMerchant(): boolean {
|
||||||
|
return this.getDashboardAccess().isHubUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour l'affichage du merchant ID
|
||||||
|
shouldShowMerchantId(): boolean {
|
||||||
|
const access = this.getDashboardAccess();
|
||||||
|
return access.isMerchantUser ||
|
||||||
|
(access.isHubUser && this.getSelectedMerchantId() !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour l'édition du merchant filter
|
||||||
|
canEditMerchantFilter(): boolean {
|
||||||
|
const access = this.getDashboardAccess();
|
||||||
|
if (access.isHubUser) {
|
||||||
|
return access.userRole === UserRole.DCB_ADMIN;
|
||||||
|
}
|
||||||
|
return access.userRole === UserRole.DCB_PARTNER_ADMIN;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,10 +14,10 @@ import { environment } from '@environments/environment';
|
|||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ReportService {
|
export class ReportService {
|
||||||
private baseUrl = `${environment.reportingApiUrl}/reporting`;
|
private baseUrl = `${environment.reportingApiUrl}/reporting`; // Mise à jour du chemin
|
||||||
private readonly DEFAULT_TIMEOUT = 5000; // Timeout réduit pour les health checks
|
private readonly DEFAULT_TIMEOUT = 5000;
|
||||||
private readonly DEFAULT_RETRY = 1;
|
private readonly DEFAULT_RETRY = 1;
|
||||||
private readonly REPORTING_TIMEOUT = 10000; // Timeout normal pour les autres requêtes
|
private readonly REPORTING_TIMEOUT = 10000;
|
||||||
|
|
||||||
// Configuration des APIs à scanner
|
// Configuration des APIs à scanner
|
||||||
private apiEndpoints = {
|
private apiEndpoints = {
|
||||||
@ -35,8 +35,15 @@ export class ReportService {
|
|||||||
private buildParams(params?: ReportParams): HttpParams {
|
private buildParams(params?: ReportParams): HttpParams {
|
||||||
let httpParams = new HttpParams();
|
let httpParams = new HttpParams();
|
||||||
if (!params) return httpParams;
|
if (!params) return httpParams;
|
||||||
|
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
if (value !== undefined && value !== null) httpParams = httpParams.set(key, value.toString());
|
if (value !== undefined && value !== null) {
|
||||||
|
if (value instanceof Date) {
|
||||||
|
httpParams = httpParams.set(key, this.formatDate(value));
|
||||||
|
} else {
|
||||||
|
httpParams = httpParams.set(key, value.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return httpParams;
|
return httpParams;
|
||||||
}
|
}
|
||||||
@ -57,7 +64,225 @@ export class ReportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------
|
// ---------------------
|
||||||
// Health checks
|
// TRANSACTIONS - NOUVELLES MÉTHODES AVEC merchantPartnerId
|
||||||
|
// ---------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transactions journalières (global ou par merchant)
|
||||||
|
*/
|
||||||
|
getDailyTransactions(merchantPartnerId?: number): Observable<TransactionReport> {
|
||||||
|
const params = merchantPartnerId ? { merchantPartnerId } : undefined;
|
||||||
|
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/daily`, {
|
||||||
|
params: this.buildParams(params)
|
||||||
|
}).pipe(
|
||||||
|
timeout(this.REPORTING_TIMEOUT),
|
||||||
|
retry(this.DEFAULT_RETRY),
|
||||||
|
catchError(err => this.handleError(err))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transactions hebdomadaires (global ou par merchant)
|
||||||
|
*/
|
||||||
|
getWeeklyTransactions(merchantPartnerId?: number): Observable<TransactionReport> {
|
||||||
|
const params = merchantPartnerId ? { merchantPartnerId } : undefined;
|
||||||
|
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/weekly`, {
|
||||||
|
params: this.buildParams(params)
|
||||||
|
}).pipe(
|
||||||
|
timeout(this.REPORTING_TIMEOUT),
|
||||||
|
retry(this.DEFAULT_RETRY),
|
||||||
|
catchError(err => this.handleError(err))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transactions mensuelles (global ou par merchant)
|
||||||
|
*/
|
||||||
|
getMonthlyTransactions(merchantPartnerId?: number): Observable<TransactionReport> {
|
||||||
|
const params = merchantPartnerId ? { merchantPartnerId } : undefined;
|
||||||
|
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/monthly`, {
|
||||||
|
params: this.buildParams(params)
|
||||||
|
}).pipe(
|
||||||
|
timeout(this.REPORTING_TIMEOUT),
|
||||||
|
retry(this.DEFAULT_RETRY),
|
||||||
|
catchError(err => this.handleError(err))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transactions avec dates spécifiques (global ou par merchant)
|
||||||
|
*/
|
||||||
|
getTransactionsByDateRange(
|
||||||
|
startDate: string,
|
||||||
|
endDate: string,
|
||||||
|
merchantPartnerId?: number
|
||||||
|
): Observable<TransactionReport> {
|
||||||
|
const params: any = { startDate, endDate };
|
||||||
|
if (merchantPartnerId) {
|
||||||
|
params.merchantPartnerId = merchantPartnerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/daily`, {
|
||||||
|
params: this.buildParams(params)
|
||||||
|
}).pipe(
|
||||||
|
timeout(this.REPORTING_TIMEOUT),
|
||||||
|
retry(this.DEFAULT_RETRY),
|
||||||
|
catchError(err => this.handleError(err))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transactions annuelles (global ou par merchant)
|
||||||
|
*/
|
||||||
|
getYearlyTransactions(year: number, merchantPartnerId?: number): Observable<TransactionReport> {
|
||||||
|
const startDate = `${year}-01-01`;
|
||||||
|
const endDate = `${year}-12-31`;
|
||||||
|
return this.getTransactionsByDateRange(startDate, endDate, merchantPartnerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// SUBSCRIPTIONS - NOUVELLES MÉTHODES AVEC merchantPartnerId
|
||||||
|
// ---------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abonnements journaliers (global ou par merchant)
|
||||||
|
*/
|
||||||
|
getDailySubscriptions(merchantPartnerId?: number): Observable<SubscriptionReport> {
|
||||||
|
const params = merchantPartnerId ? { merchantPartnerId } : undefined;
|
||||||
|
return this.http.get<SubscriptionReport>(`${this.baseUrl}/subscriptions/daily`, {
|
||||||
|
params: this.buildParams(params)
|
||||||
|
}).pipe(
|
||||||
|
timeout(this.REPORTING_TIMEOUT),
|
||||||
|
retry(this.DEFAULT_RETRY),
|
||||||
|
catchError(err => this.handleError(err))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abonnements mensuels (global ou par merchant)
|
||||||
|
*/
|
||||||
|
getMonthlySubscriptions(merchantPartnerId?: number): Observable<SubscriptionReport> {
|
||||||
|
const params = merchantPartnerId ? { merchantPartnerId } : undefined;
|
||||||
|
return this.http.get<SubscriptionReport>(`${this.baseUrl}/subscriptions/monthly`, {
|
||||||
|
params: this.buildParams(params)
|
||||||
|
}).pipe(
|
||||||
|
timeout(this.REPORTING_TIMEOUT),
|
||||||
|
retry(this.DEFAULT_RETRY),
|
||||||
|
catchError(err => this.handleError(err))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abonnements hebdomadaires (global ou par merchant)
|
||||||
|
*/
|
||||||
|
getWeeklySubscriptions(merchantPartnerId?: number): Observable<SubscriptionReport> {
|
||||||
|
const params = merchantPartnerId ? { merchantPartnerId } : undefined;
|
||||||
|
return this.http.get<SubscriptionReport>(`${this.baseUrl}/subscriptions/weekly`, {
|
||||||
|
params: this.buildParams(params)
|
||||||
|
}).pipe(
|
||||||
|
timeout(this.REPORTING_TIMEOUT),
|
||||||
|
retry(this.DEFAULT_RETRY),
|
||||||
|
catchError(err => this.handleError(err))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// SYNCHRONISATION
|
||||||
|
// ---------------------
|
||||||
|
|
||||||
|
triggerManualSync(): Observable<SyncResponse> {
|
||||||
|
return this.http.post<SyncResponse>(`${this.baseUrl}/sync/full`, {}).pipe(
|
||||||
|
timeout(this.REPORTING_TIMEOUT),
|
||||||
|
retry(this.DEFAULT_RETRY),
|
||||||
|
catchError(err => this.handleError(err))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// MÉTHODES DE SUPPORT POUR LA COMPATIBILITÉ
|
||||||
|
// ---------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compatibilité
|
||||||
|
*/
|
||||||
|
getTransactionReport(
|
||||||
|
params?: ReportParams,
|
||||||
|
period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily'
|
||||||
|
): Observable<TransactionReport> {
|
||||||
|
switch (period) {
|
||||||
|
case 'daily':
|
||||||
|
return this.getDailyTransactions(params?.merchantPartnerId);
|
||||||
|
case 'weekly':
|
||||||
|
return this.getWeeklyTransactions(params?.merchantPartnerId);
|
||||||
|
case 'monthly':
|
||||||
|
return this.getMonthlyTransactions(params?.merchantPartnerId);
|
||||||
|
case 'yearly':
|
||||||
|
return this.getYearlyTransactions(
|
||||||
|
new Date().getFullYear(),
|
||||||
|
params?.merchantPartnerId
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return this.getDailyTransactions(params?.merchantPartnerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compatibilité
|
||||||
|
*/
|
||||||
|
getSubscriptionReport(
|
||||||
|
params?: ReportParams,
|
||||||
|
period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily'
|
||||||
|
): Observable<SubscriptionReport> {
|
||||||
|
switch (period) {
|
||||||
|
case 'daily':
|
||||||
|
return this.getDailySubscriptions(params?.merchantPartnerId);
|
||||||
|
case 'weekly':
|
||||||
|
return this.getWeeklySubscriptions(params?.merchantPartnerId);
|
||||||
|
case 'monthly':
|
||||||
|
return this.getMonthlySubscriptions(params?.merchantPartnerId);
|
||||||
|
default:
|
||||||
|
return this.getDailySubscriptions(params?.merchantPartnerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compatibilité
|
||||||
|
*/
|
||||||
|
getTransactionsWithDates(
|
||||||
|
startDate: string,
|
||||||
|
endDate: string,
|
||||||
|
merchantPartnerId?: number
|
||||||
|
): Observable<TransactionReport> {
|
||||||
|
return this.getTransactionsByDateRange(startDate, endDate, merchantPartnerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// Multi-partner normalization for charts
|
||||||
|
// ---------------------
|
||||||
|
normalizeForChart(data: TransactionReport | SubscriptionReport, key: string): ChartDataNormalized {
|
||||||
|
const labels = data.items.map(i => i.period);
|
||||||
|
const dataset = data.items.map(i => (i as any)[key] ?? 0);
|
||||||
|
return { labels, dataset };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// Multi-period & Multi-partner reporting
|
||||||
|
// ---------------------
|
||||||
|
getMultiPartnerReports(
|
||||||
|
partners: number[],
|
||||||
|
period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily',
|
||||||
|
type: 'transaction' | 'subscription' = 'transaction'
|
||||||
|
): Observable<(TransactionReport | SubscriptionReport)[]> {
|
||||||
|
const requests = partners.map(id => {
|
||||||
|
return type === 'transaction'
|
||||||
|
? this.getTransactionReport({ merchantPartnerId: id }, period)
|
||||||
|
: this.getSubscriptionReport({ merchantPartnerId: id }, period);
|
||||||
|
});
|
||||||
|
return forkJoin(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// Health checks (rest of the code remains the same)
|
||||||
// ---------------------
|
// ---------------------
|
||||||
|
|
||||||
private checkApiAvailability(
|
private checkApiAvailability(
|
||||||
@ -240,57 +465,4 @@ export class ReportService {
|
|||||||
catchError(err => this.handleError(err))
|
catchError(err => this.handleError(err))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
triggerManualSync(): Observable<SyncResponse> {
|
|
||||||
return this.http.post<SyncResponse>(`${this.baseUrl}/sync/full`, {}).pipe(
|
|
||||||
timeout(this.REPORTING_TIMEOUT),
|
|
||||||
retry(this.DEFAULT_RETRY),
|
|
||||||
catchError(err => this.handleError(err))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------
|
|
||||||
// Transactions & Subscriptions Reports
|
|
||||||
// ---------------------
|
|
||||||
getTransactionReport(params?: ReportParams, period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily'): Observable<TransactionReport> {
|
|
||||||
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/${period}`, { params: this.buildParams(params) }).pipe(
|
|
||||||
timeout(this.REPORTING_TIMEOUT),
|
|
||||||
retry(this.DEFAULT_RETRY),
|
|
||||||
catchError(err => this.handleError(err))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubscriptionReport(params?: ReportParams, period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily'): Observable<SubscriptionReport> {
|
|
||||||
return this.http.get<SubscriptionReport>(`${this.baseUrl}/subscriptions/${period}`, { params: this.buildParams(params) }).pipe(
|
|
||||||
timeout(this.REPORTING_TIMEOUT),
|
|
||||||
retry(this.DEFAULT_RETRY),
|
|
||||||
catchError(err => this.handleError(err))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getTransactionsWithDates(startDate: string, endDate: string, merchantPartnerId?: number) {
|
|
||||||
return this.getTransactionReport({ startDate, endDate, merchantPartnerId }, 'daily');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------
|
|
||||||
// Multi-partner normalization for charts
|
|
||||||
// ---------------------
|
|
||||||
normalizeForChart(data: TransactionReport | SubscriptionReport, key: string): ChartDataNormalized {
|
|
||||||
const labels = data.items.map(i => i.period);
|
|
||||||
const dataset = data.items.map(i => (i as any)[key] ?? 0);
|
|
||||||
return { labels, dataset };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------
|
|
||||||
// Multi-period & Multi-partner reporting
|
|
||||||
// ---------------------
|
|
||||||
getMultiPartnerReports(partners: number[], period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily', type: 'transaction' | 'subscription' = 'transaction') {
|
|
||||||
const requests = partners.map(id => {
|
|
||||||
return type === 'transaction'
|
|
||||||
? this.getTransactionReport({ merchantPartnerId: id }, period)
|
|
||||||
: this.getSubscriptionReport({ merchantPartnerId: id }, period);
|
|
||||||
});
|
|
||||||
return forkJoin(requests);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -229,6 +229,10 @@
|
|||||||
@if (showUserTypeColumn()) {
|
@if (showUserTypeColumn()) {
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
}
|
}
|
||||||
|
<!-- Colonne Merchant Partner pour la vue admin -->
|
||||||
|
@if (showMerchantPartnerColumn()) {
|
||||||
|
<th>Merchant Partner</th>
|
||||||
|
}
|
||||||
<th (click)="sort('username')" class="cursor-pointer">
|
<th (click)="sort('username')" class="cursor-pointer">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span>Utilisateur</span>
|
<span>Utilisateur</span>
|
||||||
@ -268,6 +272,25 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
|
<!-- Colonne Merchant Partner pour la vue admin -->
|
||||||
|
@if (showMerchantPartnerColumn()) {
|
||||||
|
<td>
|
||||||
|
@if (user.merchantPartnerId) {
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="avatar-sm bg-secondary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||||
|
<ng-icon name="lucideBuilding" class="text-secondary fs-12"></ng-icon>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<small class="text-muted font-monospace" [title]="user.merchantPartnerId">
|
||||||
|
{{ user.merchantPartnerId.substring(0, 8) }}...
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
import { HubUsersService } from '../hub-users.service';
|
import { HubUsersService } from '../hub-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { UiCard } from '@app/components/ui-card';
|
import { UiCard } from '@app/components/ui-card';
|
||||||
|
|
||||||
@ -448,7 +448,7 @@ export class HubUsersList implements OnInit, OnDestroy {
|
|||||||
return roleInfo?.description || 'Description non disponible';
|
return roleInfo?.description || 'Description non disponible';
|
||||||
}
|
}
|
||||||
|
|
||||||
formatTimestamp(timestamp: number): string {
|
formatTimestamp(timestamp: number | undefined): string {
|
||||||
if (!timestamp) return 'Non disponible';
|
if (!timestamp) return 'Non disponible';
|
||||||
return new Date(timestamp).toLocaleDateString('fr-FR', {
|
return new Date(timestamp).toLocaleDateString('fr-FR', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
import { HubUsersService } from '../hub-users.service';
|
import { HubUsersService } from '../hub-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export interface UserProfileResponse {
|
|||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
role: string[];
|
role: string[];
|
||||||
|
merchantPartnerId?: string;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
createdByUsername?: string;
|
createdByUsername?: string;
|
||||||
}
|
}
|
||||||
@ -47,6 +48,10 @@ export interface MessageResponse {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MerchantPartnerIdResponse {
|
||||||
|
merchantPartnerId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class HubUsersService {
|
export class HubUsersService {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
@ -292,6 +297,7 @@ export class HubUsersService {
|
|||||||
lastName: apiUser.lastName,
|
lastName: apiUser.lastName,
|
||||||
enabled: apiUser.enabled,
|
enabled: apiUser.enabled,
|
||||||
emailVerified: apiUser.emailVerified,
|
emailVerified: apiUser.emailVerified,
|
||||||
|
merchantPartnerId: apiUser.merchantPartnerId,
|
||||||
userType: userType,
|
userType: userType,
|
||||||
role: apiUser.role,
|
role: apiUser.role,
|
||||||
createdBy: apiUser.createdBy,
|
createdBy: apiUser.createdBy,
|
||||||
@ -328,6 +334,10 @@ export class HubUsersService {
|
|||||||
filteredUsers = filteredUsers.filter(user => user.enabled === filters.enabled);
|
filteredUsers = filteredUsers.filter(user => user.enabled === filters.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filters.merchantPartnerId) {
|
||||||
|
filteredUsers = filteredUsers.filter(user => user.merchantPartnerId === filters.merchantPartnerId);
|
||||||
|
}
|
||||||
|
|
||||||
if (filters.userType) {
|
if (filters.userType) {
|
||||||
filteredUsers = filteredUsers.filter(user => user.userType === filters.userType);
|
filteredUsers = filteredUsers.filter(user => user.userType === filters.userType);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstr
|
|||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { HubUsersService } from './hub-users.service';
|
import { HubUsersService } from './hub-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { MerchantSyncService } from './merchant-sync-orchestrator.service';
|
import { MerchantSyncService } from './merchant-sync-orchestrator.service';
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { UserRole, UserType } from '@core/models/dcb-bo-hub-user.model';
|
||||||
import { Observable, of, throwError, firstValueFrom, lastValueFrom } from 'rxjs';
|
import { Observable, of, throwError, firstValueFrom, lastValueFrom } from 'rxjs';
|
||||||
|
|
||||||
// Mock des services
|
// Mock des services
|
||||||
@ -36,12 +37,12 @@ const mockHubUsersService = {
|
|||||||
createHubUser: (data: any): Observable<any> => of({
|
createHubUser: (data: any): Observable<any> => of({
|
||||||
id: 'keycloak-123',
|
id: 'keycloak-123',
|
||||||
username: data.username,
|
username: data.username,
|
||||||
merchantConfigId: 123
|
merchantPartnerId: 123
|
||||||
}),
|
}),
|
||||||
getHubUserById: (id: string): Observable<any> => of({
|
getHubUserById: (id: string): Observable<any> => of({
|
||||||
id,
|
id,
|
||||||
username: 'owner',
|
username: 'owner',
|
||||||
merchantConfigId: 123
|
merchantPartnerId: 123
|
||||||
}),
|
}),
|
||||||
updateHubUser: (id: string, data: any): Observable<any> => of({
|
updateHubUser: (id: string, data: any): Observable<any> => of({
|
||||||
id,
|
id,
|
||||||
@ -49,7 +50,7 @@ const mockHubUsersService = {
|
|||||||
}),
|
}),
|
||||||
deleteHubUser: (id: string): Observable<void> => of(void 0),
|
deleteHubUser: (id: string): Observable<void> => of(void 0),
|
||||||
getAllDcbPartners: (): Observable<{users: any[]}> => of({
|
getAllDcbPartners: (): Observable<{users: any[]}> => of({
|
||||||
users: [{ id: 'keycloak-123', merchantConfigId: 123 }]
|
users: [{ id: 'keycloak-123', merchantPartnerId: 123 }]
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -78,7 +79,7 @@ const mockMerchantUsersService = {
|
|||||||
export class MerchantCrudTest {
|
export class MerchantCrudTest {
|
||||||
private testData = {
|
private testData = {
|
||||||
currentMerchantId: 0,
|
currentMerchantId: 0,
|
||||||
currentDcbPartnerId: '',
|
currentMerchantPartnerId: '',
|
||||||
currentUserId: '',
|
currentUserId: '',
|
||||||
currentMerchantConfigUserId: 0
|
currentMerchantConfigUserId: 0
|
||||||
};
|
};
|
||||||
@ -132,9 +133,9 @@ export class MerchantCrudTest {
|
|||||||
// 2. Créer DCB_PARTNER dans Keycloak
|
// 2. Créer DCB_PARTNER dans Keycloak
|
||||||
const dcbPartnerDto = {
|
const dcbPartnerDto = {
|
||||||
...ownerData,
|
...ownerData,
|
||||||
userType: 'HUB',
|
userType: UserType.HUB,
|
||||||
role: 'DCB_PARTNER',
|
role: UserRole.DCB_PARTNER_ADMIN,
|
||||||
merchantConfigId: merchantConfig.id
|
merchantPartnerId: merchantConfig.id
|
||||||
};
|
};
|
||||||
|
|
||||||
const keycloakMerchant = await firstValueFrom(
|
const keycloakMerchant = await firstValueFrom(
|
||||||
@ -144,7 +145,7 @@ export class MerchantCrudTest {
|
|||||||
|
|
||||||
// Sauvegarder les IDs
|
// Sauvegarder les IDs
|
||||||
this.testData.currentMerchantId = merchantConfig.id;
|
this.testData.currentMerchantId = merchantConfig.id;
|
||||||
this.testData.currentDcbPartnerId = keycloakMerchant.id;
|
this.testData.currentMerchantPartnerId = keycloakMerchant.id;
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
merchantConfig,
|
merchantConfig,
|
||||||
@ -163,7 +164,7 @@ export class MerchantCrudTest {
|
|||||||
async testCreateMerchantUser(): Promise<void> {
|
async testCreateMerchantUser(): Promise<void> {
|
||||||
console.log('🧪 TEST: CREATE Merchant User');
|
console.log('🧪 TEST: CREATE Merchant User');
|
||||||
|
|
||||||
if (!this.testData.currentMerchantId || !this.testData.currentDcbPartnerId) {
|
if (!this.testData.currentMerchantId || !this.testData.currentMerchantPartnerId) {
|
||||||
console.log('⚠️ Créez d\'abord un merchant');
|
console.log('⚠️ Créez d\'abord un merchant');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -182,8 +183,7 @@ export class MerchantCrudTest {
|
|||||||
const keycloakUserDto = {
|
const keycloakUserDto = {
|
||||||
...userData,
|
...userData,
|
||||||
userType: 'MERCHANT_PARTNER',
|
userType: 'MERCHANT_PARTNER',
|
||||||
merchantPartnerId: this.testData.currentDcbPartnerId,
|
merchantPartnerId: this.testData.currentMerchantId
|
||||||
merchantConfigId: this.testData.currentMerchantId
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const keycloakUser = await firstValueFrom(
|
const keycloakUser = await firstValueFrom(
|
||||||
@ -226,7 +226,7 @@ export class MerchantCrudTest {
|
|||||||
async testReadMerchant(): Promise<void> {
|
async testReadMerchant(): Promise<void> {
|
||||||
console.log('🧪 TEST: READ Merchant');
|
console.log('🧪 TEST: READ Merchant');
|
||||||
|
|
||||||
if (!this.testData.currentMerchantId || !this.testData.currentDcbPartnerId) {
|
if (!this.testData.currentMerchantId || !this.testData.currentMerchantPartnerId) {
|
||||||
console.log('⚠️ Créez d\'abord un merchant');
|
console.log('⚠️ Créez d\'abord un merchant');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -237,14 +237,14 @@ export class MerchantCrudTest {
|
|||||||
mockMerchantConfigService.getMerchantById(this.testData.currentMerchantId)
|
mockMerchantConfigService.getMerchantById(this.testData.currentMerchantId)
|
||||||
),
|
),
|
||||||
firstValueFrom(
|
firstValueFrom(
|
||||||
mockHubUsersService.getHubUserById(this.testData.currentDcbPartnerId)
|
mockHubUsersService.getHubUserById(this.testData.currentMerchantPartnerId)
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log('🎯 RESULTAT READ:', {
|
console.log('🎯 RESULTAT READ:', {
|
||||||
merchantConfig,
|
merchantConfig,
|
||||||
keycloakMerchant,
|
keycloakMerchant,
|
||||||
coherence: keycloakMerchant.merchantConfigId === merchantConfig.id ? '✅ OK' : '❌ INCOHÉRENT'
|
coherence: keycloakMerchant.merchantPartnerId === merchantConfig.id ? '✅ OK' : '❌ INCOHÉRENT'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -255,7 +255,7 @@ export class MerchantCrudTest {
|
|||||||
async testUpdateMerchant(): Promise<void> {
|
async testUpdateMerchant(): Promise<void> {
|
||||||
console.log('🧪 TEST: UPDATE Merchant');
|
console.log('🧪 TEST: UPDATE Merchant');
|
||||||
|
|
||||||
if (!this.testData.currentMerchantId || !this.testData.currentDcbPartnerId) {
|
if (!this.testData.currentMerchantId || !this.testData.currentMerchantPartnerId) {
|
||||||
console.log('⚠️ Créez d\'abord un merchant');
|
console.log('⚠️ Créez d\'abord un merchant');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -273,7 +273,7 @@ export class MerchantCrudTest {
|
|||||||
),
|
),
|
||||||
firstValueFrom(
|
firstValueFrom(
|
||||||
mockHubUsersService.updateHubUser(
|
mockHubUsersService.updateHubUser(
|
||||||
this.testData.currentDcbPartnerId,
|
this.testData.currentMerchantPartnerId,
|
||||||
{ email: newEmail }
|
{ email: newEmail }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -363,7 +363,7 @@ export class MerchantCrudTest {
|
|||||||
async testDeleteMerchant(): Promise<void> {
|
async testDeleteMerchant(): Promise<void> {
|
||||||
console.log('🧪 TEST: DELETE Merchant');
|
console.log('🧪 TEST: DELETE Merchant');
|
||||||
|
|
||||||
if (!this.testData.currentMerchantId || !this.testData.currentDcbPartnerId) {
|
if (!this.testData.currentMerchantId || !this.testData.currentMerchantPartnerId) {
|
||||||
console.log('⚠️ Créez d\'abord un merchant');
|
console.log('⚠️ Créez d\'abord un merchant');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -382,7 +382,7 @@ export class MerchantCrudTest {
|
|||||||
mockMerchantConfigService.deleteMerchant(this.testData.currentMerchantId)
|
mockMerchantConfigService.deleteMerchant(this.testData.currentMerchantId)
|
||||||
),
|
),
|
||||||
firstValueFrom(
|
firstValueFrom(
|
||||||
mockHubUsersService.deleteHubUser(this.testData.currentDcbPartnerId)
|
mockHubUsersService.deleteHubUser(this.testData.currentMerchantPartnerId)
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -390,7 +390,7 @@ export class MerchantCrudTest {
|
|||||||
|
|
||||||
// Réinitialiser
|
// Réinitialiser
|
||||||
this.testData.currentMerchantId = 0;
|
this.testData.currentMerchantId = 0;
|
||||||
this.testData.currentDcbPartnerId = '';
|
this.testData.currentMerchantPartnerId = '';
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ ERREUR DELETE MERCHANT:', error);
|
console.error('❌ ERREUR DELETE MERCHANT:', error);
|
||||||
@ -449,7 +449,7 @@ export class MerchantCrudTest {
|
|||||||
resetTestData() {
|
resetTestData() {
|
||||||
this.testData = {
|
this.testData = {
|
||||||
currentMerchantId: 0,
|
currentMerchantId: 0,
|
||||||
currentDcbPartnerId: '',
|
currentMerchantPartnerId: '',
|
||||||
currentUserId: '',
|
currentUserId: '',
|
||||||
currentMerchantConfigUserId: 0
|
currentMerchantConfigUserId: 0
|
||||||
};
|
};
|
||||||
|
|||||||
@ -153,6 +153,10 @@
|
|||||||
<table class="table table-hover table-striped">
|
<table class="table table-hover table-striped">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
|
<!-- Colonne Merchant Partner pour les admins -->
|
||||||
|
@if (showMerchantPartnerColumn) {
|
||||||
|
<th>Merchant Partner</th>
|
||||||
|
}
|
||||||
<th (click)="sort('username')" class="cursor-pointer">
|
<th (click)="sort('username')" class="cursor-pointer">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span>Utilisateur</span>
|
<span>Utilisateur</span>
|
||||||
@ -184,6 +188,21 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@for (user of displayedUsers; track user.id) {
|
@for (user of displayedUsers; track user.id) {
|
||||||
<tr>
|
<tr>
|
||||||
|
<!-- Colonne Merchant Partner pour les admins -->
|
||||||
|
@if (showMerchantPartnerColumn) {
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="avatar-sm bg-secondary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||||
|
<ng-icon name="lucideBuilding" class="text-secondary fs-12"></ng-icon>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<small class="text-muted font-monospace" [title]="user.merchantPartnerId || 'N/A'">
|
||||||
|
{{ (user.merchantPartnerId || 'N/A').substring(0, 8) }}...
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
import { MerchantUsersService } from '../merchant-users.service';
|
import { MerchantUsersService } from '../merchant-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { UiCard } from '@app/components/ui-card';
|
import { UiCard } from '@app/components/ui-card';
|
||||||
|
|
||||||
@ -118,12 +118,6 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
this.currentUserRole = this.extractUserRole(user);
|
this.currentUserRole = this.extractUserRole(user);
|
||||||
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
|
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
|
||||||
|
|
||||||
console.log('Merchant User Context Loaded:', {
|
|
||||||
role: this.currentUserRole,
|
|
||||||
merchantPartnerId: this.currentMerchantPartnerId,
|
|
||||||
canViewAllMerchants: this.canViewAllMerchants
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loadUsers();
|
this.loadUsers();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
@ -391,7 +385,7 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
return roleInfo?.description || 'Description non disponible';
|
return roleInfo?.description || 'Description non disponible';
|
||||||
}
|
}
|
||||||
|
|
||||||
formatTimestamp(timestamp: number): string {
|
formatTimestamp(timestamp: number | undefined): string {
|
||||||
if (!timestamp) return 'Non disponible';
|
if (!timestamp) return 'Non disponible';
|
||||||
return new Date(timestamp).toLocaleDateString('fr-FR', {
|
return new Date(timestamp).toLocaleDateString('fr-FR', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
import { MerchantUsersService } from '../merchant-users.service';
|
import { MerchantUsersService } from '../merchant-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@ -271,6 +271,7 @@ export class MerchantUsersService {
|
|||||||
lastName: apiUser.lastName,
|
lastName: apiUser.lastName,
|
||||||
enabled: apiUser.enabled,
|
enabled: apiUser.enabled,
|
||||||
emailVerified: apiUser.emailVerified,
|
emailVerified: apiUser.emailVerified,
|
||||||
|
merchantPartnerId: apiUser.merchantPartnerId,
|
||||||
userType: userType,
|
userType: userType,
|
||||||
role: apiUser.role,
|
role: apiUser.role,
|
||||||
createdBy: apiUser.createdBy,
|
createdBy: apiUser.createdBy,
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstr
|
|||||||
import { catchError, map, of, Subject, takeUntil } from 'rxjs';
|
import { catchError, map, of, Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { MerchantUsersService } from './merchant-users.service';
|
import { MerchantUsersService } from './merchant-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
import { MerchantUsersList } from './merchant-users-list/merchant-users-list';
|
import { MerchantUsersList } from './merchant-users-list/merchant-users-list';
|
||||||
@ -157,8 +157,6 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.currentUserType = this.extractUserType(user);
|
this.currentUserType = this.extractUserType(user);
|
||||||
|
|
||||||
console.log(`MERCHANT User ROLE: ${this.currentUserRole}`);
|
|
||||||
|
|
||||||
if (this.currentUserRole) {
|
if (this.currentUserRole) {
|
||||||
this.roleService.setCurrentUserRole(this.currentUserRole);
|
this.roleService.setCurrentUserRole(this.currentUserRole);
|
||||||
this.userPermissions = this.roleService.getPermissionsForRole(this.currentUserRole);
|
this.userPermissions = this.roleService.getPermissionsForRole(this.currentUserRole);
|
||||||
@ -167,7 +165,6 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole);
|
this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole);
|
||||||
|
|
||||||
this.assignableRoles = this.roleService.getAssignableRoles(this.currentUserRole);
|
this.assignableRoles = this.roleService.getAssignableRoles(this.currentUserRole);
|
||||||
console.log('Assignable roles:', this.assignableRoles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
} from '@core/models/merchant-config.model';
|
} from '@core/models/merchant-config.model';
|
||||||
|
|
||||||
import { MerchantConfigService } from '../merchant-config.service';
|
import { MerchantConfigService } from '../merchant-config.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { UiCard } from '@app/components/ui-card';
|
import { UiCard } from '@app/components/ui-card';
|
||||||
|
|
||||||
@ -158,10 +158,6 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
|
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
|
||||||
|
|
||||||
console.log(`MERCHANT User ROLE: ${this.currentUserRole}`);
|
|
||||||
console.log(`Merchant Config ID: ${this.currentMerchantConfigId}`);
|
|
||||||
console.log(`canViewAllMerchants: ${this.canViewAllMerchants}`);
|
|
||||||
|
|
||||||
this.loadMerchants();
|
this.loadMerchants();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import {
|
|||||||
|
|
||||||
import { MerchantConfigService } from '../merchant-config.service';
|
import { MerchantConfigService } from '../merchant-config.service';
|
||||||
import { MerchantDataAdapter } from '../merchant-data-adapter.service';
|
import { MerchantDataAdapter } from '../merchant-data-adapter.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { UserRole } from '@core/models/dcb-bo-hub-user.model';
|
import { UserRole } from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
|
|||||||
@ -199,7 +199,6 @@ export class MerchantConfigService {
|
|||||||
timeout(this.REQUEST_TIMEOUT),
|
timeout(this.REQUEST_TIMEOUT),
|
||||||
map(apiUser => {
|
map(apiUser => {
|
||||||
console.log(`✅ User ${userId} role updated successfully`);
|
console.log(`✅ User ${userId} role updated successfully`);
|
||||||
// ✅ UTILISATION DE L'ADAPTER
|
|
||||||
return this.dataAdapter.convertApiUserToFrontend(apiUser);
|
return this.dataAdapter.convertApiUserToFrontend(apiUser);
|
||||||
}),
|
}),
|
||||||
catchError(error => this.handleError('updateUserRole', error, { merchantId, userId }))
|
catchError(error => this.handleError('updateUserRole', error, { merchantId, userId }))
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstr
|
|||||||
import { catchError, finalize, map, of, Subject, takeUntil } from 'rxjs';
|
import { catchError, finalize, map, of, Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { MerchantConfigService } from './merchant-config.service';
|
import { MerchantConfigService } from './merchant-config.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { MerchantSyncService } from '../hub-users-management/merchant-sync-orchestrator.service';
|
import { MerchantSyncService } from '../hub-users-management/merchant-sync-orchestrator.service';
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
|
|
||||||
import { HubUsersService } from '@modules/hub-users-management/hub-users.service';
|
import { HubUsersService } from '@modules/hub-users-management/hub-users.service';
|
||||||
import { MerchantUsersService } from '@modules/hub-users-management/merchant-users.service';
|
import { MerchantUsersService } from '@modules/hub-users-management/merchant-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import {
|
|||||||
Currency
|
Currency
|
||||||
} from '@core/models/dcb-bo-hub-subscription.model';
|
} from '@core/models/dcb-bo-hub-subscription.model';
|
||||||
import { User, UserRole } from '@core/models/dcb-bo-hub-user.model';
|
import { User, UserRole } from '@core/models/dcb-bo-hub-user.model';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-subscriptions',
|
selector: 'app-subscriptions',
|
||||||
|
|||||||
@ -1,4 +1,18 @@
|
|||||||
<div class="transaction-details">
|
<div class="transaction-details">
|
||||||
|
|
||||||
|
<div class="transaction-details">
|
||||||
|
<!-- Message d'accès refusé -->
|
||||||
|
@if (accessDenied) {
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<ng-icon name="lucideLock" class="text-danger fs-1 mb-3"></ng-icon>
|
||||||
|
<h5 class="text-danger">Accès refusé</h5>
|
||||||
|
<p class="text-muted mb-4">Vous n'avez pas la permission d'accéder à cette transaction.</p>
|
||||||
|
<button class="btn btn-primary" routerLink="/transactions">
|
||||||
|
<ng-icon name="lucideArrowLeft" class="me-1"></ng-icon>
|
||||||
|
Retour à la liste
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
@if (loading && !transaction) {
|
@if (loading && !transaction) {
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
@ -61,7 +75,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="transaction-amount-icon bg-primary rounded-circle p-3 me-3">
|
<div class="transaction-amount-icon bg-primary rounded-circle p-3 me-3">
|
||||||
<ng-icon name="lucideEuro" class="text-white fs-4"></ng-icon>
|
<ng-icon name="lucideBanknote" class="text-white fs-4"></ng-icon>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-muted small">Montant</div>
|
<div class="text-muted small">Montant</div>
|
||||||
@ -92,27 +106,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label class="form-label text-muted small mb-1">MSISDN</label>
|
<label class="form-label text-muted small mb-1">Plan/Service</label>
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<ng-icon name="lucideSmartphone" class="me-2 text-muted"></ng-icon>
|
|
||||||
<span class="fw-medium font-monospace">{{ transaction.msisdn }}</span>
|
|
||||||
<button class="btn btn-sm btn-link p-0 ms-2" (click)="copyToClipboard(transaction.msisdn)">
|
|
||||||
<ng-icon name="lucideCopy" class="text-muted"></ng-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label text-muted small mb-1">Opérateur</label>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<ng-icon name="lucideGlobe" class="me-2 text-muted"></ng-icon>
|
|
||||||
<span class="fw-medium">{{ transaction.operator }}</span>
|
|
||||||
<span class="badge bg-light text-dark ms-2">{{ transaction.country }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label text-muted small mb-1">Produit</label>
|
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon name="lucidePackage" class="me-2 text-muted"></ng-icon>
|
<ng-icon name="lucidePackage" class="me-2 text-muted"></ng-icon>
|
||||||
<div>
|
<div>
|
||||||
@ -122,29 +116,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
@if (transaction.externalReference) {
|
||||||
<label class="form-label text-muted small mb-1">Catégorie</label>
|
|
||||||
<div>
|
|
||||||
<span class="badge bg-info">{{ transaction.productCategory }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (transaction.merchantName) {
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label text-muted small mb-1">Marchand</label>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<ng-icon name="lucideUser" class="me-2 text-muted"></ng-icon>
|
|
||||||
<span class="fw-medium">{{ transaction.merchantName }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (transaction.externalId) {
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label class="form-label text-muted small mb-1">ID Externe</label>
|
<label class="form-label text-muted small mb-1">ID Externe</label>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span class="font-monospace small">{{ transaction.externalId }}</span>
|
<span class="font-monospace small">{{ transaction.externalReference }}</span>
|
||||||
<button class="btn btn-sm btn-link p-0 ms-2" (click)="copyToClipboard(transaction.externalId!)">
|
<button class="btn btn-sm btn-link p-0 ms-2" (click)="copyToClipboard(transaction.externalReference!)">
|
||||||
<ng-icon name="lucideCopy" class="text-muted"></ng-icon>
|
<ng-icon name="lucideCopy" class="text-muted"></ng-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -167,114 +144,14 @@
|
|||||||
<label class="form-label text-muted small mb-1">Mis à jour le</label>
|
<label class="form-label text-muted small mb-1">Mis à jour le</label>
|
||||||
<div class="small">{{ formatDate(transaction.updatedAt) }}</div>
|
<div class="small">{{ formatDate(transaction.updatedAt) }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (transaction.userAgent) {
|
|
||||||
<div class="col-12 mb-2">
|
|
||||||
<label class="form-label text-muted small mb-1">User Agent</label>
|
|
||||||
<div class="small font-monospace text-truncate">{{ transaction.userAgent }}</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (transaction.ipAddress) {
|
|
||||||
<div class="col-md-6 mb-2">
|
|
||||||
<label class="form-label text-muted small mb-1">Adresse IP</label>
|
|
||||||
<div class="small font-monospace">{{ transaction.ipAddress }}</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Détails d'erreur -->
|
|
||||||
@if (showErrorDetails()) {
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h6 class="border-bottom pb-2 mb-3 text-danger">
|
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
|
||||||
Détails de l'erreur
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (transaction.errorCode) {
|
|
||||||
<div class="col-md-6 mb-2">
|
|
||||||
<label class="form-label text-muted small mb-1">Code d'erreur</label>
|
|
||||||
<div class="fw-medium text-danger">{{ transaction.errorCode }}</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (transaction.errorMessage) {
|
|
||||||
<div class="col-12 mb-2">
|
|
||||||
<label class="form-label text-muted small mb-1">Message d'erreur</label>
|
|
||||||
<div class="alert alert-danger small mb-0">{{ transaction.errorMessage }}</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Colonne latérale - Actions et métadonnées -->
|
<!-- Colonne latérale - Actions et métadonnées -->
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<!-- Actions -->
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header bg-light">
|
|
||||||
<h6 class="card-title mb-0">Actions</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-grid gap-2">
|
|
||||||
<!-- Remboursement -->
|
|
||||||
@if (canRefund()) {
|
|
||||||
<button
|
|
||||||
class="btn btn-warning"
|
|
||||||
(click)="refundTransaction()"
|
|
||||||
[disabled]="refunding"
|
|
||||||
>
|
|
||||||
@if (refunding) {
|
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status">
|
|
||||||
<span class="visually-hidden">Chargement...</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<ng-icon name="lucideUndo2" class="me-1"></ng-icon>
|
|
||||||
Rembourser
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Réessayer -->
|
|
||||||
@if (canRetry()) {
|
|
||||||
<button
|
|
||||||
class="btn btn-info"
|
|
||||||
(click)="retryTransaction()"
|
|
||||||
[disabled]="retrying"
|
|
||||||
>
|
|
||||||
@if (retrying) {
|
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status">
|
|
||||||
<span class="visually-hidden">Chargement...</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<ng-icon name="lucideRefreshCw" class="me-1"></ng-icon>
|
|
||||||
Réessayer
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Annuler -->
|
|
||||||
@if (canCancel()) {
|
|
||||||
<button
|
|
||||||
class="btn btn-secondary"
|
|
||||||
(click)="cancelTransaction()"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideBan" class="me-1"></ng-icon>
|
|
||||||
Annuler
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Télécharger les détails -->
|
|
||||||
<button class="btn btn-outline-primary">
|
|
||||||
<ng-icon name="lucideDownload" class="me-1"></ng-icon>
|
|
||||||
Télécharger PDF
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Métadonnées -->
|
<!-- Métadonnées -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header bg-light">
|
||||||
@ -286,16 +163,10 @@
|
|||||||
<span class="text-muted">ID Transaction:</span>
|
<span class="text-muted">ID Transaction:</span>
|
||||||
<span class="font-monospace">{{ transaction.id }}</span>
|
<span class="font-monospace">{{ transaction.id }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@if (transaction.merchantPartnerId) {
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span class="text-muted">Opérateur ID:</span>
|
|
||||||
<span>{{ transaction.operatorId }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (transaction.merchantId) {
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<span class="text-muted">Marchand ID:</span>
|
<span class="text-muted">Marchand ID:</span>
|
||||||
<span class="font-monospace small">{{ transaction.merchantId }}</span>
|
<span class="font-monospace small">{{ transaction.merchantPartnerId }}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,25 +184,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Données personnalisées -->
|
|
||||||
@if (getCustomDataKeys().length > 0) {
|
|
||||||
<div class="card mt-4">
|
|
||||||
<div class="card-header bg-light">
|
|
||||||
<h6 class="card-title mb-0">Données personnalisées</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="small">
|
|
||||||
@for (key of getCustomDataKeys(); track key) {
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span class="text-muted">{{ key }}:</span>
|
|
||||||
<span class="font-monospace small">{{ transaction.customData![key] }}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -348,4 +200,5 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
// [file name]: transactions/details/details.ts (mise à jour)
|
||||||
import { Component, inject, OnInit, Input, ChangeDetectorRef } from '@angular/core';
|
import { Component, inject, OnInit, Input, ChangeDetectorRef } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
@ -21,12 +22,16 @@ import {
|
|||||||
lucideUser,
|
lucideUser,
|
||||||
lucideGlobe,
|
lucideGlobe,
|
||||||
lucideAlertCircle,
|
lucideAlertCircle,
|
||||||
lucideInfo
|
lucideInfo,
|
||||||
|
lucideShield,
|
||||||
|
lucideStore,
|
||||||
|
lucideLock
|
||||||
} from '@ng-icons/lucide';
|
} from '@ng-icons/lucide';
|
||||||
import { NgbAlertModule, NgbTooltipModule, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbAlertModule, NgbTooltipModule, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
import { TransactionsService } from '../services/transactions.service';
|
import { TransactionsService } from '../services/transactions.service';
|
||||||
import { Transaction, TransactionStatus, RefundRequest } from '../models/transaction';
|
import { TransactionAccessService, TransactionAccess } from '../services/transaction-access.service';
|
||||||
|
import { Transaction, TransactionStatus} from '@core/models/dcb-bo-hub-transaction.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-transaction-details',
|
selector: 'app-transaction-details',
|
||||||
@ -48,11 +53,15 @@ import { Transaction, TransactionStatus, RefundRequest } from '../models/transac
|
|||||||
})
|
})
|
||||||
export class TransactionDetails implements OnInit {
|
export class TransactionDetails implements OnInit {
|
||||||
private transactionsService = inject(TransactionsService);
|
private transactionsService = inject(TransactionsService);
|
||||||
|
private accessService = inject(TransactionAccessService);
|
||||||
private modalService = inject(NgbModal);
|
private modalService = inject(NgbModal);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
@Input() transactionId!: string;
|
@Input() transactionId!: string;
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
access!: TransactionAccess;
|
||||||
|
|
||||||
// Données
|
// Données
|
||||||
transaction: Transaction | null = null;
|
transaction: Transaction | null = null;
|
||||||
loading = false;
|
loading = false;
|
||||||
@ -63,100 +72,62 @@ export class TransactionDetails implements OnInit {
|
|||||||
refunding = false;
|
refunding = false;
|
||||||
retrying = false;
|
retrying = false;
|
||||||
|
|
||||||
|
// Accès
|
||||||
|
canViewSensitiveData = false;
|
||||||
|
accessDenied = false;
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.initializePermissions();
|
||||||
|
|
||||||
if (this.transactionId) {
|
if (this.transactionId) {
|
||||||
this.loadTransactionDetails();
|
this.loadTransactionDetails();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initializePermissions() {
|
||||||
|
this.access = this.accessService.getTransactionAccess();
|
||||||
|
this.canViewSensitiveData = this.access.canViewSensitiveData;
|
||||||
|
}
|
||||||
|
|
||||||
loadTransactionDetails() {
|
loadTransactionDetails() {
|
||||||
|
if (!this.access.canViewDetails) {
|
||||||
|
this.error = 'Vous n\'avez pas la permission de voir les détails des transactions';
|
||||||
|
this.accessDenied = true;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
|
this.accessDenied = false;
|
||||||
|
|
||||||
this.transactionsService.getTransactionById(this.transactionId).subscribe({
|
this.transactionsService.getTransactionById(this.transactionId).subscribe({
|
||||||
next: (transaction) => {
|
next: (transaction) => {
|
||||||
|
// Vérifier si l'utilisateur a accès à cette transaction spécifique
|
||||||
|
this.accessService.canAccessTransaction(transaction.merchantPartnerId ? transaction.merchantPartnerId : undefined)
|
||||||
|
.subscribe(canAccess => {
|
||||||
|
if (!canAccess) {
|
||||||
|
this.error = 'Vous n\'avez pas accès à cette transaction';
|
||||||
|
this.accessDenied = true;
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.transaction = transaction;
|
this.transaction = transaction;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.error = 'Erreur lors du chargement des détails de la transaction';
|
this.error = 'Erreur lors du chargement des détails de la transaction';
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
// Données mockées pour le développement
|
|
||||||
const mockTransactions = this.transactionsService.getMockTransactions();
|
|
||||||
this.transaction = mockTransactions.find(tx => tx.id === this.transactionId) || mockTransactions[0];
|
|
||||||
this.loading = false;
|
|
||||||
|
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
console.error('Error loading transaction details:', error);
|
console.error('Error loading transaction details:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions sur la transaction
|
|
||||||
refundTransaction() {
|
|
||||||
if (!this.transaction) return;
|
|
||||||
|
|
||||||
this.refunding = true;
|
|
||||||
const refundRequest: RefundRequest = {
|
|
||||||
transactionId: this.transaction.id,
|
|
||||||
reason: 'Remboursement manuel par l\'administrateur'
|
|
||||||
};
|
|
||||||
|
|
||||||
this.transactionsService.refundTransaction(refundRequest).subscribe({
|
|
||||||
next: (response) => {
|
|
||||||
this.transaction = response.transaction;
|
|
||||||
this.refunding = false;
|
|
||||||
this.success = 'Transaction remboursée avec succès';
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
this.refunding = false;
|
|
||||||
this.error = 'Erreur lors du remboursement de la transaction';
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
console.error('Error refunding transaction:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
retryTransaction() {
|
|
||||||
if (!this.transaction) return;
|
|
||||||
|
|
||||||
this.retrying = true;
|
|
||||||
this.transactionsService.retryTransaction(this.transaction.id).subscribe({
|
|
||||||
next: (response) => {
|
|
||||||
this.transaction = response.transaction;
|
|
||||||
this.retrying = false;
|
|
||||||
this.success = 'Nouvelle tentative lancée avec succès';
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
this.retrying = false;
|
|
||||||
this.error = 'Erreur lors de la nouvelle tentative';
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
console.error('Error retrying transaction:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelTransaction() {
|
|
||||||
if (!this.transaction) return;
|
|
||||||
|
|
||||||
this.transactionsService.cancelTransaction(this.transaction.id).subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.success = 'Transaction annulée avec succès';
|
|
||||||
this.loadTransactionDetails(); // Recharger les données
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
this.error = 'Erreur lors de l\'annulation de la transaction';
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
console.error('Error cancelling transaction:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utilitaires
|
// Utilitaires
|
||||||
copyToClipboard(text: string) {
|
copyToClipboard(text: string) {
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
@ -170,21 +141,12 @@ export class TransactionDetails implements OnInit {
|
|||||||
window.print();
|
window.print();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthode pour obtenir les clés des données personnalisées
|
|
||||||
getCustomDataKeys(): string[] {
|
|
||||||
if (!this.transaction?.customData) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return Object.keys(this.transaction.customData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters pour l'affichage
|
// Getters pour l'affichage
|
||||||
getStatusBadgeClass(status: TransactionStatus): string {
|
getStatusBadgeClass(status: TransactionStatus): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'SUCCESS': return 'badge bg-success';
|
case 'SUCCESS': return 'badge bg-success';
|
||||||
case 'PENDING': return 'badge bg-warning';
|
case 'PENDING': return 'badge bg-warning';
|
||||||
case 'FAILED': return 'badge bg-danger';
|
case 'FAILED': return 'badge bg-danger';
|
||||||
case 'REFUNDED': return 'badge bg-info';
|
|
||||||
case 'CANCELLED': return 'badge bg-secondary';
|
case 'CANCELLED': return 'badge bg-secondary';
|
||||||
case 'EXPIRED': return 'badge bg-dark';
|
case 'EXPIRED': return 'badge bg-dark';
|
||||||
default: return 'badge bg-secondary';
|
default: return 'badge bg-secondary';
|
||||||
@ -196,7 +158,6 @@ export class TransactionDetails implements OnInit {
|
|||||||
case 'SUCCESS': return 'lucideCheckCircle';
|
case 'SUCCESS': return 'lucideCheckCircle';
|
||||||
case 'PENDING': return 'lucideClock';
|
case 'PENDING': return 'lucideClock';
|
||||||
case 'FAILED': return 'lucideXCircle';
|
case 'FAILED': return 'lucideXCircle';
|
||||||
case 'REFUNDED': return 'lucideUndo2';
|
|
||||||
case 'CANCELLED': return 'lucideBan';
|
case 'CANCELLED': return 'lucideBan';
|
||||||
default: return 'lucideClock';
|
default: return 'lucideClock';
|
||||||
}
|
}
|
||||||
@ -241,18 +202,31 @@ export class TransactionDetails implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canRefund(): boolean {
|
canRefund(): boolean {
|
||||||
return this.transaction?.status === 'SUCCESS';
|
return this.access.canRefund && this.transaction?.status === 'SUCCESS';
|
||||||
}
|
}
|
||||||
|
|
||||||
canRetry(): boolean {
|
canRetry(): boolean {
|
||||||
return this.transaction?.status === 'FAILED';
|
return this.access.canRetry && this.transaction?.status === 'FAILED';
|
||||||
}
|
}
|
||||||
|
|
||||||
canCancel(): boolean {
|
canCancel(): boolean {
|
||||||
return this.transaction?.status === 'PENDING';
|
return this.access.canCancel && this.transaction?.status === 'PENDING';
|
||||||
}
|
}
|
||||||
|
|
||||||
showErrorDetails(): boolean {
|
// Méthodes pour le template
|
||||||
return !!this.transaction?.errorCode || !!this.transaction?.errorMessage;
|
getUserBadgeClass(): string {
|
||||||
|
return this.access.isHubUser ? 'bg-primary' : 'bg-success';
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserBadgeIcon(): string {
|
||||||
|
return this.access.isHubUser ? 'lucideShield' : 'lucideStore';
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserBadgeText(): string {
|
||||||
|
return this.access.isHubUser ? 'Hub User' : 'Merchant User';
|
||||||
|
}
|
||||||
|
|
||||||
|
showSensitiveData(): boolean {
|
||||||
|
return this.canViewSensitiveData && !this.accessDenied;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,6 +5,7 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="mb-1">Gestion des Transactions</h4>
|
<h4 class="mb-1">Gestion des Transactions</h4>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb mb-0">
|
<ol class="breadcrumb mb-0">
|
||||||
<li class="breadcrumb-item">
|
<li class="breadcrumb-item">
|
||||||
@ -12,22 +13,18 @@
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
<span [class]="getUserBadgeClass()" class="badge">
|
||||||
|
<ng-icon [name]="getUserBadgeIcon()" class="me-1"></ng-icon>
|
||||||
|
{{ getUserBadgeText() }}
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-info" *ngIf="currentMerchantId">
|
||||||
|
<ng-icon name="lucideStore" class="me-1"></ng-icon>
|
||||||
|
Merchant {{ currentMerchantId }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<!-- Export -->
|
|
||||||
<div ngbDropdown>
|
|
||||||
<button class="btn btn-outline-primary" ngbDropdownToggle>
|
|
||||||
<ng-icon name="lucideDownload" class="me-1"></ng-icon>
|
|
||||||
Exporter
|
|
||||||
</button>
|
|
||||||
<div ngbDropdownMenu>
|
|
||||||
<button ngbDropdownItem (click)="exportTransactions('csv')">CSV</button>
|
|
||||||
<button ngbDropdownItem (click)="exportTransactions('excel')">Excel</button>
|
|
||||||
<button ngbDropdownItem (click)="exportTransactions('pdf')">PDF</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Refresh -->
|
<!-- Refresh -->
|
||||||
<button class="btn btn-outline-secondary" (click)="loadTransactions()" [disabled]="loading">
|
<button class="btn btn-outline-secondary" (click)="loadTransactions()" [disabled]="loading">
|
||||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading"></ng-icon>
|
<ng-icon name="lucideRefreshCw" [class.spin]="loading"></ng-icon>
|
||||||
@ -37,6 +34,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Message d'accès refusé -->
|
||||||
|
@if (!access.canViewTransactions) {
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideLock" class="me-2"></ng-icon>
|
||||||
|
<div>
|
||||||
|
<strong>Accès refusé</strong>
|
||||||
|
<p class="mb-0">Vous n'avez pas les permissions nécessaires pour accéder à cette section.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
|
||||||
<!-- Statistiques rapides -->
|
<!-- Statistiques rapides -->
|
||||||
@if (paginatedData?.stats) {
|
@if (paginatedData?.stats) {
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
@ -61,10 +71,6 @@
|
|||||||
<div class="h5 mb-0 text-warning">{{ getPendingCount() }}</div>
|
<div class="h5 mb-0 text-warning">{{ getPendingCount() }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<small class="text-muted">Taux de succès</small>
|
|
||||||
<div class="h5 mb-0 text-primary">{{ getSuccessRate() }}%</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">Montant total</small>
|
<small class="text-muted">Montant total</small>
|
||||||
<div class="h5 mb-0">{{ formatCurrency(getTotalAmount()) }}</div>
|
<div class="h5 mb-0">{{ formatCurrency(getTotalAmount()) }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -85,7 +91,7 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Rechercher par MSISDN, ID..."
|
placeholder="Rechercher par periodicity, Type..."
|
||||||
[(ngModel)]="searchTerm"
|
[(ngModel)]="searchTerm"
|
||||||
(keyup.enter)="onSearch()"
|
(keyup.enter)="onSearch()"
|
||||||
>
|
>
|
||||||
@ -163,27 +169,23 @@
|
|||||||
<ng-icon [name]="getSortIcon('id')" class="ms-1 fs-12"></ng-icon>
|
<ng-icon [name]="getSortIcon('id')" class="ms-1 fs-12"></ng-icon>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th (click)="sort('msisdn')" class="cursor-pointer">
|
<th>Type</th>
|
||||||
<div class="d-flex align-items-center">
|
<th>Merchant</th>
|
||||||
<span>MSISDN</span>
|
|
||||||
<ng-icon [name]="getSortIcon('msisdn')" class="ms-1 fs-12"></ng-icon>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th>Opérateur</th>
|
|
||||||
<th (click)="sort('amount')" class="cursor-pointer">
|
<th (click)="sort('amount')" class="cursor-pointer">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span>Montant</span>
|
<span>Montant</span>
|
||||||
<ng-icon [name]="getSortIcon('amount')" class="ms-1 fs-12"></ng-icon>
|
<ng-icon [name]="getSortIcon('amount')" class="ms-1 fs-12"></ng-icon>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th>Produit</th>
|
<th>Périodicité</th>
|
||||||
<th>Statut</th>
|
<th>Statut</th>
|
||||||
<th (click)="sort('transactionDate')" class="cursor-pointer">
|
<th (click)="sort('transactionDate')" class="cursor-pointer">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span>Date</span>
|
<span>Date début</span>
|
||||||
<ng-icon [name]="getSortIcon('transactionDate')" class="ms-1 fs-12"></ng-icon>
|
<ng-icon [name]="getSortIcon('transactionDate')" class="ms-1 fs-12"></ng-icon>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
<th>Prochain paiement</th>
|
||||||
<th width="120">Actions</th>
|
<th width="120">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -199,9 +201,16 @@
|
|||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td class="font-monospace small">{{ transaction.id }}</td>
|
<td class="font-monospace small">{{ transaction.id }}</td>
|
||||||
<td class="font-monospace">{{ transaction.msisdn }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-light text-dark">{{ transaction.operator }}</span>
|
<span class="badge bg-light text-dark">
|
||||||
|
<ng-icon [name]="getTypeIcon(transaction.type)" class="me-1"></ng-icon>
|
||||||
|
Abonnement
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-secondary">
|
||||||
|
Merchant {{ transaction.merchantPartnerId }}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span [class]="getAmountColor(transaction.amount)">
|
<span [class]="getAmountColor(transaction.amount)">
|
||||||
@ -209,20 +218,28 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="text-truncate" style="max-width: 150px;"
|
@if (transaction.periodicity) {
|
||||||
[ngbTooltip]="transaction.productName">
|
<span [class]="getPeriodicityBadgeClass(transaction.periodicity)" class="badge">
|
||||||
{{ transaction.productName }}
|
{{ getPeriodicityDisplayName(transaction.periodicity) }}
|
||||||
</div>
|
</span>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span [class]="getStatusBadgeClass(transaction.status)" class="badge">
|
<span [class]="getStatusBadgeClass(transaction.status)" class="badge">
|
||||||
<ng-icon [name]="getStatusIcon(transaction.status)" class="me-1"></ng-icon>
|
<ng-icon [name]="getStatusIcon(transaction.status)" class="me-1"></ng-icon>
|
||||||
{{ transaction.status }}
|
{{ getStatusDisplayName(transaction.status) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="small text-muted">
|
<td class="small text-muted">
|
||||||
{{ formatDate(transaction.transactionDate) }}
|
{{ formatDate(transaction.transactionDate) }}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="small text-muted">
|
||||||
|
@if (transaction.nextPaymentDate) {
|
||||||
|
{{ formatDate(transaction.nextPaymentDate) }}
|
||||||
|
} @else {
|
||||||
|
-
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
<button
|
<button
|
||||||
@ -232,33 +249,13 @@
|
|||||||
>
|
>
|
||||||
<ng-icon name="lucideEye"></ng-icon>
|
<ng-icon name="lucideEye"></ng-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@if (transaction.status === 'SUCCESS') {
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-warning"
|
|
||||||
(click)="refundTransaction(transaction.id)"
|
|
||||||
ngbTooltip="Rembourser"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideUndo2"></ng-icon>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (transaction.status === 'FAILED') {
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-info"
|
|
||||||
(click)="retryTransaction(transaction.id)"
|
|
||||||
ngbTooltip="Réessayer"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideRefreshCw"></ng-icon>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@empty {
|
@empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center py-4">
|
<td colspan="10" class="text-center py-4">
|
||||||
<ng-icon name="lucideCreditCard" class="text-muted fs-1 mb-2 d-block"></ng-icon>
|
<ng-icon name="lucideCreditCard" class="text-muted fs-1 mb-2 d-block"></ng-icon>
|
||||||
<p class="text-muted mb-3">Aucune transaction trouvée</p>
|
<p class="text-muted mb-3">Aucune transaction trouvée</p>
|
||||||
<button class="btn btn-primary" (click)="onClearFilters()">
|
<button class="btn btn-primary" (click)="onClearFilters()">
|
||||||
@ -274,7 +271,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
@if (paginatedData && paginatedData.totalPages > 1) {
|
@if (paginatedData && paginatedData.totalPages >= 1) {
|
||||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Affichage de {{ (filters.page! - 1) * filters.limit! + 1 }} à
|
Affichage de {{ (filters.page! - 1) * filters.limit! + 1 }} à
|
||||||
@ -295,4 +292,6 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Component, inject, OnInit, ChangeDetectorRef, Output, EventEmitter } from '@angular/core';
|
import { Component, inject, OnInit, ChangeDetectorRef, Output, EventEmitter, OnDestroy } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NgIcon, provideNgIconsConfig } from '@ng-icons/core';
|
import { NgIcon, provideNgIconsConfig } from '@ng-icons/core';
|
||||||
@ -16,13 +16,32 @@ import {
|
|||||||
lucideClock,
|
lucideClock,
|
||||||
lucideXCircle,
|
lucideXCircle,
|
||||||
lucideUndo2,
|
lucideUndo2,
|
||||||
lucideBan
|
lucideBan,
|
||||||
|
lucideShield,
|
||||||
|
lucideStore,
|
||||||
|
lucideCalendar,
|
||||||
|
lucideRepeat,
|
||||||
|
lucideCreditCard
|
||||||
} from '@ng-icons/lucide';
|
} from '@ng-icons/lucide';
|
||||||
import { NgbPaginationModule, NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbPaginationModule, NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
import { TransactionsService } from '../services/transactions.service';
|
import { TransactionsService } from '../services/transactions.service';
|
||||||
import { Transaction, TransactionQuery, TransactionStatus, PaginatedTransactions } from '../models/transaction';
|
import { TransactionAccessService, TransactionAccess } from '../services/transaction-access.service';
|
||||||
import { environment } from '@environments/environment';
|
|
||||||
|
import {
|
||||||
|
Transaction,
|
||||||
|
TransactionQuery,
|
||||||
|
PaginatedTransactions,
|
||||||
|
TransactionType,
|
||||||
|
TransactionStatus,
|
||||||
|
TransactionUtils
|
||||||
|
} from '@core/models/dcb-bo-hub-transaction.model';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Currency,
|
||||||
|
SubscriptionPeriodicity,
|
||||||
|
SubscriptionUtils
|
||||||
|
} from '@core/models/dcb-bo-hub-subscription.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-transactions-list',
|
selector: 'app-transactions-list',
|
||||||
@ -42,13 +61,19 @@ import { environment } from '@environments/environment';
|
|||||||
],
|
],
|
||||||
templateUrl: './list.html'
|
templateUrl: './list.html'
|
||||||
})
|
})
|
||||||
export class TransactionsList implements OnInit {
|
export class TransactionsList implements OnInit, OnDestroy {
|
||||||
private transactionsService = inject(TransactionsService);
|
private transactionsService = inject(TransactionsService);
|
||||||
|
private accessService = inject(TransactionAccessService);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
@Output() transactionSelected = new EventEmitter<string>();
|
@Output() transactionSelected = new EventEmitter<string>();
|
||||||
@Output() openRefundModal = new EventEmitter<string>();
|
@Output() openRefundModal = new EventEmitter<string>();
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
access!: TransactionAccess;
|
||||||
|
currentUserRole = '';
|
||||||
|
currentMerchantId?: number;
|
||||||
|
|
||||||
// Données
|
// Données
|
||||||
transactions: Transaction[] = [];
|
transactions: Transaction[] = [];
|
||||||
paginatedData: PaginatedTransactions | null = null;
|
paginatedData: PaginatedTransactions | null = null;
|
||||||
@ -63,19 +88,22 @@ export class TransactionsList implements OnInit {
|
|||||||
page: 1,
|
page: 1,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
operator: '',
|
|
||||||
country: '',
|
|
||||||
startDate: undefined,
|
startDate: undefined,
|
||||||
endDate: undefined,
|
endDate: undefined,
|
||||||
msisdn: '',
|
|
||||||
sortBy: 'transactionDate',
|
sortBy: 'transactionDate',
|
||||||
sortOrder: 'desc'
|
sortOrder: 'desc'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Options de filtre
|
// Options de filtre
|
||||||
statusOptions: TransactionStatus[] = ['PENDING', 'SUCCESS', 'FAILED', 'REFUNDED', 'CANCELLED'];
|
statusOptions: TransactionStatus[] = ['PENDING', 'SUCCESS', 'FAILED', 'CANCELLED'];
|
||||||
operatorOptions: string[] = ['Orange', 'Free', 'SFR', 'Bouygues'];
|
typeOptions: TransactionType[] = [
|
||||||
countryOptions: string[] = ['FR', 'BE', 'CH', 'LU'];
|
TransactionType.SUBSCRIPTION_PAYMENT,
|
||||||
|
TransactionType.SUBSCRIPTION_RENEWAL,
|
||||||
|
TransactionType.ONE_TIME_PAYMENT
|
||||||
|
];
|
||||||
|
periodicityOptions = Object.values(SubscriptionPeriodicity);
|
||||||
|
operatorOptions: string[] = ['Orange'];
|
||||||
|
countryOptions: string[] = ['SN'];
|
||||||
|
|
||||||
// Tri
|
// Tri
|
||||||
sortField: string = 'transactionDate';
|
sortField: string = 'transactionDate';
|
||||||
@ -86,10 +114,36 @@ export class TransactionsList implements OnInit {
|
|||||||
selectAll = false;
|
selectAll = false;
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.initializePermissions();
|
||||||
this.loadTransactions();
|
this.loadTransactions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
// Nettoyage si nécessaire
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializePermissions() {
|
||||||
|
this.access = this.accessService.getTransactionAccess();
|
||||||
|
|
||||||
|
// Ajouter le merchant ID aux filtres si nécessaire
|
||||||
|
if (this.access.isMerchantUser && this.access.allowedMerchantIds.length > 0) {
|
||||||
|
this.filters.merchantPartnerId = this.access.allowedMerchantIds[0];
|
||||||
|
} else if (this.access.isHubUser && this.access.merchantId) {
|
||||||
|
// Pour les hub users qui veulent voir un merchant spécifique
|
||||||
|
this.filters.merchantPartnerId = this.access.merchantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Définir le rôle pour l'affichage
|
||||||
|
this.currentUserRole = this.access.userRoleLabel;
|
||||||
|
this.currentMerchantId = this.access.merchantId;
|
||||||
|
}
|
||||||
|
|
||||||
loadTransactions() {
|
loadTransactions() {
|
||||||
|
if (!this.access.canViewTransactions) {
|
||||||
|
this.error = 'Vous n\'avez pas la permission de voir les transactions';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
|
|
||||||
@ -104,6 +158,11 @@ export class TransactionsList implements OnInit {
|
|||||||
this.filters.sortBy = this.sortField;
|
this.filters.sortBy = this.sortField;
|
||||||
this.filters.sortOrder = this.sortDirection;
|
this.filters.sortOrder = this.sortDirection;
|
||||||
|
|
||||||
|
// Appliquer les restrictions de merchant pour les non-admin hub users
|
||||||
|
if (!this.access.canManageAll && this.access.allowedMerchantIds.length > 0) {
|
||||||
|
this.filters.merchantPartnerId = this.access.allowedMerchantIds[0];
|
||||||
|
}
|
||||||
|
|
||||||
this.transactionsService.getTransactions(this.filters).subscribe({
|
this.transactionsService.getTransactions(this.filters).subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
this.paginatedData = data;
|
this.paginatedData = data;
|
||||||
@ -114,30 +173,6 @@ export class TransactionsList implements OnInit {
|
|||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.error = 'Erreur lors du chargement des transactions';
|
this.error = 'Erreur lors du chargement des transactions';
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
// Fallback sur les données mockées en développement
|
|
||||||
if (environment.production === false) {
|
|
||||||
this.transactions = this.transactionsService.getMockTransactions();
|
|
||||||
this.paginatedData = {
|
|
||||||
data: this.transactions,
|
|
||||||
total: this.transactions.length,
|
|
||||||
page: 1,
|
|
||||||
limit: 20,
|
|
||||||
totalPages: 1,
|
|
||||||
stats: {
|
|
||||||
total: this.transactions.length,
|
|
||||||
totalAmount: this.transactions.reduce((sum, tx) => sum + tx.amount, 0),
|
|
||||||
successCount: this.transactions.filter(tx => tx.status === 'SUCCESS').length,
|
|
||||||
failedCount: this.transactions.filter(tx => tx.status === 'FAILED').length,
|
|
||||||
pendingCount: this.transactions.filter(tx => tx.status === 'PENDING').length,
|
|
||||||
refundedCount: this.transactions.filter(tx => tx.status === 'REFUNDED').length,
|
|
||||||
successRate: 75,
|
|
||||||
averageAmount: 4.74
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
console.error('Error loading transactions:', error);
|
console.error('Error loading transactions:', error);
|
||||||
}
|
}
|
||||||
@ -156,14 +191,17 @@ export class TransactionsList implements OnInit {
|
|||||||
page: 1,
|
page: 1,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
operator: '',
|
|
||||||
country: '',
|
|
||||||
startDate: undefined,
|
startDate: undefined,
|
||||||
endDate: undefined,
|
endDate: undefined,
|
||||||
msisdn: '',
|
|
||||||
sortBy: 'transactionDate',
|
sortBy: 'transactionDate',
|
||||||
sortOrder: 'desc'
|
sortOrder: 'desc'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Réappliquer les restrictions de merchant
|
||||||
|
if (this.access.isMerchantUser && this.access.allowedMerchantIds.length > 0) {
|
||||||
|
this.filters.merchantPartnerId = this.access.allowedMerchantIds[0];
|
||||||
|
}
|
||||||
|
|
||||||
this.loadTransactions();
|
this.loadTransactions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,7 +212,6 @@ export class TransactionsList implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onOperatorFilterChange(operator: string) {
|
onOperatorFilterChange(operator: string) {
|
||||||
this.filters.operator = operator;
|
|
||||||
this.filters.page = 1;
|
this.filters.page = 1;
|
||||||
this.loadTransactions();
|
this.loadTransactions();
|
||||||
}
|
}
|
||||||
@ -186,6 +223,15 @@ export class TransactionsList implements OnInit {
|
|||||||
this.loadTransactions();
|
this.loadTransactions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Permissions pour les filtres
|
||||||
|
canUseMerchantFilter(): boolean {
|
||||||
|
return this.access.canFilterByMerchant && this.access.allowedMerchantIds.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
canUseAllFilters(): boolean {
|
||||||
|
return this.access.canViewAllTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
// Tri
|
// Tri
|
||||||
sort(field: string) {
|
sort(field: string) {
|
||||||
if (this.sortField === field) {
|
if (this.sortField === field) {
|
||||||
@ -210,21 +256,12 @@ export class TransactionsList implements OnInit {
|
|||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
viewTransactionDetails(transactionId: string) {
|
viewTransactionDetails(transactionId: string) {
|
||||||
|
// Vérifier les permissions avant d'afficher
|
||||||
|
this.accessService.canAccessTransaction().subscribe(canAccess => {
|
||||||
|
if (canAccess) {
|
||||||
this.transactionSelected.emit(transactionId);
|
this.transactionSelected.emit(transactionId);
|
||||||
}
|
} else {
|
||||||
|
this.error = 'Vous n\'avez pas la permission de voir les détails de cette transaction';
|
||||||
refundTransaction(transactionId: string) {
|
|
||||||
this.openRefundModal.emit(transactionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
retryTransaction(transactionId: string) {
|
|
||||||
this.transactionsService.retryTransaction(transactionId).subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.loadTransactions();
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Error retrying transaction:', error);
|
|
||||||
this.error = 'Erreur lors de la nouvelle tentative';
|
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -253,37 +290,12 @@ export class TransactionsList implements OnInit {
|
|||||||
this.selectedTransactions.size === this.transactions.length;
|
this.selectedTransactions.size === this.transactions.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export
|
// Utilitaires d'affichage MIS À JOUR
|
||||||
exportTransactions(format: 'csv' | 'excel' | 'pdf') {
|
|
||||||
const exportRequest = {
|
|
||||||
format: format,
|
|
||||||
query: this.filters,
|
|
||||||
columns: ['id', 'msisdn', 'operator', 'amount', 'status', 'transactionDate', 'productName']
|
|
||||||
};
|
|
||||||
|
|
||||||
this.transactionsService.exportTransactions(exportRequest).subscribe({
|
|
||||||
next: (response) => {
|
|
||||||
// Télécharger le fichier
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = response.url;
|
|
||||||
link.download = response.filename;
|
|
||||||
link.click();
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Error exporting transactions:', error);
|
|
||||||
this.error = 'Erreur lors de l\'export';
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utilitaires d'affichage
|
|
||||||
getStatusBadgeClass(status: TransactionStatus): string {
|
getStatusBadgeClass(status: TransactionStatus): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'SUCCESS': return 'badge bg-success';
|
case 'SUCCESS': return 'badge bg-success';
|
||||||
case 'PENDING': return 'badge bg-warning';
|
case 'PENDING': return 'badge bg-warning';
|
||||||
case 'FAILED': return 'badge bg-danger';
|
case 'FAILED': return 'badge bg-danger';
|
||||||
case 'REFUNDED': return 'badge bg-info';
|
|
||||||
case 'CANCELLED': return 'badge bg-secondary';
|
case 'CANCELLED': return 'badge bg-secondary';
|
||||||
case 'EXPIRED': return 'badge bg-dark';
|
case 'EXPIRED': return 'badge bg-dark';
|
||||||
default: return 'badge bg-secondary';
|
default: return 'badge bg-secondary';
|
||||||
@ -295,35 +307,145 @@ export class TransactionsList implements OnInit {
|
|||||||
case 'SUCCESS': return 'lucideCheckCircle';
|
case 'SUCCESS': return 'lucideCheckCircle';
|
||||||
case 'PENDING': return 'lucideClock';
|
case 'PENDING': return 'lucideClock';
|
||||||
case 'FAILED': return 'lucideXCircle';
|
case 'FAILED': return 'lucideXCircle';
|
||||||
case 'REFUNDED': return 'lucideUndo2';
|
|
||||||
case 'CANCELLED': return 'lucideBan';
|
case 'CANCELLED': return 'lucideBan';
|
||||||
default: return 'lucideClock';
|
default: return 'lucideClock';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formatCurrency(amount: number, currency: string = 'EUR'): string {
|
getTypeIcon(type: TransactionType): string {
|
||||||
return new Intl.NumberFormat('fr-FR', {
|
switch (type) {
|
||||||
style: 'currency',
|
case TransactionType.SUBSCRIPTION_PAYMENT: return 'lucideCreditCard';
|
||||||
currency: currency
|
case TransactionType.SUBSCRIPTION_RENEWAL: return 'lucideRepeat';
|
||||||
}).format(amount);
|
case TransactionType.ONE_TIME_PAYMENT: return 'lucideCreditCard';
|
||||||
|
default: return 'lucideCreditCard';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formatDate(date: Date): string {
|
getPeriodicityBadgeClass(periodicity?: string): string {
|
||||||
|
if (!periodicity) return 'badge bg-secondary';
|
||||||
|
|
||||||
|
switch (periodicity.toLowerCase()) {
|
||||||
|
case 'daily':
|
||||||
|
return 'badge bg-primary';
|
||||||
|
case 'weekly':
|
||||||
|
return 'badge bg-info';
|
||||||
|
case 'monthly':
|
||||||
|
return 'badge bg-success';
|
||||||
|
case 'yearly':
|
||||||
|
return 'badge bg-warning';
|
||||||
|
default:
|
||||||
|
return 'badge bg-secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatCurrency(amount: number, currency: Currency = Currency.XOF): string {
|
||||||
|
return TransactionUtils.formatAmount(amount, currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(date: Date | string | undefined | null): string {
|
||||||
|
// Si la date est null/undefined, retourner une chaîne vide
|
||||||
|
if (!date) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si c'est déjà une Date valide
|
||||||
|
if (date instanceof Date) {
|
||||||
|
// Vérifier si la Date est valide
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return 'Date invalide';
|
||||||
|
}
|
||||||
return new Intl.DateTimeFormat('fr-FR', {
|
return new Intl.DateTimeFormat('fr-FR', {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
}).format(new Date(date));
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si c'est une chaîne, essayer de la convertir
|
||||||
|
if (typeof date === 'string') {
|
||||||
|
const dateObj = new Date(date);
|
||||||
|
|
||||||
|
// Vérifier si la conversion a réussi
|
||||||
|
if (isNaN(dateObj.getTime())) {
|
||||||
|
// Essayer d'autres formats
|
||||||
|
const alternativeDate = this.parseDateString(date);
|
||||||
|
if (alternativeDate && !isNaN(alternativeDate.getTime())) {
|
||||||
|
return new Intl.DateTimeFormat('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).format(alternativeDate);
|
||||||
|
}
|
||||||
|
return 'Date invalide';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).format(dateObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour tout autre type, retourner '-'
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseDateString(dateString: string): Date | null {
|
||||||
|
try {
|
||||||
|
// Essayer différents formats de date
|
||||||
|
const formats = [
|
||||||
|
dateString, // Format ISO original
|
||||||
|
dateString.replace(' ', 'T'), // Remplacer espace par T
|
||||||
|
dateString.split('.')[0], // Enlever les millisecondes
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const format of formats) {
|
||||||
|
const date = new Date(format);
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAmountColor(amount: number): string {
|
getAmountColor(amount: number): string {
|
||||||
if (amount >= 10) return 'text-danger fw-bold';
|
if (amount >= 10000) return 'text-danger fw-bold';
|
||||||
if (amount >= 5) return 'text-warning fw-semibold';
|
if (amount >= 5000) return 'text-warning fw-semibold';
|
||||||
return 'text-success';
|
return 'text-success';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// utilitaires pour les abonnements
|
||||||
|
getStatusDisplayName(status: TransactionStatus): string {
|
||||||
|
return TransactionUtils.getStatusDisplayName(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTypeDisplayName(type: TransactionType): string {
|
||||||
|
return TransactionUtils.getTypeDisplayName(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeriodicityDisplayName(periodicity?: string): string {
|
||||||
|
if (!periodicity) return '';
|
||||||
|
|
||||||
|
const periodicityNames: Record<string, string> = {
|
||||||
|
'daily': 'Quotidien',
|
||||||
|
'weekly': 'Hebdomadaire',
|
||||||
|
'monthly': 'Mensuel',
|
||||||
|
'yearly': 'Annuel'
|
||||||
|
};
|
||||||
|
|
||||||
|
return periodicityNames[periodicity.toLowerCase()] || periodicity;
|
||||||
|
}
|
||||||
|
|
||||||
// Méthodes pour sécuriser l'accès aux stats
|
// Méthodes pour sécuriser l'accès aux stats
|
||||||
getTotal(): number {
|
getTotal(): number {
|
||||||
return this.paginatedData?.stats?.total || 0;
|
return this.paginatedData?.stats?.total || 0;
|
||||||
@ -341,15 +463,27 @@ export class TransactionsList implements OnInit {
|
|||||||
return this.paginatedData?.stats?.pendingCount || 0;
|
return this.paginatedData?.stats?.pendingCount || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSuccessRate(): number {
|
|
||||||
return this.paginatedData?.stats?.successRate || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotalAmount(): number {
|
getTotalAmount(): number {
|
||||||
return this.paginatedData?.stats?.totalAmount || 0;
|
return this.paginatedData?.stats?.totalAmount || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMinValue(a: number, b: number): number {
|
// Méthodes pour le template
|
||||||
return Math.min(a, b);
|
getUserBadgeClass(): string {
|
||||||
|
return this.access.isHubUser ? 'bg-primary' : 'bg-success';
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserBadgeIcon(): string {
|
||||||
|
return this.access.isHubUser ? 'lucideShield' : 'lucideStore';
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserBadgeText(): string {
|
||||||
|
return this.access.isHubUser ? 'Hub User' : 'Merchant User';
|
||||||
|
}
|
||||||
|
|
||||||
|
getScopeText(): string {
|
||||||
|
if (this.access.isMerchantUser && this.currentMerchantId) {
|
||||||
|
return `Merchant ${this.currentMerchantId}`;
|
||||||
|
}
|
||||||
|
return this.access.canManageAll ? 'Tous les merchants' : 'Merchants autorisés';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,80 +0,0 @@
|
|||||||
export interface Transaction {
|
|
||||||
id: string;
|
|
||||||
msisdn: string;
|
|
||||||
operator: string;
|
|
||||||
operatorId: string;
|
|
||||||
country: string;
|
|
||||||
amount: number;
|
|
||||||
currency: string;
|
|
||||||
status: TransactionStatus;
|
|
||||||
productId: string;
|
|
||||||
productName: string;
|
|
||||||
productCategory: string;
|
|
||||||
transactionDate: Date;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
externalId?: string;
|
|
||||||
merchantId?: string;
|
|
||||||
merchantName?: string;
|
|
||||||
errorCode?: string;
|
|
||||||
errorMessage?: string;
|
|
||||||
userAgent?: string;
|
|
||||||
ipAddress?: string;
|
|
||||||
customData?: { [key: string]: any };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransactionQuery {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
search?: string;
|
|
||||||
status?: TransactionStatus;
|
|
||||||
operator?: string;
|
|
||||||
country?: string;
|
|
||||||
startDate?: Date;
|
|
||||||
endDate?: Date;
|
|
||||||
msisdn?: string;
|
|
||||||
productId?: string;
|
|
||||||
merchantId?: string;
|
|
||||||
sortBy?: string;
|
|
||||||
sortOrder?: 'asc' | 'desc';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransactionStats {
|
|
||||||
total: number;
|
|
||||||
totalAmount: number;
|
|
||||||
successCount: number;
|
|
||||||
failedCount: number;
|
|
||||||
pendingCount: number;
|
|
||||||
refundedCount: number;
|
|
||||||
successRate: number;
|
|
||||||
averageAmount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginatedTransactions {
|
|
||||||
data: Transaction[];
|
|
||||||
total: number;
|
|
||||||
page: number;
|
|
||||||
limit: number;
|
|
||||||
totalPages: number;
|
|
||||||
stats: TransactionStats;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TransactionStatus =
|
|
||||||
| 'PENDING'
|
|
||||||
| 'SUCCESS'
|
|
||||||
| 'FAILED'
|
|
||||||
| 'REFUNDED'
|
|
||||||
| 'CANCELLED'
|
|
||||||
| 'EXPIRED';
|
|
||||||
|
|
||||||
export interface RefundRequest {
|
|
||||||
transactionId: string;
|
|
||||||
reason?: string;
|
|
||||||
amount?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransactionExportRequest {
|
|
||||||
format: 'csv' | 'excel' | 'pdf';
|
|
||||||
query: TransactionQuery;
|
|
||||||
columns?: string[];
|
|
||||||
}
|
|
||||||
@ -0,0 +1,207 @@
|
|||||||
|
// [file name]: transactions/services/transaction-access.service.ts
|
||||||
|
import { Injectable, Injector, inject } from '@angular/core';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { RoleManagementService, UserRole } from '@core/services/hub-users-roles-management.service';
|
||||||
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
|
||||||
|
export interface TransactionAccess {
|
||||||
|
// Permissions de visualisation
|
||||||
|
canViewTransactions: boolean;
|
||||||
|
canViewAllTransactions: boolean; // Toutes vs seulement les siennes
|
||||||
|
canViewDetails: boolean;
|
||||||
|
|
||||||
|
// Permissions d'actions
|
||||||
|
canRefund: boolean;
|
||||||
|
canRetry: boolean;
|
||||||
|
canCancel: boolean;
|
||||||
|
canExport: boolean;
|
||||||
|
|
||||||
|
// Permissions administratives
|
||||||
|
canManageAll: boolean; // Toutes les transactions
|
||||||
|
canFilterByMerchant: boolean;
|
||||||
|
canViewSensitiveData: boolean; // IP, User Agent, etc.
|
||||||
|
|
||||||
|
// Scope
|
||||||
|
allowedMerchantIds: number[];
|
||||||
|
isHubUser: boolean;
|
||||||
|
isMerchantUser: boolean;
|
||||||
|
|
||||||
|
// Informations utilisateur
|
||||||
|
userRole: UserRole;
|
||||||
|
userRoleLabel: string;
|
||||||
|
merchantId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class TransactionAccessService {
|
||||||
|
private accessCache: TransactionAccess | null = null;
|
||||||
|
private readonly injector = inject(Injector);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private roleService: RoleManagementService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
getTransactionAccess(): TransactionAccess {
|
||||||
|
if (this.accessCache) {
|
||||||
|
return this.accessCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRole = this.roleService.getCurrentRole() || UserRole.DCB_SUPPORT; // Valeur par défaut
|
||||||
|
const isHubUser = this.roleService.isHubUser();
|
||||||
|
const merchantId = this.getCurrentMerchantId();
|
||||||
|
|
||||||
|
const access: TransactionAccess = {
|
||||||
|
// Pour tous les utilisateurs (avec restrictions)
|
||||||
|
canViewTransactions: this.canViewTransactions(userRole, isHubUser),
|
||||||
|
canViewAllTransactions: this.canViewAllTransactions(userRole, isHubUser),
|
||||||
|
canViewDetails: this.canViewDetails(userRole, isHubUser),
|
||||||
|
|
||||||
|
// Actions selon le rôle
|
||||||
|
canRefund: this.canPerformRefund(userRole, isHubUser),
|
||||||
|
canRetry: this.canPerformRetry(userRole, isHubUser),
|
||||||
|
canCancel: this.canPerformCancel(userRole, isHubUser),
|
||||||
|
canExport: this.canExport(userRole, isHubUser),
|
||||||
|
|
||||||
|
// Permissions administratives
|
||||||
|
canManageAll: this.canManageAll(userRole, isHubUser),
|
||||||
|
canFilterByMerchant: this.canFilterByMerchant(userRole, isHubUser),
|
||||||
|
canViewSensitiveData: this.canViewSensitiveData(userRole, isHubUser),
|
||||||
|
|
||||||
|
// Scope
|
||||||
|
allowedMerchantIds: this.getAllowedMerchantIds(isHubUser, merchantId),
|
||||||
|
isHubUser,
|
||||||
|
isMerchantUser: !isHubUser,
|
||||||
|
|
||||||
|
// Informations utilisateur
|
||||||
|
userRole,
|
||||||
|
userRoleLabel: this.roleService.getRoleLabel() || 'Utilisateur',
|
||||||
|
merchantId
|
||||||
|
};
|
||||||
|
|
||||||
|
this.accessCache = access;
|
||||||
|
return access;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MÉTHODES DE DÉTERMINATION DES PERMISSIONS ===
|
||||||
|
|
||||||
|
private canViewTransactions(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// Tous les rôles peuvent voir les transactions
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private canViewAllTransactions(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// Hub users et DCB_PARTNER_ADMIN peuvent voir toutes les transactions
|
||||||
|
return isHubUser || userRole === UserRole.DCB_PARTNER_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private canViewDetails(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// Tous peuvent voir les détails (avec restrictions de scope)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private canPerformRefund(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// DCB_ADMIN: peut rembourser toutes les transactions
|
||||||
|
// DCB_SUPPORT: peut rembourser les transactions de son périmètre
|
||||||
|
// DCB_PARTNER_ADMIN: peut rembourser les transactions de son merchant
|
||||||
|
return isHubUser
|
||||||
|
? userRole === UserRole.DCB_ADMIN || userRole === UserRole.DCB_SUPPORT
|
||||||
|
: userRole === UserRole.DCB_PARTNER_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private canPerformRetry(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// Mêmes permissions que le remboursement
|
||||||
|
return this.canPerformRefund(userRole, isHubUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
private canPerformCancel(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// Plus restrictif - seulement les admins peuvent annuler
|
||||||
|
return isHubUser
|
||||||
|
? userRole === UserRole.DCB_ADMIN
|
||||||
|
: userRole === UserRole.DCB_PARTNER_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private canExport(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// Tous peuvent exporter leurs propres données
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private canManageAll(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// Seulement DCB_ADMIN hub peut tout gérer
|
||||||
|
return isHubUser && userRole === UserRole.DCB_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private canFilterByMerchant(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// Seuls les hub users peuvent filtrer par merchant
|
||||||
|
return isHubUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private canViewSensitiveData(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// Hub users et DCB_PARTNER_ADMIN peuvent voir les données sensibles
|
||||||
|
return isHubUser || userRole === UserRole.DCB_PARTNER_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === GESTION DU SCOPE ===
|
||||||
|
|
||||||
|
private getCurrentMerchantId(): number | undefined {
|
||||||
|
// Récupérer le merchant ID de l'utilisateur courant
|
||||||
|
const authService = this.injector.get(AuthService);
|
||||||
|
|
||||||
|
const merchantPartnerId = authService.getCurrentMerchantPartnerId();
|
||||||
|
|
||||||
|
if (!merchantPartnerId) return undefined;
|
||||||
|
|
||||||
|
const merchantId = parseInt(merchantPartnerId, 10);
|
||||||
|
|
||||||
|
return isNaN(merchantId) ? undefined : merchantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAllowedMerchantIds(isHubUser: boolean, merchantId?: number): number[] {
|
||||||
|
if (isHubUser) {
|
||||||
|
// Hub users peuvent voir tous les merchants
|
||||||
|
return []; // Tableau vide = tous les merchants
|
||||||
|
} else {
|
||||||
|
// Merchant users: seulement leur merchant
|
||||||
|
return merchantId ? [merchantId] : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MÉTHODES PUBLIQUES ===
|
||||||
|
|
||||||
|
// Vérifie si l'utilisateur peut accéder à une transaction spécifique
|
||||||
|
canAccessTransaction(transactionMerchantId?: number): Observable<boolean> {
|
||||||
|
const access = this.getTransactionAccess();
|
||||||
|
|
||||||
|
// Hub users avec permission de tout voir
|
||||||
|
if (access.canManageAll) {
|
||||||
|
return of(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si pas de merchant ID sur la transaction, accès limité
|
||||||
|
if (!transactionMerchantId) {
|
||||||
|
return of(access.isHubUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merchant users: seulement leur merchant
|
||||||
|
if (access.isMerchantUser) {
|
||||||
|
return of(access.allowedMerchantIds.includes(transactionMerchantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hub users: vérifier si le merchant est dans leur liste autorisée
|
||||||
|
if (access.allowedMerchantIds.length === 0) {
|
||||||
|
return of(true); // Tous les merchants autorisés
|
||||||
|
}
|
||||||
|
|
||||||
|
return of(access.allowedMerchantIds.includes(transactionMerchantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer le cache
|
||||||
|
clearCache(): void {
|
||||||
|
this.accessCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rafraîchir les permissions (après changement de rôle/merchant)
|
||||||
|
refreshAccess(): void {
|
||||||
|
this.clearCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,177 +1,536 @@
|
|||||||
|
// transactions.service.ts
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Observable, map, catchError, throwError, of, tap } from 'rxjs';
|
||||||
import { environment } from '@environments/environment';
|
import { environment } from '@environments/environment';
|
||||||
import { Observable, map, catchError, throwError } from 'rxjs';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Transaction,
|
Transaction,
|
||||||
TransactionQuery,
|
TransactionQuery,
|
||||||
PaginatedTransactions,
|
PaginatedTransactions,
|
||||||
TransactionStats,
|
TransactionStats,
|
||||||
RefundRequest
|
TransactionType,
|
||||||
} from '../models/transaction';
|
TransactionStatus
|
||||||
|
} from '@core/models/dcb-bo-hub-transaction.model';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Currency
|
||||||
|
} from '@core/models/dcb-bo-hub-subscription.model';
|
||||||
|
|
||||||
|
interface ApiResponse {
|
||||||
|
data: any[];
|
||||||
|
meta?: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
hasPreviousPage: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CacheEntry {
|
||||||
|
key: string;
|
||||||
|
data: any[];
|
||||||
|
timestamp: number;
|
||||||
|
query: TransactionQuery;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class TransactionsService {
|
export class TransactionsService {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private apiUrl = `${environment.localServiceTestApiUrl}/transactions`;
|
|
||||||
|
|
||||||
// === CRUD OPERATIONS ===
|
private baseApiUrl = `${environment.apiCoreUrl}`;
|
||||||
|
private subscriptionsUrl = `${this.baseApiUrl}/subscriptions`;
|
||||||
|
private paymentsUrl = `${this.baseApiUrl}/payments`;
|
||||||
|
|
||||||
|
// Cache pour éviter de recharger toutes les données
|
||||||
|
private cache: CacheEntry | null = null;
|
||||||
|
private readonly CACHE_TTL = 30000; // 30 secondes
|
||||||
|
|
||||||
|
// === OPÉRATIONS PRINCIPALES ===
|
||||||
|
|
||||||
getTransactions(query: TransactionQuery): Observable<PaginatedTransactions> {
|
getTransactions(query: TransactionQuery): Observable<PaginatedTransactions> {
|
||||||
let params = new HttpParams();
|
const cacheKey = this.generateCacheKey(query);
|
||||||
|
|
||||||
// Ajouter tous les paramètres de query
|
const canUseCache = this.cache &&
|
||||||
Object.keys(query).forEach(key => {
|
this.cache.key === cacheKey &&
|
||||||
const value = query[key as keyof TransactionQuery];
|
Date.now() - this.cache.timestamp < this.CACHE_TTL;
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
if (value instanceof Date) {
|
|
||||||
params = params.set(key, value.toISOString());
|
|
||||||
} else {
|
|
||||||
params = params.set(key, value.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.http.get<PaginatedTransactions>(`${this.apiUrl}`, { params }).pipe(
|
if (query.merchantPartnerId) {
|
||||||
catchError(error => {
|
if (canUseCache) {
|
||||||
console.error('Error loading transactions:', error);
|
console.log('Using cached data for merchant:', query.merchantPartnerId);
|
||||||
return throwError(() => error);
|
return of(this.createPaginatedResponseFromCache(query));
|
||||||
|
}
|
||||||
|
return this.getSubscriptionsByMerchant(query).pipe(
|
||||||
|
tap((response: PaginatedTransactions & { rawApiData?: any[] }) => {
|
||||||
|
this.cache = {
|
||||||
|
key: cacheKey,
|
||||||
|
data: response.rawApiData || [],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
query: { ...query, page: 1, limit: 1000 }
|
||||||
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
if (canUseCache) {
|
||||||
|
console.log('Using cached data for all subscriptions');
|
||||||
|
return of(this.createPaginatedResponseFromCache(query));
|
||||||
|
}
|
||||||
|
return this.getAllSubscriptions(query).pipe(
|
||||||
|
tap((response: PaginatedTransactions & { rawApiData?: any[] }) => {
|
||||||
|
// Stocker les données BRUTES de l'API
|
||||||
|
this.cache = {
|
||||||
|
key: cacheKey,
|
||||||
|
data: response.rawApiData || [],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
query: { ...query, page: 1, limit: 1000 }
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getTransactionById(id: string): Observable<Transaction> {
|
getTransactionById(id: string): Observable<Transaction> {
|
||||||
return this.http.get<Transaction>(`${this.apiUrl}/${id}`).pipe(
|
const subscriptionId = parseInt(id, 10);
|
||||||
|
if (isNaN(subscriptionId)) {
|
||||||
|
return throwError(() => new Error(`ID de transaction invalide: ${id}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<any>(`${this.subscriptionsUrl}/${subscriptionId}`).pipe(
|
||||||
|
map(subscription => this.mapSubscriptionToTransaction(subscription)),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error('Error loading transaction:', error);
|
console.error(`Error loading transaction ${id}:`, error);
|
||||||
return throwError(() => error);
|
return throwError(() => error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === ACTIONS ===
|
getTransactionPayments(merchantId: number, subscriptionId: number, page: number = 1, limit: number = 10): Observable<PaginatedTransactions> {
|
||||||
refundTransaction(refundRequest: RefundRequest): Observable<{ message: string; transaction: Transaction }> {
|
let params = new HttpParams()
|
||||||
return this.http.post<{ message: string; transaction: Transaction }>(
|
.set('page', page.toString())
|
||||||
`${this.apiUrl}/${refundRequest.transactionId}/refund`,
|
.set('limit', limit.toString());
|
||||||
refundRequest
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelTransaction(transactionId: string): Observable<{ message: string }> {
|
return this.http.get<ApiResponse>(`${this.paymentsUrl}/merchant/${merchantId}/subscription/${subscriptionId}`, { params }).pipe(
|
||||||
return this.http.post<{ message: string }>(
|
map(response => {
|
||||||
`${this.apiUrl}/${transactionId}/cancel`,
|
const data = response?.data || [];
|
||||||
{}
|
const meta = response?.meta || {
|
||||||
);
|
total: data.length,
|
||||||
}
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
totalPages: Math.ceil(data.length / limit),
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false
|
||||||
|
};
|
||||||
|
|
||||||
retryTransaction(transactionId: string): Observable<{ message: string; transaction: Transaction }> {
|
const transactions = data.map((payment: any) => this.mapPaymentToTransaction(payment));
|
||||||
return this.http.post<{ message: string; transaction: Transaction }>(
|
const stats = this.calculateStats(transactions);
|
||||||
`${this.apiUrl}/${transactionId}/retry`,
|
|
||||||
{}
|
return {
|
||||||
|
data: transactions,
|
||||||
|
total: meta.total,
|
||||||
|
page: meta.page,
|
||||||
|
limit: meta.limit,
|
||||||
|
totalPages: meta.totalPages,
|
||||||
|
stats
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error loading payments for subscription ${subscriptionId}:`, error);
|
||||||
|
return of({
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalPages: 1,
|
||||||
|
stats: this.calculateStats([])
|
||||||
|
});
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === STATISTIQUES ===
|
// === STATISTIQUES ===
|
||||||
|
|
||||||
getTransactionStats(query?: Partial<TransactionQuery>): Observable<TransactionStats> {
|
getTransactionStats(query?: Partial<TransactionQuery>): Observable<TransactionStats> {
|
||||||
let params = new HttpParams();
|
const transactionQuery: TransactionQuery = {
|
||||||
|
page: 1,
|
||||||
|
limit: 1000,
|
||||||
|
...query
|
||||||
|
};
|
||||||
|
|
||||||
if (query) {
|
return this.getTransactions(transactionQuery).pipe(
|
||||||
Object.keys(query).forEach(key => {
|
map(response => response.stats)
|
||||||
const value = query[key as keyof TransactionQuery];
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
if (value instanceof Date) {
|
|
||||||
params = params.set(key, value.toISOString());
|
|
||||||
} else {
|
|
||||||
params = params.set(key, value.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.http.get<TransactionStats>(`${this.apiUrl}/stats`, { params });
|
|
||||||
}
|
|
||||||
|
|
||||||
// === EXPORT ===
|
|
||||||
exportTransactions(exportRequest: any): Observable<{ url: string; filename: string }> {
|
|
||||||
return this.http.post<{ url: string; filename: string }>(
|
|
||||||
`${this.apiUrl}/export`,
|
|
||||||
exportRequest
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === MOCK DATA POUR LE DÉVELOPPEMENT ===
|
// === MÉTHODES PRIVÉES ===
|
||||||
getMockTransactions(): Transaction[] {
|
|
||||||
|
private getAllSubscriptions(query: TransactionQuery): Observable<PaginatedTransactions & { rawApiData?: any[] }> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
|
||||||
|
// Paramètres supportés par l'API subscription
|
||||||
|
if (query.status) {
|
||||||
|
const subscriptionStatus = this.mapToSubscriptionStatus(query.status);
|
||||||
|
if (subscriptionStatus) {
|
||||||
|
params = params.set('status', subscriptionStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.periodicity) {
|
||||||
|
params = params.set('periodicity', query.periodicity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.startDate) {
|
||||||
|
params = params.set('startDate', query.startDate.toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.endDate) {
|
||||||
|
params = params.set('endDate', query.endDate.toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<ApiResponse>(this.subscriptionsUrl, { params }).pipe(
|
||||||
|
map(apiResponse => this.createPaginatedResponse(apiResponse, query, true)),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error loading all subscriptions:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSubscriptionsByMerchant(query: TransactionQuery): Observable<PaginatedTransactions & { rawApiData?: any[] }> {
|
||||||
|
if (!query.merchantPartnerId) {
|
||||||
|
return throwError(() => new Error('Merchant ID is required'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = new HttpParams();
|
||||||
|
|
||||||
|
// Paramètres supportés par l'API
|
||||||
|
if (query.status) {
|
||||||
|
const subscriptionStatus = this.mapToSubscriptionStatus(query.status);
|
||||||
|
if (subscriptionStatus) {
|
||||||
|
params = params.set('status', subscriptionStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.periodicity) {
|
||||||
|
params = params.set('periodicity', query.periodicity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.startDate) {
|
||||||
|
params = params.set('startDate', query.startDate.toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.endDate) {
|
||||||
|
params = params.set('endDate', query.endDate.toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<ApiResponse>(`${this.subscriptionsUrl}/merchant/${query.merchantPartnerId}`, { params }).pipe(
|
||||||
|
map(apiResponse => this.createPaginatedResponse(apiResponse, query, true)),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error loading subscriptions for merchant ${query.merchantPartnerId}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createPaginatedResponse(
|
||||||
|
apiResponse: ApiResponse,
|
||||||
|
query: TransactionQuery,
|
||||||
|
clientSidePagination: boolean = false
|
||||||
|
): PaginatedTransactions & { rawApiData?: any[] } { // <-- Ajout du type de retour
|
||||||
|
const rawData = apiResponse?.data || [];
|
||||||
|
|
||||||
|
// Convertir toutes les données en transactions
|
||||||
|
const allTransactions = rawData.map((item: any) => this.mapSubscriptionToTransaction(item));
|
||||||
|
|
||||||
|
// Appliquer la recherche locale si fournie
|
||||||
|
let filteredTransactions = [...allTransactions];
|
||||||
|
|
||||||
|
if (query.search) {
|
||||||
|
const searchLower = query.search.toLowerCase();
|
||||||
|
filteredTransactions = filteredTransactions.filter(tx =>
|
||||||
|
tx.id.toString().toLowerCase().includes(searchLower) ||
|
||||||
|
(tx.externalReference && tx.externalReference.toLowerCase().includes(searchLower)) ||
|
||||||
|
tx.productName.toLowerCase().includes(searchLower)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appliquer le tri côté client
|
||||||
|
if (query.sortBy) {
|
||||||
|
this.sortTransactions(filteredTransactions, query.sortBy, query.sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: Transaction[];
|
||||||
|
let total: number;
|
||||||
|
let page: number;
|
||||||
|
let limit: number;
|
||||||
|
let totalPages: number;
|
||||||
|
|
||||||
|
if (clientSidePagination) {
|
||||||
|
// Pagination côté client
|
||||||
|
page = query.page || 1;
|
||||||
|
limit = query.limit || 20;
|
||||||
|
total = filteredTransactions.length;
|
||||||
|
totalPages = Math.ceil(total / limit);
|
||||||
|
const startIndex = (page - 1) * limit;
|
||||||
|
data = filteredTransactions.slice(startIndex, startIndex + limit);
|
||||||
|
} else {
|
||||||
|
// Pagination côté serveur (si meta existe)
|
||||||
|
const meta = apiResponse?.meta || {
|
||||||
|
total: filteredTransactions.length,
|
||||||
|
page: query.page || 1,
|
||||||
|
limit: query.limit || 20,
|
||||||
|
totalPages: 1,
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false
|
||||||
|
};
|
||||||
|
|
||||||
|
data = filteredTransactions;
|
||||||
|
total = meta.total;
|
||||||
|
page = meta.page;
|
||||||
|
limit = meta.limit;
|
||||||
|
totalPages = meta.totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = this.calculateStats(filteredTransactions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages,
|
||||||
|
stats,
|
||||||
|
rawApiData: rawData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private createPaginatedResponseFromCache(query: TransactionQuery): PaginatedTransactions {
|
||||||
|
if (!this.cache) {
|
||||||
|
throw new Error('No cache available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawData = this.cache.data || [];
|
||||||
|
|
||||||
|
// Convertir les données brutes en transactions
|
||||||
|
const allTransactions = rawData.map((item: any) => this.mapSubscriptionToTransaction(item));
|
||||||
|
|
||||||
|
// Appliquer la recherche
|
||||||
|
let filteredTransactions = [...allTransactions];
|
||||||
|
if (query.search) {
|
||||||
|
const searchLower = query.search.toLowerCase();
|
||||||
|
filteredTransactions = filteredTransactions.filter(tx =>
|
||||||
|
tx.id.toString().toLowerCase().includes(searchLower) ||
|
||||||
|
(tx.externalReference && tx.externalReference.toLowerCase().includes(searchLower))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appliquer le tri
|
||||||
|
if (query.sortBy) {
|
||||||
|
this.sortTransactions(filteredTransactions, query.sortBy, query.sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
const page = query.page || 1;
|
||||||
|
const limit = query.limit || 20;
|
||||||
|
const total = filteredTransactions.length;
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
const startIndex = (page - 1) * limit;
|
||||||
|
const data = filteredTransactions.slice(startIndex, startIndex + limit);
|
||||||
|
|
||||||
|
// Calculer les stats
|
||||||
|
const stats = this.calculateStats(filteredTransactions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages,
|
||||||
|
stats
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCacheKey(query: TransactionQuery): string {
|
||||||
return [
|
return [
|
||||||
{
|
query.merchantPartnerId || 'all',
|
||||||
id: 'tx_001',
|
query.status || 'all',
|
||||||
msisdn: '+33612345678',
|
query.periodicity || 'all',
|
||||||
operator: 'Orange',
|
query.startDate?.toISOString().split('T')[0] || 'all',
|
||||||
operatorId: 'orange_fr',
|
query.endDate?.toISOString().split('T')[0] || 'all',
|
||||||
country: 'FR',
|
query.search || 'all'
|
||||||
amount: 4.99,
|
].join('|');
|
||||||
currency: 'EUR',
|
|
||||||
status: 'SUCCESS',
|
|
||||||
productId: 'prod_premium',
|
|
||||||
productName: 'Contenu Premium',
|
|
||||||
productCategory: 'ENTERTAINMENT',
|
|
||||||
transactionDate: new Date('2024-01-15T14:30:00'),
|
|
||||||
createdAt: new Date('2024-01-15T14:30:00'),
|
|
||||||
updatedAt: new Date('2024-01-15T14:30:00'),
|
|
||||||
externalId: 'ext_123456',
|
|
||||||
merchantName: 'MediaCorp'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tx_002',
|
|
||||||
msisdn: '+33798765432',
|
|
||||||
operator: 'Free',
|
|
||||||
operatorId: 'free_fr',
|
|
||||||
country: 'FR',
|
|
||||||
amount: 2.99,
|
|
||||||
currency: 'EUR',
|
|
||||||
status: 'PENDING',
|
|
||||||
productId: 'prod_basic',
|
|
||||||
productName: 'Abonnement Basique',
|
|
||||||
productCategory: 'SUBSCRIPTION',
|
|
||||||
transactionDate: new Date('2024-01-15T14:25:00'),
|
|
||||||
createdAt: new Date('2024-01-15T14:25:00'),
|
|
||||||
updatedAt: new Date('2024-01-15T14:25:00'),
|
|
||||||
externalId: 'ext_123457'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tx_003',
|
|
||||||
msisdn: '+33687654321',
|
|
||||||
operator: 'SFR',
|
|
||||||
operatorId: 'sfr_fr',
|
|
||||||
country: 'FR',
|
|
||||||
amount: 9.99,
|
|
||||||
currency: 'EUR',
|
|
||||||
status: 'FAILED',
|
|
||||||
productId: 'prod_pro',
|
|
||||||
productName: 'Pack Professionnel',
|
|
||||||
productCategory: 'BUSINESS',
|
|
||||||
transactionDate: new Date('2024-01-15T14:20:00'),
|
|
||||||
createdAt: new Date('2024-01-15T14:20:00'),
|
|
||||||
updatedAt: new Date('2024-01-15T14:20:00'),
|
|
||||||
errorCode: 'INSUFFICIENT_FUNDS',
|
|
||||||
errorMessage: 'Solde insuffisant'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tx_004',
|
|
||||||
msisdn: '+33611223344',
|
|
||||||
operator: 'Bouygues',
|
|
||||||
operatorId: 'bouygues_fr',
|
|
||||||
country: 'FR',
|
|
||||||
amount: 1.99,
|
|
||||||
currency: 'EUR',
|
|
||||||
status: 'REFUNDED',
|
|
||||||
productId: 'prod_mini',
|
|
||||||
productName: 'Pack Découverte',
|
|
||||||
productCategory: 'GAMING',
|
|
||||||
transactionDate: new Date('2024-01-15T14:15:00'),
|
|
||||||
createdAt: new Date('2024-01-15T14:15:00'),
|
|
||||||
updatedAt: new Date('2024-01-15T16:30:00'),
|
|
||||||
merchantName: 'GameStudio'
|
|
||||||
}
|
}
|
||||||
];
|
|
||||||
|
private mapSubscriptionToTransaction(subscription: any): Transaction {
|
||||||
|
return {
|
||||||
|
// Identifiants
|
||||||
|
id: subscription.id.toString(),
|
||||||
|
externalReference: subscription.externalReference || undefined,
|
||||||
|
subscriptionId: subscription.id,
|
||||||
|
|
||||||
|
// Informations financières
|
||||||
|
amount: subscription.amount,
|
||||||
|
currency: subscription.currency as Currency,
|
||||||
|
|
||||||
|
// Statut et type
|
||||||
|
status: this.mapToTransactionStatus(subscription.status),
|
||||||
|
type: TransactionType.SUBSCRIPTION_PAYMENT,
|
||||||
|
|
||||||
|
// Informations produit/abonnement
|
||||||
|
productId: subscription.planId?.toString() || '',
|
||||||
|
productName: `Abonnement ${subscription.periodicity}`,
|
||||||
|
periodicity: subscription.periodicity,
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
transactionDate: new Date(subscription.startDate),
|
||||||
|
createdAt: new Date(subscription.createdAt),
|
||||||
|
updatedAt: new Date(subscription.updatedAt),
|
||||||
|
nextPaymentDate: subscription.nextPaymentDate ? new Date(subscription.nextPaymentDate) : undefined,
|
||||||
|
|
||||||
|
// Informations marchand
|
||||||
|
merchantPartnerId: subscription.merchantPartnerId,
|
||||||
|
|
||||||
|
// Métadonnées
|
||||||
|
metadata: subscription.metadata || {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapPaymentToTransaction(payment: any): Transaction {
|
||||||
|
return {
|
||||||
|
id: payment.id.toString(),
|
||||||
|
externalReference: payment.externalReference || undefined,
|
||||||
|
amount: payment.amount,
|
||||||
|
currency: payment.currency as Currency,
|
||||||
|
status: payment.status as TransactionStatus,
|
||||||
|
type: TransactionType.SUBSCRIPTION_PAYMENT,
|
||||||
|
transactionDate: new Date(payment.createdAt),
|
||||||
|
createdAt: new Date(payment.createdAt),
|
||||||
|
updatedAt: new Date(payment.updatedAt),
|
||||||
|
merchantPartnerId: payment.merchantPartnerId,
|
||||||
|
subscriptionId: payment.subscriptionId,
|
||||||
|
productId: payment.subscriptionId?.toString() || '',
|
||||||
|
productName: 'Paiement abonnement',
|
||||||
|
metadata: payment.metadata || {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapToTransactionStatus(subscriptionStatus: string): TransactionStatus {
|
||||||
|
const statusMap: Record<string, TransactionStatus> = {
|
||||||
|
'ACTIVE': 'SUCCESS',
|
||||||
|
'PENDING': 'PENDING',
|
||||||
|
'SUSPENDED': 'FAILED',
|
||||||
|
'CANCELLED': 'CANCELLED',
|
||||||
|
'EXPIRED': 'EXPIRED'
|
||||||
|
};
|
||||||
|
return statusMap[subscriptionStatus] || 'PENDING';
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapToSubscriptionStatus(transactionStatus: TransactionStatus): string | null {
|
||||||
|
const statusMap: Record<TransactionStatus, string> = {
|
||||||
|
'SUCCESS': 'ACTIVE',
|
||||||
|
'PENDING': 'PENDING',
|
||||||
|
'FAILED': 'SUSPENDED',
|
||||||
|
'CANCELLED': 'CANCELLED',
|
||||||
|
'EXPIRED': 'EXPIRED'
|
||||||
|
};
|
||||||
|
return statusMap[transactionStatus] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sortTransactions(transactions: Transaction[], field: string, order: 'asc' | 'desc' = 'desc'): void {
|
||||||
|
transactions.sort((a, b) => {
|
||||||
|
let aValue: any;
|
||||||
|
let bValue: any;
|
||||||
|
|
||||||
|
switch (field) {
|
||||||
|
case 'id':
|
||||||
|
aValue = parseInt(a.id);
|
||||||
|
bValue = parseInt(b.id);
|
||||||
|
break;
|
||||||
|
case 'amount':
|
||||||
|
aValue = a.amount;
|
||||||
|
bValue = b.amount;
|
||||||
|
break;
|
||||||
|
case 'transactionDate':
|
||||||
|
aValue = a.transactionDate?.getTime();
|
||||||
|
bValue = b.transactionDate?.getTime();
|
||||||
|
break;
|
||||||
|
case 'createdAt':
|
||||||
|
aValue = a.createdAt?.getTime();
|
||||||
|
bValue = b.createdAt?.getTime();
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
aValue = a.status;
|
||||||
|
bValue = b.status;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
aValue = a[field as keyof Transaction];
|
||||||
|
bValue = b[field as keyof Transaction];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gérer les valeurs undefined/null
|
||||||
|
if (aValue === undefined || aValue === null) aValue = order === 'asc' ? Infinity : -Infinity;
|
||||||
|
if (bValue === undefined || bValue === null) bValue = order === 'asc' ? Infinity : -Infinity;
|
||||||
|
|
||||||
|
// Comparer
|
||||||
|
if (aValue < bValue) return order === 'asc' ? -1 : 1;
|
||||||
|
if (aValue > bValue) return order === 'asc' ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateStats(transactions: Transaction[]): TransactionStats {
|
||||||
|
const total = transactions.length;
|
||||||
|
const totalAmount = transactions.reduce((sum, tx) => sum + tx.amount, 0);
|
||||||
|
const successCount = transactions.filter(tx => tx.status === 'SUCCESS').length;
|
||||||
|
const failedCount = transactions.filter(tx => tx.status === 'FAILED').length;
|
||||||
|
const pendingCount = transactions.filter(tx => tx.status === 'PENDING').length;
|
||||||
|
const cancelledCount = transactions.filter(tx => tx.status === 'CANCELLED').length;
|
||||||
|
|
||||||
|
const byPeriodicity = {
|
||||||
|
DAILY: transactions.filter(tx => tx.periodicity === 'Daily').length,
|
||||||
|
WEEKLY: transactions.filter(tx => tx.periodicity === 'Weekly').length,
|
||||||
|
MONTHLY: transactions.filter(tx => tx.periodicity === 'Monthly').length,
|
||||||
|
YEARLY: transactions.filter(tx => tx.periodicity === 'Yearly').length
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
totalAmount,
|
||||||
|
successCount,
|
||||||
|
failedCount,
|
||||||
|
pendingCount,
|
||||||
|
averageAmount: total > 0 ? totalAmount / total : 0,
|
||||||
|
byPeriodicity
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MÉTHODES PUBLIQUES UTILITAIRES ===
|
||||||
|
|
||||||
|
clearCache(): void {
|
||||||
|
this.cache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshTransactions(query: TransactionQuery): Observable<PaginatedTransactions> {
|
||||||
|
this.clearCache();
|
||||||
|
return this.getTransactions(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MÉTHODES NON IMPLÉMENTÉES (APIs non disponibles) ===
|
||||||
|
|
||||||
|
refundTransaction(): Observable<{ message: string; transaction: Transaction }> {
|
||||||
|
return throwError(() => new Error('Remboursement non implémenté - API non disponible'));
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelTransaction(): Observable<{ message: string }> {
|
||||||
|
return throwError(() => new Error('Annulation non implémentée - API non disponible'));
|
||||||
|
}
|
||||||
|
|
||||||
|
retryTransaction(): Observable<{ message: string; transaction: Transaction }> {
|
||||||
|
return throwError(() => new Error('Nouvelle tentative non implémentée - API non disponible'));
|
||||||
|
}
|
||||||
|
|
||||||
|
exportTransactions(): Observable<{ url: string; filename: string }> {
|
||||||
|
return throwError(() => new Error('Export non implémenté - API non disponible'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,10 +1,11 @@
|
|||||||
import { Component, inject, TemplateRef, ViewChild } from '@angular/core';
|
import { Component, inject, TemplateRef, ViewChild, OnInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
import { TransactionsList } from './list/list';
|
import { TransactionsList } from './list/list';
|
||||||
import { TransactionDetails } from './details/details';
|
import { TransactionDetails } from './details/details';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
|
import { TransactionAccessService } from './services/transaction-access.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-transactions',
|
selector: 'app-transactions',
|
||||||
@ -19,12 +20,27 @@ import { NgIcon } from '@ng-icons/core';
|
|||||||
],
|
],
|
||||||
templateUrl: './transactions.html',
|
templateUrl: './transactions.html',
|
||||||
})
|
})
|
||||||
export class Transactions {
|
export class Transactions implements OnInit {
|
||||||
private modalService = inject(NgbModal);
|
private modalService = inject(NgbModal);
|
||||||
|
private accessService = inject(TransactionAccessService);
|
||||||
|
|
||||||
activeView: 'list' | 'details' = 'list';
|
activeView: 'list' | 'details' = 'list';
|
||||||
selectedTransactionId: string | null = null;
|
selectedTransactionId: string | null = null;
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
canAccessModule = true; // Par défaut true, pourrait être configuré par rôle
|
||||||
|
accessDenied = false;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.checkAccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkAccess() {
|
||||||
|
const access = this.accessService.getTransactionAccess();
|
||||||
|
this.canAccessModule = access.canViewTransactions;
|
||||||
|
this.accessDenied = !access.canViewTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
showListView() {
|
showListView() {
|
||||||
this.activeView = 'list';
|
this.activeView = 'list';
|
||||||
this.selectedTransactionId = null;
|
this.selectedTransactionId = null;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user