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 { filter, map, mergeMap } from 'rxjs';
|
||||
import { AuthService } from './core/services/auth.service';
|
||||
import { RoleSyncService } from './core/services/role-sync.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@ -22,6 +23,7 @@ export class App implements OnInit {
|
||||
private router = inject(Router);
|
||||
private activatedRoute = inject(ActivatedRoute);
|
||||
private authService = inject(AuthService);
|
||||
private roleSyncService = inject(RoleSyncService);
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// dcb-bo-hub-user.model.ts - MIS À JOUR
|
||||
export interface User {
|
||||
id: string; // UUID Keycloak
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
enabled: boolean;
|
||||
emailVerified: boolean;
|
||||
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;
|
||||
createdByUsername?: string;
|
||||
createdTimestamp: number;
|
||||
createdTimestamp?: 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 {
|
||||
@ -84,6 +96,7 @@ export interface CreateUserDto {
|
||||
role: UserRole;
|
||||
enabled?: boolean;
|
||||
emailVerified?: boolean;
|
||||
merchantPartnerId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserDto {
|
||||
@ -142,6 +155,7 @@ export interface SearchUsersParams {
|
||||
role?: UserRole;
|
||||
enabled?: boolean;
|
||||
userType?: UserType;
|
||||
merchantPartnerId?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
@ -63,9 +63,7 @@ export interface MerchantUser {
|
||||
username?: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
merchantPartnerId?: number
|
||||
merchantConfigId?: string; // Référence au merchant dans MerchantConfig
|
||||
lastName?: string; // Référence au merchant dans MerchantConfig
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
@ -113,8 +111,6 @@ export interface ApiMerchantUser {
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
merchantPartnerId?: number;
|
||||
merchantConfigId?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
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 { 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 { DashboardAccessService } from '@modules/dcb-dashboard/services/dashboard-access.service';
|
||||
|
||||
import {
|
||||
User,
|
||||
UserType,
|
||||
UserRole,
|
||||
} from '@core/models/dcb-bo-hub-user.model';
|
||||
import { TransactionAccessService } from '@modules/transactions/services/transaction-access.service';
|
||||
|
||||
// === INTERFACES DTO AUTH ===
|
||||
export interface LoginDto {
|
||||
@ -59,6 +62,9 @@ export class AuthService {
|
||||
private readonly tokenKey = 'access_token';
|
||||
private readonly refreshTokenKey = 'refresh_token';
|
||||
|
||||
private readonly dashboardAccessService = inject(DashboardAccessService);
|
||||
private readonly transactionAccessService = inject(TransactionAccessService);
|
||||
|
||||
private authState$ = new BehaviorSubject<boolean>(this.isAuthenticated());
|
||||
private userProfile$ = new BehaviorSubject<User | null>(null);
|
||||
private initialized$ = new BehaviorSubject<boolean>(false);
|
||||
@ -168,19 +174,113 @@ export class AuthService {
|
||||
/**
|
||||
* Déconnexion utilisateur
|
||||
*/
|
||||
/**
|
||||
* Déconnexion utilisateur avec nettoyage complet
|
||||
*/
|
||||
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>(
|
||||
`${environment.iamApiUrl}/auth/logout`,
|
||||
{}
|
||||
{},
|
||||
{ headers }
|
||||
).pipe(
|
||||
tap(() => this.clearAuthData()),
|
||||
catchError(error => {
|
||||
tap(() => {
|
||||
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
|
||||
*/
|
||||
@ -231,6 +331,7 @@ export class AuthService {
|
||||
lastName: apiUser.lastName || apiUser.lastname || apiUser.family_name || '',
|
||||
enabled: apiUser.enabled ?? apiUser.active ?? true,
|
||||
emailVerified: apiUser.emailVerified ?? apiUser.email_verified ?? false,
|
||||
merchantPartnerId: apiUser.merchantPartnerId || apiUser.partnerId || apiUser.merchantId || null,
|
||||
userType: userType,
|
||||
role: apiUser.clientRoles || apiUser.clientRoles?.[0] || '', // Gérer rôle unique ou tableau
|
||||
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 ===
|
||||
|
||||
validateToken(): Observable<TokenValidationResponseDto> {
|
||||
@ -449,6 +543,14 @@ export class AuthService {
|
||||
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
|
||||
*/
|
||||
|
||||
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 { 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';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
// 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 enum UserRole {
|
||||
// Rôles Hub
|
||||
DCB_ADMIN = 'dcb-admin',
|
||||
DCB_SUPPORT = 'dcb-support',
|
||||
|
||||
// Rôles Merchant User
|
||||
// Rôles Partenaires (Business)
|
||||
DCB_PARTNER_ADMIN = 'dcb-partner-admin',
|
||||
DCB_PARTNER_MANAGER = 'dcb-partner-manager',
|
||||
DCB_PARTNER_SUPPORT = 'dcb-partner-support',
|
||||
|
||||
// Rôles Configuration Marchands (Technique)
|
||||
MERCHANT_CONFIG_ADMIN = 'ADMIN',
|
||||
MERCHANT_CONFIG_MANAGER = 'MANAGER',
|
||||
MERCHANT_CONFIG_TECHNICAL = 'TECHNICAL',
|
||||
MERCHANT_CONFIG_VIEWER = 'VIEWER',
|
||||
}
|
||||
|
||||
export interface AvailableRolesWithPermissions {
|
||||
roles: (AvailableRole & { permissions: RolePermission })[];
|
||||
}
|
||||
type RoleCategory = 'hub' | 'partner' | 'config';
|
||||
|
||||
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'
|
||||
})
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class RoleManagementService {
|
||||
private hubUsersService = inject(HubUsersService);
|
||||
private merchantUsersService = inject(MerchantUsersService);
|
||||
private currentRole: UserRole | null = null;
|
||||
|
||||
private availableRoles$ = new BehaviorSubject<AvailableRolesWithPermissions | null>(null);
|
||||
private currentUserRole$ = new BehaviorSubject<UserRole | null>(null);
|
||||
// Mapping des rôles équivalents
|
||||
private readonly roleEquivalents = new Map<UserRole, UserRole[]>([
|
||||
[UserRole.DCB_PARTNER_ADMIN, [UserRole.MERCHANT_CONFIG_ADMIN]],
|
||||
[UserRole.DCB_PARTNER_MANAGER, [UserRole.MERCHANT_CONFIG_MANAGER]],
|
||||
[UserRole.DCB_PARTNER_SUPPORT, [UserRole.MERCHANT_CONFIG_TECHNICAL, UserRole.MERCHANT_CONFIG_VIEWER]],
|
||||
[UserRole.MERCHANT_CONFIG_ADMIN, [UserRole.DCB_PARTNER_ADMIN]],
|
||||
[UserRole.MERCHANT_CONFIG_MANAGER, [UserRole.DCB_PARTNER_MANAGER]],
|
||||
[UserRole.MERCHANT_CONFIG_TECHNICAL, [UserRole.DCB_PARTNER_SUPPORT]],
|
||||
[UserRole.MERCHANT_CONFIG_VIEWER, [UserRole.DCB_PARTNER_SUPPORT]]
|
||||
]);
|
||||
|
||||
/**
|
||||
* Charge les rôles Hub disponibles
|
||||
*/
|
||||
loadAvailableHubRoles(): Observable<AvailableRolesWithPermissions> {
|
||||
return this.loadRoles(
|
||||
() => this.hubUsersService.getAvailableHubRoles(),
|
||||
'hub'
|
||||
);
|
||||
// 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'
|
||||
};
|
||||
|
||||
// Labels des rôles
|
||||
private readonly roleLabels: Record<UserRole, string> = {
|
||||
[UserRole.DCB_ADMIN]: 'Administrateur DCB',
|
||||
[UserRole.DCB_SUPPORT]: 'Support DCB',
|
||||
[UserRole.DCB_PARTNER_ADMIN]: 'Admin Partenaire',
|
||||
[UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire',
|
||||
[UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire',
|
||||
[UserRole.MERCHANT_CONFIG_ADMIN]: 'Admin Configuration',
|
||||
[UserRole.MERCHANT_CONFIG_MANAGER]: 'Manager Configuration',
|
||||
[UserRole.MERCHANT_CONFIG_TECHNICAL]: 'Technique Configuration',
|
||||
[UserRole.MERCHANT_CONFIG_VIEWER]: 'Visualiseur Configuration'
|
||||
};
|
||||
|
||||
// Icônes des rôles
|
||||
private readonly roleIcons: Record<UserRole, string> = {
|
||||
[UserRole.DCB_ADMIN]: 'lucideShield',
|
||||
[UserRole.DCB_SUPPORT]: 'lucideHeadphones',
|
||||
[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'
|
||||
};
|
||||
|
||||
// === GESTION DU RÔLE COURANT ===
|
||||
setCurrentRole(role: UserRole | null): void {
|
||||
this.currentRole = role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les rôles Marchands disponibles
|
||||
*/
|
||||
loadAvailableMerchantRoles(): Observable<AvailableRolesWithPermissions> {
|
||||
return this.loadRoles(
|
||||
() => this.merchantUsersService.getAvailableMerchantRoles(),
|
||||
'merchant'
|
||||
);
|
||||
getCurrentRole(): UserRole | null {
|
||||
return this.currentRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
})
|
||||
);
|
||||
// === VÉRIFICATIONS DE RÔLES INDIVIDUELS ===
|
||||
|
||||
isAdmin(): boolean {
|
||||
return this.currentRole === UserRole.DCB_ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit le rôle de l'utilisateur courant
|
||||
*/
|
||||
setCurrentUserRole(role: UserRole): void {
|
||||
this.currentUserRole$.next(role);
|
||||
isSupport(): boolean {
|
||||
return this.currentRole === UserRole.DCB_SUPPORT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le rôle de l'utilisateur courant
|
||||
*/
|
||||
getCurrentUserRole(): Observable<UserRole | null> {
|
||||
return this.currentUserRole$.asObservable();
|
||||
isPartnerAdmin(): boolean {
|
||||
return this.currentRole === UserRole.DCB_PARTNER_ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la valeur actuelle du rôle utilisateur (synchrone)
|
||||
*/
|
||||
getCurrentUserRoleValue(): UserRole | null {
|
||||
return this.currentUserRole$.value;
|
||||
isPartnerManager(): boolean {
|
||||
return this.currentRole === UserRole.DCB_PARTNER_MANAGER;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
isPartnerSupport(): boolean {
|
||||
return this.currentRole === UserRole.DCB_PARTNER_SUPPORT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
isConfigAdmin(): boolean {
|
||||
return this.currentRole === UserRole.MERCHANT_CONFIG_ADMIN;
|
||||
}
|
||||
|
||||
const permissions = this.getPermissionsForRole(currentUserRole);
|
||||
return permissions.assignableRoles.includes(targetRole);
|
||||
isConfigManager(): boolean {
|
||||
return this.currentRole === UserRole.MERCHANT_CONFIG_MANAGER;
|
||||
}
|
||||
|
||||
// Méthodes d'utilité pour les permissions
|
||||
canCreateUsers(currentUserRole: UserRole | null): boolean {
|
||||
return this.getPermission(currentUserRole, 'canCreateUsers');
|
||||
isConfigTechnical(): boolean {
|
||||
return this.currentRole === UserRole.MERCHANT_CONFIG_TECHNICAL;
|
||||
}
|
||||
|
||||
canEditUsers(currentUserRole: UserRole | null): boolean {
|
||||
return this.getPermission(currentUserRole, 'canEditUsers');
|
||||
isConfigViewer(): boolean {
|
||||
return this.currentRole === UserRole.MERCHANT_CONFIG_VIEWER;
|
||||
}
|
||||
|
||||
canDeleteUsers(currentUserRole: UserRole | null): boolean {
|
||||
return this.getPermission(currentUserRole, 'canDeleteUsers');
|
||||
// === VÉRIFICATIONS AVEC MAPPING ===
|
||||
|
||||
isAnyAdmin(): boolean {
|
||||
return this.isAdmin() || this.isPartnerAdmin() || this.isConfigAdmin();
|
||||
}
|
||||
|
||||
canManageRoles(currentUserRole: UserRole | null): boolean {
|
||||
return this.getPermission(currentUserRole, 'canManageRoles');
|
||||
isAnyManager(): boolean {
|
||||
return this.isPartnerManager() || this.isConfigManager();
|
||||
}
|
||||
|
||||
canViewStats(currentUserRole: UserRole | null): boolean {
|
||||
return this.getPermission(currentUserRole, 'canViewStats');
|
||||
isAnySupport(): boolean {
|
||||
return this.isSupport() || this.isPartnerSupport() || this.isConfigTechnical() || this.isConfigViewer();
|
||||
}
|
||||
|
||||
canManageMerchants(currentUserRole: UserRole | null): boolean {
|
||||
return this.getPermission(currentUserRole, 'canManageMerchants');
|
||||
// === VÉRIFICATIONS DE CATÉGORIES ===
|
||||
|
||||
isHubUser(): boolean {
|
||||
return this.isAdmin() || this.isSupport();
|
||||
}
|
||||
|
||||
canAccessAdmin(currentUserRole: UserRole | null): boolean {
|
||||
return this.getPermission(currentUserRole, 'canAccessAdmin');
|
||||
isPartnerUser(): boolean {
|
||||
return this.isPartnerAdmin() || this.isPartnerManager() || this.isPartnerSupport();
|
||||
}
|
||||
|
||||
canAccessSupport(currentUserRole: UserRole | null): boolean {
|
||||
return this.getPermission(currentUserRole, 'canAccessSupport');
|
||||
isConfigUser(): boolean {
|
||||
return this.isConfigAdmin() || this.isConfigManager() || this.isConfigTechnical() || this.isConfigViewer();
|
||||
}
|
||||
|
||||
canAccessPartner(currentUserRole: UserRole | null): boolean {
|
||||
return this.getPermission(currentUserRole, 'canAccessPartner');
|
||||
getRoleCategory(): RoleCategory | null {
|
||||
if (!this.currentRole) return null;
|
||||
return this.roleCategories[this.currentRole];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
// === MAPPING ET ÉQUIVALENTS ===
|
||||
|
||||
getEquivalentRoles(): UserRole[] {
|
||||
if (!this.currentRole) return [];
|
||||
|
||||
const equivalents = [this.currentRole];
|
||||
const mappedRoles = this.roleEquivalents.get(this.currentRole);
|
||||
|
||||
if (mappedRoles) {
|
||||
equivalents.push(...mappedRoles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthodes d'utilité pour les rôles
|
||||
*/
|
||||
getRoleLabel(role: string): string {
|
||||
const userRole = role as UserRole;
|
||||
return ROLE_CONFIG[userRole]?.label || role;
|
||||
return [...new Set(equivalents)];
|
||||
}
|
||||
|
||||
getRoleDescription(role: string | UserRole): string {
|
||||
const userRole = role as UserRole;
|
||||
return ROLE_CONFIG[userRole]?.description || 'Description non disponible';
|
||||
hasEquivalentRole(targetRole: UserRole): boolean {
|
||||
if (!this.currentRole) return false;
|
||||
|
||||
if (this.currentRole === targetRole) return true;
|
||||
|
||||
const equivalents = this.roleEquivalents.get(this.currentRole);
|
||||
return equivalents ? equivalents.includes(targetRole) : false;
|
||||
}
|
||||
|
||||
getRoleBadgeClass(role: string): string {
|
||||
const userRole = role as UserRole;
|
||||
return ROLE_CONFIG[userRole]?.badgeClass || 'bg-secondary';
|
||||
getMappedRole(): UserRole | null {
|
||||
if (!this.currentRole) return null;
|
||||
|
||||
const equivalents = this.roleEquivalents.get(this.currentRole);
|
||||
return equivalents && equivalents.length > 0 ? equivalents[0] : this.currentRole;
|
||||
}
|
||||
|
||||
getRoleIcon(role: string): string {
|
||||
const userRole = role as UserRole;
|
||||
return ROLE_CONFIG[userRole]?.icon || 'lucideUser';
|
||||
// === VÉRIFICATIONS GÉNÉRIQUES ===
|
||||
|
||||
hasRole(role: UserRole): boolean {
|
||||
return this.currentRole === role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifications de type de rôle
|
||||
*/
|
||||
isAdminRole(role: UserRole): boolean {
|
||||
return role === UserRole.DCB_ADMIN;
|
||||
hasAnyRole(...roles: UserRole[]): boolean {
|
||||
if (!this.currentRole) return false;
|
||||
return roles.includes(this.currentRole);
|
||||
}
|
||||
|
||||
isSupportRole(role: UserRole): boolean {
|
||||
return role === UserRole.DCB_SUPPORT;
|
||||
// === UTILITAIRES ===
|
||||
|
||||
getRoleLabel(role?: UserRole): string {
|
||||
const targetRole = role || this.currentRole;
|
||||
return targetRole ? this.roleLabels[targetRole] || targetRole : '';
|
||||
}
|
||||
|
||||
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;
|
||||
getRoleIcon(role?: UserRole): string {
|
||||
const targetRole = role || this.currentRole;
|
||||
return targetRole ? this.roleIcons[targetRole] || 'user' : 'user';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gestion des listes de rôles
|
||||
*/
|
||||
getAllRoles(): UserRole[] {
|
||||
return Object.values(UserRole);
|
||||
}
|
||||
|
||||
getHubRoles(): UserRole[] {
|
||||
return [...HUB_ROLES];
|
||||
return [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
|
||||
}
|
||||
|
||||
getMerchantRoles(): UserRole[] {
|
||||
return [...MERCHANT_ROLES];
|
||||
getPartnerRoles(): UserRole[] {
|
||||
return [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
|
||||
}
|
||||
|
||||
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();
|
||||
getConfigRoles(): UserRole[] {
|
||||
return [
|
||||
UserRole.MERCHANT_CONFIG_ADMIN,
|
||||
UserRole.MERCHANT_CONFIG_MANAGER,
|
||||
UserRole.MERCHANT_CONFIG_TECHNICAL,
|
||||
UserRole.MERCHANT_CONFIG_VIEWER
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ import { Injectable, inject } from '@angular/core';
|
||||
import { AuthService } from './auth.service';
|
||||
import { PermissionsService } from './permissions.service';
|
||||
import { MenuItemType, UserDropdownItemType } from '@/app/types/layout';
|
||||
import { UserRole } from './hub-users-roles-management.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MenuService {
|
||||
@ -23,7 +24,7 @@ export class MenuService {
|
||||
return this.permissionsService.canAccessModule(modulePath, userRoles);
|
||||
}
|
||||
|
||||
private filterMenuItems(items: MenuItemType[], userRoles: string[]): MenuItemType[] {
|
||||
private filterMenuItems(items: MenuItemType[], userRoles: UserRole[]): MenuItemType[] {
|
||||
return items
|
||||
.filter(item => this.shouldDisplayMenuItem(item, userRoles))
|
||||
.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.url && item.url !== '#') {
|
||||
@ -47,7 +48,7 @@ export class MenuService {
|
||||
return true;
|
||||
}
|
||||
|
||||
private filterUserDropdownItems(items: UserDropdownItemType[], userRoles: string[]): UserDropdownItemType[] {
|
||||
private filterUserDropdownItems(items: UserDropdownItemType[], userRoles: UserRole[]): UserDropdownItemType[] {
|
||||
return items.filter(item => {
|
||||
if (item.isDivider || item.isHeader || !item.url || item.url === '#') {
|
||||
return true;
|
||||
@ -132,7 +133,6 @@ export class MenuService {
|
||||
];
|
||||
}
|
||||
|
||||
// Mettez à jour votre méthode
|
||||
private getFullUserDropdown(): UserDropdownItemType[] {
|
||||
return [
|
||||
{ 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">
|
||||
<!-- É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
|
||||
src="assets/images/users/user-2.jpg"
|
||||
[src]="getUserAvatar()"
|
||||
class="rounded-circle me-2"
|
||||
width="36"
|
||||
height="36"
|
||||
alt="user-image"
|
||||
onerror="this.src='assets/images/users/user-default.jpg'"
|
||||
(error)="onAvatarError($event)"
|
||||
/>
|
||||
<div>
|
||||
<h5 class="my-0 fw-semibold">
|
||||
{{ getUserInitials() }} | {{ getDisplayName() }}
|
||||
{{ getDisplayName() || 'Utilisateur' }}
|
||||
</h5>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -3,7 +3,7 @@ import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { userDropdownItems } from '@layouts/components/data';
|
||||
import { AuthService } from '@/app/core/services/auth.service';
|
||||
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({
|
||||
selector: 'app-user-profile',
|
||||
@ -19,42 +19,158 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
|
||||
user: User | null = null;
|
||||
isLoading = true;
|
||||
hasError = false;
|
||||
currentProfileLoaded = false;
|
||||
|
||||
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()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (isAuthenticated) => {
|
||||
console.log('🔐 Auth state changed:', 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 {
|
||||
// Si l'utilisateur s'est déconnecté
|
||||
console.log('👋 User logged out');
|
||||
this.user = null;
|
||||
this.currentProfileLoaded = 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.hasError = false;
|
||||
this.cdr.detectChanges();
|
||||
|
||||
this.authService.getProfile()
|
||||
this.authService.loadUserProfile()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
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.cdr.detectChanges();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load user profile:', error);
|
||||
this.user = null;
|
||||
console.error('❌ Failed to load user profile:', error);
|
||||
this.hasError = true;
|
||||
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 {
|
||||
this.destroy$.next();
|
||||
@ -64,7 +180,7 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
// Helper methods for template
|
||||
getUserInitials(): string {
|
||||
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();
|
||||
}
|
||||
@ -75,26 +191,70 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
// Get user role with proper mapping
|
||||
getUserRole(user: User | null): string {
|
||||
if (!user) return 'Utilisateur';
|
||||
getUserRole(): string {
|
||||
if (!this.user) return 'Utilisateur';
|
||||
|
||||
// Use role from profile or fallback to token roles
|
||||
const role = user.role || this.authService.getCurrentUserRoles();
|
||||
// Utiliser le rôle du profil ou la méthode publique du service
|
||||
const role = this.user.role || this.authService.getCurrentUserPrimaryRole();
|
||||
|
||||
if (!role) return 'Utilisateur';
|
||||
|
||||
// Map role to display name
|
||||
const roleDisplayNames: { [key in UserRole]: string } = {
|
||||
[UserRole.DCB_ADMIN]: 'Administrateur système avec tous les accès',
|
||||
[UserRole.DCB_SUPPORT]: 'Support technique avec accès étendus',
|
||||
[UserRole.DCB_PARTNER_ADMIN]: 'Administrateur de partenaire marchand',
|
||||
[UserRole.DCB_PARTNER_MANAGER]: 'Manager opérationnel partenaire',
|
||||
[UserRole.DCB_PARTNER_SUPPORT]: 'Support technique partenaire',
|
||||
[UserRole.MERCHANT_CONFIG_ADMIN]: 'Administrateur de partenaire marchand',
|
||||
[UserRole.MERCHANT_CONFIG_MANAGER]: 'Manager opérationnel partenaire',
|
||||
[UserRole.MERCHANT_CONFIG_TECHNICAL]: 'Support technique partenaire',
|
||||
[UserRole.MERCHANT_CONFIG_VIEWER]: 'Support technique partenaire'
|
||||
const roleDisplayNames: { [key: string]: string } = {
|
||||
[UserRole.DCB_ADMIN]: 'Administrateur système',
|
||||
[UserRole.DCB_SUPPORT]: 'Support technique',
|
||||
[UserRole.DCB_PARTNER_ADMIN]: 'Administrateur partenaire',
|
||||
[UserRole.DCB_PARTNER_MANAGER]: 'Manager opérationnel',
|
||||
[UserRole.DCB_PARTNER_SUPPORT]: 'Support technique',
|
||||
[UserRole.MERCHANT_CONFIG_ADMIN]: 'Administrateur configuration',
|
||||
[UserRole.MERCHANT_CONFIG_MANAGER]: 'Manager configuration',
|
||||
[UserRole.MERCHANT_CONFIG_TECHNICAL]: 'Technicien',
|
||||
[UserRole.MERCHANT_CONFIG_VIEWER]: 'Consultant'
|
||||
};
|
||||
|
||||
const primaryRole = role;
|
||||
return roleDisplayNames[primaryRole] || 'Utilisateur';
|
||||
return roleDisplayNames[role] || '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, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthService } from '@core/services/auth.service';
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { AuthService } from "@core/services/auth.service";
|
||||
import { Subject, takeUntil, finalize } from "rxjs";
|
||||
|
||||
@Component({
|
||||
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(
|
||||
private authService: AuthService,
|
||||
@ -30,22 +32,59 @@ export class Logout implements OnInit {
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
// Vérifier si nous sommes déjà en train de déconnecter
|
||||
if (this.isLoggingOut) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoggingOut = true;
|
||||
this.logout();
|
||||
}
|
||||
|
||||
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: () => {
|
||||
// Redirection vers la page de login après déconnexion
|
||||
this.router.navigate(['/auth/login'], {
|
||||
queryParams: { logout: 'success' }
|
||||
});
|
||||
// Succès - navigation gérée dans finalize
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Erreur lors de la déconnexion:', error);
|
||||
// Rediriger même en cas d'erreur
|
||||
this.router.navigate(['/auth/login']);
|
||||
console.warn('Erreur lors de la déconnexion:', error);
|
||||
}
|
||||
});
|
||||
}, 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">
|
||||
<ng-icon name="lucideLayoutDashboard" class="me-2"></ng-icon>
|
||||
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>
|
||||
<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 class="d-flex gap-2 align-items-center">
|
||||
<!-- Contrôles rapides -->
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<!-- Bouton Actualiser selon le type -->
|
||||
<button class="btn btn-outline-primary btn-sm"
|
||||
(click)="loadAllData()"
|
||||
[disabled]="loading.all">
|
||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading.all" class="me-1"></ng-icon>
|
||||
{{ loading.all ? 'Chargement...' : 'Actualiser' }}
|
||||
(click)="refreshData()"
|
||||
[disabled]="loading.globalData || loading.merchantData">
|
||||
<ng-icon name="lucideRefreshCw"
|
||||
[class.spin]="loading.globalData || loading.merchantData"
|
||||
class="me-1"></ng-icon>
|
||||
{{ (loading.globalData || loading.merchantData) ? 'Chargement...' : 'Actualiser' }}
|
||||
</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()"
|
||||
[disabled]="loading.sync">
|
||||
<ng-icon name="lucideRefreshCcw" [class.spin]="loading.sync" class="me-1"></ng-icon>
|
||||
@ -26,56 +43,58 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filtres -->
|
||||
<div class="filters-card">
|
||||
<!-- Filtres selon le type -->
|
||||
<div *ngIf="canSelectMerchant() && shouldShowMerchantSelector()" class="filters-card">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<!-- Filtre Merchant pour hub users -->
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-light">
|
||||
<ng-icon name="lucideStore"></ng-icon>
|
||||
</span>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
[(ngModel)]="merchantId" min="1"
|
||||
placeholder="Merchant ID"
|
||||
(change)="refreshMerchantData()"
|
||||
style="width: 100px;">
|
||||
|
||||
<select class="form-control form-control-sm"
|
||||
[ngModel]="merchantId"
|
||||
(ngModelChange)="selectMerchant($event)"
|
||||
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 class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-light">
|
||||
<ng-icon name="lucideCalendar"></ng-icon>
|
||||
</span>
|
||||
<input type="date" class="form-control form-control-sm"
|
||||
[(ngModel)]="startDate"
|
||||
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>
|
||||
<!-- Badge de contexte -->
|
||||
<div class="badge" [ngClass]="isViewingGlobal() ? 'bg-info' : 'bg-success'">
|
||||
<ng-icon [name]="isViewingGlobal() ? 'lucideGlobe' : 'lucideStore'" class="me-1"></ng-icon>
|
||||
{{ getCurrentMerchantName() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown" ngbDropdown>
|
||||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
||||
type="button"
|
||||
ngbDropdownToggle>
|
||||
<ng-icon name="lucideSettings" class="me-1"></ng-icon>
|
||||
<!-- Options dropdown seulement pour hub users admin -->
|
||||
<div *ngIf="access.isHubUser && canManageMerchants()" ngbDropdown class="dropdown">
|
||||
<!-- Bouton déclencheur -->
|
||||
<button class="btn btn-outline-secondary dropdown-toggle"
|
||||
ngbDropdownToggle
|
||||
type="button">
|
||||
<ng-icon name="lucideSettings" class="me-2"></ng-icon>
|
||||
Options
|
||||
</button>
|
||||
|
||||
<!-- Menu déroulant -->
|
||||
<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>
|
||||
Vérifier la santé
|
||||
</a>
|
||||
</button>
|
||||
<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>
|
||||
Rafraîchir tout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -85,31 +104,72 @@
|
||||
<!-- Barre d'état rapide -->
|
||||
<div class="status-bar mb-4">
|
||||
<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="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||
<ng-icon name="lucideClock" class="text-primary"></ng-icon>
|
||||
<small>Mis à jour: {{ lastUpdated | date:'HH:mm:ss' }}</small>
|
||||
</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">
|
||||
<ng-icon name="lucideCpu" class="text-success"></ng-icon>
|
||||
<small>Services: {{ stats.onlineServices }}/{{ stats.totalServices }} en ligne</small>
|
||||
</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">
|
||||
<ng-icon name="lucidePhone" class="text-warning"></ng-icon>
|
||||
<small>Opérateur: Orange</small>
|
||||
<ng-icon name="lucideStore" class="text-success"></ng-icon>
|
||||
<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>
|
||||
|
||||
<!-- Message de sync -->
|
||||
<div *ngIf="syncResponse" class="alert alert-success alert-dismissible fade show mb-4">
|
||||
<!-- Message d'erreur si pas de permissions -->
|
||||
<div *ngIf="!shouldShowTransactions()"
|
||||
class="alert alert-warning mb-4">
|
||||
<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">
|
||||
<strong>{{ syncResponse.message }}</strong>
|
||||
<div class="text-muted small">Synchronisée à {{ formatDate(syncResponse.timestamp) }}</div>
|
||||
@ -119,7 +179,7 @@
|
||||
</div>
|
||||
|
||||
<!-- ==================== SECTION DES KPIs HORIZONTAUX ==================== -->
|
||||
<div class="kpi-section mb-4">
|
||||
<div *ngIf="shouldShowKPIs()" class="kpi-section mb-4">
|
||||
<div class="row g-3">
|
||||
<!-- Transactions Journalières -->
|
||||
<div class="col-xl-2 col-md-4 col-sm-6">
|
||||
@ -171,9 +231,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transactions Mensuelles -->
|
||||
<!-- Transactions Mensuel -->
|
||||
<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="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
@ -181,13 +241,13 @@
|
||||
<h4 class="fw-bold mb-0">{{ formatNumber(getPaymentStats().monthly.transactions) }}</h4>
|
||||
<small class="text-muted">Mensuel</small>
|
||||
</div>
|
||||
<div class="avatar-sm bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
||||
<ng-icon name="lucideCalendar" class="text-success fs-5"></ng-icon>
|
||||
<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-info fs-5"></ng-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<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>
|
||||
{{ getPaymentStats().monthly.successRate | number:'1.0-0' }}%
|
||||
</span>
|
||||
@ -257,7 +317,7 @@
|
||||
<small class="text-muted">Global</small>
|
||||
</div>
|
||||
<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 class="d-flex justify-content-between align-items-center">
|
||||
@ -274,7 +334,7 @@
|
||||
</div>
|
||||
|
||||
<!-- ==================== 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">
|
||||
<!-- Graphique principal dynamique -->
|
||||
<div class="col-xl-8">
|
||||
@ -285,6 +345,14 @@
|
||||
<h5 class="card-title mb-0">
|
||||
<ng-icon [name]="getMetricIcon(dataSelection.metric)" class="text-primary me-2"></ng-icon>
|
||||
{{ 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>
|
||||
<p class="text-muted small mb-0">Visualisation en temps réel</p>
|
||||
</div>
|
||||
@ -334,15 +402,16 @@
|
||||
<div class="spinner-border text-primary"></div>
|
||||
<p class="mt-2 text-muted">Chargement du graphique...</p>
|
||||
</div>
|
||||
<div *ngIf="!loading.chart && !dailyTransactions"
|
||||
<div *ngIf="!loading.chart && !getCurrentTransactionData()"
|
||||
class="text-center py-5 text-muted">
|
||||
<ng-icon name="lucideBarChart3" class="fs-1 opacity-25"></ng-icon>
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="!loading.chart && dailyTransactions"
|
||||
<div *ngIf="!loading.chart && getCurrentTransactionData()"
|
||||
class="position-relative" style="height: 300px;">
|
||||
<canvas #mainChartCanvas></canvas>
|
||||
</div>
|
||||
@ -353,8 +422,8 @@
|
||||
<!-- Panneau droit avec 2 mini-graphiques -->
|
||||
<div class="col-xl-4">
|
||||
<div class="row h-100 g-4">
|
||||
<!-- Graphique de comparaison -->
|
||||
<div class="col-12">
|
||||
<!-- Graphique de comparaison seulement pour hub users en mode global -->
|
||||
<div *ngIf="shouldShowChart('comparison')" class="col-12">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-transparent border-bottom-0">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
@ -380,18 +449,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Taux de succès circulaire -->
|
||||
<div class="col-12">
|
||||
<div *ngIf="shouldShowChart('successRate')" class="col-12">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-transparent border-bottom-0">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<ng-icon name="lucideActivity" class="text-success me-2"></ng-icon>
|
||||
Performance Globale
|
||||
Performance
|
||||
</h5>
|
||||
<button class="btn btn-sm btn-outline-success btn-sm"
|
||||
(click)="refreshDailyTransactions()"
|
||||
[disabled]="loading.dailyTransactions">
|
||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading.dailyTransactions"></ng-icon>
|
||||
(click)="refreshData()"
|
||||
[disabled]="loading.globalData || loading.merchantData">
|
||||
<ng-icon name="lucideRefreshCw"
|
||||
[class.spin]="loading.globalData || loading.merchantData"></ng-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -412,7 +482,7 @@
|
||||
<div class="col-4">
|
||||
<div class="p-2 border rounded bg-success bg-opacity-10">
|
||||
<div class="text-success fw-bold">
|
||||
{{ formatNumber(dailyTransactions?.items?.[0]?.successCount || 0) }}
|
||||
{{ formatNumber(getCurrentTransactionData()?.items?.[0]?.successCount || 0) }}
|
||||
</div>
|
||||
<small class="text-muted">Réussies</small>
|
||||
</div>
|
||||
@ -420,7 +490,7 @@
|
||||
<div class="col-4">
|
||||
<div class="p-2 border rounded bg-danger bg-opacity-10">
|
||||
<div class="text-danger fw-bold">
|
||||
{{ formatNumber(dailyTransactions?.items?.[0]?.failedCount || 0) }}
|
||||
{{ formatNumber(getCurrentTransactionData()?.items?.[0]?.failedCount || 0) }}
|
||||
</div>
|
||||
<small class="text-muted">Échouées</small>
|
||||
</div>
|
||||
@ -428,7 +498,7 @@
|
||||
<div class="col-4">
|
||||
<div class="p-2 border rounded bg-info bg-opacity-10">
|
||||
<div class="text-info fw-bold">
|
||||
{{ formatNumber(dailyTransactions?.items?.[0]?.pendingCount || 0) }}
|
||||
{{ formatNumber(getCurrentTransactionData()?.items?.[0]?.pendingCount || 0) }}
|
||||
</div>
|
||||
<small class="text-muted">En attente</small>
|
||||
</div>
|
||||
@ -443,7 +513,7 @@
|
||||
</div>
|
||||
|
||||
<!-- ==================== SECTION SANTÉ DES APIS ==================== -->
|
||||
<div class="health-section mb-4">
|
||||
<div *ngIf="shouldShowSystemHealth()" class="health-section mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-transparent border-bottom-0">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
@ -462,7 +532,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
(click)="refreshHealthCheck()"
|
||||
(click)="checkSystemHealth()"
|
||||
[disabled]="loading.healthCheck">
|
||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading.healthCheck"></ng-icon>
|
||||
</button>
|
||||
@ -522,7 +592,7 @@
|
||||
</div>
|
||||
|
||||
<!-- ==================== SECTION DES TABLEAUX ==================== -->
|
||||
<div class="tables-section">
|
||||
<div *ngIf="shouldShowTransactions()" class="tables-section">
|
||||
<div class="row g-4">
|
||||
<!-- Transactions récentes -->
|
||||
<div class="col-xl-6">
|
||||
@ -533,14 +603,19 @@
|
||||
<h5 class="card-title mb-0">
|
||||
<ng-icon name="lucideListChecks" class="text-primary me-2"></ng-icon>
|
||||
Transactions récentes
|
||||
<span class="badge ms-2"
|
||||
[ngClass]="isViewingGlobal() ? 'bg-info' : 'bg-success'">
|
||||
{{ getCurrentMerchantName() }}
|
||||
</span>
|
||||
</h5>
|
||||
<p class="text-muted small mb-0">Dernières 24 heures</p>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
(click)="refreshDailyTransactions()"
|
||||
[disabled]="loading.dailyTransactions">
|
||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading.dailyTransactions"></ng-icon>
|
||||
(click)="refreshData()"
|
||||
[disabled]="loading.globalData || loading.merchantData">
|
||||
<ng-icon name="lucideRefreshCw"
|
||||
[class.spin]="loading.globalData || loading.merchantData"></ng-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -557,10 +632,10 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<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">
|
||||
<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 class="text-end">
|
||||
<div class="fw-medium">{{ formatCurrency(item.totalAmount) }}</div>
|
||||
@ -574,7 +649,8 @@
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="!(dailyTransactions?.items?.length)">
|
||||
|
||||
<tr *ngIf="!getCurrentTransactionData()?.items?.length">
|
||||
<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>
|
||||
<p class="mb-0">Aucune transaction disponible</p>
|
||||
@ -588,7 +664,7 @@
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">
|
||||
<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>
|
||||
<a href="#" class="btn btn-sm btn-outline-primary">Voir tout</a>
|
||||
</div>
|
||||
@ -596,8 +672,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alertes système -->
|
||||
<div class="col-xl-6">
|
||||
<!-- Alertes système - Masqué si pas autorisé -->
|
||||
<div *ngIf="shouldShowAlerts()" class="col-xl-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-transparent border-bottom-0">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
@ -627,7 +703,7 @@
|
||||
name="lucideInfo"
|
||||
class="text-info fs-5"></ng-icon>
|
||||
<ng-icon *ngIf="alert.type === 'success'"
|
||||
name="lucideCheckCircle"
|
||||
name="lucideCheckCircle2"
|
||||
class="text-success fs-5"></ng-icon>
|
||||
<ng-icon *ngIf="alert.type === 'danger'"
|
||||
name="lucideAlertCircle"
|
||||
@ -643,7 +719,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<small class="text-muted">Tous les systèmes fonctionnent normalement</small>
|
||||
</div>
|
||||
@ -673,11 +749,13 @@
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="text-muted small">
|
||||
<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 class="text-muted small">
|
||||
<ng-icon name="lucideDatabase" class="me-1"></ng-icon>
|
||||
APIs: IAM, Config, Core, Reporting
|
||||
<ng-icon [name]="isViewingGlobal() ? 'lucideGlobe' : 'lucideStore'" class="me-1"></ng-icon>
|
||||
{{ getCurrentMerchantName() }}
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<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' })
|
||||
export class ReportService {
|
||||
private baseUrl = `${environment.reportingApiUrl}/reporting`;
|
||||
private readonly DEFAULT_TIMEOUT = 5000; // Timeout réduit pour les health checks
|
||||
private baseUrl = `${environment.reportingApiUrl}/reporting`; // Mise à jour du chemin
|
||||
private readonly DEFAULT_TIMEOUT = 5000;
|
||||
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
|
||||
private apiEndpoints = {
|
||||
@ -35,8 +35,15 @@ export class ReportService {
|
||||
private buildParams(params?: ReportParams): HttpParams {
|
||||
let httpParams = new HttpParams();
|
||||
if (!params) return httpParams;
|
||||
|
||||
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;
|
||||
}
|
||||
@ -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(
|
||||
@ -240,57 +465,4 @@ export class ReportService {
|
||||
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()) {
|
||||
<th>Type</th>
|
||||
}
|
||||
<!-- Colonne Merchant Partner pour la vue admin -->
|
||||
@if (showMerchantPartnerColumn()) {
|
||||
<th>Merchant Partner</th>
|
||||
}
|
||||
<th (click)="sort('username')" class="cursor-pointer">
|
||||
<div class="d-flex align-items-center">
|
||||
<span>Utilisateur</span>
|
||||
@ -268,6 +272,25 @@
|
||||
</span>
|
||||
</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>
|
||||
<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">
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
} from '@core/models/dcb-bo-hub-user.model';
|
||||
|
||||
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 { UiCard } from '@app/components/ui-card';
|
||||
|
||||
@ -448,7 +448,7 @@ export class HubUsersList implements OnInit, OnDestroy {
|
||||
return roleInfo?.description || 'Description non disponible';
|
||||
}
|
||||
|
||||
formatTimestamp(timestamp: number): string {
|
||||
formatTimestamp(timestamp: number | undefined): string {
|
||||
if (!timestamp) return 'Non disponible';
|
||||
return new Date(timestamp).toLocaleDateString('fr-FR', {
|
||||
year: 'numeric',
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
} from '@core/models/dcb-bo-hub-user.model';
|
||||
|
||||
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';
|
||||
|
||||
@Component({
|
||||
|
||||
@ -39,6 +39,7 @@ export interface UserProfileResponse {
|
||||
emailVerified: boolean;
|
||||
enabled: boolean;
|
||||
role: string[];
|
||||
merchantPartnerId?: string;
|
||||
createdBy?: string;
|
||||
createdByUsername?: string;
|
||||
}
|
||||
@ -47,6 +48,10 @@ export interface MessageResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface MerchantPartnerIdResponse {
|
||||
merchantPartnerId: string | null;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HubUsersService {
|
||||
private http = inject(HttpClient);
|
||||
@ -292,6 +297,7 @@ export class HubUsersService {
|
||||
lastName: apiUser.lastName,
|
||||
enabled: apiUser.enabled,
|
||||
emailVerified: apiUser.emailVerified,
|
||||
merchantPartnerId: apiUser.merchantPartnerId,
|
||||
userType: userType,
|
||||
role: apiUser.role,
|
||||
createdBy: apiUser.createdBy,
|
||||
@ -328,6 +334,10 @@ export class HubUsersService {
|
||||
filteredUsers = filteredUsers.filter(user => user.enabled === filters.enabled);
|
||||
}
|
||||
|
||||
if (filters.merchantPartnerId) {
|
||||
filteredUsers = filteredUsers.filter(user => user.merchantPartnerId === filters.merchantPartnerId);
|
||||
}
|
||||
|
||||
if (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 { 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 { MerchantSyncService } from './merchant-sync-orchestrator.service';
|
||||
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';
|
||||
|
||||
// Mock des services
|
||||
@ -36,12 +37,12 @@ const mockHubUsersService = {
|
||||
createHubUser: (data: any): Observable<any> => of({
|
||||
id: 'keycloak-123',
|
||||
username: data.username,
|
||||
merchantConfigId: 123
|
||||
merchantPartnerId: 123
|
||||
}),
|
||||
getHubUserById: (id: string): Observable<any> => of({
|
||||
id,
|
||||
username: 'owner',
|
||||
merchantConfigId: 123
|
||||
merchantPartnerId: 123
|
||||
}),
|
||||
updateHubUser: (id: string, data: any): Observable<any> => of({
|
||||
id,
|
||||
@ -49,7 +50,7 @@ const mockHubUsersService = {
|
||||
}),
|
||||
deleteHubUser: (id: string): Observable<void> => of(void 0),
|
||||
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 {
|
||||
private testData = {
|
||||
currentMerchantId: 0,
|
||||
currentDcbPartnerId: '',
|
||||
currentMerchantPartnerId: '',
|
||||
currentUserId: '',
|
||||
currentMerchantConfigUserId: 0
|
||||
};
|
||||
@ -132,9 +133,9 @@ export class MerchantCrudTest {
|
||||
// 2. Créer DCB_PARTNER dans Keycloak
|
||||
const dcbPartnerDto = {
|
||||
...ownerData,
|
||||
userType: 'HUB',
|
||||
role: 'DCB_PARTNER',
|
||||
merchantConfigId: merchantConfig.id
|
||||
userType: UserType.HUB,
|
||||
role: UserRole.DCB_PARTNER_ADMIN,
|
||||
merchantPartnerId: merchantConfig.id
|
||||
};
|
||||
|
||||
const keycloakMerchant = await firstValueFrom(
|
||||
@ -144,7 +145,7 @@ export class MerchantCrudTest {
|
||||
|
||||
// Sauvegarder les IDs
|
||||
this.testData.currentMerchantId = merchantConfig.id;
|
||||
this.testData.currentDcbPartnerId = keycloakMerchant.id;
|
||||
this.testData.currentMerchantPartnerId = keycloakMerchant.id;
|
||||
|
||||
const result = {
|
||||
merchantConfig,
|
||||
@ -163,7 +164,7 @@ export class MerchantCrudTest {
|
||||
async testCreateMerchantUser(): Promise<void> {
|
||||
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');
|
||||
return;
|
||||
}
|
||||
@ -182,8 +183,7 @@ export class MerchantCrudTest {
|
||||
const keycloakUserDto = {
|
||||
...userData,
|
||||
userType: 'MERCHANT_PARTNER',
|
||||
merchantPartnerId: this.testData.currentDcbPartnerId,
|
||||
merchantConfigId: this.testData.currentMerchantId
|
||||
merchantPartnerId: this.testData.currentMerchantId
|
||||
};
|
||||
|
||||
const keycloakUser = await firstValueFrom(
|
||||
@ -226,7 +226,7 @@ export class MerchantCrudTest {
|
||||
async testReadMerchant(): Promise<void> {
|
||||
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');
|
||||
return;
|
||||
}
|
||||
@ -237,14 +237,14 @@ export class MerchantCrudTest {
|
||||
mockMerchantConfigService.getMerchantById(this.testData.currentMerchantId)
|
||||
),
|
||||
firstValueFrom(
|
||||
mockHubUsersService.getHubUserById(this.testData.currentDcbPartnerId)
|
||||
mockHubUsersService.getHubUserById(this.testData.currentMerchantPartnerId)
|
||||
)
|
||||
]);
|
||||
|
||||
console.log('🎯 RESULTAT READ:', {
|
||||
merchantConfig,
|
||||
keycloakMerchant,
|
||||
coherence: keycloakMerchant.merchantConfigId === merchantConfig.id ? '✅ OK' : '❌ INCOHÉRENT'
|
||||
coherence: keycloakMerchant.merchantPartnerId === merchantConfig.id ? '✅ OK' : '❌ INCOHÉRENT'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@ -255,7 +255,7 @@ export class MerchantCrudTest {
|
||||
async testUpdateMerchant(): Promise<void> {
|
||||
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');
|
||||
return;
|
||||
}
|
||||
@ -273,7 +273,7 @@ export class MerchantCrudTest {
|
||||
),
|
||||
firstValueFrom(
|
||||
mockHubUsersService.updateHubUser(
|
||||
this.testData.currentDcbPartnerId,
|
||||
this.testData.currentMerchantPartnerId,
|
||||
{ email: newEmail }
|
||||
)
|
||||
)
|
||||
@ -363,7 +363,7 @@ export class MerchantCrudTest {
|
||||
async testDeleteMerchant(): Promise<void> {
|
||||
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');
|
||||
return;
|
||||
}
|
||||
@ -382,7 +382,7 @@ export class MerchantCrudTest {
|
||||
mockMerchantConfigService.deleteMerchant(this.testData.currentMerchantId)
|
||||
),
|
||||
firstValueFrom(
|
||||
mockHubUsersService.deleteHubUser(this.testData.currentDcbPartnerId)
|
||||
mockHubUsersService.deleteHubUser(this.testData.currentMerchantPartnerId)
|
||||
)
|
||||
]);
|
||||
|
||||
@ -390,7 +390,7 @@ export class MerchantCrudTest {
|
||||
|
||||
// Réinitialiser
|
||||
this.testData.currentMerchantId = 0;
|
||||
this.testData.currentDcbPartnerId = '';
|
||||
this.testData.currentMerchantPartnerId = '';
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ ERREUR DELETE MERCHANT:', error);
|
||||
@ -449,7 +449,7 @@ export class MerchantCrudTest {
|
||||
resetTestData() {
|
||||
this.testData = {
|
||||
currentMerchantId: 0,
|
||||
currentDcbPartnerId: '',
|
||||
currentMerchantPartnerId: '',
|
||||
currentUserId: '',
|
||||
currentMerchantConfigUserId: 0
|
||||
};
|
||||
|
||||
@ -153,6 +153,10 @@
|
||||
<table class="table table-hover table-striped">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<!-- Colonne Merchant Partner pour les admins -->
|
||||
@if (showMerchantPartnerColumn) {
|
||||
<th>Merchant Partner</th>
|
||||
}
|
||||
<th (click)="sort('username')" class="cursor-pointer">
|
||||
<div class="d-flex align-items-center">
|
||||
<span>Utilisateur</span>
|
||||
@ -184,6 +188,21 @@
|
||||
<tbody>
|
||||
@for (user of displayedUsers; track user.id) {
|
||||
<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>
|
||||
<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">
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
} from '@core/models/dcb-bo-hub-user.model';
|
||||
|
||||
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 { UiCard } from '@app/components/ui-card';
|
||||
|
||||
@ -118,12 +118,6 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
this.currentUserRole = this.extractUserRole(user);
|
||||
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
|
||||
|
||||
console.log('Merchant User Context Loaded:', {
|
||||
role: this.currentUserRole,
|
||||
merchantPartnerId: this.currentMerchantPartnerId,
|
||||
canViewAllMerchants: this.canViewAllMerchants
|
||||
});
|
||||
|
||||
this.loadUsers();
|
||||
},
|
||||
error: (error) => {
|
||||
@ -391,7 +385,7 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
return roleInfo?.description || 'Description non disponible';
|
||||
}
|
||||
|
||||
formatTimestamp(timestamp: number): string {
|
||||
formatTimestamp(timestamp: number | undefined): string {
|
||||
if (!timestamp) return 'Non disponible';
|
||||
return new Date(timestamp).toLocaleDateString('fr-FR', {
|
||||
year: 'numeric',
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
} from '@core/models/dcb-bo-hub-user.model';
|
||||
|
||||
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';
|
||||
|
||||
@Component({
|
||||
|
||||
@ -271,6 +271,7 @@ export class MerchantUsersService {
|
||||
lastName: apiUser.lastName,
|
||||
enabled: apiUser.enabled,
|
||||
emailVerified: apiUser.emailVerified,
|
||||
merchantPartnerId: apiUser.merchantPartnerId,
|
||||
userType: userType,
|
||||
role: apiUser.role,
|
||||
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 { 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 { PageTitle } from '@app/components/page-title/page-title';
|
||||
import { MerchantUsersList } from './merchant-users-list/merchant-users-list';
|
||||
@ -157,8 +157,6 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
|
||||
this.currentUserType = this.extractUserType(user);
|
||||
|
||||
console.log(`MERCHANT User ROLE: ${this.currentUserRole}`);
|
||||
|
||||
if (this.currentUserRole) {
|
||||
this.roleService.setCurrentUserRole(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.assignableRoles = this.roleService.getAssignableRoles(this.currentUserRole);
|
||||
console.log('Assignable roles:', this.assignableRoles);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
} from '@core/models/merchant-config.model';
|
||||
|
||||
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 { UiCard } from '@app/components/ui-card';
|
||||
|
||||
@ -158,10 +158,6 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
||||
|
||||
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();
|
||||
},
|
||||
error: (error) => {
|
||||
|
||||
@ -19,7 +19,7 @@ import {
|
||||
|
||||
import { MerchantConfigService } from '../merchant-config.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 { UserRole } from '@core/models/dcb-bo-hub-user.model';
|
||||
|
||||
|
||||
@ -199,7 +199,6 @@ export class MerchantConfigService {
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
map(apiUser => {
|
||||
console.log(`✅ User ${userId} role updated successfully`);
|
||||
// ✅ UTILISATION DE L'ADAPTER
|
||||
return this.dataAdapter.convertApiUserToFrontend(apiUser);
|
||||
}),
|
||||
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 { 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 { MerchantSyncService } from '../hub-users-management/merchant-sync-orchestrator.service';
|
||||
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 { 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';
|
||||
|
||||
@Component({
|
||||
|
||||
@ -19,7 +19,7 @@ import {
|
||||
Currency
|
||||
} from '@core/models/dcb-bo-hub-subscription.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({
|
||||
selector: 'app-subscriptions',
|
||||
|
||||
@ -1,4 +1,18 @@
|
||||
<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 -->
|
||||
@if (loading && !transaction) {
|
||||
<div class="text-center py-5">
|
||||
@ -61,7 +75,7 @@
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<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 class="text-muted small">Montant</div>
|
||||
@ -92,27 +106,7 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label text-muted small mb-1">MSISDN</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>
|
||||
<label class="form-label text-muted small mb-1">Plan/Service</label>
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucidePackage" class="me-2 text-muted"></ng-icon>
|
||||
<div>
|
||||
@ -122,29 +116,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<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) {
|
||||
@if (transaction.externalReference) {
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label text-muted small mb-1">ID Externe</label>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="font-monospace small">{{ transaction.externalId }}</span>
|
||||
<button class="btn btn-sm btn-link p-0 ms-2" (click)="copyToClipboard(transaction.externalId!)">
|
||||
<span class="font-monospace small">{{ transaction.externalReference }}</span>
|
||||
<button class="btn btn-sm btn-link p-0 ms-2" (click)="copyToClipboard(transaction.externalReference!)">
|
||||
<ng-icon name="lucideCopy" class="text-muted"></ng-icon>
|
||||
</button>
|
||||
</div>
|
||||
@ -167,114 +144,14 @@
|
||||
<label class="form-label text-muted small mb-1">Mis à jour le</label>
|
||||
<div class="small">{{ formatDate(transaction.updatedAt) }}</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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Colonne latérale - Actions et métadonnées -->
|
||||
<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 -->
|
||||
<div class="card">
|
||||
<div class="card-header bg-light">
|
||||
@ -286,16 +163,10 @@
|
||||
<span class="text-muted">ID Transaction:</span>
|
||||
<span class="font-monospace">{{ transaction.id }}</span>
|
||||
</div>
|
||||
|
||||
<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) {
|
||||
@if (transaction.merchantPartnerId) {
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Marchand ID:</span>
|
||||
<span class="font-monospace small">{{ transaction.merchantId }}</span>
|
||||
<span class="font-monospace small">{{ transaction.merchantPartnerId }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -313,25 +184,6 @@
|
||||
</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>
|
||||
}
|
||||
@ -348,4 +200,5 @@
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@ -1,3 +1,4 @@
|
||||
// [file name]: transactions/details/details.ts (mise à jour)
|
||||
import { Component, inject, OnInit, Input, ChangeDetectorRef } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
@ -21,12 +22,16 @@ import {
|
||||
lucideUser,
|
||||
lucideGlobe,
|
||||
lucideAlertCircle,
|
||||
lucideInfo
|
||||
lucideInfo,
|
||||
lucideShield,
|
||||
lucideStore,
|
||||
lucideLock
|
||||
} from '@ng-icons/lucide';
|
||||
import { NgbAlertModule, NgbTooltipModule, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
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({
|
||||
selector: 'app-transaction-details',
|
||||
@ -48,11 +53,15 @@ import { Transaction, TransactionStatus, RefundRequest } from '../models/transac
|
||||
})
|
||||
export class TransactionDetails implements OnInit {
|
||||
private transactionsService = inject(TransactionsService);
|
||||
private accessService = inject(TransactionAccessService);
|
||||
private modalService = inject(NgbModal);
|
||||
private cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
@Input() transactionId!: string;
|
||||
|
||||
// Permissions
|
||||
access!: TransactionAccess;
|
||||
|
||||
// Données
|
||||
transaction: Transaction | null = null;
|
||||
loading = false;
|
||||
@ -63,100 +72,62 @@ export class TransactionDetails implements OnInit {
|
||||
refunding = false;
|
||||
retrying = false;
|
||||
|
||||
// Accès
|
||||
canViewSensitiveData = false;
|
||||
accessDenied = false;
|
||||
|
||||
ngOnInit() {
|
||||
this.initializePermissions();
|
||||
|
||||
if (this.transactionId) {
|
||||
this.loadTransactionDetails();
|
||||
}
|
||||
}
|
||||
|
||||
private initializePermissions() {
|
||||
this.access = this.accessService.getTransactionAccess();
|
||||
this.canViewSensitiveData = this.access.canViewSensitiveData;
|
||||
}
|
||||
|
||||
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.error = '';
|
||||
this.accessDenied = false;
|
||||
|
||||
this.transactionsService.getTransactionById(this.transactionId).subscribe({
|
||||
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.loading = false;
|
||||
this.cdRef.detectChanges();
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
this.error = 'Erreur lors du chargement des détails de la transaction';
|
||||
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();
|
||||
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
|
||||
copyToClipboard(text: string) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
@ -170,21 +141,12 @@ export class TransactionDetails implements OnInit {
|
||||
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
|
||||
getStatusBadgeClass(status: TransactionStatus): string {
|
||||
switch (status) {
|
||||
case 'SUCCESS': return 'badge bg-success';
|
||||
case 'PENDING': return 'badge bg-warning';
|
||||
case 'FAILED': return 'badge bg-danger';
|
||||
case 'REFUNDED': return 'badge bg-info';
|
||||
case 'CANCELLED': return 'badge bg-secondary';
|
||||
case 'EXPIRED': return 'badge bg-dark';
|
||||
default: return 'badge bg-secondary';
|
||||
@ -196,7 +158,6 @@ export class TransactionDetails implements OnInit {
|
||||
case 'SUCCESS': return 'lucideCheckCircle';
|
||||
case 'PENDING': return 'lucideClock';
|
||||
case 'FAILED': return 'lucideXCircle';
|
||||
case 'REFUNDED': return 'lucideUndo2';
|
||||
case 'CANCELLED': return 'lucideBan';
|
||||
default: return 'lucideClock';
|
||||
}
|
||||
@ -241,18 +202,31 @@ export class TransactionDetails implements OnInit {
|
||||
}
|
||||
|
||||
canRefund(): boolean {
|
||||
return this.transaction?.status === 'SUCCESS';
|
||||
return this.access.canRefund && this.transaction?.status === 'SUCCESS';
|
||||
}
|
||||
|
||||
canRetry(): boolean {
|
||||
return this.transaction?.status === 'FAILED';
|
||||
return this.access.canRetry && this.transaction?.status === 'FAILED';
|
||||
}
|
||||
|
||||
canCancel(): boolean {
|
||||
return this.transaction?.status === 'PENDING';
|
||||
return this.access.canCancel && this.transaction?.status === 'PENDING';
|
||||
}
|
||||
|
||||
showErrorDetails(): boolean {
|
||||
return !!this.transaction?.errorCode || !!this.transaction?.errorMessage;
|
||||
// Méthodes pour le template
|
||||
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>
|
||||
<h4 class="mb-1">Gestion des Transactions</h4>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item">
|
||||
@ -12,22 +13,18 @@
|
||||
</li>
|
||||
</ol>
|
||||
</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 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 -->
|
||||
<button class="btn btn-outline-secondary" (click)="loadTransactions()" [disabled]="loading">
|
||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading"></ng-icon>
|
||||
@ -37,6 +34,19 @@
|
||||
</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 -->
|
||||
@if (paginatedData?.stats) {
|
||||
<div class="row mb-4">
|
||||
@ -61,10 +71,6 @@
|
||||
<div class="h5 mb-0 text-warning">{{ getPendingCount() }}</div>
|
||||
</div>
|
||||
<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>
|
||||
<div class="h5 mb-0">{{ formatCurrency(getTotalAmount()) }}</div>
|
||||
</div>
|
||||
@ -85,7 +91,7 @@
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Rechercher par MSISDN, ID..."
|
||||
placeholder="Rechercher par periodicity, Type..."
|
||||
[(ngModel)]="searchTerm"
|
||||
(keyup.enter)="onSearch()"
|
||||
>
|
||||
@ -163,27 +169,23 @@
|
||||
<ng-icon [name]="getSortIcon('id')" class="ms-1 fs-12"></ng-icon>
|
||||
</div>
|
||||
</th>
|
||||
<th (click)="sort('msisdn')" class="cursor-pointer">
|
||||
<div class="d-flex align-items-center">
|
||||
<span>MSISDN</span>
|
||||
<ng-icon [name]="getSortIcon('msisdn')" class="ms-1 fs-12"></ng-icon>
|
||||
</div>
|
||||
</th>
|
||||
<th>Opérateur</th>
|
||||
<th>Type</th>
|
||||
<th>Merchant</th>
|
||||
<th (click)="sort('amount')" class="cursor-pointer">
|
||||
<div class="d-flex align-items-center">
|
||||
<span>Montant</span>
|
||||
<ng-icon [name]="getSortIcon('amount')" class="ms-1 fs-12"></ng-icon>
|
||||
</div>
|
||||
</th>
|
||||
<th>Produit</th>
|
||||
<th>Périodicité</th>
|
||||
<th>Statut</th>
|
||||
<th (click)="sort('transactionDate')" class="cursor-pointer">
|
||||
<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>
|
||||
</div>
|
||||
</th>
|
||||
<th>Prochain paiement</th>
|
||||
<th width="120">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -199,9 +201,16 @@
|
||||
>
|
||||
</td>
|
||||
<td class="font-monospace small">{{ transaction.id }}</td>
|
||||
<td class="font-monospace">{{ transaction.msisdn }}</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>
|
||||
<span [class]="getAmountColor(transaction.amount)">
|
||||
@ -209,20 +218,28 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-truncate" style="max-width: 150px;"
|
||||
[ngbTooltip]="transaction.productName">
|
||||
{{ transaction.productName }}
|
||||
</div>
|
||||
@if (transaction.periodicity) {
|
||||
<span [class]="getPeriodicityBadgeClass(transaction.periodicity)" class="badge">
|
||||
{{ getPeriodicityDisplayName(transaction.periodicity) }}
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<span [class]="getStatusBadgeClass(transaction.status)" class="badge">
|
||||
<ng-icon [name]="getStatusIcon(transaction.status)" class="me-1"></ng-icon>
|
||||
{{ transaction.status }}
|
||||
{{ getStatusDisplayName(transaction.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="small text-muted">
|
||||
{{ formatDate(transaction.transactionDate) }}
|
||||
</td>
|
||||
<td class="small text-muted">
|
||||
@if (transaction.nextPaymentDate) {
|
||||
{{ formatDate(transaction.nextPaymentDate) }}
|
||||
} @else {
|
||||
-
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button
|
||||
@ -232,33 +249,13 @@
|
||||
>
|
||||
<ng-icon name="lucideEye"></ng-icon>
|
||||
</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>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@empty {
|
||||
<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>
|
||||
<p class="text-muted mb-3">Aucune transaction trouvée</p>
|
||||
<button class="btn btn-primary" (click)="onClearFilters()">
|
||||
@ -274,7 +271,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 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="text-muted">
|
||||
Affichage de {{ (filters.page! - 1) * filters.limit! + 1 }} à
|
||||
@ -295,4 +292,6 @@
|
||||
</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 { FormsModule } from '@angular/forms';
|
||||
import { NgIcon, provideNgIconsConfig } from '@ng-icons/core';
|
||||
@ -16,13 +16,32 @@ import {
|
||||
lucideClock,
|
||||
lucideXCircle,
|
||||
lucideUndo2,
|
||||
lucideBan
|
||||
lucideBan,
|
||||
lucideShield,
|
||||
lucideStore,
|
||||
lucideCalendar,
|
||||
lucideRepeat,
|
||||
lucideCreditCard
|
||||
} from '@ng-icons/lucide';
|
||||
import { NgbPaginationModule, NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { TransactionsService } from '../services/transactions.service';
|
||||
import { Transaction, TransactionQuery, TransactionStatus, PaginatedTransactions } from '../models/transaction';
|
||||
import { environment } from '@environments/environment';
|
||||
import { TransactionAccessService, TransactionAccess } from '../services/transaction-access.service';
|
||||
|
||||
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({
|
||||
selector: 'app-transactions-list',
|
||||
@ -42,13 +61,19 @@ import { environment } from '@environments/environment';
|
||||
],
|
||||
templateUrl: './list.html'
|
||||
})
|
||||
export class TransactionsList implements OnInit {
|
||||
export class TransactionsList implements OnInit, OnDestroy {
|
||||
private transactionsService = inject(TransactionsService);
|
||||
private accessService = inject(TransactionAccessService);
|
||||
private cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
@Output() transactionSelected = new EventEmitter<string>();
|
||||
@Output() openRefundModal = new EventEmitter<string>();
|
||||
|
||||
// Permissions
|
||||
access!: TransactionAccess;
|
||||
currentUserRole = '';
|
||||
currentMerchantId?: number;
|
||||
|
||||
// Données
|
||||
transactions: Transaction[] = [];
|
||||
paginatedData: PaginatedTransactions | null = null;
|
||||
@ -63,19 +88,22 @@ export class TransactionsList implements OnInit {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
status: undefined,
|
||||
operator: '',
|
||||
country: '',
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
msisdn: '',
|
||||
sortBy: 'transactionDate',
|
||||
sortOrder: 'desc'
|
||||
};
|
||||
|
||||
// Options de filtre
|
||||
statusOptions: TransactionStatus[] = ['PENDING', 'SUCCESS', 'FAILED', 'REFUNDED', 'CANCELLED'];
|
||||
operatorOptions: string[] = ['Orange', 'Free', 'SFR', 'Bouygues'];
|
||||
countryOptions: string[] = ['FR', 'BE', 'CH', 'LU'];
|
||||
statusOptions: TransactionStatus[] = ['PENDING', 'SUCCESS', 'FAILED', 'CANCELLED'];
|
||||
typeOptions: TransactionType[] = [
|
||||
TransactionType.SUBSCRIPTION_PAYMENT,
|
||||
TransactionType.SUBSCRIPTION_RENEWAL,
|
||||
TransactionType.ONE_TIME_PAYMENT
|
||||
];
|
||||
periodicityOptions = Object.values(SubscriptionPeriodicity);
|
||||
operatorOptions: string[] = ['Orange'];
|
||||
countryOptions: string[] = ['SN'];
|
||||
|
||||
// Tri
|
||||
sortField: string = 'transactionDate';
|
||||
@ -86,10 +114,36 @@ export class TransactionsList implements OnInit {
|
||||
selectAll = false;
|
||||
|
||||
ngOnInit() {
|
||||
this.initializePermissions();
|
||||
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() {
|
||||
if (!this.access.canViewTransactions) {
|
||||
this.error = 'Vous n\'avez pas la permission de voir les transactions';
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
@ -104,6 +158,11 @@ export class TransactionsList implements OnInit {
|
||||
this.filters.sortBy = this.sortField;
|
||||
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({
|
||||
next: (data) => {
|
||||
this.paginatedData = data;
|
||||
@ -114,30 +173,6 @@ export class TransactionsList implements OnInit {
|
||||
error: (error) => {
|
||||
this.error = 'Erreur lors du chargement des transactions';
|
||||
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();
|
||||
console.error('Error loading transactions:', error);
|
||||
}
|
||||
@ -156,14 +191,17 @@ export class TransactionsList implements OnInit {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
status: undefined,
|
||||
operator: '',
|
||||
country: '',
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
msisdn: '',
|
||||
sortBy: 'transactionDate',
|
||||
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();
|
||||
}
|
||||
|
||||
@ -174,7 +212,6 @@ export class TransactionsList implements OnInit {
|
||||
}
|
||||
|
||||
onOperatorFilterChange(operator: string) {
|
||||
this.filters.operator = operator;
|
||||
this.filters.page = 1;
|
||||
this.loadTransactions();
|
||||
}
|
||||
@ -186,6 +223,15 @@ export class TransactionsList implements OnInit {
|
||||
this.loadTransactions();
|
||||
}
|
||||
|
||||
// Permissions pour les filtres
|
||||
canUseMerchantFilter(): boolean {
|
||||
return this.access.canFilterByMerchant && this.access.allowedMerchantIds.length > 1;
|
||||
}
|
||||
|
||||
canUseAllFilters(): boolean {
|
||||
return this.access.canViewAllTransactions;
|
||||
}
|
||||
|
||||
// Tri
|
||||
sort(field: string) {
|
||||
if (this.sortField === field) {
|
||||
@ -210,21 +256,12 @@ export class TransactionsList implements OnInit {
|
||||
|
||||
// Actions
|
||||
viewTransactionDetails(transactionId: string) {
|
||||
// Vérifier les permissions avant d'afficher
|
||||
this.accessService.canAccessTransaction().subscribe(canAccess => {
|
||||
if (canAccess) {
|
||||
this.transactionSelected.emit(transactionId);
|
||||
}
|
||||
|
||||
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';
|
||||
} else {
|
||||
this.error = 'Vous n\'avez pas la permission de voir les détails de cette transaction';
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
});
|
||||
@ -253,37 +290,12 @@ export class TransactionsList implements OnInit {
|
||||
this.selectedTransactions.size === this.transactions.length;
|
||||
}
|
||||
|
||||
// Export
|
||||
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
|
||||
// Utilitaires d'affichage MIS À JOUR
|
||||
getStatusBadgeClass(status: TransactionStatus): string {
|
||||
switch (status) {
|
||||
case 'SUCCESS': return 'badge bg-success';
|
||||
case 'PENDING': return 'badge bg-warning';
|
||||
case 'FAILED': return 'badge bg-danger';
|
||||
case 'REFUNDED': return 'badge bg-info';
|
||||
case 'CANCELLED': return 'badge bg-secondary';
|
||||
case 'EXPIRED': return 'badge bg-dark';
|
||||
default: return 'badge bg-secondary';
|
||||
@ -295,35 +307,145 @@ export class TransactionsList implements OnInit {
|
||||
case 'SUCCESS': return 'lucideCheckCircle';
|
||||
case 'PENDING': return 'lucideClock';
|
||||
case 'FAILED': return 'lucideXCircle';
|
||||
case 'REFUNDED': return 'lucideUndo2';
|
||||
case 'CANCELLED': return 'lucideBan';
|
||||
default: return 'lucideClock';
|
||||
}
|
||||
}
|
||||
|
||||
formatCurrency(amount: number, currency: string = 'EUR'): string {
|
||||
return new Intl.NumberFormat('fr-FR', {
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}).format(amount);
|
||||
getTypeIcon(type: TransactionType): string {
|
||||
switch (type) {
|
||||
case TransactionType.SUBSCRIPTION_PAYMENT: return 'lucideCreditCard';
|
||||
case TransactionType.SUBSCRIPTION_RENEWAL: return 'lucideRepeat';
|
||||
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', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '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 {
|
||||
if (amount >= 10) return 'text-danger fw-bold';
|
||||
if (amount >= 5) return 'text-warning fw-semibold';
|
||||
if (amount >= 10000) return 'text-danger fw-bold';
|
||||
if (amount >= 5000) return 'text-warning fw-semibold';
|
||||
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
|
||||
getTotal(): number {
|
||||
return this.paginatedData?.stats?.total || 0;
|
||||
@ -341,15 +463,27 @@ export class TransactionsList implements OnInit {
|
||||
return this.paginatedData?.stats?.pendingCount || 0;
|
||||
}
|
||||
|
||||
getSuccessRate(): number {
|
||||
return this.paginatedData?.stats?.successRate || 0;
|
||||
}
|
||||
|
||||
getTotalAmount(): number {
|
||||
return this.paginatedData?.stats?.totalAmount || 0;
|
||||
}
|
||||
|
||||
getMinValue(a: number, b: number): number {
|
||||
return Math.min(a, b);
|
||||
// Méthodes pour le template
|
||||
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 { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable, map, catchError, throwError, of, tap } from 'rxjs';
|
||||
import { environment } from '@environments/environment';
|
||||
import { Observable, map, catchError, throwError } from 'rxjs';
|
||||
|
||||
import {
|
||||
Transaction,
|
||||
TransactionQuery,
|
||||
PaginatedTransactions,
|
||||
TransactionStats,
|
||||
RefundRequest
|
||||
} from '../models/transaction';
|
||||
TransactionType,
|
||||
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' })
|
||||
export class TransactionsService {
|
||||
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> {
|
||||
let params = new HttpParams();
|
||||
const cacheKey = this.generateCacheKey(query);
|
||||
|
||||
// Ajouter tous les paramètres de query
|
||||
Object.keys(query).forEach(key => {
|
||||
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());
|
||||
}
|
||||
}
|
||||
});
|
||||
const canUseCache = this.cache &&
|
||||
this.cache.key === cacheKey &&
|
||||
Date.now() - this.cache.timestamp < this.CACHE_TTL;
|
||||
|
||||
return this.http.get<PaginatedTransactions>(`${this.apiUrl}`, { params }).pipe(
|
||||
catchError(error => {
|
||||
console.error('Error loading transactions:', error);
|
||||
return throwError(() => error);
|
||||
if (query.merchantPartnerId) {
|
||||
if (canUseCache) {
|
||||
console.log('Using cached data for merchant:', query.merchantPartnerId);
|
||||
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> {
|
||||
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 => {
|
||||
console.error('Error loading transaction:', error);
|
||||
console.error(`Error loading transaction ${id}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// === ACTIONS ===
|
||||
refundTransaction(refundRequest: RefundRequest): Observable<{ message: string; transaction: Transaction }> {
|
||||
return this.http.post<{ message: string; transaction: Transaction }>(
|
||||
`${this.apiUrl}/${refundRequest.transactionId}/refund`,
|
||||
refundRequest
|
||||
);
|
||||
}
|
||||
getTransactionPayments(merchantId: number, subscriptionId: number, page: number = 1, limit: number = 10): Observable<PaginatedTransactions> {
|
||||
let params = new HttpParams()
|
||||
.set('page', page.toString())
|
||||
.set('limit', limit.toString());
|
||||
|
||||
cancelTransaction(transactionId: string): Observable<{ message: string }> {
|
||||
return this.http.post<{ message: string }>(
|
||||
`${this.apiUrl}/${transactionId}/cancel`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
return this.http.get<ApiResponse>(`${this.paymentsUrl}/merchant/${merchantId}/subscription/${subscriptionId}`, { params }).pipe(
|
||||
map(response => {
|
||||
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 }> {
|
||||
return this.http.post<{ message: string; transaction: Transaction }>(
|
||||
`${this.apiUrl}/${transactionId}/retry`,
|
||||
{}
|
||||
const transactions = data.map((payment: any) => this.mapPaymentToTransaction(payment));
|
||||
const stats = this.calculateStats(transactions);
|
||||
|
||||
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 ===
|
||||
|
||||
getTransactionStats(query?: Partial<TransactionQuery>): Observable<TransactionStats> {
|
||||
let params = new HttpParams();
|
||||
const transactionQuery: TransactionQuery = {
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
...query
|
||||
};
|
||||
|
||||
if (query) {
|
||||
Object.keys(query).forEach(key => {
|
||||
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
|
||||
return this.getTransactions(transactionQuery).pipe(
|
||||
map(response => response.stats)
|
||||
);
|
||||
}
|
||||
|
||||
// === MOCK DATA POUR LE DÉVELOPPEMENT ===
|
||||
getMockTransactions(): Transaction[] {
|
||||
return [
|
||||
{
|
||||
id: 'tx_001',
|
||||
msisdn: '+33612345678',
|
||||
operator: 'Orange',
|
||||
operatorId: 'orange_fr',
|
||||
country: 'FR',
|
||||
amount: 4.99,
|
||||
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'
|
||||
// === MÉTHODES PRIVÉES ===
|
||||
|
||||
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 [
|
||||
query.merchantPartnerId || 'all',
|
||||
query.status || 'all',
|
||||
query.periodicity || 'all',
|
||||
query.startDate?.toISOString().split('T')[0] || 'all',
|
||||
query.endDate?.toISOString().split('T')[0] || 'all',
|
||||
query.search || 'all'
|
||||
].join('|');
|
||||
}
|
||||
|
||||
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 { NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { PageTitle } from '@app/components/page-title/page-title';
|
||||
import { TransactionsList } from './list/list';
|
||||
import { TransactionDetails } from './details/details';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { TransactionAccessService } from './services/transaction-access.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transactions',
|
||||
@ -19,12 +20,27 @@ import { NgIcon } from '@ng-icons/core';
|
||||
],
|
||||
templateUrl: './transactions.html',
|
||||
})
|
||||
export class Transactions {
|
||||
export class Transactions implements OnInit {
|
||||
private modalService = inject(NgbModal);
|
||||
private accessService = inject(TransactionAccessService);
|
||||
|
||||
activeView: 'list' | 'details' = 'list';
|
||||
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() {
|
||||
this.activeView = 'list';
|
||||
this.selectedTransactionId = null;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user