feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
25d99b4edc
commit
ab49f56ea7
@ -22,7 +22,7 @@ export enum Currency {
|
|||||||
|
|
||||||
// === MODÈLE SUBSCRIPTION PRINCIPAL ===
|
// === MODÈLE SUBSCRIPTION PRINCIPAL ===
|
||||||
export interface Subscription {
|
export interface Subscription {
|
||||||
id: number;
|
id: string;
|
||||||
externalReference?: string | null;
|
externalReference?: string | null;
|
||||||
periodicity: SubscriptionPeriodicity;
|
periodicity: SubscriptionPeriodicity;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
|
|||||||
@ -152,14 +152,27 @@ export interface RoleOperationResponse {
|
|||||||
// === SEARCH ===
|
// === SEARCH ===
|
||||||
export interface SearchUsersParams {
|
export interface SearchUsersParams {
|
||||||
query?: string;
|
query?: string;
|
||||||
role?: UserRole;
|
role?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
userType?: UserType;
|
userType?: UserType;
|
||||||
merchantPartnerId?: string;
|
merchantPartnerId?: string;
|
||||||
|
searchTerm?: string;
|
||||||
|
status?: 'all' | 'enabled' | 'disabled';
|
||||||
|
emailVerified?: 'all' | 'verified' | 'not-verified';
|
||||||
|
sortField?: keyof User;
|
||||||
|
sortDirection?: 'asc' | 'desc';
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserStats {
|
||||||
|
total: number;
|
||||||
|
enabled: number;
|
||||||
|
disabled: number;
|
||||||
|
emailVerified: number;
|
||||||
|
roleCounts: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
// === UTILITAIRES ===
|
// === UTILITAIRES ===
|
||||||
export class UserUtils {
|
export class UserUtils {
|
||||||
static isHubUser(user: User): boolean {
|
static isHubUser(user: User): boolean {
|
||||||
|
|||||||
@ -111,6 +111,7 @@ export interface ApiMerchantUser {
|
|||||||
email?: string;
|
email?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
|
merchantPartnerId?: number;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
}
|
}
|
||||||
@ -129,6 +130,10 @@ export interface ApiMerchant {
|
|||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MerchantUserWithMerchant extends MerchantUser {
|
||||||
|
merchant: Merchant;
|
||||||
|
}
|
||||||
|
|
||||||
// === DTOs CRUD ===
|
// === DTOs CRUD ===
|
||||||
export interface CreateMerchantDto {
|
export interface CreateMerchantDto {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -1,18 +1,14 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject, EventEmitter } from '@angular/core';
|
||||||
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { environment } from '@environments/environment';
|
import { environment } from '@environments/environment';
|
||||||
import { BehaviorSubject, Observable, throwError, tap, catchError, finalize, of } from 'rxjs';
|
import { BehaviorSubject, Observable, throwError, tap, catchError, finalize, of, filter, take } from 'rxjs';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import { DashboardAccessService } from '@modules/dcb-dashboard/services/dashboard-access.service';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
UserType,
|
UserType,
|
||||||
UserRole,
|
UserRole,
|
||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
import { TransactionAccessService } from '@modules/transactions/services/transaction-access.service';
|
|
||||||
|
|
||||||
// === INTERFACES DTO AUTH ===
|
// === INTERFACES DTO AUTH ===
|
||||||
export interface LoginDto {
|
export interface LoginDto {
|
||||||
@ -66,18 +62,32 @@ export class AuthService {
|
|||||||
private userProfile$ = new BehaviorSubject<User | null>(null);
|
private userProfile$ = new BehaviorSubject<User | null>(null);
|
||||||
private initialized$ = new BehaviorSubject<boolean>(false);
|
private initialized$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
private readonly transactionAccessService = inject(TransactionAccessService);
|
// Observable pour les changements d'état d'authentification
|
||||||
|
private authStateChanged$ = new BehaviorSubject<boolean>(this.isAuthenticated());
|
||||||
|
|
||||||
|
private profileLoaded$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
// Événement émis lors de la déconnexion
|
||||||
|
private logoutEvent = new EventEmitter<void>();
|
||||||
|
|
||||||
|
// Observable pour les autres services
|
||||||
|
onLogout$ = this.logoutEvent.asObservable();
|
||||||
|
|
||||||
// === INITIALISATION DE L'APPLICATION ===
|
// === INITIALISATION DE L'APPLICATION ===
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialise l'authentification au démarrage de l'application
|
* Initialise l'authentification au démarrage de l'application
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise l'authentification et charge le profil
|
||||||
|
*/
|
||||||
async initialize(): Promise<boolean> {
|
async initialize(): Promise<boolean> {
|
||||||
const token = this.getAccessToken();
|
const token = this.getAccessToken();
|
||||||
|
|
||||||
// Pas de token → pas authentifié
|
// Pas de token → pas authentifié
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
this.profileLoaded$.next(true);
|
||||||
this.initialized$.next(true);
|
this.initialized$.next(true);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -85,6 +95,7 @@ export class AuthService {
|
|||||||
// Token expiré → tenter refresh
|
// Token expiré → tenter refresh
|
||||||
if (this.isTokenExpired(token)) {
|
if (this.isTokenExpired(token)) {
|
||||||
const ok = await this.tryRefreshToken();
|
const ok = await this.tryRefreshToken();
|
||||||
|
this.profileLoaded$.next(true);
|
||||||
this.initialized$.next(true);
|
this.initialized$.next(true);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
@ -93,15 +104,50 @@ export class AuthService {
|
|||||||
try {
|
try {
|
||||||
await firstValueFrom(this.loadUserProfile());
|
await firstValueFrom(this.loadUserProfile());
|
||||||
this.authState$.next(true);
|
this.authState$.next(true);
|
||||||
|
this.profileLoaded$.next(true);
|
||||||
this.initialized$.next(true);
|
this.initialized$.next(true);
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
this.clearAuthData();
|
this.clearAuthData();
|
||||||
|
this.profileLoaded$.next(true);
|
||||||
this.initialized$.next(true);
|
this.initialized$.next(true);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attendre que le profil soit chargé
|
||||||
|
*/
|
||||||
|
waitForProfile(): Observable<boolean> {
|
||||||
|
return this.profileLoaded$.pipe(
|
||||||
|
filter(loaded => loaded),
|
||||||
|
take(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier si le profil est chargé
|
||||||
|
*/
|
||||||
|
isProfileLoaded(): boolean {
|
||||||
|
return this.profileLoaded$.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcer le chargement du profil
|
||||||
|
*/
|
||||||
|
loadProfileIfNeeded(): Observable<User> {
|
||||||
|
if (this.userProfile$.value) {
|
||||||
|
return of(this.userProfile$.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.loadUserProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Émettre les changements d'état
|
||||||
|
getAuthStateChanged(): Observable<boolean> {
|
||||||
|
return this.authStateChanged$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tente de rafraîchir le token de manière synchrone
|
* Tente de rafraîchir le token de manière synchrone
|
||||||
*/
|
*/
|
||||||
@ -171,21 +217,16 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Déconnexion utilisateur
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Déconnexion utilisateur avec nettoyage complet
|
* Déconnexion utilisateur avec nettoyage complet
|
||||||
*/
|
*/
|
||||||
logout(): Observable<LogoutResponseDto> {
|
logout(): Observable<LogoutResponseDto> {
|
||||||
const token = this.getAccessToken();
|
const token = this.getAccessToken();
|
||||||
|
|
||||||
// Si pas de token, nettoyer et retourner un observable complet
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
this.clearAuthData();
|
this.performLogoutCleanup(); // Nettoyage local
|
||||||
return of({ message: 'Already logged out' });
|
return of({ message: 'Already logged out' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajouter le token dans le header si nécessaire
|
|
||||||
const headers = new HttpHeaders({
|
const headers = new HttpHeaders({
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
});
|
});
|
||||||
@ -196,48 +237,44 @@ export class AuthService {
|
|||||||
{ headers }
|
{ headers }
|
||||||
).pipe(
|
).pipe(
|
||||||
tap(() => {
|
tap(() => {
|
||||||
this.clearAuthData();
|
this.performLogoutCleanup();
|
||||||
|
|
||||||
this.transactionAccessService.clearCache();
|
|
||||||
this.clearAllStorage(); // Nettoyer tout le storage
|
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
// Même en cas d'erreur, nettoyer tout
|
this.performLogoutCleanup();
|
||||||
this.clearAuthData();
|
|
||||||
|
|
||||||
this.transactionAccessService.clearCache();
|
|
||||||
this.clearAllStorage();
|
|
||||||
console.warn('Logout API error, but local data cleared:', error);
|
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' });
|
return of({ message: 'Local session cleared' });
|
||||||
}),
|
}),
|
||||||
finalize(() => {
|
finalize(() => {
|
||||||
// Garantir le nettoyage dans tous les cas
|
this.performLogoutCleanup();
|
||||||
this.clearAuthData();
|
|
||||||
|
|
||||||
this.transactionAccessService.clearCache();
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private performLogoutCleanup(): void {
|
||||||
* Déconnexion forcée sans appel API
|
// 1. Nettoyer les données d'authentification
|
||||||
*/
|
localStorage.removeItem(this.tokenKey);
|
||||||
forceLogout(): void {
|
localStorage.removeItem(this.refreshTokenKey);
|
||||||
this.clearAuthData();
|
localStorage.removeItem('user_profile');
|
||||||
|
sessionStorage.clear();
|
||||||
|
|
||||||
this.transactionAccessService.clearCache();
|
// 2. Réinitialiser les BehaviorSubjects
|
||||||
this.clearAllStorage();
|
this.authState$.next(false);
|
||||||
|
this.userProfile$.next(null);
|
||||||
|
|
||||||
|
// 3. Émettre l'événement de déconnexion
|
||||||
|
this.logoutEvent.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
forceLogout(): void {
|
||||||
|
this.performLogoutCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nettoyer toutes les données d'authentification
|
* Nettoyer toutes les données d'authentification
|
||||||
*/
|
*/
|
||||||
private clearAuthData(): void {
|
private clearAuthData(): void {
|
||||||
|
|
||||||
|
|
||||||
this.transactionAccessService.clearCache();
|
|
||||||
|
|
||||||
// Supprimer tous les tokens et données utilisateur
|
// Supprimer tous les tokens et données utilisateur
|
||||||
localStorage.removeItem(this.tokenKey);
|
localStorage.removeItem(this.tokenKey);
|
||||||
localStorage.removeItem(this.refreshTokenKey);
|
localStorage.removeItem(this.refreshTokenKey);
|
||||||
@ -250,6 +287,7 @@ export class AuthService {
|
|||||||
// Réinitialiser les BehaviorSubjects
|
// Réinitialiser les BehaviorSubjects
|
||||||
this.authState$.next(false);
|
this.authState$.next(false);
|
||||||
this.userProfile$.next(null);
|
this.userProfile$.next(null);
|
||||||
|
this.authStateChanged$.next(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -307,18 +345,28 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
private determineUserType(apiUser: any): UserType {
|
private determineUserType(apiUser: any): UserType {
|
||||||
|
|
||||||
const hubRoles = [UserRole.DCB_ADMIN || UserRole.DCB_SUPPORT];
|
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
|
||||||
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN || UserRole.DCB_PARTNER_MANAGER || UserRole.DCB_PARTNER_SUPPORT];
|
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
|
||||||
|
|
||||||
// Logique pour déterminer le type d'utilisateur
|
// Récupérer le rôle depuis l'API
|
||||||
if (apiUser.clientRoles?.[0].includes(merchantRoles)) {
|
const clientRole = apiUser.clientRoles?.[0];
|
||||||
return UserType.MERCHANT_PARTNER;
|
|
||||||
} else if (apiUser.clientRoles?.[0].includes(hubRoles)) {
|
if (!clientRole) {
|
||||||
return UserType.HUB;
|
console.warn('Aucun rôle trouvé dans le profil');
|
||||||
} else {
|
throw new Error(`Type d'utilisateur non reconnu: ${clientRole}`);
|
||||||
console.warn('Type d\'utilisateur non reconnu, rôle:', apiUser.clientRoles?.[0]);
|
|
||||||
return UserType.HUB; // Fallback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérifier si c'est un rôle marchand
|
||||||
|
if (merchantRoles.some(role => clientRole.includes(role))) {
|
||||||
|
return UserType.MERCHANT_PARTNER;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si c'est un rôle hub
|
||||||
|
if (hubRoles.some(role => clientRole.includes(role))) {
|
||||||
|
return UserType.HUB;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Type d'utilisateur non reconnu: ${clientRole}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapToUserModel(apiUser: any, userType: UserType): User {
|
private mapToUserModel(apiUser: any, userType: UserType): User {
|
||||||
@ -374,6 +422,9 @@ export class AuthService {
|
|||||||
return this.userProfile$.asObservable();
|
return this.userProfile$.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer le profil utilisateur (synchrone, après chargement)
|
||||||
|
*/
|
||||||
getCurrentUserProfile(): User | null {
|
getCurrentUserProfile(): User | null {
|
||||||
return this.userProfile$.value;
|
return this.userProfile$.value;
|
||||||
}
|
}
|
||||||
@ -557,6 +608,8 @@ export class AuthService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Merchant Partner ID : " + profile.merchantPartnerId)
|
||||||
|
|
||||||
const merchantId = profile.merchantPartnerId
|
const merchantId = profile.merchantPartnerId
|
||||||
|
|
||||||
if (merchantId === null || merchantId === undefined) {
|
if (merchantId === null || merchantId === undefined) {
|
||||||
|
|||||||
@ -20,6 +20,88 @@ export enum UserRole {
|
|||||||
|
|
||||||
type RoleCategory = 'hub' | 'partner' | 'config';
|
type RoleCategory = 'hub' | 'partner' | 'config';
|
||||||
|
|
||||||
|
interface RoleConfig {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
badgeClass: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
},
|
||||||
|
[UserRole.DCB_SUPPORT]: {
|
||||||
|
label: 'Support DCB',
|
||||||
|
description: 'Support technique avec accès étendus',
|
||||||
|
badgeClass: 'bg-info',
|
||||||
|
icon: 'lucideHeadphones'
|
||||||
|
},
|
||||||
|
[UserRole.DCB_PARTNER_ADMIN]: {
|
||||||
|
label: 'Admin Partenaire',
|
||||||
|
description: 'Administrateur de partenaire marchand',
|
||||||
|
badgeClass: 'bg-warning',
|
||||||
|
icon: 'lucideShieldCheck'
|
||||||
|
},
|
||||||
|
[UserRole.DCB_PARTNER_MANAGER]: {
|
||||||
|
label: 'Manager Partenaire',
|
||||||
|
description: 'Manager opérationnel partenaire',
|
||||||
|
badgeClass: 'bg-success',
|
||||||
|
icon: 'lucideUserCog'
|
||||||
|
},
|
||||||
|
[UserRole.DCB_PARTNER_SUPPORT]: {
|
||||||
|
label: 'Support Partenaire',
|
||||||
|
description: 'Support technique partenaire',
|
||||||
|
badgeClass: 'bg-secondary',
|
||||||
|
icon: 'lucideHeadphones'
|
||||||
|
},
|
||||||
|
[UserRole.MERCHANT_CONFIG_ADMIN]: {
|
||||||
|
label: 'Admin Marchand',
|
||||||
|
description: 'Administrateur de configuration marchand',
|
||||||
|
badgeClass: 'bg-warning',
|
||||||
|
icon: 'lucideSettings'
|
||||||
|
},
|
||||||
|
[UserRole.MERCHANT_CONFIG_MANAGER]: {
|
||||||
|
label: 'Manager Marchand',
|
||||||
|
description: 'Manager de configuration marchand',
|
||||||
|
badgeClass: 'bg-success',
|
||||||
|
icon: 'lucideUserCog'
|
||||||
|
},
|
||||||
|
[UserRole.MERCHANT_CONFIG_TECHNICAL]: {
|
||||||
|
label: 'Technique Marchand',
|
||||||
|
description: 'Support technique configuration marchand',
|
||||||
|
badgeClass: 'bg-secondary',
|
||||||
|
icon: 'lucideWrench'
|
||||||
|
},
|
||||||
|
[UserRole.MERCHANT_CONFIG_VIEWER]: {
|
||||||
|
label: 'Visualiseur Marchand',
|
||||||
|
description: 'Visualiseur de configuration marchand',
|
||||||
|
badgeClass: 'bg-light',
|
||||||
|
icon: 'lucideEye'
|
||||||
|
}
|
||||||
|
} 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 {
|
export class RoleManagementService {
|
||||||
private currentRole: UserRole | null = null;
|
private currentRole: UserRole | null = null;
|
||||||
@ -198,14 +280,27 @@ export class RoleManagementService {
|
|||||||
|
|
||||||
// === UTILITAIRES ===
|
// === UTILITAIRES ===
|
||||||
|
|
||||||
getRoleLabel(role?: UserRole): string {
|
/**
|
||||||
const targetRole = role || this.currentRole;
|
* Méthodes d'utilité pour les rôles
|
||||||
return targetRole ? this.roleLabels[targetRole] || targetRole : '';
|
*/
|
||||||
|
getRoleLabel(role: string): string {
|
||||||
|
const userRole = role as UserRole;
|
||||||
|
return ROLE_CONFIG[userRole]?.label || role;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleIcon(role?: UserRole): string {
|
getRoleDescription(role: string | UserRole): string {
|
||||||
const targetRole = role || this.currentRole;
|
const userRole = role as UserRole;
|
||||||
return targetRole ? this.roleIcons[targetRole] || 'user' : 'user';
|
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllRoles(): UserRole[] {
|
getAllRoles(): UserRole[] {
|
||||||
|
|||||||
@ -77,33 +77,9 @@ export class MenuService {
|
|||||||
icon: 'lucideCreditCard',
|
icon: 'lucideCreditCard',
|
||||||
url: '/transactions',
|
url: '/transactions',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Opérateurs',
|
|
||||||
icon: 'lucideServer',
|
|
||||||
isCollapsed: true,
|
|
||||||
children: [
|
|
||||||
{ label: 'Paramètres d\'Intégration', url: '/operators/config' },
|
|
||||||
{ label: 'Performance & Monitoring', url: '/operators/stats' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
label: 'Webhooks',
|
|
||||||
icon: 'lucideShare',
|
|
||||||
isCollapsed: true,
|
|
||||||
children: [
|
|
||||||
{ label: 'Historique', url: '/webhooks/history' },
|
|
||||||
{ label: 'Statut des Requêtes', url: '/webhooks/status' },
|
|
||||||
{ label: 'Relancer Webhook', url: '/webhooks/retry' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
{ label: 'Abonnements', isTitle: true },
|
{ label: 'Abonnements', isTitle: true },
|
||||||
{ label: 'Gestion des Abonnements', icon: 'lucideRepeat', url: '/subscriptions' },
|
{ label: 'Gestion des Abonnements', icon: 'lucideRepeat', url: '/subscriptions' },
|
||||||
{ label: 'Abonnements par Merchant', icon: 'lucideStore', url: '/subscriptions/merchant' },
|
|
||||||
|
|
||||||
{ label: 'Paiements', isTitle: true },
|
|
||||||
{ label: 'Historique des Paiements', icon: 'lucideCreditCard', url: '/subscriptions/payments' },
|
|
||||||
|
|
||||||
{ label: 'Utilisateurs & Sécurité', isTitle: true },
|
{ label: 'Utilisateurs & Sécurité', isTitle: true },
|
||||||
{
|
{
|
||||||
@ -119,8 +95,6 @@ export class MenuService {
|
|||||||
|
|
||||||
{ label: 'Configurations', isTitle: true },
|
{ label: 'Configurations', isTitle: true },
|
||||||
{ label: 'Merchant Config', icon: 'lucideStore', url: '/merchant-config' },
|
{ label: 'Merchant Config', icon: 'lucideStore', url: '/merchant-config' },
|
||||||
{ label: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' },
|
|
||||||
{ label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' },
|
|
||||||
|
|
||||||
{ label: 'Support & Profil', isTitle: true },
|
{ label: 'Support & Profil', isTitle: true },
|
||||||
{ label: 'Support', icon: 'lucideLifeBuoy', url: '/support' },
|
{ label: 'Support', icon: 'lucideLifeBuoy', url: '/support' },
|
||||||
|
|||||||
@ -33,25 +33,7 @@ export class PermissionsService {
|
|||||||
module: 'merchant-users-management',
|
module: 'merchant-users-management',
|
||||||
roles: this.allRoles,
|
roles: this.allRoles,
|
||||||
},
|
},
|
||||||
// Operators - Admin seulement
|
|
||||||
{
|
|
||||||
module: 'operators',
|
|
||||||
roles: [UserRole.DCB_ADMIN],
|
|
||||||
children: {
|
|
||||||
'config': [UserRole.DCB_ADMIN],
|
|
||||||
'stats': [UserRole.DCB_ADMIN]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Webhooks - Admin et Partner
|
|
||||||
{
|
|
||||||
module: 'webhooks',
|
|
||||||
roles: [UserRole.DCB_ADMIN, UserRole.DCB_PARTNER_ADMIN],
|
|
||||||
children: {
|
|
||||||
'history': [UserRole.DCB_ADMIN, UserRole.DCB_PARTNER_ADMIN],
|
|
||||||
'status': [UserRole.DCB_ADMIN, UserRole.DCB_PARTNER_ADMIN],
|
|
||||||
'retry': [UserRole.DCB_ADMIN]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Settings - Tout le monde
|
// Settings - Tout le monde
|
||||||
{
|
{
|
||||||
module: 'settings',
|
module: 'settings',
|
||||||
@ -63,16 +45,6 @@ export class PermissionsService {
|
|||||||
module: 'subscriptions',
|
module: 'subscriptions',
|
||||||
roles: this.allRoles
|
roles: this.allRoles
|
||||||
},
|
},
|
||||||
{
|
|
||||||
module: 'subscriptions-merchant',
|
|
||||||
roles: this.allRoles
|
|
||||||
},
|
|
||||||
|
|
||||||
// Payments
|
|
||||||
{
|
|
||||||
module: 'subscriptions-payments',
|
|
||||||
roles: this.allRoles
|
|
||||||
},
|
|
||||||
|
|
||||||
// Settings - Tout le monde
|
// Settings - Tout le monde
|
||||||
{
|
{
|
||||||
@ -80,11 +52,6 @@ export class PermissionsService {
|
|||||||
roles: this.allRoles
|
roles: this.allRoles
|
||||||
},
|
},
|
||||||
|
|
||||||
// Integrations - Admin seulement
|
|
||||||
{
|
|
||||||
module: 'integrations',
|
|
||||||
roles: [UserRole.DCB_ADMIN]
|
|
||||||
},
|
|
||||||
// Modules publics - Tout le monde
|
// Modules publics - Tout le monde
|
||||||
{
|
{
|
||||||
module: 'support',
|
module: 'support',
|
||||||
|
|||||||
@ -59,49 +59,11 @@ export const menuItems: MenuItemType[] = [
|
|||||||
icon: 'lucideCreditCard',
|
icon: 'lucideCreditCard',
|
||||||
url: '/transactions',
|
url: '/transactions',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Opérateurs',
|
|
||||||
icon: 'lucideServer',
|
|
||||||
isCollapsed: true,
|
|
||||||
children: [
|
|
||||||
{ label: 'Paramètres d’Intégration', url: '/operators/config' },
|
|
||||||
{ label: 'Performance & Monitoring', url: '/operators/stats' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---------------------------
|
|
||||||
// Notifications & Communication
|
|
||||||
// ---------------------------
|
|
||||||
{ label: 'Communication', isTitle: true },
|
|
||||||
{
|
|
||||||
label: 'Notifications',
|
|
||||||
icon: 'lucideBell',
|
|
||||||
isCollapsed: true,
|
|
||||||
children: [
|
|
||||||
{ label: 'Liste des Notifications', url: '/notifications/list' },
|
|
||||||
{ label: 'Filtrage par Type', url: '/notifications/filters' },
|
|
||||||
{ label: 'Actions Automatiques', url: '/notifications/actions' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Webhooks',
|
|
||||||
icon: 'lucideShare',
|
|
||||||
isCollapsed: true,
|
|
||||||
children: [
|
|
||||||
{ label: 'Historique', url: '/webhooks/history' },
|
|
||||||
{ label: 'Statut des Requêtes', url: '/webhooks/status' },
|
|
||||||
{ label: 'Relancer Webhook', url: '/webhooks/retry' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
{ label: 'Abonnements', isTitle: true },
|
{ label: 'Abonnements', isTitle: true },
|
||||||
{ label: 'Gestion des Abonnements', icon: 'lucideRepeat', url: '/subscriptions' },
|
{ label: 'Gestion des Abonnements', icon: 'lucideRepeat', url: '/subscriptions' },
|
||||||
{ label: 'Abonnements par Merchant', icon: 'lucideStore', url: '/subscriptions/merchant' },
|
{ label: 'Abonnements par Merchant', icon: 'lucideStore', url: '/subscriptions/merchant' },
|
||||||
|
|
||||||
{ label: 'Paiements', isTitle: true },
|
|
||||||
{ label: 'Historique des Paiements', icon: 'lucideCreditCard', url: '/subscriptions/payments' },
|
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Utilisateurs & Sécurité
|
// Utilisateurs & Sécurité
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@ -128,7 +90,6 @@ export const menuItems: MenuItemType[] = [
|
|||||||
{ label: 'Configurations', isTitle: true },
|
{ label: 'Configurations', isTitle: true },
|
||||||
{ label: 'Merchant Config', icon: 'lucideStore', url: '/merchant-config' },
|
{ label: 'Merchant Config', icon: 'lucideStore', url: '/merchant-config' },
|
||||||
{ label: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' },
|
{ label: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' },
|
||||||
{ label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' },
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Support & Profil
|
// Support & Profil
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -39,6 +39,7 @@ import { ReportService } from './services/dcb-reporting.service';
|
|||||||
import { DashboardAccess, AllowedMerchant, DashboardAccessService } from './services/dashboard-access.service';
|
import { DashboardAccess, AllowedMerchant, DashboardAccessService } from './services/dashboard-access.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
// ============ TYPES ET INTERFACES ============
|
// ============ TYPES ET INTERFACES ============
|
||||||
|
|
||||||
@ -138,7 +139,9 @@ interface SubscriptionStats {
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
NgIconComponent,
|
NgIconComponent,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
PageTitle],
|
PageTitle,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
provideIcons({
|
provideIcons({
|
||||||
lucideActivity, lucideAlertCircle, lucideCheckCircle2, lucideRefreshCw,
|
lucideActivity, lucideAlertCircle, lucideCheckCircle2, lucideRefreshCw,
|
||||||
@ -309,103 +312,100 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
Chart.register(...registerables);
|
Chart.register(...registerables);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
dashboardInitialized = false;
|
||||||
// 1. Initialiser l'accès
|
|
||||||
this.initializeAccess();
|
|
||||||
|
|
||||||
// 2. Initialiser le dashboard (avec délai pour laisser le temps à l'accès)
|
|
||||||
setTimeout(() => {
|
|
||||||
this.initializeDashboard();
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
// 3. Charger les merchants (avec délai)
|
|
||||||
setTimeout(() => {
|
|
||||||
this.loadAllowedMerchants();
|
|
||||||
}, 150);
|
|
||||||
|
|
||||||
if (this.accessService.shouldShowSystemHealth()) {
|
|
||||||
setInterval(() => {
|
|
||||||
this.checkSystemHealth();
|
|
||||||
}, 5 * 60 * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ============ INITIALISATION ============
|
// ============ INITIALISATION ============
|
||||||
|
|
||||||
private initializeAccess(): void {
|
ngOnInit(): void {
|
||||||
// Attendre que l'accès soit prêt
|
console.log('🔍 Dashboard: ngOnInit() appelé');
|
||||||
|
|
||||||
|
// Attendre que le DashboardAccessService soit VRAIMENT prêt
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
this.accessService.waitForAccess().subscribe(() => {
|
this.accessService.waitForReady().subscribe({
|
||||||
this.access = this.accessService.getDashboardAccess();
|
next: () => {
|
||||||
this.currentRoleLabel = this.access.roleLabel;
|
console.log('✅ Dashboard: waitForReady() a émis - Initialisation...');
|
||||||
this.currentRoleIcon = this.access.roleIcon;
|
this.dashboardInitialized = true;
|
||||||
|
this.initializeDashboard();
|
||||||
console.log('✅ Dashboard initialisé avec:', {
|
},
|
||||||
access: this.access,
|
error: (err) => {
|
||||||
merchantId: this.access.merchantId,
|
console.error('❌ Dashboard: Erreur dans waitForReady():', err);
|
||||||
isHubUser: this.access.isHubUser,
|
// Gérer l'erreur - peut-être rediriger vers une page d'erreur
|
||||||
});
|
this.addAlert('danger', 'Erreur d\'initialisation',
|
||||||
|
'Impossible de charger les informations d\'accès', 'Maintenant');
|
||||||
// Pour les merchant users
|
|
||||||
if (this.access.isMerchantUser) {
|
|
||||||
const merchantId = this.access.merchantId;
|
|
||||||
|
|
||||||
if (merchantId && merchantId > 0) {
|
|
||||||
this.merchantId = merchantId;
|
|
||||||
this.accessService.setSelectedMerchantId(merchantId);
|
|
||||||
this.isViewingGlobalData = false;
|
|
||||||
this.dataSelection.merchantPartnerId = merchantId;
|
|
||||||
|
|
||||||
console.log(`✅ Merchant User: ID = ${this.merchantId}`);
|
|
||||||
} else {
|
|
||||||
console.error('❌ Merchant ID invalide pour Merchant User:', merchantId);
|
|
||||||
// Essayer de récupérer directement depuis le profil
|
|
||||||
const merchantPartnerId = this.authService.getCurrentMerchantPartnerId();
|
|
||||||
if (merchantPartnerId) {
|
|
||||||
const id = Number(merchantPartnerId);
|
|
||||||
if (!isNaN(id) && id > 0) {
|
|
||||||
this.merchantId = id;
|
|
||||||
this.accessService.setSelectedMerchantId(id);
|
|
||||||
this.isViewingGlobalData = false;
|
|
||||||
this.dataSelection.merchantPartnerId = id;
|
|
||||||
console.log(`✅ Merchant ID récupéré depuis profil: ${id}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.addAlert('danger', 'Erreur de configuration',
|
|
||||||
'Impossible de déterminer le merchant ID', 'Maintenant');
|
|
||||||
this.isViewingGlobalData = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Pour les hub users
|
|
||||||
else if (this.access.isHubUser) {
|
|
||||||
const selectedMerchantId = this.accessService.getSelectedMerchantId();
|
|
||||||
|
|
||||||
if (selectedMerchantId && selectedMerchantId > 0) {
|
|
||||||
this.merchantId = selectedMerchantId;
|
|
||||||
this.isViewingGlobalData = false;
|
|
||||||
this.dataSelection.merchantPartnerId = selectedMerchantId;
|
|
||||||
console.log(`✅ Hub User: Merchant sélectionné = ${this.merchantId}`);
|
|
||||||
} else {
|
|
||||||
this.isViewingGlobalData = true;
|
|
||||||
this.merchantId = undefined;
|
|
||||||
this.dataSelection.merchantPartnerId = undefined;
|
|
||||||
console.log('✅ Hub User: Mode global (aucun merchant sélectionné)');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initializeDashboard(): void {
|
||||||
|
console.log('🚀 Dashboard: initializeDashboard() appelé');
|
||||||
|
|
||||||
isValidMerchantId(id: any): boolean {
|
try {
|
||||||
if (id === null || id === undefined) {
|
// 1. Récupérer l'accès
|
||||||
return false;
|
this.access = this.accessService.getDashboardAccess();
|
||||||
|
|
||||||
|
console.log('✅ Dashboard: Accès déterminé avec succès:', {
|
||||||
|
isHubUser: this.access.isHubUser,
|
||||||
|
merchantId: this.access.merchantId,
|
||||||
|
role: this.access.userRole,
|
||||||
|
profile: this.authService.getCurrentUserProfile()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Configurer les paramètres
|
||||||
|
if (this.access.isMerchantUser) {
|
||||||
|
if (this.access.merchantId) {
|
||||||
|
this.merchantId = this.access.merchantId;
|
||||||
|
this.isViewingGlobalData = false;
|
||||||
|
this.dataSelection.merchantPartnerId = this.merchantId;
|
||||||
|
console.log(`✅ Dashboard: Merchant User avec ID: ${this.merchantId}`);
|
||||||
|
} else {
|
||||||
|
console.error('❌ Dashboard: Merchant User sans merchantId!');
|
||||||
|
this.addAlert('danger', 'Erreur de configuration',
|
||||||
|
'Impossible de déterminer le merchant ID', 'Maintenant');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (this.access.isHubUser) {
|
||||||
|
const selectedMerchantId = this.accessService.getSelectedMerchantId();
|
||||||
|
|
||||||
|
if (selectedMerchantId && selectedMerchantId > 0) {
|
||||||
|
this.merchantId = selectedMerchantId;
|
||||||
|
this.isViewingGlobalData = false;
|
||||||
|
this.dataSelection.merchantPartnerId = selectedMerchantId;
|
||||||
|
console.log(`✅ Dashboard: Hub User avec merchant sélectionné: ${this.merchantId}`);
|
||||||
|
} else {
|
||||||
|
this.isViewingGlobalData = true;
|
||||||
|
this.merchantId = undefined;
|
||||||
|
this.dataSelection.merchantPartnerId = undefined;
|
||||||
|
console.log('✅ Dashboard: Hub User en mode global');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Charger les merchants
|
||||||
|
this.loadAllowedMerchants();
|
||||||
|
|
||||||
|
// 4. Charger les données
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadData();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 5. Vérifier la santé du système
|
||||||
|
if (this.accessService.shouldShowSystemHealth()) {
|
||||||
|
this.checkSystemHealth();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Dashboard: Erreur lors de l\'initialisation:', error);
|
||||||
|
this.addAlert('danger', 'Erreur d\'initialisation',
|
||||||
|
'Impossible de configurer le dashboard', 'Maintenant');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const numId = Number(id);
|
private loadData(): void {
|
||||||
return !isNaN(numId) && Number.isInteger(numId) && numId > 0;
|
if (this.access.isHubUser && this.isViewingGlobalData) {
|
||||||
|
this.loadGlobalData();
|
||||||
|
} else {
|
||||||
|
this.loadMerchantData(this.merchantId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadAllowedMerchants(): void {
|
private loadAllowedMerchants(): void {
|
||||||
@ -423,30 +423,6 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeDashboard(): void {
|
|
||||||
// Vérifier d'abord si le profil est chargé
|
|
||||||
const profile = this.authService.getProfile();
|
|
||||||
|
|
||||||
if (!profile) {
|
|
||||||
console.log('⏳ Profil non chargé, attente...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.access.isHubUser) {
|
|
||||||
if (this.isViewingGlobalData) {
|
|
||||||
this.loadGlobalData();
|
|
||||||
} else {
|
|
||||||
this.loadMerchantData(this.merchantId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.loadMerchantData(this.merchantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.accessService.shouldShowSystemHealth()) {
|
|
||||||
this.checkSystemHealth();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ CHARGEMENT DES DONNÉES ============
|
// ============ CHARGEMENT DES DONNÉES ============
|
||||||
private loadGlobalData(): void {
|
private loadGlobalData(): void {
|
||||||
if (!this.access.isHubUser) return;
|
if (!this.access.isHubUser) return;
|
||||||
@ -485,25 +461,11 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
private loadMerchantData(merchantId: number | undefined): void {
|
private loadMerchantData(merchantId: number | undefined): void {
|
||||||
console.log('Chargement des données pour merchant:', merchantId);
|
console.log('Chargement des données pour merchant:', merchantId);
|
||||||
|
|
||||||
// Vérification plus robuste
|
|
||||||
if (!merchantId || merchantId <= 0 || isNaN(merchantId)) {
|
if (!merchantId || merchantId <= 0 || isNaN(merchantId)) {
|
||||||
console.error('Merchant ID invalide ou manquant:', merchantId);
|
console.error('Merchant ID invalide ou manquant:', merchantId);
|
||||||
this.addAlert('warning', 'Merchant non spécifié',
|
this.addAlert('warning', 'Merchant non spécifié',
|
||||||
'Veuillez sélectionner un merchant valide', 'Maintenant');
|
'Veuillez sélectionner un merchant valide', 'Maintenant');
|
||||||
|
return;
|
||||||
// Pour les merchant users, essayer de récupérer l'ID depuis le cache
|
|
||||||
if (this.access.isMerchantUser) {
|
|
||||||
const cachedId = this.accessService.getSelectedMerchantId();
|
|
||||||
if (cachedId && cachedId > 0) {
|
|
||||||
merchantId = cachedId;
|
|
||||||
this.merchantId = cachedId;
|
|
||||||
console.log(`Utilisation du merchant ID du cache: ${merchantId}`);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading.merchantData = true;
|
this.loading.merchantData = true;
|
||||||
@ -1507,6 +1469,15 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============ MÉTHODES SPÉCIFIQUES AU CONTEXTE ============
|
// ============ MÉTHODES SPÉCIFIQUES AU CONTEXTE ============
|
||||||
|
isValidMerchantId(id: any): boolean {
|
||||||
|
if (id === null || id === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numId = Number(id);
|
||||||
|
return !isNaN(numId) && Number.isInteger(numId) && numId > 0;
|
||||||
|
}
|
||||||
|
|
||||||
isViewingGlobal(): boolean {
|
isViewingGlobal(): boolean {
|
||||||
return this.isViewingGlobalData;
|
return this.isViewingGlobalData;
|
||||||
}
|
}
|
||||||
@ -1525,7 +1496,6 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getCurrentMerchantPartnerId(): string | null {
|
private getCurrentMerchantPartnerId(): string | null {
|
||||||
// Utiliser une valeur par défaut sécurisée
|
|
||||||
return this.access?.merchantId?.toString() || null;
|
return this.access?.merchantId?.toString() || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable, of, BehaviorSubject } from 'rxjs';
|
import { Observable, of, BehaviorSubject } from 'rxjs';
|
||||||
import { map, catchError, switchMap, take, filter, first } from 'rxjs/operators';
|
import { map, catchError, switchMap, take, filter, tap } from 'rxjs/operators';
|
||||||
import { UserRole, RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { UserRole, RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service';
|
import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
@ -8,8 +8,6 @@ import { AuthService } from '@core/services/auth.service';
|
|||||||
export interface DashboardAccess {
|
export interface DashboardAccess {
|
||||||
isHubUser: boolean;
|
isHubUser: boolean;
|
||||||
isMerchantUser: boolean;
|
isMerchantUser: boolean;
|
||||||
roleLabel: string;
|
|
||||||
roleIcon: string;
|
|
||||||
userRole: UserRole;
|
userRole: UserRole;
|
||||||
merchantId?: number;
|
merchantId?: number;
|
||||||
}
|
}
|
||||||
@ -24,130 +22,124 @@ export class DashboardAccessService {
|
|||||||
private accessCache: DashboardAccess | null = null;
|
private accessCache: DashboardAccess | null = null;
|
||||||
private merchantsCache: AllowedMerchant[] | null = null;
|
private merchantsCache: AllowedMerchant[] | null = null;
|
||||||
private currentMerchantId: number | null = null;
|
private currentMerchantId: number | null = null;
|
||||||
private accessReady$ = new BehaviorSubject<boolean>(false);
|
private ready$ = new BehaviorSubject<boolean>(false);
|
||||||
|
private profileLoaded = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private roleService: RoleManagementService,
|
private roleService: RoleManagementService,
|
||||||
private merchantService: MerchantConfigService,
|
private merchantService: MerchantConfigService,
|
||||||
private authService: AuthService
|
private authService: AuthService
|
||||||
) {
|
) {
|
||||||
// S'abonner aux changements du profil utilisateur
|
// Initialisation simple
|
||||||
this.initializeProfileSubscription();
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private initialize(): void {
|
||||||
* Initialise la surveillance du profil utilisateur
|
console.log('🚀 DashboardAccessService: Initialisation');
|
||||||
*/
|
|
||||||
private initializeProfileSubscription(): void {
|
|
||||||
this.authService.getUserProfile().pipe(
|
|
||||||
filter(profile => profile !== null),
|
|
||||||
first()
|
|
||||||
).subscribe(profile => {
|
|
||||||
console.log('📊 DashboardAccessService: Profil utilisateur chargé', {
|
|
||||||
username: profile.username,
|
|
||||||
merchantPartnerId: profile.merchantPartnerId,
|
|
||||||
userType: profile.userType
|
|
||||||
});
|
|
||||||
|
|
||||||
this.clearCache();
|
// S'abonner aux changements de profil
|
||||||
this.accessReady$.next(true);
|
this.authService.getUserProfile().subscribe({
|
||||||
|
next: (profile) => {
|
||||||
|
if (profile) {
|
||||||
|
console.log('✅ DashboardAccessService: Profil chargé', {
|
||||||
|
username: profile.username,
|
||||||
|
merchantPartnerId: profile.merchantPartnerId,
|
||||||
|
userType: profile.userType
|
||||||
|
});
|
||||||
|
this.profileLoaded = true;
|
||||||
|
this.ready$.next(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('❌ DashboardAccessService: Erreur de profil:', err);
|
||||||
|
this.profileLoaded = false;
|
||||||
|
this.ready$.next(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nettoyer à la déconnexion
|
||||||
|
this.authService.getAuthState().subscribe(isAuthenticated => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
console.log('🚨 DashboardAccessService: Déconnexion détectée');
|
||||||
|
this.clearCache();
|
||||||
|
this.profileLoaded = false;
|
||||||
|
this.ready$.next(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Attendre que le service soit prêt AVEC PROFIL CHARGÉ
|
||||||
* Attend que l'accès soit prêt
|
waitForReady(): Observable<boolean> {
|
||||||
*/
|
return this.ready$.pipe(
|
||||||
waitForAccess(): Observable<boolean> {
|
filter(ready => ready && this.profileLoaded),
|
||||||
return this.accessReady$.pipe(
|
take(1),
|
||||||
filter(ready => ready),
|
tap(() => {
|
||||||
take(1)
|
console.log('✅ DashboardAccessService: waitForReady() - Service vraiment prêt');
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Obtenir l'accès dashboard
|
||||||
* Obtient l'accès dashboard (version synchrone)
|
|
||||||
*/
|
|
||||||
getDashboardAccess(): DashboardAccess {
|
getDashboardAccess(): DashboardAccess {
|
||||||
if (this.accessCache) {
|
if (this.accessCache) {
|
||||||
return this.accessCache;
|
return this.accessCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VÉRIFIER que le profil est chargé
|
||||||
|
if (!this.profileLoaded) {
|
||||||
|
console.warn('⚠️ DashboardAccessService: Tentative d\'accès avant chargement du profil');
|
||||||
|
throw new Error('Profil non chargé');
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = this.authService.getCurrentUserProfile();
|
||||||
|
|
||||||
|
// VÉRIFIER que le profil existe
|
||||||
|
if (!profile) {
|
||||||
|
console.error('❌ DashboardAccessService: Profil null dans getDashboardAccess()');
|
||||||
|
throw new Error('Profil utilisateur non disponible');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📊 DashboardAccessService: getDashboardAccess() avec profil:', {
|
||||||
|
username: profile.username,
|
||||||
|
merchantPartnerId: profile.merchantPartnerId,
|
||||||
|
userType: profile.userType
|
||||||
|
});
|
||||||
|
|
||||||
const userRole = this.roleService.getCurrentRole();
|
const userRole = this.roleService.getCurrentRole();
|
||||||
const isHubUser = this.roleService.isHubUser();
|
const isHubUser = this.roleService.isHubUser();
|
||||||
|
|
||||||
let merchantId: number | undefined = undefined;
|
let merchantId: number | undefined = undefined;
|
||||||
|
|
||||||
if (!isHubUser) {
|
if (!isHubUser && profile.merchantPartnerId) {
|
||||||
merchantId = this.getMerchantIdForCurrentUser();
|
merchantId = Number(profile.merchantPartnerId);
|
||||||
|
if (isNaN(merchantId) || merchantId <= 0) {
|
||||||
|
console.warn(`⚠️ DashboardAccessService: merchantPartnerId invalide: ${profile.merchantPartnerId}`);
|
||||||
|
merchantId = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const access: DashboardAccess = {
|
this.accessCache = {
|
||||||
isHubUser,
|
isHubUser,
|
||||||
isMerchantUser: !isHubUser,
|
isMerchantUser: !isHubUser,
|
||||||
roleLabel: this.roleService.getRoleLabel(),
|
|
||||||
roleIcon: this.roleService.getRoleIcon(),
|
|
||||||
userRole: userRole || UserRole.DCB_SUPPORT,
|
userRole: userRole || UserRole.DCB_SUPPORT,
|
||||||
merchantId
|
merchantId
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('📊 DashboardAccess créé:', {
|
console.log('🎯 DashboardAccessService: Accès créé:', this.accessCache);
|
||||||
...access,
|
|
||||||
merchantId,
|
|
||||||
userRoleLabel: userRole
|
|
||||||
});
|
|
||||||
|
|
||||||
this.accessCache = access;
|
return this.accessCache;
|
||||||
return access;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Obtenir l'accès dashboard avec attente
|
||||||
* Obtient l'accès dashboard (version asynchrone)
|
|
||||||
*/
|
|
||||||
getDashboardAccessAsync(): Observable<DashboardAccess> {
|
getDashboardAccessAsync(): Observable<DashboardAccess> {
|
||||||
return this.waitForAccess().pipe(
|
return this.waitForReady().pipe(
|
||||||
map(() => this.getDashboardAccess())
|
map(() => this.getDashboardAccess())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Obtenir les marchands disponibles
|
||||||
* Récupère le merchant ID pour l'utilisateur courant
|
|
||||||
*/
|
|
||||||
private getMerchantIdForCurrentUser(): number | undefined {
|
|
||||||
// Utiliser la méthode optimisée d'AuthService
|
|
||||||
const merchantId = this.authService.getCurrentMerchantPartnerIdAsNumber();
|
|
||||||
|
|
||||||
if (this.isValidMerchantId(merchantId)) {
|
|
||||||
console.log(`✅ Merchant ID récupéré: ${merchantId}`);
|
|
||||||
return merchantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn('⚠️ Aucun merchant ID valide trouvé pour l\'utilisateur');
|
|
||||||
console.log('Debug:', {
|
|
||||||
merchantId,
|
|
||||||
profile: this.authService.getCurrentUserProfile(),
|
|
||||||
isAuthenticated: this.authService.isAuthenticated()
|
|
||||||
});
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Valide si un ID marchand est valide
|
|
||||||
*/
|
|
||||||
private isValidMerchantId(id: any): id is number {
|
|
||||||
if (id === null || id === undefined) return false;
|
|
||||||
|
|
||||||
const numId = Number(id);
|
|
||||||
return !isNaN(numId) &&
|
|
||||||
Number.isInteger(numId) &&
|
|
||||||
numId > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtient la liste des merchants disponibles
|
|
||||||
*/
|
|
||||||
getAvailableMerchants(): Observable<AllowedMerchant[]> {
|
getAvailableMerchants(): Observable<AllowedMerchant[]> {
|
||||||
return this.waitForAccess().pipe(
|
return this.waitForReady().pipe(
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
if (this.merchantsCache) {
|
if (this.merchantsCache) {
|
||||||
return of(this.merchantsCache);
|
return of(this.merchantsCache);
|
||||||
@ -155,162 +147,71 @@ export class DashboardAccessService {
|
|||||||
|
|
||||||
const access = this.getDashboardAccess();
|
const access = this.getDashboardAccess();
|
||||||
|
|
||||||
console.log('📊 getAvailableMerchants pour:', {
|
|
||||||
isHubUser: access.isHubUser,
|
|
||||||
merchantId: access.merchantId,
|
|
||||||
role: access.userRole
|
|
||||||
});
|
|
||||||
|
|
||||||
if (access.isHubUser) {
|
if (access.isHubUser) {
|
||||||
// Hub users: tous les merchants + option globale
|
return this.merchantService.getAllMerchants().pipe(
|
||||||
return this.loadAllMerchantsForHubUser();
|
map(merchants => {
|
||||||
|
const available: AllowedMerchant[] = merchants.map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
name: m.name
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Option globale pour les hub users
|
||||||
|
available.unshift({ id: 0, name: '🌐 Données globales' });
|
||||||
|
|
||||||
|
this.merchantsCache = available;
|
||||||
|
return available;
|
||||||
|
}),
|
||||||
|
catchError(() => {
|
||||||
|
return of([{ id: 0, name: '🌐 Données globales' }]);
|
||||||
|
})
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Merchant users: seulement leur merchant
|
// Merchant user: seulement son merchant
|
||||||
return this.loadSingleMerchantForUser(access.merchantId);
|
const merchantId = access.merchantId;
|
||||||
|
if (merchantId) {
|
||||||
|
const merchants = [{ id: merchantId, name: `🏪 Merchant ${merchantId}` }];
|
||||||
|
this.merchantsCache = merchants;
|
||||||
|
return of(merchants);
|
||||||
|
}
|
||||||
|
return of([]);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Définir le marchand sélectionné (hub users seulement)
|
||||||
* Charge tous les merchants pour les hub users
|
|
||||||
*/
|
|
||||||
private loadAllMerchantsForHubUser(): Observable<AllowedMerchant[]> {
|
|
||||||
return this.merchantService.getAllMerchants().pipe(
|
|
||||||
map(merchants => {
|
|
||||||
const availableMerchants: AllowedMerchant[] = merchants.map(m => ({
|
|
||||||
id: m.id,
|
|
||||||
name: m.name
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Ajouter l'option "Global"
|
|
||||||
availableMerchants.unshift({
|
|
||||||
id: 0,
|
|
||||||
name: '🌐 Données globales'
|
|
||||||
});
|
|
||||||
|
|
||||||
this.merchantsCache = availableMerchants;
|
|
||||||
return availableMerchants;
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('❌ Erreur lors du chargement des merchants:', error);
|
|
||||||
// Retourner au moins l'option globale
|
|
||||||
return of([{
|
|
||||||
id: 0,
|
|
||||||
name: '🌐 Données globales'
|
|
||||||
}]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Charge le merchant unique pour un merchant user
|
|
||||||
*/
|
|
||||||
private loadSingleMerchantForUser(merchantId?: number): Observable<AllowedMerchant[]> {
|
|
||||||
if (!merchantId || merchantId <= 0) {
|
|
||||||
console.warn('⚠️ Aucun merchant ID valide pour merchant user');
|
|
||||||
|
|
||||||
// Dernière tentative de récupération
|
|
||||||
const fallbackId = this.getMerchantIdForCurrentUser();
|
|
||||||
if (this.isValidMerchantId(fallbackId)) {
|
|
||||||
const merchants = [{
|
|
||||||
id: fallbackId,
|
|
||||||
name: `🏪 Merchant ${fallbackId}`
|
|
||||||
}];
|
|
||||||
this.merchantsCache = merchants;
|
|
||||||
return of(merchants);
|
|
||||||
}
|
|
||||||
|
|
||||||
return of([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const merchants: AllowedMerchant[] = [{
|
|
||||||
id: merchantId,
|
|
||||||
name: `🏪 Merchant ${merchantId}`
|
|
||||||
}];
|
|
||||||
|
|
||||||
this.merchantsCache = merchants;
|
|
||||||
return of(merchants);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Définit le merchant sélectionné (pour hub users)
|
|
||||||
*/
|
|
||||||
setSelectedMerchantId(merchantId: number): void {
|
setSelectedMerchantId(merchantId: number): void {
|
||||||
if (this.getDashboardAccess().isHubUser) {
|
if (this.getDashboardAccess().isHubUser) {
|
||||||
this.currentMerchantId = merchantId;
|
this.currentMerchantId = merchantId;
|
||||||
console.log(`📌 Merchant sélectionné: ${merchantId}`);
|
|
||||||
} else {
|
|
||||||
console.warn('⚠️ Seuls les hub users peuvent sélectionner un merchant');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Obtenir le marchand sélectionné
|
||||||
* Obtient le merchant sélectionné
|
|
||||||
*/
|
|
||||||
getSelectedMerchantId(): number | null {
|
getSelectedMerchantId(): number | null {
|
||||||
const access = this.getDashboardAccess();
|
const access = this.getDashboardAccess();
|
||||||
|
|
||||||
if (access.isMerchantUser) {
|
if (access.isMerchantUser) {
|
||||||
// Merchant users: toujours leur merchant
|
|
||||||
return access.merchantId || null;
|
return access.merchantId || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hub users: le merchant sélectionné
|
|
||||||
return this.currentMerchantId;
|
return this.currentMerchantId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Nettoyer le cache
|
||||||
* Vérifie si un merchant est accessible
|
|
||||||
*/
|
|
||||||
canAccessMerchant(merchantId: number): Observable<boolean> {
|
|
||||||
const access = this.getDashboardAccess();
|
|
||||||
|
|
||||||
if (access.isHubUser) {
|
|
||||||
return of(true); // Hub users: accès à tous
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merchant users: seulement leur merchant
|
|
||||||
return of(access.merchantId === merchantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nettoie le cache
|
|
||||||
*/
|
|
||||||
clearCache(): void {
|
clearCache(): void {
|
||||||
this.accessCache = null;
|
this.accessCache = null;
|
||||||
this.merchantsCache = null;
|
this.merchantsCache = null;
|
||||||
this.currentMerchantId = null;
|
this.currentMerchantId = null;
|
||||||
console.log('🗑️ DashboardAccessService: Cache nettoyé');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ============ MÉTHODES UTILITAIRES SIMPLES ============
|
||||||
* Méthode de débogage
|
|
||||||
*/
|
|
||||||
debug(): void {
|
|
||||||
console.log('=== 🔍 DEBUG DashboardAccessService ===');
|
|
||||||
console.log('1. Access cache:', this.accessCache);
|
|
||||||
console.log('2. Merchants cache:', this.merchantsCache);
|
|
||||||
console.log('3. Current merchant ID:', this.currentMerchantId);
|
|
||||||
console.log('4. Access ready:', this.accessReady$.value);
|
|
||||||
|
|
||||||
console.log('5. AuthService info:');
|
|
||||||
console.log(' - Merchant ID (number):', this.authService.getCurrentMerchantPartnerIdAsNumber());
|
|
||||||
console.log(' - Merchant ID (string):', this.authService.getCurrentMerchantPartnerId());
|
|
||||||
console.log(' - Profil courant:', this.authService.getCurrentUserProfile());
|
|
||||||
console.log(' - Token présent:', !!this.authService.getAccessToken());
|
|
||||||
console.log(' - Is authenticated:', this.authService.isAuthenticated());
|
|
||||||
console.log('==============================');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ MÉTHODES UTILITAIRES POUR LE TEMPLATE ============
|
|
||||||
|
|
||||||
shouldShowSystemHealth(): boolean {
|
shouldShowSystemHealth(): boolean {
|
||||||
return this.getDashboardAccess().isHubUser;
|
return this.getDashboardAccess().isHubUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldShowAllTransactions(): boolean {
|
shouldShowAlerts(): boolean {
|
||||||
return this.getDashboardAccess().isHubUser;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
canTriggerSync(): boolean {
|
canTriggerSync(): boolean {
|
||||||
@ -323,36 +224,10 @@ export class DashboardAccessService {
|
|||||||
return access.isHubUser && access.userRole === UserRole.DCB_ADMIN;
|
return access.isHubUser && access.userRole === UserRole.DCB_ADMIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldShowTransactions(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldShowCharts(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldShowKPIs(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldShowAlerts(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
canRefreshData(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
canSelectMerchant(): boolean {
|
canSelectMerchant(): boolean {
|
||||||
return this.getDashboardAccess().isHubUser;
|
return this.getDashboardAccess().isHubUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldShowMerchantId(): boolean {
|
|
||||||
const access = this.getDashboardAccess();
|
|
||||||
return access.isMerchantUser ||
|
|
||||||
(access.isHubUser && this.getSelectedMerchantId() !== null);
|
|
||||||
}
|
|
||||||
|
|
||||||
canEditMerchantFilter(): boolean {
|
canEditMerchantFilter(): boolean {
|
||||||
const access = this.getDashboardAccess();
|
const access = this.getDashboardAccess();
|
||||||
if (access.isHubUser) {
|
if (access.isHubUser) {
|
||||||
@ -361,18 +236,16 @@ export class DashboardAccessService {
|
|||||||
return access.userRole === UserRole.DCB_PARTNER_ADMIN;
|
return access.userRole === UserRole.DCB_PARTNER_ADMIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ MÉTHODES UTILITAIRES SUPPLÉMENTAIRES ============
|
shouldShowMerchantId(): boolean {
|
||||||
|
const access = this.getDashboardAccess();
|
||||||
|
return access.isMerchantUser ||
|
||||||
|
(access.isHubUser && this.getSelectedMerchantId() !== null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si l'utilisateur peut voir les données globales
|
|
||||||
*/
|
|
||||||
canViewGlobalData(): boolean {
|
canViewGlobalData(): boolean {
|
||||||
return this.getDashboardAccess().isHubUser;
|
return this.getDashboardAccess().isHubUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si l'utilisateur est en mode "données globales"
|
|
||||||
*/
|
|
||||||
isViewingGlobalData(): boolean {
|
isViewingGlobalData(): boolean {
|
||||||
const access = this.getDashboardAccess();
|
const access = this.getDashboardAccess();
|
||||||
if (access.isHubUser) {
|
if (access.isHubUser) {
|
||||||
@ -381,9 +254,6 @@ export class DashboardAccessService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retourne le nom du merchant courant
|
|
||||||
*/
|
|
||||||
getCurrentMerchantName(): string {
|
getCurrentMerchantName(): string {
|
||||||
const access = this.getDashboardAccess();
|
const access = this.getDashboardAccess();
|
||||||
|
|
||||||
@ -401,14 +271,4 @@ export class DashboardAccessService {
|
|||||||
|
|
||||||
return 'Inconnu';
|
return 'Inconnu';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Réinitialise complètement le service
|
|
||||||
*/
|
|
||||||
reset(): void {
|
|
||||||
this.clearCache();
|
|
||||||
this.accessReady$.next(false);
|
|
||||||
// Réinitialiser la surveillance du profil
|
|
||||||
this.initializeProfileSubscription();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -15,7 +15,7 @@ import {
|
|||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
import { HubUsersService } from '../hub-users.service';
|
import { HubUsersService } from '../hub-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { UiCard } from '@app/components/ui-card';
|
import { UiCard } from '@app/components/ui-card';
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
import { HubUsersService } from '../hub-users.service';
|
import { HubUsersService } from '../hub-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -214,12 +214,6 @@ export class HubUserProfile implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier que l'utilisateur peut attribuer ce rôle
|
|
||||||
if (!this.roleService.canAssignRole(this.currentUserRole, newRole)) {
|
|
||||||
this.error = 'Vous n\'avez pas la permission d\'attribuer ce rôle';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier que le rôle est valide pour les utilisateurs Hub
|
// Vérifier que le rôle est valide pour les utilisateurs Hub
|
||||||
if (!this.isValidHubRole(newRole)) {
|
if (!this.isValidHubRole(newRole)) {
|
||||||
this.error = 'Rôle invalide pour un utilisateur Hub';
|
this.error = 'Rôle invalide pour un utilisateur Hub';
|
||||||
@ -310,7 +304,8 @@ export class HubUserProfile implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pour les utilisateurs Hub, utiliser les permissions du service de rôle
|
// Pour les utilisateurs Hub, utiliser les permissions du service de rôle
|
||||||
return this.roleService.canEditUsers(this.currentUserRole);
|
|
||||||
|
return this.roleService.isAnyAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -318,7 +313,8 @@ export class HubUserProfile implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
canManageRoles(): boolean {
|
canManageRoles(): boolean {
|
||||||
// Pour les Hub, utiliser les permissions du service de rôle
|
// Pour les Hub, utiliser les permissions du service de rôle
|
||||||
return this.roleService.canManageRoles(this.currentUserRole);
|
|
||||||
|
return this.roleService.isAnyAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -331,7 +327,8 @@ export class HubUserProfile implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pour les Hub, utiliser les permissions du service de rôle
|
// Pour les Hub, utiliser les permissions du service de rôle
|
||||||
return this.roleService.canEditUsers(this.currentUserRole);
|
|
||||||
|
return this.roleService.isAnyAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -344,7 +341,8 @@ export class HubUserProfile implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pour les Hub, utiliser les permissions générales
|
// Pour les Hub, utiliser les permissions générales
|
||||||
return this.roleService.canEditUsers(this.currentUserRole);
|
|
||||||
|
return this.roleService.isAnyAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -357,7 +355,8 @@ export class HubUserProfile implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pour les Hub, utiliser les permissions du service de rôle
|
// Pour les Hub, utiliser les permissions du service de rôle
|
||||||
return this.roleService.canDeleteUsers(this.currentUserRole);
|
|
||||||
|
return this.roleService.isAnyAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== UTILITAIRES D'AFFICHAGE ====================
|
// ==================== UTILITAIRES D'AFFICHAGE ====================
|
||||||
@ -526,8 +525,7 @@ export class HubUserProfile implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAssignableRoles(): UserRole[] {
|
getAssignableRoles(): UserRole[] {
|
||||||
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
|
return [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
|
||||||
return hubRoles.filter(role => this.roleService.canAssignRole(this.currentUserRole, role));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthodes pour les actions spécifiques
|
// Méthodes pour les actions spécifiques
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstr
|
|||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { HubUsersService } from './hub-users.service';
|
import { HubUsersService } from './hub-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { MerchantSyncService } from './merchant-sync-orchestrator.service';
|
import { MerchantSyncService } from './merchant-sync-orchestrator.service';
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
@ -145,13 +145,7 @@ export class HubUsersManagement implements OnInit, OnDestroy {
|
|||||||
console.log(`HUB User ROLE: ${this.currentUserRole}`);
|
console.log(`HUB User ROLE: ${this.currentUserRole}`);
|
||||||
|
|
||||||
if (this.currentUserRole) {
|
if (this.currentUserRole) {
|
||||||
this.roleService.setCurrentUserRole(this.currentUserRole);
|
this.roleService.setCurrentRole(this.currentUserRole);
|
||||||
this.userPermissions = this.roleService.getPermissionsForRole(this.currentUserRole);
|
|
||||||
this.canCreateUsers = this.roleService.canCreateUsers(this.currentUserRole);
|
|
||||||
this.canDeleteUsers = this.roleService.canDeleteUsers(this.currentUserRole);
|
|
||||||
this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole);
|
|
||||||
|
|
||||||
this.assignableRoles = this.roleService.getAssignableRoles(this.currentUserRole);
|
|
||||||
console.log('Assignable roles:', this.assignableRoles);
|
console.log('Assignable roles:', this.assignableRoles);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -176,14 +170,8 @@ export class HubUsersManagement implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Fallback en cas d'erreur de chargement du profil
|
* Fallback en cas d'erreur de chargement du profil
|
||||||
*/
|
*/
|
||||||
private fallbackPermissions(): void {
|
private fallbackPermissions(): boolean {
|
||||||
this.currentUserRole = this.authService.getCurrentUserRole();
|
return this.roleService.isAnyAdmin();
|
||||||
|
|
||||||
if (this.currentUserRole) {
|
|
||||||
this.canCreateUsers = this.roleService.canCreateUsers(this.currentUserRole);
|
|
||||||
this.canDeleteUsers = this.roleService.canDeleteUsers(this.currentUserRole);
|
|
||||||
this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -541,7 +529,7 @@ export class HubUsersManagement implements OnInit, OnDestroy {
|
|||||||
* Vérifie si l'utilisateur peut attribuer un rôle spécifique
|
* Vérifie si l'utilisateur peut attribuer un rôle spécifique
|
||||||
*/
|
*/
|
||||||
canAssignRole(targetRole: UserRole): boolean {
|
canAssignRole(targetRole: UserRole): boolean {
|
||||||
return this.roleService.canAssignRole(this.currentUserRole, targetRole);
|
return this.roleService.isAnyAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Réinitialiser le mot de passe
|
// Réinitialiser le mot de passe
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
[class.active]="roleFilter === 'all'"
|
[class.active]="roleFilter === 'all'"
|
||||||
(click)="filterByRole('all')"
|
(click)="filterByRole('all')"
|
||||||
>
|
>
|
||||||
Tous ({{ getTotalUsersCount() }})
|
Tous ({{ userStats?.total }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -94,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<select class="form-select" [(ngModel)]="statusFilter" (change)="applyFiltersAndPagination()">
|
<select class="form-select" [(ngModel)]="statusFilter" (change)="onFilterChange()">
|
||||||
<option value="all">Tous les statuts</option>
|
<option value="all">Tous les statuts</option>
|
||||||
<option value="enabled">Activés seulement</option>
|
<option value="enabled">Activés seulement</option>
|
||||||
<option value="disabled">Désactivés seulement</option>
|
<option value="disabled">Désactivés seulement</option>
|
||||||
@ -102,7 +102,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<select class="form-select" [(ngModel)]="emailVerifiedFilter" (change)="applyFiltersAndPagination()">
|
<select class="form-select" [(ngModel)]="emailVerifiedFilter" (change)="onFilterChange()">
|
||||||
<option value="all">Tous les emails</option>
|
<option value="all">Tous les emails</option>
|
||||||
<option value="verified">Email vérifié</option>
|
<option value="verified">Email vérifié</option>
|
||||||
<option value="not-verified">Email non vérifié</option>
|
<option value="not-verified">Email non vérifié</option>
|
||||||
@ -110,14 +110,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<select class="form-select" [(ngModel)]="roleFilter" (change)="applyFiltersAndPagination()">
|
<select class="form-select" [(ngModel)]="roleFilter" (change)="onFilterChange()">
|
||||||
<option value="all">Tous les rôles</option>
|
<option value="all">Tous les rôles</option>
|
||||||
@for (role of availableRoles; track role.value) {
|
@for (role of availableRoles; track role.value) {
|
||||||
<option [value]="role.value">{{ role.label }}</option>
|
<option [value]="role.value">{{ role.label }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<button class="btn btn-outline-secondary w-100" (click)="onClearFilters()" [disabled]="loading">
|
<button class="btn btn-outline-secondary w-100" (click)="onClearFilters()" [disabled]="loading">
|
||||||
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||||
@ -313,10 +312,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
@if (totalPages > 1) {
|
@if (totalPages >= 1) {
|
||||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Affichage de {{ getStartIndex() }} à {{ getEndIndex() }} sur {{ totalItems }} utilisateurs
|
Affichage de {{ getStartIndex() }}
|
||||||
|
à {{ getEndIndex() }}
|
||||||
|
sur {{ totalItems }} utilisateurs
|
||||||
</div>
|
</div>
|
||||||
<nav>
|
<nav>
|
||||||
<ngb-pagination
|
<ngb-pagination
|
||||||
@ -338,7 +339,7 @@
|
|||||||
<div class="row text-center">
|
<div class="row text-center">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<strong>Total :</strong> {{ allUsers.length }} utilisateurs
|
<strong>Total :</strong> {{ userStats?.total }} utilisateurs
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Observable, Subject, map, of } from 'rxjs';
|
import { Observable, Subject, forkJoin, map, of } from 'rxjs';
|
||||||
import { catchError, takeUntil } from 'rxjs/operators';
|
import { catchError, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -11,13 +11,17 @@ import {
|
|||||||
PaginatedUserResponse,
|
PaginatedUserResponse,
|
||||||
UserRole,
|
UserRole,
|
||||||
UserType,
|
UserType,
|
||||||
UserUtils
|
UserUtils,
|
||||||
|
SearchUsersParams,
|
||||||
|
UserStats
|
||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
import { MerchantUsersService } from '../merchant-users.service';
|
import { MerchantUsersService } from '../merchant-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { UiCard } from '@app/components/ui-card';
|
import { UiCard } from '@app/components/ui-card';
|
||||||
|
import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service';
|
||||||
|
import { MerchantUser } from '@core/models/merchant-config.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-merchant-users-list',
|
selector: 'app-merchant-users-list',
|
||||||
@ -34,6 +38,7 @@ import { UiCard } from '@app/components/ui-card';
|
|||||||
export class MerchantUsersList implements OnInit, OnDestroy {
|
export class MerchantUsersList implements OnInit, OnDestroy {
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
private merchantUsersService = inject(MerchantUsersService);
|
private merchantUsersService = inject(MerchantUsersService);
|
||||||
|
private merchantConfigService = inject(MerchantConfigService);
|
||||||
protected roleService = inject(RoleManagementService);
|
protected roleService = inject(RoleManagementService);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
@ -85,11 +90,13 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
// ID du merchant partner courant et permissions
|
// ID du merchant partner courant et permissions
|
||||||
currentMerchantPartnerId: string = '';
|
currentMerchantPartnerId: string = '';
|
||||||
currentUserRole: UserRole | null = null;
|
currentUserRole: UserRole | null = null;
|
||||||
canViewAllMerchants = false;
|
canViewAllMerchantsUsers = false;
|
||||||
|
canViewOwnMerchantUsers = false;
|
||||||
|
userStats: UserStats | undefined;
|
||||||
|
|
||||||
// Getters pour la logique conditionnelle
|
// Getters pour la logique conditionnelle
|
||||||
get showMerchantPartnerColumn(): boolean {
|
get showMerchantPartnerColumn(): boolean {
|
||||||
return this.canViewAllMerchants;
|
return this.canViewAllMerchantsUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
get showCreateButton(): boolean {
|
get showCreateButton(): boolean {
|
||||||
@ -116,14 +123,15 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
.subscribe({
|
.subscribe({
|
||||||
next: (user) => {
|
next: (user) => {
|
||||||
this.currentUserRole = this.extractUserRole(user);
|
this.currentUserRole = this.extractUserRole(user);
|
||||||
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
|
this.canViewAllMerchantsUsers = this.canViewAllMerchantsUsersCheck();
|
||||||
|
this.canViewOwnMerchantUsers = this.canViewOwnMerchantUsersCheck(); // Propriété différente
|
||||||
|
|
||||||
this.loadUsers();
|
setTimeout(() => {
|
||||||
|
this.loadData();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Error loading current user permissions:', error);
|
console.error('Error loading current user permissions:', error);
|
||||||
this.fallbackPermissions();
|
|
||||||
this.loadUsers();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -136,21 +144,12 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private canViewAllMerchantsCheck(role: UserRole | null): boolean {
|
private canViewAllMerchantsUsersCheck(): boolean {
|
||||||
if (!role) return false;
|
return this.roleService.isHubUser()
|
||||||
|
|
||||||
const canViewAllRoles = [
|
|
||||||
UserRole.DCB_ADMIN,
|
|
||||||
UserRole.DCB_SUPPORT,
|
|
||||||
UserRole.DCB_PARTNER_ADMIN
|
|
||||||
];
|
|
||||||
|
|
||||||
return canViewAllRoles.includes(role);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fallbackPermissions(): void {
|
private canViewOwnMerchantUsersCheck(): boolean {
|
||||||
this.currentUserRole = this.authService.getCurrentUserRole();
|
return !this.roleService.isHubUser()
|
||||||
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeAvailableRoles() {
|
private initializeAvailableRoles() {
|
||||||
@ -162,49 +161,86 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
loadUsers() {
|
|
||||||
|
private loadData() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
|
|
||||||
const usersObservable: Observable<User[]> = this.canViewAllMerchants
|
// Marquer pour détection de changement
|
||||||
? this.merchantUsersService
|
this.cdRef.markForCheck();
|
||||||
.getMerchantUsers(this.currentPage, this.itemsPerPage)
|
|
||||||
.pipe(map((response: PaginatedUserResponse) => response.users))
|
|
||||||
: of([]); // fallback propre
|
|
||||||
|
|
||||||
|
const filters: SearchUsersParams = {
|
||||||
|
searchTerm: this.searchTerm,
|
||||||
|
status: this.statusFilter,
|
||||||
|
emailVerified: this.emailVerifiedFilter,
|
||||||
|
role: this.roleFilter,
|
||||||
|
sortField: this.sortField,
|
||||||
|
sortDirection: this.sortDirection
|
||||||
|
};
|
||||||
|
|
||||||
usersObservable
|
// Charger les données et les statistiques en parallèle
|
||||||
.pipe(
|
forkJoin({
|
||||||
takeUntil(this.destroy$),
|
users: this.merchantUsersService.getMerchantUsers(this.currentPage, this.itemsPerPage, filters),
|
||||||
catchError(error => {
|
stats: this.merchantUsersService.getMerchantUsersStats(filters)
|
||||||
console.error('Error loading merchant users:', error);
|
})
|
||||||
this.error = 'Erreur lors du chargement des utilisateurs marchands';
|
.pipe(
|
||||||
return of([] as User[]);
|
takeUntil(this.destroy$),
|
||||||
})
|
catchError(error => {
|
||||||
)
|
console.error('Error loading data:', error);
|
||||||
.subscribe({
|
this.error = 'Erreur lors du chargement des données';
|
||||||
next: (users) => {
|
this.loading = false;
|
||||||
this.allUsers = users || [];
|
this.cdRef.markForCheck();
|
||||||
console.log(`✅ Loaded ${this.allUsers.length} merchant users`);
|
return of({
|
||||||
this.applyFiltersAndPagination();
|
users: {
|
||||||
this.loading = false;
|
users: [],
|
||||||
this.cdRef.detectChanges();
|
total: 0,
|
||||||
},
|
page: this.currentPage,
|
||||||
error: () => {
|
limit: this.itemsPerPage,
|
||||||
this.error = 'Erreur lors du chargement des utilisateurs marchands';
|
totalPages: 0
|
||||||
this.loading = false;
|
} as PaginatedUserResponse,
|
||||||
this.allUsers = [];
|
stats: {
|
||||||
this.filteredUsers = [];
|
total: 0,
|
||||||
this.displayedUsers = [];
|
enabled: 0,
|
||||||
this.cdRef.detectChanges();
|
disabled: 0,
|
||||||
}
|
emailVerified: 0,
|
||||||
});
|
roleCounts: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: ({ users, stats }) => {
|
||||||
|
this.allUsers = users.users || [];
|
||||||
|
this.displayedUsers = this.allUsers;
|
||||||
|
this.totalItems = users.total;
|
||||||
|
this.totalPages = users.totalPages;
|
||||||
|
|
||||||
|
this.userStats = stats;
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
console.log(`✅ Loaded ${this.allUsers.length} merchant users (total: ${this.totalItems})`);
|
||||||
|
this.cdRef.markForCheck(); // Marquer pour détection
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.error = 'Erreur lors du chargement des utilisateurs marchands';
|
||||||
|
this.loading = false;
|
||||||
|
this.allUsers = [];
|
||||||
|
this.displayedUsers = [];
|
||||||
|
this.totalItems = 0;
|
||||||
|
this.totalPages = 0;
|
||||||
|
this.cdRef.markForCheck(); // Marquer pour détection
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recherche et filtres
|
|
||||||
onSearch() {
|
onSearch() {
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.applyFiltersAndPagination();
|
this.loadData(); // Recharger avec les nouveaux filtres
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterChange() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
onClearFilters() {
|
onClearFilters() {
|
||||||
@ -212,64 +248,10 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
this.statusFilter = 'all';
|
this.statusFilter = 'all';
|
||||||
this.emailVerifiedFilter = 'all';
|
this.emailVerifiedFilter = 'all';
|
||||||
this.roleFilter = 'all';
|
this.roleFilter = 'all';
|
||||||
|
this.sortField = 'username';
|
||||||
|
this.sortDirection = 'asc';
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.applyFiltersAndPagination();
|
this.loadData(); // Recharger avec filtres réinitialisés
|
||||||
}
|
|
||||||
|
|
||||||
applyFiltersAndPagination() {
|
|
||||||
if (!this.allUsers) {
|
|
||||||
this.allUsers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Appliquer les filtres
|
|
||||||
this.filteredUsers = this.allUsers.filter(user => {
|
|
||||||
const matchesSearch = !this.searchTerm ||
|
|
||||||
user.username.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
|
|
||||||
user.email.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
|
|
||||||
(user.firstName && user.firstName.toLowerCase().includes(this.searchTerm.toLowerCase())) ||
|
|
||||||
(user.lastName && user.lastName.toLowerCase().includes(this.searchTerm.toLowerCase()));
|
|
||||||
|
|
||||||
const matchesStatus = this.statusFilter === 'all' ||
|
|
||||||
(this.statusFilter === 'enabled' && user.enabled) ||
|
|
||||||
(this.statusFilter === 'disabled' && !user.enabled);
|
|
||||||
|
|
||||||
const matchesEmailVerified = this.emailVerifiedFilter === 'all' ||
|
|
||||||
(this.emailVerifiedFilter === 'verified' && user.emailVerified) ||
|
|
||||||
(this.emailVerifiedFilter === 'not-verified' && !user.emailVerified);
|
|
||||||
|
|
||||||
const matchesRole = this.roleFilter === 'all' ||
|
|
||||||
(user.role && user.role.includes(this.roleFilter));
|
|
||||||
|
|
||||||
return matchesSearch && matchesStatus && matchesEmailVerified && matchesRole;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Appliquer le tri
|
|
||||||
this.filteredUsers.sort((a, b) => {
|
|
||||||
const aValue = a[this.sortField];
|
|
||||||
const bValue = b[this.sortField];
|
|
||||||
|
|
||||||
if (aValue === bValue) return 0;
|
|
||||||
|
|
||||||
let comparison = 0;
|
|
||||||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
|
||||||
comparison = aValue.localeCompare(bValue);
|
|
||||||
} else if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
||||||
comparison = aValue - bValue;
|
|
||||||
} else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
|
|
||||||
comparison = (aValue === bValue) ? 0 : aValue ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.sortDirection === 'asc' ? comparison : -comparison;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculer la pagination
|
|
||||||
this.totalItems = this.filteredUsers.length;
|
|
||||||
this.totalPages = Math.ceil(this.totalItems / this.itemsPerPage);
|
|
||||||
|
|
||||||
// Appliquer la pagination
|
|
||||||
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
|
||||||
const endIndex = startIndex + this.itemsPerPage;
|
|
||||||
this.displayedUsers = this.filteredUsers.slice(startIndex, endIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tri
|
// Tri
|
||||||
@ -280,26 +262,25 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
this.sortField = field;
|
this.sortField = field;
|
||||||
this.sortDirection = 'asc';
|
this.sortDirection = 'asc';
|
||||||
}
|
}
|
||||||
this.applyFiltersAndPagination();
|
this.loadData(); // Recharger avec le nouveau tri
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
onPageChange(newPage: number) {
|
||||||
|
this.currentPage = newPage;
|
||||||
|
this.loadData(); // Recharger avec la nouvelle page
|
||||||
|
}
|
||||||
getSortIcon(field: string): string {
|
getSortIcon(field: string): string {
|
||||||
if (this.sortField !== field) return 'lucideArrowUpDown';
|
if (this.sortField !== field) return 'lucideArrowUpDown';
|
||||||
return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown';
|
return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pagination
|
getEndIndex(): number {
|
||||||
onPageChange(page: number) {
|
return Math.min(this.currentPage * this.itemsPerPage, this.totalItems);
|
||||||
this.currentPage = page;
|
|
||||||
this.applyFiltersAndPagination();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getStartIndex(): number {
|
getStartIndex(): number {
|
||||||
return (this.currentPage - 1) * this.itemsPerPage + 1;
|
return this.totalItems > 0 ? (this.currentPage - 1) * this.itemsPerPage + 1 : 0;
|
||||||
}
|
|
||||||
|
|
||||||
getEndIndex(): number {
|
|
||||||
return Math.min(this.currentPage * this.itemsPerPage, this.totalItems);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
@ -324,7 +305,7 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.allUsers[index] = updatedUser;
|
this.allUsers[index] = updatedUser;
|
||||||
}
|
}
|
||||||
this.applyFiltersAndPagination();
|
this.loadData();
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
@ -344,7 +325,7 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.allUsers[index] = updatedUser;
|
this.allUsers[index] = updatedUser;
|
||||||
}
|
}
|
||||||
this.applyFiltersAndPagination();
|
this.loadData();
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
@ -407,14 +388,6 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
return user.username;
|
return user.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEnabledUsersCount(): number {
|
|
||||||
return this.allUsers.filter(user => user.enabled).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisabledUsersCount(): number {
|
|
||||||
return this.allUsers.filter(user => !user.enabled).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
userHasRole(user: User, role: UserRole): boolean {
|
userHasRole(user: User, role: UserRole): boolean {
|
||||||
return UserUtils.hasRole(user, role);
|
return UserUtils.hasRole(user, role);
|
||||||
}
|
}
|
||||||
@ -423,12 +396,12 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
filterByRole(role: UserRole | 'all') {
|
filterByRole(role: UserRole | 'all') {
|
||||||
this.roleFilter = role;
|
this.roleFilter = role;
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.applyFiltersAndPagination();
|
this.loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recharger les données
|
// Recharger les données
|
||||||
refreshData() {
|
refreshData() {
|
||||||
this.loadUsers();
|
this.loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthodes pour le template
|
// Méthodes pour le template
|
||||||
@ -437,22 +410,13 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getHelperText(): string {
|
getHelperText(): string {
|
||||||
return this.canViewAllMerchants
|
return this.canViewAllMerchantsUsers
|
||||||
? 'Vue administrative - Tous les utilisateurs marchands'
|
? 'Vue administrative - Tous les utilisateurs marchands'
|
||||||
: 'Votre équipe marchande';
|
: 'Votre équipe marchande';
|
||||||
}
|
}
|
||||||
|
|
||||||
getHelperIcon(): string {
|
getHelperIcon(): string {
|
||||||
return this.canViewAllMerchants ? 'lucideShield' : 'lucideUsers';
|
return this.canViewAllMerchantsUsers ? 'lucideShield' : 'lucideUsers';
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode pour compter les utilisateurs par rôle
|
|
||||||
getUsersCountByRole(role: UserRole): number {
|
|
||||||
if (!this.allUsers || this.allUsers.length === 0) return 0;
|
|
||||||
|
|
||||||
return this.allUsers.filter(user =>
|
|
||||||
user.role && user.role.includes(role)
|
|
||||||
).length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getLoadingText(): string {
|
getLoadingText(): string {
|
||||||
@ -476,19 +440,34 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showMerchantPartnerId(): boolean {
|
showMerchantPartnerId(): boolean {
|
||||||
return !this.canViewAllMerchants;
|
return !this.canViewAllMerchantsUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Statistiques
|
// Statistiques
|
||||||
getTotalUsersCount(): number {
|
getTotalUsersCount(): number {
|
||||||
return this.allUsers.length;
|
return this.userStats?.total || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveUsersCount(): number {
|
getActiveUsersCount(): number {
|
||||||
return this.allUsers.filter(user => user.enabled).length;
|
return this.userStats?.enabled || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVerifiedUsersCount(): number {
|
getVerifiedUsersCount(): number {
|
||||||
return this.allUsers.filter(user => user.emailVerified).length;
|
return this.userStats?.emailVerified || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Méthode pour compter les utilisateurs par rôle
|
||||||
|
getUsersCountByRole(role?: UserRole): number {
|
||||||
|
if (!role || !this.userStats?.roleCounts) return 0;
|
||||||
|
return this.userStats.roleCounts[role] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEnabledUsersCount(): number {
|
||||||
|
return this.userStats?.enabled || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisabledUsersCount(): number {
|
||||||
|
return this.userStats?.disabled || 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
import { MerchantUsersService } from '../merchant-users.service';
|
import { MerchantUsersService } from '../merchant-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -210,12 +210,6 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier que l'utilisateur peut attribuer ce rôle
|
|
||||||
if (!this.roleService.canAssignRole(this.currentUserRole, newRole)) {
|
|
||||||
this.error = 'Vous n\'avez pas la permission d\'attribuer ce rôle';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier que le rôle est valide pour les utilisateurs Merchant
|
// Vérifier que le rôle est valide pour les utilisateurs Merchant
|
||||||
if (!this.isValidMerchantRole(newRole)) {
|
if (!this.isValidMerchantRole(newRole)) {
|
||||||
this.error = 'Rôle invalide pour un utilisateur Merchant';
|
this.error = 'Rôle invalide pour un utilisateur Merchant';
|
||||||
@ -306,7 +300,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pour les utilisateurs Merchant, utiliser les permissions du service de rôle
|
// Pour les utilisateurs Merchant, utiliser les permissions du service de rôle
|
||||||
return this.roleService.canEditUsers(this.currentUserRole);
|
return this.roleService.isAnyAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -314,7 +308,8 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
canManageRoles(): boolean {
|
canManageRoles(): boolean {
|
||||||
// Pour les Merchant, utiliser les permissions du service de rôle
|
// Pour les Merchant, utiliser les permissions du service de rôle
|
||||||
return this.roleService.canManageRoles(this.currentUserRole);
|
|
||||||
|
return this.roleService.isAnyAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -327,7 +322,8 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pour les Merchant, utiliser les permissions du service de rôle
|
// Pour les Merchant, utiliser les permissions du service de rôle
|
||||||
return this.roleService.canEditUsers(this.currentUserRole);
|
|
||||||
|
return this.roleService.isAnyAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -340,7 +336,8 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pour les Merchant, utiliser les permissions générales
|
// Pour les Merchant, utiliser les permissions générales
|
||||||
return this.roleService.canEditUsers(this.currentUserRole);
|
|
||||||
|
return this.roleService.isAnyAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -353,7 +350,8 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pour les Merchant, utiliser les permissions du service de rôle
|
// Pour les Merchant, utiliser les permissions du service de rôle
|
||||||
return this.roleService.canDeleteUsers(this.currentUserRole);
|
|
||||||
|
return this.roleService.isAnyAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== UTILITAIRES D'AFFICHAGE ====================
|
// ==================== UTILITAIRES D'AFFICHAGE ====================
|
||||||
@ -522,8 +520,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAssignableRoles(): UserRole[] {
|
getAssignableRoles(): UserRole[] {
|
||||||
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
|
return [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
|
||||||
return merchantRoles.filter(role => this.roleService.canAssignRole(this.currentUserRole, role));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthodes pour les actions spécifiques
|
// Méthodes pour les actions spécifiques
|
||||||
|
|||||||
@ -136,6 +136,49 @@
|
|||||||
|
|
||||||
<form (ngSubmit)="createUser()" #userForm="ngForm">
|
<form (ngSubmit)="createUser()" #userForm="ngForm">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
<!-- Pour les Hub Admin/Support : afficher la liste déroulante -->
|
||||||
|
<div *ngIf="isMerchantRole(newUser.role) && !(isMerchantUser && currentUserType === UserType.MERCHANT_PARTNER)" class="mb-3">
|
||||||
|
<label class="form-label">Merchant *</label>
|
||||||
|
|
||||||
|
<div *ngIf="loadingMerchantPartners" class="text-muted">
|
||||||
|
Chargement des merchants...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="merchantPartnersError" class="alert alert-danger">
|
||||||
|
{{ merchantPartnersError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
class="form-select"
|
||||||
|
[(ngModel)]="selectedMerchantPartnerId"
|
||||||
|
(ngModelChange)="onMerchantSelected($event)"
|
||||||
|
[disabled]="loadingMerchantPartners"
|
||||||
|
name="merchantPartnerId">
|
||||||
|
<option value="">Sélectionner un merchant</option>
|
||||||
|
<option *ngFor="let merchant of merchantPartners" [value]="merchant.id">
|
||||||
|
{{ merchant.name }} ({{ merchant.id }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Sélectionnez le merchant auquel associer cet utilisateur
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pour les Merchant users : afficher un message informatif -->
|
||||||
|
<div *ngIf="isMerchantRole(newUser.role) && isMerchantUser && currentUserType === UserType.MERCHANT_PARTNER" class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
<ng-container *ngIf="loadingMerchantPartners">
|
||||||
|
Chargement de votre merchant...
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="!loadingMerchantPartners && merchantPartnersError">
|
||||||
|
<span class="text-danger">{{ merchantPartnersError }}</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="!loadingMerchantPartners && !merchantPartnersError">
|
||||||
|
Cet utilisateur sera automatiquement associé à votre merchant :
|
||||||
|
<strong>{{ getCurrentUserMerchantName() }}</strong>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Informations de base -->
|
<!-- Informations de base -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@ -286,12 +329,8 @@
|
|||||||
@for (role of availableRoles; track role.value) {
|
@for (role of availableRoles; track role.value) {
|
||||||
<option
|
<option
|
||||||
[value]="role.value"
|
[value]="role.value"
|
||||||
[disabled]="!canAssignRole(role.value)"
|
|
||||||
>
|
>
|
||||||
{{ role.label }} - {{ role.description }}
|
{{ role.label }} - {{ role.description }}
|
||||||
@if (!canAssignRole(role.value)) {
|
|
||||||
(Non autorisé)
|
|
||||||
}
|
|
||||||
</option>
|
</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@ -13,7 +13,8 @@ import {
|
|||||||
SearchUsersParams,
|
SearchUsersParams,
|
||||||
UserRole,
|
UserRole,
|
||||||
UserType,
|
UserType,
|
||||||
UserUtils
|
UserUtils,
|
||||||
|
UserStats
|
||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
// Interfaces pour les nouvelles réponses
|
// Interfaces pour les nouvelles réponses
|
||||||
@ -87,7 +88,8 @@ export class MerchantUsersService {
|
|||||||
role: createUserDto.role,
|
role: createUserDto.role,
|
||||||
enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true,
|
enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true,
|
||||||
emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : true,
|
emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : true,
|
||||||
userType: createUserDto.userType.trim()
|
userType: createUserDto.userType.trim(),
|
||||||
|
merchantPartnerId: (createUserDto.merchantPartnerId || '').trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.http.post<User>(`${this.baseApiUrl}`, payload).pipe(
|
return this.http.post<User>(`${this.baseApiUrl}`, payload).pipe(
|
||||||
@ -119,9 +121,51 @@ export class MerchantUsersService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMerchantUsersStats(filters?: SearchUsersParams): Observable<UserStats> {
|
||||||
|
return this.getAllMerchantUsers(filters).pipe(
|
||||||
|
map(users => {
|
||||||
|
const enabled = users.filter(u => u.enabled).length;
|
||||||
|
const disabled = users.filter(u => !u.enabled).length;
|
||||||
|
const emailVerified = users.filter(u => u.emailVerified).length;
|
||||||
|
|
||||||
|
// Compter par rôle
|
||||||
|
const roleCounts: Record<string, number> = {};
|
||||||
|
users.forEach(user => {
|
||||||
|
if (user.role) {
|
||||||
|
const role = user.role;
|
||||||
|
roleCounts[role] = (roleCounts[role] || 0) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: users.length,
|
||||||
|
enabled,
|
||||||
|
disabled,
|
||||||
|
emailVerified,
|
||||||
|
roleCounts
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllMerchantUsers(filters?: SearchUsersParams): Observable<User[]> {
|
||||||
|
return this.getMyMerchantUsers().pipe(
|
||||||
|
map(allUsers => {
|
||||||
|
// Appliquer les filtres
|
||||||
|
return this.applyFiltersToUsers(allUsers, filters);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getMerchantUsers(page: number = 1, limit: number = 10, filters?: SearchUsersParams): Observable<PaginatedUserResponse> {
|
getMerchantUsers(page: number = 1, limit: number = 10, filters?: SearchUsersParams): Observable<PaginatedUserResponse> {
|
||||||
return this.getMyMerchantUsers().pipe(
|
return this.getMyMerchantUsers().pipe(
|
||||||
map(users => this.filterAndPaginateUsers(users, page, limit, filters)),
|
map(allUsers => {
|
||||||
|
// Appliquer les filtres sur TOUTES les données
|
||||||
|
const filteredUsers = this.applyFiltersToUsers(allUsers, filters);
|
||||||
|
|
||||||
|
// Puis paginer
|
||||||
|
return this.paginateUsers(filteredUsers, page, limit);
|
||||||
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error('Error loading merchant users:', error);
|
console.error('Error loading merchant users:', error);
|
||||||
return throwError(() => error);
|
return throwError(() => error);
|
||||||
@ -129,6 +173,47 @@ export class MerchantUsersService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyFiltersToUsers(users: User[], filters?: SearchUsersParams): User[] {
|
||||||
|
if (!filters) return users;
|
||||||
|
|
||||||
|
return users.filter(user => {
|
||||||
|
const matchesSearch = !filters.searchTerm ||
|
||||||
|
user.username.toLowerCase().includes(filters.searchTerm.toLowerCase()) ||
|
||||||
|
user.email.toLowerCase().includes(filters.searchTerm.toLowerCase()) ||
|
||||||
|
(user.firstName && user.firstName.toLowerCase().includes(filters.searchTerm.toLowerCase())) ||
|
||||||
|
(user.lastName && user.lastName.toLowerCase().includes(filters.searchTerm.toLowerCase()));
|
||||||
|
|
||||||
|
const matchesStatus = !filters.status || filters.status === 'all' ||
|
||||||
|
(filters.status === 'enabled' && user.enabled) ||
|
||||||
|
(filters.status === 'disabled' && !user.enabled);
|
||||||
|
|
||||||
|
const matchesEmailVerified = !filters.emailVerified || filters.emailVerified === 'all' ||
|
||||||
|
(filters.emailVerified === 'verified' && user.emailVerified) ||
|
||||||
|
(filters.emailVerified === 'not-verified' && !user.emailVerified);
|
||||||
|
|
||||||
|
const matchesRole = !filters.role || filters.role === 'all' ||
|
||||||
|
(user.role && user.role.includes(filters.role));
|
||||||
|
|
||||||
|
return matchesSearch && matchesStatus && matchesEmailVerified && matchesRole;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private paginateUsers(users: User[], page: number, limit: number): PaginatedUserResponse {
|
||||||
|
const total = users.length;
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
const startIndex = (page - 1) * limit;
|
||||||
|
const endIndex = startIndex + limit;
|
||||||
|
const paginatedItems = users.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
return {
|
||||||
|
users: paginatedItems,
|
||||||
|
total: total,
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
totalPages: totalPages
|
||||||
|
};
|
||||||
|
}
|
||||||
updateMerchantUser(id: string, updateUserDto: UpdateUserDto): Observable<User> {
|
updateMerchantUser(id: string, updateUserDto: UpdateUserDto): Observable<User> {
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
firstName: updateUserDto.firstName,
|
firstName: updateUserDto.firstName,
|
||||||
@ -280,50 +365,4 @@ export class MerchantUsersService {
|
|||||||
lastLogin: apiUser.lastLogin
|
lastLogin: apiUser.lastLogin
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterAndPaginateUsers(
|
|
||||||
users: User[],
|
|
||||||
page: number,
|
|
||||||
limit: number,
|
|
||||||
filters?: SearchUsersParams
|
|
||||||
): PaginatedUserResponse {
|
|
||||||
let filteredUsers = users;
|
|
||||||
|
|
||||||
if (filters) {
|
|
||||||
if (filters.query) {
|
|
||||||
const query = filters.query.toLowerCase();
|
|
||||||
filteredUsers = filteredUsers.filter(user =>
|
|
||||||
user.username.toLowerCase().includes(query) ||
|
|
||||||
user.email.toLowerCase().includes(query) ||
|
|
||||||
user.firstName?.toLowerCase().includes(query) ||
|
|
||||||
user.lastName?.toLowerCase().includes(query)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.role) {
|
|
||||||
filteredUsers = filteredUsers.filter(user => user.role.includes(filters.role!));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.enabled !== undefined) {
|
|
||||||
filteredUsers = filteredUsers.filter(user => user.enabled === filters.enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.userType) {
|
|
||||||
filteredUsers = filteredUsers.filter(user => user.userType === filters.userType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pagination côté client
|
|
||||||
const startIndex = (page - 1) * limit;
|
|
||||||
const endIndex = startIndex + limit;
|
|
||||||
const paginatedUsers = filteredUsers.slice(startIndex, endIndex);
|
|
||||||
|
|
||||||
return {
|
|
||||||
users: paginatedUsers,
|
|
||||||
total: filteredUsers.length,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
totalPages: Math.ceil(filteredUsers.length / limit)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { catchError, map, of, Subject, takeUntil } from 'rxjs';
|
import { catchError, map, of, Subject, switchMap, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { MerchantUsersService } from './merchant-users.service';
|
import { MerchantUsersService } from './merchant-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
import { MerchantUsersList } from './merchant-users-list/merchant-users-list';
|
import { MerchantUsersList } from './merchant-users-list/merchant-users-list';
|
||||||
@ -15,11 +15,12 @@ import { MerchantUserProfile } from './merchant-users-profile/merchant-users-pro
|
|||||||
import {
|
import {
|
||||||
PaginatedUserResponse,
|
PaginatedUserResponse,
|
||||||
ResetPasswordDto,
|
ResetPasswordDto,
|
||||||
User,
|
|
||||||
UserRole,
|
UserRole,
|
||||||
UserType
|
UserType
|
||||||
} from '@core/models/dcb-bo-hub-user.model';
|
} from '@core/models/dcb-bo-hub-user.model';
|
||||||
import { HubUsersService } from './hub-users.service';
|
import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service';
|
||||||
|
import { AddUserToMerchantDto, Merchant } from '@core/models/merchant-config.model';
|
||||||
|
import { MerchantSyncService } from './merchant-sync-orchestrator.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-merchant-users',
|
selector: 'app-merchant-users',
|
||||||
@ -40,13 +41,14 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
private modalService = inject(NgbModal);
|
private modalService = inject(NgbModal);
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
private merchantUsersService = inject(MerchantUsersService);
|
private merchantUsersService = inject(MerchantUsersService);
|
||||||
private hubUsersService = inject(HubUsersService);
|
private merchantConfigService = inject(MerchantConfigService);
|
||||||
protected roleService = inject(RoleManagementService);
|
protected roleService = inject(RoleManagementService);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
readonly UserRole = UserRole;
|
readonly UserRole = UserRole;
|
||||||
|
readonly UserType = UserType;
|
||||||
|
|
||||||
// Propriétés de configuration
|
// Propriétés de configuration
|
||||||
pageTitle: string = 'Gestion des Utilisateurs Marchands';
|
pageTitle: string = 'Gestion des Utilisateurs Marchands';
|
||||||
@ -76,6 +78,7 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
userType: UserType;
|
userType: UserType;
|
||||||
|
merchantPartnerId?: string;
|
||||||
} = this.getDefaultUserForm();
|
} = this.getDefaultUserForm();
|
||||||
|
|
||||||
// États des opérations
|
// États des opérations
|
||||||
@ -110,22 +113,49 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
availableRoles: { value: UserRole; label: string; description: string }[] = [];
|
availableRoles: { value: UserRole; label: string; description: string }[] = [];
|
||||||
assignableRoles: UserRole[] = [];
|
assignableRoles: UserRole[] = [];
|
||||||
|
|
||||||
merchantPartners: User[] = [];
|
merchantPartners: Merchant[] = [];
|
||||||
|
merchantPartner: Merchant | undefined;
|
||||||
loadingMerchantPartners = false;
|
loadingMerchantPartners = false;
|
||||||
merchantPartnersError = '';
|
merchantPartnersError = '';
|
||||||
selectedMerchantPartnerId: string = '';
|
selectedMerchantPartnerId: string = '';
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
|
||||||
this.activeTab = 'list';
|
this.activeTab = 'list';
|
||||||
this.loadCurrentUserPermissions();
|
this.loadCurrentUserPermissions();
|
||||||
this.loadAvailableRoles();
|
this.loadAvailableRoles();
|
||||||
|
|
||||||
|
// Charger les données selon le type d'utilisateur
|
||||||
|
this.loadUserSpecificData();
|
||||||
|
|
||||||
this.newUser.role = UserRole.DCB_PARTNER_SUPPORT;
|
this.newUser.role = UserRole.DCB_PARTNER_SUPPORT;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private loadUserSpecificData(): void {
|
||||||
|
if (this.isMerchantUser && this.currentUserType === UserType.MERCHANT_PARTNER) {
|
||||||
|
// Pour un utilisateur Merchant : charger son propre merchant seulement
|
||||||
|
this.loadCurrentUserMerchant();
|
||||||
|
} else {
|
||||||
|
// Pour un Hub Admin/Support : charger tous les merchants
|
||||||
|
this.loadAllMerchants();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
|
private updateNewUserMerchantId() {
|
||||||
|
if (this.selectedMerchantPartnerId) {
|
||||||
|
this.newUser.merchantPartnerId = this.selectedMerchantPartnerId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMerchantSelected(merchantId: string) {
|
||||||
|
this.selectedMerchantPartnerId = merchantId;
|
||||||
|
this.updateNewUserMerchantId();
|
||||||
|
}
|
||||||
|
|
||||||
onRoleSelectionChange(selectedRole: UserRole) {
|
onRoleSelectionChange(selectedRole: UserRole) {
|
||||||
this.newUser.role = selectedRole;
|
this.newUser.role = selectedRole;
|
||||||
}
|
}
|
||||||
@ -157,14 +187,10 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.currentUserType = this.extractUserType(user);
|
this.currentUserType = this.extractUserType(user);
|
||||||
|
|
||||||
if (this.currentUserRole) {
|
this.canCreateUsers = this.roleService.isAnyAdmin();
|
||||||
this.roleService.setCurrentUserRole(this.currentUserRole);
|
|
||||||
this.userPermissions = this.roleService.getPermissionsForRole(this.currentUserRole);
|
|
||||||
this.canCreateUsers = this.roleService.canCreateUsers(this.currentUserRole);
|
|
||||||
this.canDeleteUsers = this.roleService.canDeleteUsers(this.currentUserRole);
|
|
||||||
this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole);
|
|
||||||
|
|
||||||
this.assignableRoles = this.roleService.getAssignableRoles(this.currentUserRole);
|
if (this.currentUserRole) {
|
||||||
|
this.roleService.setCurrentRole(this.currentUserRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -215,7 +241,7 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getDefaultUserForm() {
|
private getDefaultUserForm() {
|
||||||
return {
|
const defaultForm = {
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
firstName: '',
|
firstName: '',
|
||||||
@ -224,8 +250,12 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
role: UserRole.DCB_PARTNER_SUPPORT,
|
role: UserRole.DCB_PARTNER_SUPPORT,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
emailVerified: false,
|
emailVerified: false,
|
||||||
userType: UserType.MERCHANT_PARTNER
|
userType: UserType.MERCHANT_PARTNER,
|
||||||
|
merchantPartnerId: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Si l'utilisateur est déjà connecté comme Merchant, on pourra pré-remplir plus tard
|
||||||
|
return defaultForm;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== MÉTHODES D'INTERFACE ====================
|
// ==================== MÉTHODES D'INTERFACE ====================
|
||||||
@ -279,6 +309,71 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
return this.selectedUserId ? this.loadingProfiles[this.selectedUserId] : false;
|
return this.selectedUserId ? this.loadingProfiles[this.selectedUserId] : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private loadCurrentUserMerchant(): void {
|
||||||
|
this.loadingMerchantPartners = true;
|
||||||
|
this.merchantPartnersError = '';
|
||||||
|
|
||||||
|
const currentUserMerchantPartnerId = this.authService.getCurrentMerchantPartnerId();
|
||||||
|
if (!currentUserMerchantPartnerId) {
|
||||||
|
this.merchantPartnersError = 'Impossible de déterminer votre ID utilisateur';
|
||||||
|
this.loadingMerchantPartners = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.merchantConfigService.getMerchantById(Number(currentUserMerchantPartnerId))
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (merchant) => {
|
||||||
|
if (merchant) {
|
||||||
|
this.merchantPartner = merchant;
|
||||||
|
// Pré-remplir le merchantPartnerId
|
||||||
|
if (this.merchantPartner?.id) {
|
||||||
|
this.newUser.merchantPartnerId = this.merchantPartner.id.toString();
|
||||||
|
console.log('✅ Current user merchant loaded:', this.merchantPartner);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.merchantPartnersError = 'Aucun merchant trouvé pour votre compte';
|
||||||
|
console.warn('❌ No merchant found for current user');
|
||||||
|
}
|
||||||
|
this.loadingMerchantPartners = false;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('❌ Error loading current user merchant:', error);
|
||||||
|
this.merchantPartnersError = 'Erreur lors du chargement de votre merchant';
|
||||||
|
this.loadingMerchantPartners = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadAllMerchants(): void {
|
||||||
|
this.loadingMerchantPartners = true;
|
||||||
|
this.merchantPartnersError = '';
|
||||||
|
|
||||||
|
this.merchantConfigService.getAllMerchants()
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (merchants) => {
|
||||||
|
this.merchantPartners = merchants;
|
||||||
|
this.loadingMerchantPartners = false;
|
||||||
|
console.log('✅ All merchants loaded for Hub Admin:', merchants.length);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('❌ Error loading all merchants:', error);
|
||||||
|
this.merchantPartnersError = 'Erreur lors du chargement des merchants';
|
||||||
|
this.loadingMerchantPartners = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentUserMerchantName(): string {
|
||||||
|
if (!this.newUser.merchantPartnerId || !this.merchantPartner) {
|
||||||
|
return 'Non défini';
|
||||||
|
}
|
||||||
|
|
||||||
|
const merchant = this.merchantPartner;
|
||||||
|
|
||||||
|
return merchant ? `${merchant.name} (${merchant.id})` : 'Non trouvé';
|
||||||
|
}
|
||||||
|
|
||||||
backToList() {
|
backToList() {
|
||||||
console.log('🔙 Returning to list view');
|
console.log('🔙 Returning to list view');
|
||||||
@ -324,17 +419,17 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private resetUserForm() {
|
private resetUserForm() {
|
||||||
this.newUser = {
|
this.newUser = this.getDefaultUserForm();
|
||||||
username: '',
|
|
||||||
email: '',
|
// Si l'utilisateur est un Merchant, utiliser son merchantPartnerId
|
||||||
firstName: '',
|
if (this.isMerchantUser && this.currentUserType === UserType.MERCHANT_PARTNER && this.merchantPartner) {
|
||||||
lastName: '',
|
const currentUserMerchant = this.merchantPartner;
|
||||||
password: '',
|
if (currentUserMerchant?.id) {
|
||||||
role: UserRole.DCB_PARTNER_SUPPORT,
|
this.newUser.merchantPartnerId = currentUserMerchant.id.toString();
|
||||||
enabled: true,
|
console.log('🔄 Form reset with current user merchant:', this.newUser.merchantPartnerId);
|
||||||
emailVerified: false,
|
}
|
||||||
userType: UserType.MERCHANT_PARTNER,
|
}
|
||||||
};
|
|
||||||
console.log('🔄 Merchant user form reset');
|
console.log('🔄 Merchant user form reset');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -401,9 +496,62 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier la permission pour attribuer le rôle sélectionné
|
// Vérifications pour les utilisateurs Merchant
|
||||||
if (!this.canAssignRole(this.newUser.role)) {
|
if (this.isMerchantRole(this.newUser.role)) {
|
||||||
this.createUserError = `Vous n'avez pas la permission d'attribuer le rôle: ${this.getRoleLabel(this.newUser.role)}`;
|
// Déterminer merchantPartnerId
|
||||||
|
if (this.isMerchantUser && this.currentUserType === UserType.MERCHANT_PARTNER) {
|
||||||
|
// L'utilisateur connecté est un Merchant, utiliser son merchantPartnerId
|
||||||
|
const currentUserMerchantPartnerId = this.authService.getCurrentMerchantPartnerId();
|
||||||
|
if (currentUserMerchantPartnerId) {
|
||||||
|
this.newUser.merchantPartnerId = currentUserMerchantPartnerId;
|
||||||
|
console.log('✅ Using current user merchantPartnerId:', currentUserMerchantPartnerId);
|
||||||
|
} else {
|
||||||
|
// Si l'utilisateur n'a pas de merchantPartnerId, essayez de le trouver
|
||||||
|
this.findMerchantPartnerIdForCurrentUser();
|
||||||
|
if (!this.newUser.merchantPartnerId) {
|
||||||
|
this.createUserError = 'Impossible de déterminer votre merchant partner ID';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.selectedMerchantPartnerId) {
|
||||||
|
// Hub admin crée pour un merchant spécifique
|
||||||
|
this.newUser.merchantPartnerId = this.selectedMerchantPartnerId;
|
||||||
|
console.log('✅ Using selected merchantPartnerId:', this.selectedMerchantPartnerId);
|
||||||
|
} else {
|
||||||
|
this.createUserError = 'Merchant partner est requis pour les utilisateurs Merchant';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let merchantExists = false;
|
||||||
|
|
||||||
|
// Si vous avez une liste de merchants
|
||||||
|
if (this.merchantPartners && this.merchantPartners.length > 0) {
|
||||||
|
// Vérifier que le merchant existe dans MerchantConfig
|
||||||
|
merchantExists = this.merchantPartners.some(
|
||||||
|
merchant => Number(merchant?.id) === Number(this.newUser.merchantPartnerId)
|
||||||
|
);
|
||||||
|
console.log("Merchant exists in list:", merchantExists);
|
||||||
|
}
|
||||||
|
// Si vous avez un merchant individuel (pour un utilisateur Merchant)
|
||||||
|
else if (this.merchantPartner) {
|
||||||
|
merchantExists = Number(this.merchantPartner?.id) === Number(this.newUser.merchantPartnerId);
|
||||||
|
console.log("Merchant exists (single):", merchantExists);
|
||||||
|
}
|
||||||
|
// Si aucun des deux n'est disponible
|
||||||
|
else {
|
||||||
|
console.warn("No merchant data available for validation");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!merchantExists) {
|
||||||
|
this.createUserError = `Le merchant avec l'ID ${this.newUser.merchantPartnerId} n'existe pas dans MerchantConfig`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedRole = this.mapToMerchantConfigRole(this.newUser.role);
|
||||||
|
|
||||||
|
if (!mappedRole) {
|
||||||
|
this.createUserError = `Impossible de mapper le rôle ${this.getRoleLabel(this.newUser.role)} vers un rôle MerchantConfig valide`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -412,18 +560,60 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
console.log('📤 Creating merchant user with data:', this.newUser);
|
console.log('📤 Creating merchant user with data:', this.newUser);
|
||||||
|
|
||||||
this.merchantUsersService.createMerchantUser(this.newUser)
|
// 1. Créer l'utilisateur dans Keycloak
|
||||||
.pipe(takeUntil(this.destroy$))
|
const userDto = {
|
||||||
|
username: this.newUser.username,
|
||||||
|
email: this.newUser.email,
|
||||||
|
password: this.newUser.password,
|
||||||
|
firstName: this.newUser.firstName,
|
||||||
|
lastName: this.newUser.lastName,
|
||||||
|
role: this.newUser.role,
|
||||||
|
enabled: this.newUser.enabled,
|
||||||
|
emailVerified: this.newUser.emailVerified,
|
||||||
|
userType: this.newUser.userType,
|
||||||
|
merchantPartnerId: this.newUser.merchantPartnerId // Passer l'ID du merchant
|
||||||
|
};
|
||||||
|
|
||||||
|
this.merchantUsersService.createMerchantUser(userDto)
|
||||||
|
.pipe(
|
||||||
|
switchMap((createdKeycloakUser) => {
|
||||||
|
console.log('✅ Keycloak user created successfully:', createdKeycloakUser);
|
||||||
|
|
||||||
|
// 2. Ajouter l'utilisateur au merchant dans MerchantConfig
|
||||||
|
if (this.isMerchantRole(this.newUser.role) && this.newUser.merchantPartnerId) {
|
||||||
|
const merchantPartnerId = Number(this.newUser.merchantPartnerId);
|
||||||
|
|
||||||
|
const addUserDto: AddUserToMerchantDto = {
|
||||||
|
userId: createdKeycloakUser.id,
|
||||||
|
role: mappedRole,
|
||||||
|
merchantPartnerId: merchantPartnerId
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📤 Adding user to merchant config:', addUserDto);
|
||||||
|
return this.merchantConfigService.addUserToMerchant(addUserDto).pipe(
|
||||||
|
map((merchantConfigUser) => {
|
||||||
|
return {
|
||||||
|
keycloakUser: createdKeycloakUser,
|
||||||
|
merchantConfigUser
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return of({ keycloakUser: createdKeycloakUser });
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (createdUser) => {
|
next: (result) => {
|
||||||
console.log('✅ Merchant user created successfully:', createdUser);
|
console.log('✅ Complete user creation successful:', result);
|
||||||
this.creatingUser = false;
|
this.creatingUser = false;
|
||||||
this.modalService.dismissAll();
|
this.modalService.dismissAll();
|
||||||
this.refreshUsersList();
|
this.refreshUsersList();
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('❌ Error creating merchant user:', error);
|
console.error('❌ Error in user creation process:', error);
|
||||||
this.creatingUser = false;
|
this.creatingUser = false;
|
||||||
this.createUserError = this.getErrorMessage(error);
|
this.createUserError = this.getErrorMessage(error);
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
@ -431,6 +621,50 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Méthode pour trouver le merchantPartnerId de l'utilisateur connecté
|
||||||
|
private findMerchantPartnerIdForCurrentUser(): void {
|
||||||
|
const currentUserId = this.authService.getCurrentUserId();
|
||||||
|
if (!currentUserId) return;
|
||||||
|
|
||||||
|
// Chercher dans les merchants configs
|
||||||
|
for (const merchant of this.merchantPartners) {
|
||||||
|
if (merchant.users?.some(user => user.userId === currentUserId)) {
|
||||||
|
this.newUser.merchantPartnerId = merchant?.id?.toString();
|
||||||
|
console.log('✅ Found merchantPartnerId for current user:', this.newUser.merchantPartnerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si non trouvé, essayer via le service
|
||||||
|
this.merchantUsersService.getMerchantUserById(currentUserId)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (user) => {
|
||||||
|
if (user.merchantPartnerId) {
|
||||||
|
this.newUser.merchantPartnerId = user.merchantPartnerId;
|
||||||
|
console.log('✅ Found merchantPartnerId via service:', this.newUser.merchantPartnerId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.warn('Could not find merchantPartnerId for current user:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mappe un rôle Keycloak Merchant vers un rôle MerchantConfig
|
||||||
|
*/
|
||||||
|
private mapToMerchantConfigRole(keycloakRole: UserRole): UserRole | null {
|
||||||
|
// Déclarez comme Partial<Record<UserRole, UserRole>> ou utilisez un type plus spécifique
|
||||||
|
const roleMapping: Partial<Record<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_VIEWER
|
||||||
|
};
|
||||||
|
|
||||||
|
return roleMapping[keycloakRole] || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si un rôle est un rôle marchand
|
* Vérifie si un rôle est un rôle marchand
|
||||||
*/
|
*/
|
||||||
@ -444,13 +678,6 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
return merchantRoles.includes(role);
|
return merchantRoles.includes(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si l'utilisateur peut attribuer un rôle spécifique
|
|
||||||
*/
|
|
||||||
canAssignRole(targetRole: UserRole): boolean {
|
|
||||||
return this.roleService.canAssignRole(this.currentUserRole, targetRole);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Réinitialiser le mot de passe
|
// Réinitialiser le mot de passe
|
||||||
confirmResetPassword() {
|
confirmResetPassword() {
|
||||||
if (!this.selectedUserForReset || !this.newPassword || this.newPassword.length < 8) {
|
if (!this.selectedUserForReset || !this.newPassword || this.newPassword.length < 8) {
|
||||||
@ -537,11 +764,6 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthodes proxy pour le template
|
|
||||||
getRoleBadgeClass(role: UserRole): string {
|
|
||||||
return this.roleService.getRoleBadgeClass(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoleLabel(role: UserRole): string {
|
getRoleLabel(role: UserRole): string {
|
||||||
return this.roleService.getRoleLabel(role);
|
return this.roleService.getRoleLabel(role);
|
||||||
}
|
}
|
||||||
@ -550,6 +772,10 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
return this.roleService.getRoleIcon(role);
|
return this.roleService.getRoleIcon(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRoleBadgeClass(role: UserRole): string {
|
||||||
|
return this.roleService.getRoleBadgeClass(role);
|
||||||
|
}
|
||||||
|
|
||||||
getRoleDescription(role: UserRole): string {
|
getRoleDescription(role: UserRole): string {
|
||||||
const roleInfo = this.availableRoles.find(r => r.value === role);
|
const roleInfo = this.availableRoles.find(r => r.value === role);
|
||||||
return roleInfo?.description || 'Description non disponible';
|
return roleInfo?.description || 'Description non disponible';
|
||||||
@ -616,7 +842,8 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
{ field: this.newUser.username?.trim(), name: 'Nom d\'utilisateur' },
|
{ field: this.newUser.username?.trim(), name: 'Nom d\'utilisateur' },
|
||||||
{ field: this.newUser.email?.trim(), name: 'Email' },
|
{ field: this.newUser.email?.trim(), name: 'Email' },
|
||||||
{ field: this.newUser.firstName?.trim(), name: 'Prénom' },
|
{ field: this.newUser.firstName?.trim(), name: 'Prénom' },
|
||||||
{ field: this.newUser.lastName?.trim(), name: 'Nom' }
|
{ field: this.newUser.lastName?.trim(), name: 'Nom' },
|
||||||
|
{ field: this.newUser.merchantPartnerId?.trim(), name: 'Merchant Partner ID' }
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const { field, name } of requiredFields) {
|
for (const { field, name } of requiredFields) {
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
} from '@core/models/merchant-config.model';
|
} from '@core/models/merchant-config.model';
|
||||||
|
|
||||||
import { MerchantConfigService } from '../merchant-config.service';
|
import { MerchantConfigService } from '../merchant-config.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { UiCard } from '@app/components/ui-card';
|
import { UiCard } from '@app/components/ui-card';
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import {
|
|||||||
|
|
||||||
import { MerchantConfigService } from '../merchant-config.service';
|
import { MerchantConfigService } from '../merchant-config.service';
|
||||||
import { MerchantDataAdapter } from '../merchant-data-adapter.service';
|
import { MerchantDataAdapter } from '../merchant-data-adapter.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { UserRole } from '@core/models/dcb-bo-hub-user.model';
|
import { UserRole } from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,8 @@ import {
|
|||||||
ApiMerchant,
|
ApiMerchant,
|
||||||
ApiMerchantConfig,
|
ApiMerchantConfig,
|
||||||
ApiTechnicalContact,
|
ApiTechnicalContact,
|
||||||
ApiMerchantUser
|
ApiMerchantUser,
|
||||||
|
MerchantUserWithMerchant
|
||||||
} from '@core/models/merchant-config.model';
|
} from '@core/models/merchant-config.model';
|
||||||
|
|
||||||
// SERVICE DE CONVERSION
|
// SERVICE DE CONVERSION
|
||||||
@ -189,6 +190,53 @@ export class MerchantConfigService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllMerchantUsers(page: number = 1, limit: number = 10, params?: SearchMerchantsParams): Observable<PaginatedResponse<MerchantUser>> {
|
||||||
|
let httpParams = new HttpParams();
|
||||||
|
|
||||||
|
if (params?.query) {
|
||||||
|
httpParams = httpParams.set('query', params.query.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📥 Loading all merchant users - page ${page}, limit ${limit}`, params);
|
||||||
|
|
||||||
|
return this.http.get<ApiMerchant[]>(this.baseApiUrl, { params: httpParams }).pipe(
|
||||||
|
timeout(this.REQUEST_TIMEOUT),
|
||||||
|
map(apiMerchants => {
|
||||||
|
// Récupérer tous les utilisateurs de tous les marchands
|
||||||
|
const allUsers: MerchantUser[] = [];
|
||||||
|
|
||||||
|
apiMerchants.forEach(apiMerchant => {
|
||||||
|
if (apiMerchant.users && apiMerchant.users.length > 0) {
|
||||||
|
const merchantUsers = apiMerchant.users.map(user =>
|
||||||
|
this.dataAdapter.convertApiUserToFrontend(user)
|
||||||
|
);
|
||||||
|
allUsers.push(...merchantUsers);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gestion de la pagination côté client
|
||||||
|
const total = allUsers.length;
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
const startIndex = (page - 1) * limit;
|
||||||
|
const endIndex = startIndex + limit;
|
||||||
|
const paginatedItems = allUsers.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const response: PaginatedResponse<MerchantUser> = {
|
||||||
|
items: paginatedItems,
|
||||||
|
total: total,
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
totalPages: totalPages
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`✅ Loaded ${paginatedItems.length} merchant users out of ${total} total`);
|
||||||
|
return response;
|
||||||
|
}),
|
||||||
|
catchError(error => this.handleError('getAllMerchantUsers', error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
updateUserRole(merchantId: number, userId: string, updateRoleDto: UpdateUserRoleDto): Observable<MerchantUser> {
|
updateUserRole(merchantId: number, userId: string, updateRoleDto: UpdateUserRoleDto): Observable<MerchantUser> {
|
||||||
//const merchantId = this.convertIdToNumber(merchantId);
|
//const merchantId = this.convertIdToNumber(merchantId);
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstr
|
|||||||
import { catchError, finalize, map, of, Subject, takeUntil } from 'rxjs';
|
import { catchError, finalize, map, of, Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { MerchantConfigService } from './merchant-config.service';
|
import { MerchantConfigService } from './merchant-config.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { MerchantSyncService } from '../hub-users-management/merchant-sync-orchestrator.service';
|
import { MerchantSyncService } from '../hub-users-management/merchant-sync-orchestrator.service';
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
@ -421,7 +421,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
this.currentMerchantConfigId = this.extractMerchantConfigId(user);
|
this.currentMerchantConfigId = this.extractMerchantConfigId(user);
|
||||||
|
|
||||||
if (this.currentUserRole) {
|
if (this.currentUserRole) {
|
||||||
this.userPermissions = this.roleService.getPermissionsForRole(this.currentUserRole);
|
|
||||||
this.canCreateMerchants = this.canManageMerchant();
|
this.canCreateMerchants = this.canManageMerchant();
|
||||||
this.canDeleteMerchants = this.canManageMerchant();
|
this.canDeleteMerchants = this.canManageMerchant();
|
||||||
this.canManageMerchants = this.canManageMerchant();
|
this.canManageMerchants = this.canManageMerchant();
|
||||||
@ -448,7 +447,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
// ==================== PERMISSIONS SPÉCIFIQUES MARCHAND ====================
|
// ==================== PERMISSIONS SPÉCIFIQUES MARCHAND ====================
|
||||||
|
|
||||||
private canManageMerchant(): boolean {
|
private canManageMerchant(): boolean {
|
||||||
return this.roleService.canManageMerchants(this.currentUserRole);
|
return this.roleService.isAnyAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -21,8 +21,8 @@ import { MyProfile } from '@modules/profile/profile';
|
|||||||
import { Documentation } from '@modules/documentation/documentation';
|
import { Documentation } from '@modules/documentation/documentation';
|
||||||
import { Help } from '@modules/help/help';
|
import { Help } from '@modules/help/help';
|
||||||
import { About } from '@modules/about/about';
|
import { About } from '@modules/about/about';
|
||||||
import { SubscriptionsManagement } from './subscriptions/subscriptions';
|
import { Subscriptions } from './subscriptions/subscriptions';
|
||||||
import { SubscriptionPayments } from './subscriptions/subscription-payments/subscription-payments';
|
import { SubscriptionDetails } from './subscriptions/subscription-details/subscription-details';
|
||||||
import { MerchantConfigManagement } from './merchant-config/merchant-config';
|
import { MerchantConfigManagement } from './merchant-config/merchant-config';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@ -93,7 +93,7 @@ const routes: Routes = [
|
|||||||
// ---------------------------
|
// ---------------------------
|
||||||
{
|
{
|
||||||
path: 'subscriptions',
|
path: 'subscriptions',
|
||||||
component: SubscriptionsManagement,
|
component: Subscriptions,
|
||||||
canActivate: [authGuard, roleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
title: 'Gestion des Abonnements',
|
title: 'Gestion des Abonnements',
|
||||||
@ -108,61 +108,6 @@ const routes: Routes = [
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'subscriptions/merchant',
|
|
||||||
component: SubscriptionsManagement,
|
|
||||||
canActivate: [authGuard, roleGuard],
|
|
||||||
data: {
|
|
||||||
title: 'Abonnements par Merchant',
|
|
||||||
module: 'subscriptions-merchant',
|
|
||||||
requiredRoles: [
|
|
||||||
'dcb-admin',
|
|
||||||
'dcb-support',
|
|
||||||
'dcb-partner',
|
|
||||||
'dcb-partner-admin',
|
|
||||||
'dcb-partner-manager',
|
|
||||||
'dcb-partner-support',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---------------------------
|
|
||||||
// Subscriptions Payments
|
|
||||||
// ---------------------------
|
|
||||||
{
|
|
||||||
path: 'subscriptions/payments',
|
|
||||||
component: SubscriptionsManagement,
|
|
||||||
canActivate: [authGuard, roleGuard],
|
|
||||||
data: {
|
|
||||||
title: 'Paiements des Abonnements',
|
|
||||||
module: 'subscriptions-payments',
|
|
||||||
requiredRoles: [
|
|
||||||
'dcb-admin',
|
|
||||||
'dcb-support',
|
|
||||||
'dcb-partner',
|
|
||||||
'dcb-partner-admin',
|
|
||||||
'dcb-partner-manager',
|
|
||||||
'dcb-partner-support',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'subscriptions/:id/payments',
|
|
||||||
component: SubscriptionPayments,
|
|
||||||
canActivate: [authGuard, roleGuard],
|
|
||||||
data: {
|
|
||||||
title: 'Détails des Paiements',
|
|
||||||
module: 'subscriptions-payments',
|
|
||||||
requiredRoles: [
|
|
||||||
'dcb-admin',
|
|
||||||
'dcb-support',
|
|
||||||
'dcb-partner',
|
|
||||||
'dcb-partner-admin',
|
|
||||||
'dcb-partner-manager',
|
|
||||||
'dcb-partner-support',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Partners
|
// Partners
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
|
|
||||||
import { HubUsersService } from '@modules/hub-users-management/hub-users.service';
|
import { HubUsersService } from '@modules/hub-users-management/hub-users.service';
|
||||||
import { MerchantUsersService } from '@modules/hub-users-management/merchant-users.service';
|
import { MerchantUsersService } from '@modules/hub-users-management/merchant-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@ -0,0 +1,219 @@
|
|||||||
|
import { Injectable, Injector, inject } from '@angular/core';
|
||||||
|
import { BehaviorSubject, filter, map, Observable, of, take, tap } from 'rxjs';
|
||||||
|
import { RoleManagementService, UserRole } from '@core/services/hub-users-roles-management.service';
|
||||||
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
|
||||||
|
export interface SubscriptionAccess {
|
||||||
|
// Permissions de visualisation
|
||||||
|
canViewSubscriptions: boolean;
|
||||||
|
canViewAllSubscriptions: boolean;
|
||||||
|
canViewDetails: boolean;
|
||||||
|
canViewPayments: boolean;
|
||||||
|
|
||||||
|
// Permissions administratives
|
||||||
|
canManageAll: boolean;
|
||||||
|
canFilterByMerchant: boolean;
|
||||||
|
|
||||||
|
// Scope
|
||||||
|
allowedMerchantIds: number[];
|
||||||
|
isHubUser: boolean;
|
||||||
|
isMerchantUser: boolean;
|
||||||
|
|
||||||
|
// Informations utilisateur
|
||||||
|
userRole: UserRole;
|
||||||
|
userRoleLabel: string;
|
||||||
|
merchantId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class SubscriptionAccessService {
|
||||||
|
private accessCache: SubscriptionAccess | null = null;
|
||||||
|
private ready$ = new BehaviorSubject<boolean>(false);
|
||||||
|
private profileLoaded = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private roleService: RoleManagementService,
|
||||||
|
private authService: AuthService
|
||||||
|
) {
|
||||||
|
// Initialisation simple
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize(): void {
|
||||||
|
console.log('🚀 DashboardAccessService: Initialisation');
|
||||||
|
|
||||||
|
// S'abonner aux changements de profil
|
||||||
|
this.authService.getUserProfile().subscribe({
|
||||||
|
next: (profile) => {
|
||||||
|
if (profile) {
|
||||||
|
console.log('✅ TransactionAccessService: Profil chargé', {
|
||||||
|
username: profile.username,
|
||||||
|
merchantPartnerId: profile.merchantPartnerId,
|
||||||
|
userType: profile.userType
|
||||||
|
});
|
||||||
|
this.profileLoaded = true;
|
||||||
|
this.ready$.next(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('❌ DashboardAccessService: Erreur de profil:', err);
|
||||||
|
this.profileLoaded = false;
|
||||||
|
this.ready$.next(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nettoyer à la déconnexion
|
||||||
|
this.authService.getAuthState().subscribe(isAuthenticated => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
console.log('🚨 DashboardAccessService: Déconnexion détectée');
|
||||||
|
this.clearCache();
|
||||||
|
this.profileLoaded = false;
|
||||||
|
this.ready$.next(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attendre que le service soit prêt AVEC PROFIL CHARGÉ
|
||||||
|
waitForReady(): Observable<boolean> {
|
||||||
|
return this.ready$.pipe(
|
||||||
|
filter(ready => ready && this.profileLoaded), // <-- VÉRIFIER LES DEUX
|
||||||
|
take(1),
|
||||||
|
tap(() => {
|
||||||
|
console.log('✅ DashboardAccessService: waitForReady() - Service vraiment prêt');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubscriptionAccess(): SubscriptionAccess {
|
||||||
|
if (this.accessCache) {
|
||||||
|
return this.accessCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRole = this.roleService.getCurrentRole() || UserRole.DCB_SUPPORT;
|
||||||
|
const isHubUser = this.roleService.isHubUser();
|
||||||
|
const merchantId = this.getCurrentMerchantId();
|
||||||
|
|
||||||
|
const access: SubscriptionAccess = {
|
||||||
|
// Permissions de visualisation
|
||||||
|
canViewSubscriptions: this.canViewSubscriptions(userRole, isHubUser),
|
||||||
|
canViewAllSubscriptions: this.canViewAllSubscriptions(userRole, isHubUser),
|
||||||
|
canViewDetails: this.canViewDetails(userRole, isHubUser),
|
||||||
|
canViewPayments: this.canViewPayments(userRole, isHubUser),
|
||||||
|
|
||||||
|
// Permissions administratives
|
||||||
|
canManageAll: this.canManageAll(userRole, isHubUser),
|
||||||
|
canFilterByMerchant: this.canFilterByMerchant(userRole, isHubUser),
|
||||||
|
|
||||||
|
// Scope
|
||||||
|
allowedMerchantIds: this.getAllowedMerchantIds(isHubUser, merchantId),
|
||||||
|
isHubUser,
|
||||||
|
isMerchantUser: !isHubUser,
|
||||||
|
|
||||||
|
// Informations utilisateur
|
||||||
|
userRole,
|
||||||
|
userRoleLabel: this.roleService.getRoleLabel(userRole) || 'Utilisateur',
|
||||||
|
merchantId
|
||||||
|
};
|
||||||
|
|
||||||
|
this.accessCache = access;
|
||||||
|
return access;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtenir l'accès dashboard avec attente
|
||||||
|
getSubscriptionAccessAsync(): Observable<SubscriptionAccess> {
|
||||||
|
return this.waitForReady().pipe(
|
||||||
|
map(() => this.getSubscriptionAccess())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MÉTHODES DE DÉTERMINATION DES PERMISSIONS ===
|
||||||
|
|
||||||
|
private canViewSubscriptions(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// Tous les utilisateurs peuvent voir les abonnements (avec restrictions de scope)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private canViewAllSubscriptions(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// Hub users et DCB_PARTNER_ADMIN peuvent voir tous les abonnements
|
||||||
|
return isHubUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private canViewDetails(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// Tous peuvent voir les détails (avec restrictions de scope)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private canViewPayments(userRole: UserRole, isHubUser: boolean): boolean {
|
||||||
|
// Tous peuvent voir les paiements
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === GESTION DU SCOPE ===
|
||||||
|
|
||||||
|
private getCurrentMerchantId(): number | undefined {
|
||||||
|
const merchantPartnerId = this.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 [];
|
||||||
|
} else {
|
||||||
|
// Merchant users: seulement leur merchant
|
||||||
|
return merchantId ? [merchantId] : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MÉTHODES PUBLIQUES ===
|
||||||
|
|
||||||
|
canAccessSubscription(subscriptionMerchantId?: number): Observable<boolean> {
|
||||||
|
const access = this.getSubscriptionAccess();
|
||||||
|
|
||||||
|
// Hub users avec permission de tout voir
|
||||||
|
if (access.canManageAll) {
|
||||||
|
return of(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si pas de merchant ID sur l'abonnement, accès limité
|
||||||
|
if (!subscriptionMerchantId) {
|
||||||
|
return of(access.isHubUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merchant users: seulement leur merchant
|
||||||
|
if (access.isMerchantUser) {
|
||||||
|
return of(access.allowedMerchantIds.includes(subscriptionMerchantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hub users: vérifier si le merchant est dans leur liste autorisée
|
||||||
|
if (access.allowedMerchantIds.length === 0) {
|
||||||
|
return of(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return of(access.allowedMerchantIds.includes(subscriptionMerchantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer le cache
|
||||||
|
clearCache(): void {
|
||||||
|
this.accessCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rafraîchir les permissions
|
||||||
|
refreshAccess(): void {
|
||||||
|
this.clearCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,269 @@
|
|||||||
|
<div class="subscription-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 à cet abonnement.</p>
|
||||||
|
<button class="btn btn-primary" routerLink="/subscriptions">
|
||||||
|
<ng-icon name="lucideArrowLeft" class="me-1"></ng-icon>
|
||||||
|
Retour à la liste
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<!-- Loading State -->
|
||||||
|
@if (loading && !subscription) {
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-muted">Chargement des détails de l'abonnement...</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
@if (error) {
|
||||||
|
<div class="alert alert-danger d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
|
<div class="flex-grow-1">{{ error }}</div>
|
||||||
|
<button class="btn-close" (click)="error = ''"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (success) {
|
||||||
|
<div class="alert alert-success d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||||
|
<div class="flex-grow-1">{{ success }}</div>
|
||||||
|
<button class="btn-close" (click)="success = ''"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (subscription && !loading) {
|
||||||
|
<div class="row">
|
||||||
|
<!-- Colonne principale -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<!-- En-tête de l'abonnement -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<h5 class="card-title mb-0 me-3">Abonnement #{{ subscription.id }}</h5>
|
||||||
|
<span [class]="getStatusBadgeClass(subscription.status)" class="badge">
|
||||||
|
<ng-icon [name]="getStatusIcon(subscription.status)" class="me-1"></ng-icon>
|
||||||
|
{{ getStatusDisplayName(subscription.status) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" (click)="copyToClipboard(subscription.id)"
|
||||||
|
ngbTooltip="Copier l'ID">
|
||||||
|
<ng-icon name="lucideCopy"></ng-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" (click)="printDetails()"
|
||||||
|
ngbTooltip="Imprimer">
|
||||||
|
<ng-icon name="lucidePrinter"></ng-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-primary btn-sm" (click)="loadSubscriptionDetails()"
|
||||||
|
[disabled]="loading" ngbTooltip="Actualiser">
|
||||||
|
<ng-icon name="lucideRefreshCw" [class.spin]="loading"></ng-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Montant et informations principales -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="subscription-amount-icon bg-primary rounded-circle p-3 me-3">
|
||||||
|
<ng-icon name="lucideEuro" class="text-white fs-4"></ng-icon>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-muted small">Montant</div>
|
||||||
|
<div class="h3 mb-0 text-success">
|
||||||
|
{{ formatCurrency(subscription.amount, subscription.currency) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center h-100">
|
||||||
|
<div class="subscription-periodicity-icon bg-info rounded-circle p-3 me-3">
|
||||||
|
<ng-icon name="lucideRepeat" class="text-white fs-4"></ng-icon>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-muted small">Périodicité</div>
|
||||||
|
<div class="h6 mb-0">
|
||||||
|
<span [class]="getPeriodicityBadgeClass(subscription.periodicity)" class="badge">
|
||||||
|
{{ getPeriodicityDisplayName(subscription.periodicity) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informations détaillées -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h6 class="border-bottom pb-2 mb-3">Informations de l'abonnement</h6>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label text-muted small mb-1">Date de début</label>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
||||||
|
<div>
|
||||||
|
<div class="fw-medium">{{ formatDate(subscription.startDate) }}</div>
|
||||||
|
<small class="text-muted">{{ formatRelativeTime(subscription.startDate) }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (subscription.nextPaymentDate) {
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label text-muted small mb-1">Prochain paiement</label>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
||||||
|
<div>
|
||||||
|
<div class="fw-medium">{{ formatDate(subscription.nextPaymentDate) }}</div>
|
||||||
|
<small class="text-muted">Dans {{ getDaysUntilNextPayment() }} jour(s)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (subscription.endDate) {
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label text-muted small mb-1">Date de fin</label>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
||||||
|
<div>
|
||||||
|
<div class="fw-medium">{{ formatDate(subscription.endDate) }}</div>
|
||||||
|
@if (isExpiringSoon()) {
|
||||||
|
<small class="text-warning">
|
||||||
|
<ng-icon name="lucideAlertCircle" class="me-1"></ng-icon>
|
||||||
|
Expire bientôt
|
||||||
|
</small>
|
||||||
|
} @else if (isExpired()) {
|
||||||
|
<small class="text-danger">
|
||||||
|
<ng-icon name="lucideXCircle" class="me-1"></ng-icon>
|
||||||
|
Expiré
|
||||||
|
</small>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (subscription.externalReference) {
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label text-muted small mb-1">Référence externe</label>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="font-monospace small">{{ subscription.externalReference }}</span>
|
||||||
|
<button class="btn btn-sm btn-link p-0 ms-2" (click)="copyToClipboard(subscription.externalReference!)">
|
||||||
|
<ng-icon name="lucideCopy" class="text-muted"></ng-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informations techniques -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h6 class="border-bottom pb-2 mb-3">Informations techniques</h6>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<label class="form-label text-muted small mb-1">Créé le</label>
|
||||||
|
<div class="small">{{ formatDate(subscription.createdAt) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<label class="form-label text-muted small mb-1">Mis à jour le</label>
|
||||||
|
<div class="small">{{ formatDate(subscription.updatedAt) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<label class="form-label text-muted small mb-1">Token</label>
|
||||||
|
<div class="small font-monospace text-truncate" [title]="subscription.token">
|
||||||
|
{{ subscription.token.substring(0, 30) }}...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<label class="form-label text-muted small mb-1">Échecs</label>
|
||||||
|
<div class="small">{{ subscription.failureCount || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonne latérale - Informations complémentaires -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<!-- Informations marchand et client -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="card-title mb-0">Informations marchand & client</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="small">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="text-muted">Merchant ID:</span>
|
||||||
|
<span class="font-monospace">{{ subscription.merchantPartnerId }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (subscription.customerId) {
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="text-muted">Client ID:</span>
|
||||||
|
<span class="font-monospace small">{{ subscription.customerId }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (subscription.planId) {
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="text-muted">Plan ID:</span>
|
||||||
|
<span class="font-monospace small">{{ subscription.planId }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (subscription.serviceId) {
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="text-muted">Service ID:</span>
|
||||||
|
<span class="font-monospace small">{{ subscription.serviceId }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Métadonnées -->
|
||||||
|
@if (subscription.metadata) {
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="card-title mb-0">Métadonnées</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre class="small mb-0">{{ subscription.metadata | json }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Abonnement non trouvé -->
|
||||||
|
@if (!subscription && !loading) {
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<ng-icon name="lucideAlertCircle" class="text-muted fs-1 mb-3"></ng-icon>
|
||||||
|
<h5 class="text-muted">Abonnement non trouvé</h5>
|
||||||
|
<p class="text-muted mb-4">L'abonnement avec l'ID "{{ subscriptionId }}" n'existe pas ou a été supprimé.</p>
|
||||||
|
<button class="btn btn-primary" routerLink="/subscriptions">
|
||||||
|
<ng-icon name="lucideArrowLeft" class="me-1"></ng-icon>
|
||||||
|
Retour à la liste
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,297 @@
|
|||||||
|
import { Component, inject, OnInit, Input, ChangeDetectorRef } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { NgIcon } from '@ng-icons/core';
|
||||||
|
import {
|
||||||
|
lucideArrowLeft,
|
||||||
|
lucideCopy,
|
||||||
|
lucideRefreshCw,
|
||||||
|
lucidePrinter,
|
||||||
|
lucideCheckCircle,
|
||||||
|
lucideClock,
|
||||||
|
lucideXCircle,
|
||||||
|
lucidePauseCircle,
|
||||||
|
lucideCalendarOff,
|
||||||
|
lucideCalendar,
|
||||||
|
lucideEuro,
|
||||||
|
lucidePackage,
|
||||||
|
lucideUser,
|
||||||
|
lucideAlertCircle,
|
||||||
|
lucideInfo,
|
||||||
|
lucideShield,
|
||||||
|
lucideStore,
|
||||||
|
lucideLock,
|
||||||
|
lucideRepeat
|
||||||
|
} from '@ng-icons/lucide';
|
||||||
|
import { NgbAlertModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { SubscriptionsService } from '../services/subscriptions.service';
|
||||||
|
import { SubscriptionAccessService, SubscriptionAccess } from '../services/subscription-access.service';
|
||||||
|
import {
|
||||||
|
Subscription,
|
||||||
|
SubscriptionStatus,
|
||||||
|
SubscriptionPeriodicity,
|
||||||
|
Currency
|
||||||
|
} from '@core/models/dcb-bo-hub-subscription.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-subscription-details',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
RouterModule,
|
||||||
|
NgIcon,
|
||||||
|
NgbAlertModule,
|
||||||
|
NgbTooltipModule
|
||||||
|
],
|
||||||
|
templateUrl: './subscription-details.html'
|
||||||
|
})
|
||||||
|
export class SubscriptionDetails implements OnInit {
|
||||||
|
private subscriptionsService = inject(SubscriptionsService);
|
||||||
|
private accessService = inject(SubscriptionAccessService);
|
||||||
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
|
@Input() subscriptionId!: string;
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
access!: SubscriptionAccess;
|
||||||
|
|
||||||
|
// Données
|
||||||
|
subscription: Subscription | null = null;
|
||||||
|
loading = false;
|
||||||
|
error = '';
|
||||||
|
success = '';
|
||||||
|
|
||||||
|
// Accès
|
||||||
|
accessDenied = false;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.initializePermissions();
|
||||||
|
|
||||||
|
if (this.subscriptionId) {
|
||||||
|
this.loadSubscriptionDetails();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializePermissions() {
|
||||||
|
this.access = this.accessService.getSubscriptionAccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSubscriptionDetails() {
|
||||||
|
if (!this.access.canViewDetails) {
|
||||||
|
this.error = 'Vous n\'avez pas la permission de voir les détails des abonnements';
|
||||||
|
this.accessDenied = true;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
this.accessDenied = false;
|
||||||
|
|
||||||
|
this.subscriptionsService.getSubscriptionById(this.subscriptionId).subscribe({
|
||||||
|
next: (subscription) => {
|
||||||
|
// Vérifier si l'utilisateur a accès à cet abonnement spécifique
|
||||||
|
this.accessService.canAccessSubscription(subscription.merchantPartnerId)
|
||||||
|
.subscribe(canAccess => {
|
||||||
|
if (!canAccess) {
|
||||||
|
this.error = 'Vous n\'avez pas accès à cet abonnement';
|
||||||
|
this.accessDenied = true;
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subscription = subscription;
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.error = 'Erreur lors du chargement des détails de l\'abonnement';
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
console.error('Error loading subscription details:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilitaires
|
||||||
|
copyToClipboard(text: string) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
this.success = 'Copié dans le presse-papier';
|
||||||
|
setTimeout(() => this.success = '', 3000);
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
printDetails() {
|
||||||
|
window.print();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters pour l'affichage
|
||||||
|
getStatusBadgeClass(status: SubscriptionStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case SubscriptionStatus.ACTIVE: return 'badge bg-success';
|
||||||
|
case SubscriptionStatus.SUSPENDED: return 'badge bg-warning';
|
||||||
|
case SubscriptionStatus.CANCELLED: return 'badge bg-danger';
|
||||||
|
case SubscriptionStatus.EXPIRED: return 'badge bg-secondary';
|
||||||
|
case SubscriptionStatus.PENDING: return 'badge bg-info';
|
||||||
|
default: return 'badge bg-secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatusIcon(status: SubscriptionStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case SubscriptionStatus.ACTIVE: return 'lucideCheckCircle';
|
||||||
|
case SubscriptionStatus.SUSPENDED: return 'lucidePauseCircle';
|
||||||
|
case SubscriptionStatus.CANCELLED: return 'lucideXCircle';
|
||||||
|
case SubscriptionStatus.EXPIRED: return 'lucideCalendarOff';
|
||||||
|
case SubscriptionStatus.PENDING: return 'lucideClock';
|
||||||
|
default: return 'lucideClock';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeriodicityBadgeClass(periodicity: SubscriptionPeriodicity): string {
|
||||||
|
switch (periodicity) {
|
||||||
|
case SubscriptionPeriodicity.DAILY: return 'badge bg-primary';
|
||||||
|
case SubscriptionPeriodicity.WEEKLY: return 'badge bg-info';
|
||||||
|
case SubscriptionPeriodicity.MONTHLY: return 'badge bg-success';
|
||||||
|
case SubscriptionPeriodicity.YEARLY: return 'badge bg-warning';
|
||||||
|
default: return 'badge bg-secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatCurrency(amount: number, currency: Currency = Currency.XOF): string {
|
||||||
|
return new Intl.NumberFormat('fr-FR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: currency
|
||||||
|
}).format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(date: string | Date): string {
|
||||||
|
if (!date) return '-';
|
||||||
|
|
||||||
|
const dateObj = date instanceof Date ? date : new Date(date);
|
||||||
|
|
||||||
|
if (isNaN(dateObj.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(dateObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatRelativeTime(date: string | Date): string {
|
||||||
|
const now = new Date();
|
||||||
|
const dateObj = date instanceof Date ? date : new Date(date);
|
||||||
|
|
||||||
|
if (isNaN(dateObj.getTime())) {
|
||||||
|
return 'Date invalide';
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffMs = now.getTime() - dateObj.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
const diffHours = Math.floor(diffMins / 60);
|
||||||
|
const diffDays = Math.floor(diffHours / 24);
|
||||||
|
|
||||||
|
if (diffMins < 1) return 'À l\'instant';
|
||||||
|
if (diffMins < 60) return `Il y a ${diffMins} min`;
|
||||||
|
if (diffHours < 24) return `Il y a ${diffHours} h`;
|
||||||
|
if (diffDays < 7) return `Il y a ${diffDays} j`;
|
||||||
|
return this.formatDate(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatusDisplayName(status: SubscriptionStatus): string {
|
||||||
|
const statusNames = {
|
||||||
|
[SubscriptionStatus.ACTIVE]: 'Actif',
|
||||||
|
[SubscriptionStatus.SUSPENDED]: 'Suspendu',
|
||||||
|
[SubscriptionStatus.CANCELLED]: 'Annulé',
|
||||||
|
[SubscriptionStatus.EXPIRED]: 'Expiré',
|
||||||
|
[SubscriptionStatus.PENDING]: 'En attente'
|
||||||
|
};
|
||||||
|
return statusNames[status] || status;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeriodicityDisplayName(periodicity: SubscriptionPeriodicity): string {
|
||||||
|
const periodicityNames = {
|
||||||
|
[SubscriptionPeriodicity.DAILY]: 'Quotidien',
|
||||||
|
[SubscriptionPeriodicity.WEEKLY]: 'Hebdomadaire',
|
||||||
|
[SubscriptionPeriodicity.MONTHLY]: 'Mensuel',
|
||||||
|
[SubscriptionPeriodicity.YEARLY]: 'Annuel'
|
||||||
|
};
|
||||||
|
return periodicityNames[periodicity] || periodicity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si l'abonnement est sur le point d'expirer
|
||||||
|
isExpiringSoon(): boolean {
|
||||||
|
if (!this.subscription?.endDate) return false;
|
||||||
|
const endDate = new Date(this.subscription.endDate);
|
||||||
|
const today = new Date();
|
||||||
|
const diffTime = endDate.getTime() - today.getTime();
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
return diffDays <= 7 && diffDays > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si l'abonnement est expiré
|
||||||
|
isExpired(): boolean {
|
||||||
|
if (!this.subscription?.endDate) return false;
|
||||||
|
return new Date(this.subscription.endDate) < new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer les jours jusqu'au prochain paiement
|
||||||
|
getDaysUntilNextPayment(): number {
|
||||||
|
if (!this.subscription?.nextPaymentDate) return 0;
|
||||||
|
const nextPayment = new Date(this.subscription.nextPaymentDate);
|
||||||
|
const today = new Date();
|
||||||
|
const diffTime = nextPayment.getTime() - today.getTime();
|
||||||
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
// AJOUTEZ CES MÉTHODES POUR LES MÉTADONNÉES :
|
||||||
|
getMetadataItems(): { key: string; value: string }[] {
|
||||||
|
if (!this.subscription?.metadata) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(this.subscription.metadata).map(([key, value]) => ({
|
||||||
|
key: this.formatMetadataKey(key),
|
||||||
|
value: this.formatMetadataValue(value)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatMetadataKey(key: string): string {
|
||||||
|
// Convertir camelCase ou snake_case en texte lisible
|
||||||
|
return key
|
||||||
|
.replace(/([A-Z])/g, ' $1')
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.replace(/^./, str => str.toUpperCase())
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatMetadataValue(value: any): string {
|
||||||
|
if (value === null || value === undefined) return 'N/A';
|
||||||
|
if (typeof value === 'object') return JSON.stringify(value);
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,332 +0,0 @@
|
|||||||
<app-ui-card [title]="getCardTitle()">
|
|
||||||
<a
|
|
||||||
helper-text
|
|
||||||
href="javascript:void(0);"
|
|
||||||
class="icon-link icon-link-hover link-primary fw-semibold"
|
|
||||||
>
|
|
||||||
<ng-icon [name]="getHelperIcon()" class="me-1"></ng-icon>
|
|
||||||
{{ getHelperText() }}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div card-body>
|
|
||||||
|
|
||||||
<!-- Barre d'actions supérieure -->
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<!-- Statistiques rapides -->
|
|
||||||
<div class="btn-group btn-group-sm">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-primary"
|
|
||||||
[class.active]="statusFilter === 'all'"
|
|
||||||
(click)="filterByStatus('all')"
|
|
||||||
>
|
|
||||||
Tous ({{ getTotalPaymentsCount() }})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-success"
|
|
||||||
[class.active]="statusFilter === 'SUCCESS'"
|
|
||||||
(click)="filterByStatus('SUCCESS')"
|
|
||||||
>
|
|
||||||
Réussis ({{ getSuccessfulPaymentsCount() }})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-danger"
|
|
||||||
[class.active]="statusFilter === 'FAILED'"
|
|
||||||
(click)="filterByStatus('FAILED')"
|
|
||||||
>
|
|
||||||
Échoués ({{ getFailedPaymentsCount() }})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-warning text-dark"
|
|
||||||
[class.active]="statusFilter === 'PENDING'"
|
|
||||||
(click)="filterByStatus('PENDING')"
|
|
||||||
>
|
|
||||||
En attente ({{ getPendingPaymentsCount() }})
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="d-flex justify-content-end gap-2">
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
(click)="refreshData()"
|
|
||||||
[disabled]="loading"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideRefreshCw" class="me-1" [class.spin]="loading"></ng-icon>
|
|
||||||
Actualiser
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Barre de recherche et filtres avancés -->
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-text">
|
|
||||||
<ng-icon name="lucideSearch"></ng-icon>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="Rechercher par référence, description..."
|
|
||||||
[(ngModel)]="searchTerm"
|
|
||||||
(input)="onSearch()"
|
|
||||||
[disabled]="loading"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<select class="form-select" [(ngModel)]="statusFilter" (change)="applyFiltersAndPagination()">
|
|
||||||
@for (status of availableStatuses; track status.value) {
|
|
||||||
<option [value]="status.value">{{ status.label }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<select class="form-select" [(ngModel)]="dateFilter" (change)="applyFiltersAndPagination()">
|
|
||||||
@for (dateRange of availableDateRanges; track dateRange.value) {
|
|
||||||
<option [value]="dateRange.value">{{ dateRange.label }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-2">
|
|
||||||
<button class="btn btn-outline-secondary w-100" (click)="onClearFilters()" [disabled]="loading">
|
|
||||||
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
|
||||||
Effacer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading State -->
|
|
||||||
@if (loading) {
|
|
||||||
<div class="text-center py-4">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden">Chargement...</span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-muted">{{ getLoadingText() }}</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Error State -->
|
|
||||||
@if (error && !loading) {
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
|
||||||
<div>{{ error }}</div>
|
|
||||||
<button class="btn-close ms-auto" (click)="error = ''"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Payments Table -->
|
|
||||||
@if (!loading && !error) {
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover table-striped">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<!-- Colonne Abonnement pour la vue globale -->
|
|
||||||
@if (showSubscriptionColumn) {
|
|
||||||
<th>Abonnement</th>
|
|
||||||
}
|
|
||||||
<th (click)="sort('reference')" class="cursor-pointer">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<span>Référence</span>
|
|
||||||
<ng-icon [name]="getSortIcon('reference')" class="ms-1 fs-12"></ng-icon>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th (click)="sort('createdAt')" class="cursor-pointer">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<span>Date</span>
|
|
||||||
<ng-icon [name]="getSortIcon('createdAt')" class="ms-1 fs-12"></ng-icon>
|
|
||||||
</div>
|
|
||||||
</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 (click)="sort('status')" class="cursor-pointer">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<span>Statut</span>
|
|
||||||
<ng-icon [name]="getSortIcon('status')" class="ms-1 fs-12"></ng-icon>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th width="120">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (payment of displayedPayments; track payment.id) {
|
|
||||||
<tr>
|
|
||||||
<!-- Colonne Abonnement pour la vue globale -->
|
|
||||||
@if (showSubscriptionColumn) {
|
|
||||||
<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">
|
|
||||||
<ng-icon name="lucideRepeat" class="text-primary fs-12"></ng-icon>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<small class="text-muted">
|
|
||||||
#{{ payment.subscriptionId }}
|
|
||||||
</small>
|
|
||||||
<button
|
|
||||||
class="btn btn-link btn-sm p-0 ms-1"
|
|
||||||
(click)="navigateToSubscription(payment.subscriptionId)"
|
|
||||||
title="Voir l'abonnement"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideExternalLink" size="12"></ng-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
}
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="avatar-sm bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
|
||||||
<ng-icon name="lucideCreditCard" class="text-info fs-12"></ng-icon>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong class="d-block font-monospace small">
|
|
||||||
{{ truncateText(payment.reference, 15) }}
|
|
||||||
</strong>
|
|
||||||
<small class="text-muted">
|
|
||||||
Ref: {{ getInternalReference(payment) }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div>
|
|
||||||
<small class="text-muted d-block">
|
|
||||||
{{ formatDate(payment.createdAt) }}
|
|
||||||
</small>
|
|
||||||
<small class="text-muted">
|
|
||||||
{{ formatDateTime(payment.createdAt) }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<strong class="d-block text-success">
|
|
||||||
{{ formatAmount(payment.amount, payment.currency) }}
|
|
||||||
</strong>
|
|
||||||
<small class="text-muted">{{ payment.currency }}</small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge d-flex align-items-center" [ngClass]="getPaymentStatusBadgeClass(payment.status)">
|
|
||||||
<ng-icon [name]="getPaymentStatusIcon(payment.status)" class="me-1" size="14"></ng-icon>
|
|
||||||
{{ getPaymentStatusDisplayName(payment.status) }}
|
|
||||||
</span>
|
|
||||||
@if (payment.description) {
|
|
||||||
<div class="mt-1">
|
|
||||||
<small class="text-muted">
|
|
||||||
{{ truncateText(payment.description, 30) }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-primary btn-sm"
|
|
||||||
(click)="viewPaymentDetails(payment.id)"
|
|
||||||
title="Voir les détails"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideEye"></ng-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-info btn-sm"
|
|
||||||
(click)="navigateToSubscription(payment.subscriptionId)"
|
|
||||||
title="Voir l'abonnement"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideRepeat"></ng-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
@empty {
|
|
||||||
<tr>
|
|
||||||
<td [attr.colspan]="getColumnCount()" class="text-center py-4">
|
|
||||||
<div class="text-muted">
|
|
||||||
<ng-icon name="lucideCreditCard" class="fs-1 mb-3 opacity-50"></ng-icon>
|
|
||||||
<h5 class="mb-2">{{ getEmptyStateTitle() }}</h5>
|
|
||||||
<p class="mb-3">{{ getEmptyStateDescription() }}</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
@if (totalPages > 1) {
|
|
||||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
|
||||||
<div class="text-muted">
|
|
||||||
Affichage de {{ getStartIndex() }} à {{ getEndIndex() }} sur {{ totalItems }} paiements
|
|
||||||
</div>
|
|
||||||
<nav>
|
|
||||||
<ngb-pagination
|
|
||||||
[collectionSize]="totalItems"
|
|
||||||
[page]="currentPage"
|
|
||||||
[pageSize]="itemsPerPage"
|
|
||||||
[maxSize]="5"
|
|
||||||
[rotate]="true"
|
|
||||||
[boundaryLinks]="true"
|
|
||||||
(pageChange)="onPageChange($event)"
|
|
||||||
/>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Résumé des résultats -->
|
|
||||||
@if (displayedPayments.length > 0) {
|
|
||||||
<div class="mt-3 pt-3 border-top">
|
|
||||||
<div class="row text-center">
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">
|
|
||||||
<strong>Total :</strong> {{ getPaymentsSummary().total }} paiements
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">
|
|
||||||
<strong>Réussis :</strong> {{ getPaymentsSummary().success }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">
|
|
||||||
<strong>Échoués :</strong> {{ getPaymentsSummary().failed }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">
|
|
||||||
<strong>En attente :</strong> {{ getPaymentsSummary().pending }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">
|
|
||||||
<strong>Montant total :</strong> {{ formatAmount(getPaymentsSummary().amount, Currency.XOF) }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">
|
|
||||||
<strong>Taux de réussite :</strong> {{ getSuccessRate().toFixed(1) }}%
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</app-ui-card>
|
|
||||||
@ -1,478 +0,0 @@
|
|||||||
import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, Input, OnDestroy } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { NgIcon } from '@ng-icons/core';
|
|
||||||
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
import { Observable, Subject, map, of } from 'rxjs';
|
|
||||||
import { catchError, takeUntil } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import {
|
|
||||||
SubscriptionPayment,
|
|
||||||
SubscriptionPaymentsResponse,
|
|
||||||
Currency
|
|
||||||
} from '@core/models/dcb-bo-hub-subscription.model';
|
|
||||||
|
|
||||||
import { SubscriptionsService } from '../subscriptions.service';
|
|
||||||
import { AuthService } from '@core/services/auth.service';
|
|
||||||
import { UiCard } from '@app/components/ui-card';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-subscription-payments-list',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
NgIcon,
|
|
||||||
UiCard,
|
|
||||||
NgbPaginationModule
|
|
||||||
],
|
|
||||||
templateUrl: './subscription-payments-list.html',
|
|
||||||
})
|
|
||||||
export class SubscriptionPaymentsList implements OnInit, OnDestroy {
|
|
||||||
private authService = inject(AuthService);
|
|
||||||
private subscriptionsService = inject(SubscriptionsService);
|
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
|
||||||
private destroy$ = new Subject<void>();
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
readonly Currency = Currency;
|
|
||||||
|
|
||||||
// Inputs
|
|
||||||
@Input() subscriptionId!: string;
|
|
||||||
|
|
||||||
// Outputs
|
|
||||||
@Output() paymentSelected = new EventEmitter<string>();
|
|
||||||
@Output() viewSubscriptionDetails = new EventEmitter<string>();
|
|
||||||
|
|
||||||
// Données
|
|
||||||
allPayments: SubscriptionPayment[] = [];
|
|
||||||
filteredPayments: SubscriptionPayment[] = [];
|
|
||||||
displayedPayments: SubscriptionPayment[] = [];
|
|
||||||
|
|
||||||
// États
|
|
||||||
loading = false;
|
|
||||||
error = '';
|
|
||||||
|
|
||||||
// Recherche et filtres
|
|
||||||
searchTerm = '';
|
|
||||||
statusFilter: 'all' | 'SUCCESS' | 'FAILED' | 'PENDING' = 'all';
|
|
||||||
dateFilter: 'all' | 'week' | 'month' | 'quarter' = 'all';
|
|
||||||
|
|
||||||
// Pagination
|
|
||||||
currentPage = 1;
|
|
||||||
itemsPerPage = 10;
|
|
||||||
totalItems = 0;
|
|
||||||
totalPages = 0;
|
|
||||||
|
|
||||||
// Tri
|
|
||||||
sortField: keyof SubscriptionPayment = 'createdAt';
|
|
||||||
sortDirection: 'asc' | 'desc' = 'desc';
|
|
||||||
|
|
||||||
// Filtres disponibles
|
|
||||||
availableStatuses: { value: 'all' | 'SUCCESS' | 'FAILED' | 'PENDING'; label: string }[] = [
|
|
||||||
{ value: 'all', label: 'Tous les statuts' },
|
|
||||||
{ value: 'SUCCESS', label: 'Réussis' },
|
|
||||||
{ value: 'FAILED', label: 'Échoués' },
|
|
||||||
{ value: 'PENDING', label: 'En attente' }
|
|
||||||
];
|
|
||||||
|
|
||||||
availableDateRanges: { value: 'all' | 'week' | 'month' | 'quarter'; label: string }[] = [
|
|
||||||
{ value: 'all', label: 'Toute période' },
|
|
||||||
{ value: 'week', label: '7 derniers jours' },
|
|
||||||
{ value: 'month', label: '30 derniers jours' },
|
|
||||||
{ value: 'quarter', label: '3 derniers mois' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Informations de l'abonnement
|
|
||||||
subscriptionDetails: any = null;
|
|
||||||
|
|
||||||
// Gestion des permissions
|
|
||||||
currentUserRole: string | null = null;
|
|
||||||
currentMerchantPartnerId: string = '';
|
|
||||||
|
|
||||||
// Getters pour la logique conditionnelle
|
|
||||||
get showSubscriptionColumn(): boolean {
|
|
||||||
return !this.subscriptionId; // Afficher la colonne abonnement seulement si on est dans la vue globale
|
|
||||||
}
|
|
||||||
|
|
||||||
get showAmountColumn(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
if (this.subscriptionId) {
|
|
||||||
this.loadCurrentUserPermissions();
|
|
||||||
this.loadPayments();
|
|
||||||
this.loadSubscriptionDetails();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.destroy$.next();
|
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadCurrentUserPermissions() {
|
|
||||||
this.authService.getUserProfile()
|
|
||||||
.pipe(takeUntil(this.destroy$))
|
|
||||||
.subscribe({
|
|
||||||
next: (user) => {
|
|
||||||
this.currentUserRole = this.extractUserRole(user);
|
|
||||||
|
|
||||||
console.log('Payments Context Loaded:', {
|
|
||||||
role: this.currentUserRole,
|
|
||||||
merchantPartnerId: this.currentMerchantPartnerId
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Error loading current user permissions:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractUserRole(user: any): string | null {
|
|
||||||
const userRoles = this.authService.getCurrentUserRoles();
|
|
||||||
if (userRoles && userRoles.length > 0) {
|
|
||||||
return userRoles[0];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private loadSubscriptionDetails() {
|
|
||||||
if (this.subscriptionId) {
|
|
||||||
this.subscriptionsService.getSubscriptionById(this.subscriptionId)
|
|
||||||
.pipe(takeUntil(this.destroy$))
|
|
||||||
.subscribe({
|
|
||||||
next: (subscription) => {
|
|
||||||
this.subscriptionDetails = subscription;
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Error loading subscription details:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPayments() {
|
|
||||||
this.loading = true;
|
|
||||||
this.error = '';
|
|
||||||
|
|
||||||
let paymentsObservable: Observable<SubscriptionPayment[]>;
|
|
||||||
|
|
||||||
if (this.subscriptionId) {
|
|
||||||
// Paiements d'un abonnement spécifique
|
|
||||||
paymentsObservable = this.subscriptionsService.getSubscriptionPayments(this.currentMerchantPartnerId, this.subscriptionId).pipe(
|
|
||||||
map((response: SubscriptionPaymentsResponse) => response.payments)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Vue globale des paiements (à implémenter si nécessaire)
|
|
||||||
paymentsObservable = of([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
paymentsObservable
|
|
||||||
.pipe(
|
|
||||||
takeUntil(this.destroy$),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('Error loading subscription payments:', error);
|
|
||||||
this.error = 'Erreur lors du chargement des paiements';
|
|
||||||
return of([] as SubscriptionPayment[]);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.subscribe({
|
|
||||||
next: (payments) => {
|
|
||||||
this.allPayments = payments || [];
|
|
||||||
console.log(`✅ Loaded ${this.allPayments.length} payments`);
|
|
||||||
this.applyFiltersAndPagination();
|
|
||||||
this.loading = false;
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
this.error = 'Erreur lors du chargement des paiements';
|
|
||||||
this.loading = false;
|
|
||||||
this.allPayments = [];
|
|
||||||
this.filteredPayments = [];
|
|
||||||
this.displayedPayments = [];
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recherche et filtres
|
|
||||||
onSearch() {
|
|
||||||
this.currentPage = 1;
|
|
||||||
this.applyFiltersAndPagination();
|
|
||||||
}
|
|
||||||
|
|
||||||
onClearFilters() {
|
|
||||||
this.searchTerm = '';
|
|
||||||
this.statusFilter = 'all';
|
|
||||||
this.dateFilter = 'all';
|
|
||||||
this.currentPage = 1;
|
|
||||||
this.applyFiltersAndPagination();
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFiltersAndPagination() {
|
|
||||||
if (!this.allPayments) {
|
|
||||||
this.allPayments = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Appliquer les filtres
|
|
||||||
this.filteredPayments = this.allPayments.filter(payment => {
|
|
||||||
const matchesSearch = !this.searchTerm ||
|
|
||||||
payment.reference.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
|
|
||||||
payment.description.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
|
|
||||||
(payment.metadata?.internatRef && payment.metadata.internatRef.toLowerCase().includes(this.searchTerm.toLowerCase()));
|
|
||||||
|
|
||||||
const matchesStatus = this.statusFilter === 'all' ||
|
|
||||||
payment.status === this.statusFilter;
|
|
||||||
|
|
||||||
const matchesDate = this.applyDateFilter(payment);
|
|
||||||
|
|
||||||
return matchesSearch && matchesStatus && matchesDate;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Appliquer le tri
|
|
||||||
this.filteredPayments.sort((a, b) => {
|
|
||||||
const aValue = a[this.sortField];
|
|
||||||
const bValue = b[this.sortField];
|
|
||||||
|
|
||||||
if (aValue === bValue) return 0;
|
|
||||||
|
|
||||||
let comparison = 0;
|
|
||||||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
|
||||||
comparison = aValue.localeCompare(bValue);
|
|
||||||
} else if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
||||||
comparison = aValue - bValue;
|
|
||||||
} else if (aValue instanceof Date && bValue instanceof Date) {
|
|
||||||
comparison = aValue.getTime() - bValue.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.sortDirection === 'asc' ? comparison : -comparison;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculer la pagination
|
|
||||||
this.totalItems = this.filteredPayments.length;
|
|
||||||
this.totalPages = Math.ceil(this.totalItems / this.itemsPerPage);
|
|
||||||
|
|
||||||
// Appliquer la pagination
|
|
||||||
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
|
||||||
const endIndex = startIndex + this.itemsPerPage;
|
|
||||||
this.displayedPayments = this.filteredPayments.slice(startIndex, endIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyDateFilter(payment: SubscriptionPayment): boolean {
|
|
||||||
if (this.dateFilter === 'all') return true;
|
|
||||||
|
|
||||||
const paymentDate = new Date(payment.createdAt);
|
|
||||||
const now = new Date();
|
|
||||||
let startDate = new Date();
|
|
||||||
|
|
||||||
switch (this.dateFilter) {
|
|
||||||
case 'week':
|
|
||||||
startDate.setDate(now.getDate() - 7);
|
|
||||||
break;
|
|
||||||
case 'month':
|
|
||||||
startDate.setMonth(now.getMonth() - 1);
|
|
||||||
break;
|
|
||||||
case 'quarter':
|
|
||||||
startDate.setMonth(now.getMonth() - 3);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return paymentDate >= startDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tri
|
|
||||||
sort(field: keyof SubscriptionPayment) {
|
|
||||||
if (this.sortField === field) {
|
|
||||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
|
||||||
} else {
|
|
||||||
this.sortField = field;
|
|
||||||
this.sortDirection = 'desc'; // Par défaut, tri décroissant pour les dates
|
|
||||||
}
|
|
||||||
this.applyFiltersAndPagination();
|
|
||||||
}
|
|
||||||
|
|
||||||
getSortIcon(field: string): string {
|
|
||||||
if (this.sortField !== field) return 'lucideArrowUpDown';
|
|
||||||
return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pagination
|
|
||||||
onPageChange(page: number) {
|
|
||||||
this.currentPage = page;
|
|
||||||
this.applyFiltersAndPagination();
|
|
||||||
}
|
|
||||||
|
|
||||||
getStartIndex(): number {
|
|
||||||
return (this.currentPage - 1) * this.itemsPerPage + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEndIndex(): number {
|
|
||||||
return Math.min(this.currentPage * this.itemsPerPage, this.totalItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
viewPaymentDetails(paymentId: string | number) {
|
|
||||||
this.paymentSelected.emit(paymentId.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToSubscription(subscriptionId: string | number) {
|
|
||||||
this.viewSubscriptionDetails.emit(subscriptionId.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utilitaires d'affichage
|
|
||||||
getPaymentStatusBadgeClass(status: string): string {
|
|
||||||
const statusClasses: { [key: string]: string } = {
|
|
||||||
'SUCCESS': 'badge bg-success',
|
|
||||||
'FAILED': 'badge bg-danger',
|
|
||||||
'PENDING': 'badge bg-warning'
|
|
||||||
};
|
|
||||||
return statusClasses[status] || 'badge bg-secondary';
|
|
||||||
}
|
|
||||||
|
|
||||||
getPaymentStatusDisplayName(status: string): string {
|
|
||||||
const statusNames: { [key: string]: string } = {
|
|
||||||
'SUCCESS': 'Réussi',
|
|
||||||
'FAILED': 'Échoué',
|
|
||||||
'PENDING': 'En attente'
|
|
||||||
};
|
|
||||||
return statusNames[status] || status;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPaymentStatusIcon(status: string): string {
|
|
||||||
const statusIcons: { [key: string]: string } = {
|
|
||||||
'SUCCESS': 'lucideCheckCircle',
|
|
||||||
'FAILED': 'lucideXCircle',
|
|
||||||
'PENDING': 'lucideClock'
|
|
||||||
};
|
|
||||||
return statusIcons[status] || 'lucideHelpCircle';
|
|
||||||
}
|
|
||||||
|
|
||||||
formatAmount(amount: number, currency: Currency): string {
|
|
||||||
return new Intl.NumberFormat('fr-FR', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: currency
|
|
||||||
}).format(amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDate(dateString: string): string {
|
|
||||||
return new Date(dateString).toLocaleDateString('fr-FR', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDateTime(dateString: string): string {
|
|
||||||
return new Date(dateString).toLocaleDateString('fr-FR', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Statistiques
|
|
||||||
getTotalPaymentsCount(): number {
|
|
||||||
return this.allPayments.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSuccessfulPaymentsCount(): number {
|
|
||||||
return this.allPayments.filter(payment => payment.status === 'SUCCESS').length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFailedPaymentsCount(): number {
|
|
||||||
return this.allPayments.filter(payment => payment.status === 'FAILED').length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPendingPaymentsCount(): number {
|
|
||||||
return this.allPayments.filter(payment => payment.status === 'PENDING').length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotalAmount(): number {
|
|
||||||
return this.allPayments
|
|
||||||
.filter(payment => payment.status === 'SUCCESS')
|
|
||||||
.reduce((sum, payment) => sum + payment.amount, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSuccessRate(): number {
|
|
||||||
if (this.allPayments.length === 0) return 0;
|
|
||||||
return (this.getSuccessfulPaymentsCount() / this.allPayments.length) * 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recherche rapide par statut
|
|
||||||
filterByStatus(status: 'all' | 'SUCCESS' | 'FAILED' | 'PENDING') {
|
|
||||||
this.statusFilter = status;
|
|
||||||
this.currentPage = 1;
|
|
||||||
this.applyFiltersAndPagination();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recharger les données
|
|
||||||
refreshData() {
|
|
||||||
this.loadPayments();
|
|
||||||
if (this.subscriptionId) {
|
|
||||||
this.loadSubscriptionDetails();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthodes pour le template
|
|
||||||
getCardTitle(): string {
|
|
||||||
if (this.subscriptionId && this.subscriptionDetails) {
|
|
||||||
return `Paiements - Abonnement #${this.subscriptionDetails.id}`;
|
|
||||||
}
|
|
||||||
return 'Historique des Paiements';
|
|
||||||
}
|
|
||||||
|
|
||||||
getHelperText(): string {
|
|
||||||
if (this.subscriptionId) {
|
|
||||||
return 'Historique des paiements pour cet abonnement';
|
|
||||||
}
|
|
||||||
return 'Vue globale de tous les paiements';
|
|
||||||
}
|
|
||||||
|
|
||||||
getHelperIcon(): string {
|
|
||||||
return 'lucideCreditCard';
|
|
||||||
}
|
|
||||||
|
|
||||||
getLoadingText(): string {
|
|
||||||
return 'Chargement des paiements...';
|
|
||||||
}
|
|
||||||
|
|
||||||
getEmptyStateTitle(): string {
|
|
||||||
return 'Aucun paiement trouvé';
|
|
||||||
}
|
|
||||||
|
|
||||||
getEmptyStateDescription(): string {
|
|
||||||
return 'Aucun paiement ne correspond à vos critères de recherche.';
|
|
||||||
}
|
|
||||||
|
|
||||||
getColumnCount(): number {
|
|
||||||
let count = 5; // Référence, Date, Montant, Statut, Actions
|
|
||||||
if (this.showSubscriptionColumn) count++;
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthodes pour les résumés
|
|
||||||
getPaymentsSummary(): { total: number; success: number; failed: number; pending: number; amount: number } {
|
|
||||||
return {
|
|
||||||
total: this.getTotalPaymentsCount(),
|
|
||||||
success: this.getSuccessfulPaymentsCount(),
|
|
||||||
failed: this.getFailedPaymentsCount(),
|
|
||||||
pending: this.getPendingPaymentsCount(),
|
|
||||||
amount: this.getTotalAmount()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Formatage pour l'affichage
|
|
||||||
truncateText(text: string, maxLength: number = 20): string {
|
|
||||||
if (!text) return '';
|
|
||||||
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
|
|
||||||
}
|
|
||||||
|
|
||||||
getInternalReference(payment: SubscriptionPayment): string {
|
|
||||||
return payment.metadata?.internatRef || 'N/A';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -13,8 +13,8 @@ import {
|
|||||||
Currency
|
Currency
|
||||||
} from '@core/models/dcb-bo-hub-subscription.model';
|
} from '@core/models/dcb-bo-hub-subscription.model';
|
||||||
|
|
||||||
import { SubscriptionsService } from '../subscriptions.service';
|
import { SubscriptionsService } from '../services/subscriptions.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { SubscriptionAccessService, SubscriptionAccess } from '../services/subscription-access.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-subscription-payments',
|
selector: 'app-subscription-payments',
|
||||||
@ -33,7 +33,7 @@ import { AuthService } from '@core/services/auth.service';
|
|||||||
})
|
})
|
||||||
export class SubscriptionPayments implements OnInit, OnDestroy {
|
export class SubscriptionPayments implements OnInit, OnDestroy {
|
||||||
private subscriptionsService = inject(SubscriptionsService);
|
private subscriptionsService = inject(SubscriptionsService);
|
||||||
private authService = inject(AuthService);
|
private accessService = inject(SubscriptionAccessService); // CHANGÉ
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
@ -51,8 +51,9 @@ export class SubscriptionPayments implements OnInit, OnDestroy {
|
|||||||
error = '';
|
error = '';
|
||||||
success = '';
|
success = '';
|
||||||
|
|
||||||
// Gestion des permissions
|
// Gestion des permissions MIS À JOUR
|
||||||
currentUserRole: string | null = null;
|
access!: SubscriptionAccess;
|
||||||
|
accessDenied = false;
|
||||||
|
|
||||||
merchantPartnerId: number | undefined;
|
merchantPartnerId: number | undefined;
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ export class SubscriptionPayments implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.subscriptionId) {
|
if (this.subscriptionId) {
|
||||||
this.loadCurrentUserPermissions();
|
this.initializePermissions(); // NOUVELLE MÉTHODE
|
||||||
this.loadSubscriptionDetails();
|
this.loadSubscriptionDetails();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,49 +74,49 @@ export class SubscriptionPayments implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Charge les permissions de l'utilisateur courant
|
* Initialise les permissions
|
||||||
*/
|
*/
|
||||||
private loadCurrentUserPermissions(): void {
|
private initializePermissions(): void {
|
||||||
this.authService.getUserProfile()
|
this.access = this.accessService.getSubscriptionAccess();
|
||||||
.pipe(takeUntil(this.destroy$))
|
|
||||||
.subscribe({
|
|
||||||
next: (profile) => {
|
|
||||||
this.currentUserRole = this.authService.getCurrentUserRole();
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Error loading user permissions:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Charge les détails de l'abonnement puis les paiements
|
* Charge les détails de l'abonnement puis les paiements
|
||||||
*/
|
*/
|
||||||
loadSubscriptionDetails() {
|
loadSubscriptionDetails() {
|
||||||
|
if (!this.access.canViewPayments) {
|
||||||
|
this.error = 'Vous n\'avez pas la permission de voir les paiements';
|
||||||
|
this.accessDenied = true;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
|
this.accessDenied = false;
|
||||||
|
|
||||||
this.subscriptionsService.getSubscriptionById(this.subscriptionId)
|
this.subscriptionsService.getSubscriptionById(this.subscriptionId)
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (subscription) => {
|
next: (subscription) => {
|
||||||
console.log('Subscription loaded:', {
|
// Vérifier l'accès à cet abonnement spécifique
|
||||||
id: subscription.id,
|
this.accessService.canAccessSubscription(subscription.merchantPartnerId)
|
||||||
merchantPartnerId: subscription.merchantPartnerId,
|
.subscribe(canAccess => {
|
||||||
hasMerchantId: !!subscription.merchantPartnerId
|
if (!canAccess) {
|
||||||
});
|
this.error = 'Vous n\'avez pas accès aux paiements de cet abonnement';
|
||||||
|
this.accessDenied = true;
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.merchantPartnerId = subscription.merchantPartnerId,
|
this.merchantPartnerId = subscription.merchantPartnerId;
|
||||||
|
this.subscription = subscription;
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
|
||||||
console.log("loadSubscriptionDetails " + this.merchantPartnerId);
|
this.loadPayments(subscription.merchantPartnerId);
|
||||||
|
});
|
||||||
this.subscription = subscription;
|
|
||||||
this.loading = false;
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
|
|
||||||
// Passer explicitement le merchantPartnerId à loadPayments
|
|
||||||
this.loadPayments(subscription.merchantPartnerId);
|
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.error = 'Erreur lors du chargement des détails de l\'abonnement';
|
this.error = 'Erreur lors du chargement des détails de l\'abonnement';
|
||||||
@ -132,12 +133,8 @@ export class SubscriptionPayments implements OnInit, OnDestroy {
|
|||||||
loadPayments(merchantPartnerId: number | undefined) {
|
loadPayments(merchantPartnerId: number | undefined) {
|
||||||
this.loadingPayments = true;
|
this.loadingPayments = true;
|
||||||
|
|
||||||
console.log("loadPayments " + merchantPartnerId);
|
|
||||||
|
|
||||||
// Utiliser le merchantPartnerId passé en paramètre ou celui de l'abonnement
|
|
||||||
const merchantId = merchantPartnerId;
|
const merchantId = merchantPartnerId;
|
||||||
|
|
||||||
// Vérifier que nous avons les IDs nécessaires
|
|
||||||
if (!merchantId) {
|
if (!merchantId) {
|
||||||
console.error('MerchantPartnerId manquant pour charger les paiements');
|
console.error('MerchantPartnerId manquant pour charger les paiements');
|
||||||
this.error = 'Impossible de charger les paiements : Merchant ID manquant';
|
this.error = 'Impossible de charger les paiements : Merchant ID manquant';
|
||||||
@ -146,12 +143,6 @@ export class SubscriptionPayments implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Loading payments with:', {
|
|
||||||
merchantId: merchantId,
|
|
||||||
subscriptionId: this.subscriptionId,
|
|
||||||
merchantIdType: typeof merchantId
|
|
||||||
});
|
|
||||||
|
|
||||||
this.subscriptionsService.getSubscriptionPayments(merchantId, this.subscriptionId)
|
this.subscriptionsService.getSubscriptionPayments(merchantId, this.subscriptionId)
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
@ -163,7 +154,6 @@ export class SubscriptionPayments implements OnInit, OnDestroy {
|
|||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Error loading subscription payments:', error);
|
console.error('Error loading subscription payments:', error);
|
||||||
|
|
||||||
// Message d'erreur plus spécifique
|
|
||||||
if (error.status === 400) {
|
if (error.status === 400) {
|
||||||
this.error = 'Données invalides pour charger les paiements';
|
this.error = 'Données invalides pour charger les paiements';
|
||||||
} else if (error.status === 404) {
|
} else if (error.status === 404) {
|
||||||
@ -177,11 +167,12 @@ export class SubscriptionPayments implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applique les filtres aux paiements
|
* Applique les filtres aux paiements
|
||||||
*/
|
*/
|
||||||
applyFilters() {
|
applyFilters() {
|
||||||
this.loadPayments(this.merchantPartnerId); // Recharger avec les filtres actuels
|
this.loadPayments(this.merchantPartnerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -420,20 +411,16 @@ export class SubscriptionPayments implements OnInit, OnDestroy {
|
|||||||
return colors[health];
|
return colors[health];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion des erreurs
|
// NOUVELLES MÉTHODES POUR LE TEMPLATE
|
||||||
private getErrorMessage(error: any): string {
|
getUserBadgeClass(): string {
|
||||||
if (error.error?.message) {
|
return this.access.isHubUser ? 'bg-primary' : 'bg-success';
|
||||||
return error.error.message;
|
}
|
||||||
}
|
|
||||||
if (error.status === 400) {
|
getUserBadgeIcon(): string {
|
||||||
return 'Données invalides.';
|
return this.access.isHubUser ? 'lucideShield' : 'lucideStore';
|
||||||
}
|
}
|
||||||
if (error.status === 403) {
|
|
||||||
return 'Vous n\'avez pas les permissions pour accéder à ces informations.';
|
getUserBadgeText(): string {
|
||||||
}
|
return this.access.isHubUser ? 'Hub User' : 'Merchant User';
|
||||||
if (error.status === 404) {
|
|
||||||
return 'Abonnement ou paiements non trouvés.';
|
|
||||||
}
|
|
||||||
return 'Erreur lors de l\'opération. Veuillez réessayer.';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,72 +1,54 @@
|
|||||||
<app-ui-card [title]="getCardTitle()">
|
<div class="transactions-container">
|
||||||
<a
|
<!-- En-tête avec actions -->
|
||||||
helper-text
|
<div class="row mb-4">
|
||||||
href="javascript:void(0);"
|
<div class="col-12">
|
||||||
class="icon-link icon-link-hover link-primary fw-semibold"
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
>
|
<div>
|
||||||
<ng-icon [name]="getHelperIcon()" class="me-1"></ng-icon>
|
<h4 class="mb-1">Gestion des Abonnements</h4>
|
||||||
{{ getHelperText() }}
|
<div class="d-flex align-items-center gap-2">
|
||||||
</a>
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb mb-0">
|
||||||
<div card-body>
|
<li class="breadcrumb-item">
|
||||||
|
<a href="javascript:void(0)" class="text-decoration-none">DCB Abonnements</a>
|
||||||
<!-- Barre d'actions supérieure -->
|
</li>
|
||||||
<div class="row mb-3">
|
</ol>
|
||||||
<div class="col-md-6">
|
</nav>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<span [class]="getUserBadgeClass()" class="badge">
|
||||||
<!-- Statistiques rapides -->
|
<ng-icon [name]="getUserBadgeIcon()" class="me-1"></ng-icon>
|
||||||
<div class="btn-group btn-group-sm">
|
{{ getUserBadgeText() }}
|
||||||
<button
|
</span>
|
||||||
type="button"
|
@if (currentMerchantId) {
|
||||||
class="btn btn-outline-primary"
|
<span class="badge bg-info">
|
||||||
[class.active]="statusFilter === 'all'"
|
<ng-icon name="lucideStore" class="me-1"></ng-icon>
|
||||||
(click)="filterByStatus('all')"
|
Merchant {{ currentMerchantId }}
|
||||||
>
|
</span>
|
||||||
Tous ({{ getTotalSubscriptionsCount() }})
|
}
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-success"
|
|
||||||
[class.active]="statusFilter === getActiveStatus()"
|
|
||||||
(click)="filterByStatus(getActiveStatus())"
|
|
||||||
>
|
|
||||||
Actifs ({{ getActiveSubscriptionsCount() }})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-warning text-dark"
|
|
||||||
[class.active]="statusFilter === getSuspendedStatus()"
|
|
||||||
(click)="filterByStatus(getSuspendedStatus())"
|
|
||||||
>
|
|
||||||
Suspendus ({{ getSuspendedSubscriptionsCount() }})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-danger"
|
|
||||||
[class.active]="statusFilter === getCancelledStatus()"
|
|
||||||
(click)="filterByStatus(getCancelledStatus())"
|
|
||||||
>
|
|
||||||
Annulés ({{ getCancelledSubscriptionsCount() }})
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="d-flex gap-2">
|
||||||
<div class="d-flex justify-content-end gap-2">
|
<button class="btn btn-outline-secondary" (click)="loadAllSubscriptions()" [disabled]="loading">
|
||||||
<button
|
<ng-icon name="lucideRefreshCw" [class.spin]="loading"></ng-icon>
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
(click)="refreshData()"
|
|
||||||
[disabled]="loading"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideRefreshCw" class="me-1" [class.spin]="loading"></ng-icon>
|
|
||||||
Actualiser
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Barre de recherche et filtres avancés -->
|
<!-- Message d'accès refusé -->
|
||||||
|
@if (!access.canViewSubscriptions) {
|
||||||
|
<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 {
|
||||||
|
|
||||||
|
<!-- Barre de recherche et filtres -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@ -78,237 +60,163 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Rechercher par ID, token..."
|
placeholder="Rechercher par ID, token..."
|
||||||
[(ngModel)]="searchTerm"
|
[(ngModel)]="searchTerm"
|
||||||
(input)="onSearch()"
|
(keyup.enter)="onSearch()"
|
||||||
[disabled]="loading"
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-8">
|
||||||
<select class="form-select" [(ngModel)]="statusFilter" (change)="applyFiltersAndPagination()">
|
<div class="d-flex gap-2">
|
||||||
@for (status of availableStatuses; track status.value) {
|
<!-- Filtre statut -->
|
||||||
<option [value]="status.value">{{ status.label }}</option>
|
<select class="form-select" style="width: auto;" (change)="onStatusFilterChange($any($event.target).value)">
|
||||||
}
|
@for (status of statusOptions; track status.value) {
|
||||||
</select>
|
<option [value]="status.value">{{ status.label }}</option>
|
||||||
</div>
|
}
|
||||||
|
</select>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<!-- Filtre périodicité -->
|
||||||
<select class="form-select" [(ngModel)]="periodicityFilter" (change)="applyFiltersAndPagination()">
|
<select class="form-select" style="width: auto;" (change)="onPeriodicityFilterChange($any($event.target).value)">
|
||||||
@for (periodicity of availablePeriodicities; track periodicity.value) {
|
@for (periodicity of periodicityOptions; track periodicity.value) {
|
||||||
<option [value]="periodicity.value">{{ periodicity.label }}</option>
|
<option [value]="periodicity.value">{{ periodicity.label }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-2">
|
<button class="btn btn-outline-primary" (click)="onSearch()">
|
||||||
<select class="form-select" [(ngModel)]="merchantFilter" (change)="applyFiltersAndPagination()">
|
<ng-icon name="lucideFilter" class="me-1"></ng-icon>
|
||||||
<option value="all">Tous les merchants</option>
|
Filtrer
|
||||||
<!-- Les options merchants seraient dynamiquement chargées si nécessaire -->
|
</button>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-2">
|
<button class="btn btn-outline-secondary" (click)="onClearFilters()">
|
||||||
<button class="btn btn-outline-secondary w-100" (click)="onClearFilters()" [disabled]="loading">
|
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||||
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
Effacer
|
||||||
Effacer
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Messages d'erreur -->
|
||||||
|
@if (error) {
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<ng-icon name="lucideXCircle" class="me-2"></ng-icon>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
@if (loading) {
|
@if (loading) {
|
||||||
<div class="text-center py-4">
|
<div class="text-center py-4">
|
||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Chargement...</span>
|
<span class="visually-hidden">Chargement...</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-muted">{{ getLoadingText() }}</p>
|
<p class="mt-2 text-muted">Chargement des abonnements...</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Error State -->
|
<!-- Tableau des abonnements -->
|
||||||
@if (error && !loading) {
|
@if (!loading) {
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="card">
|
||||||
<div class="d-flex align-items-center">
|
<div class="card-body p-0">
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
<div class="table-responsive">
|
||||||
<div>{{ error }}</div>
|
<table class="table table-hover mb-0">
|
||||||
<button class="btn-close ms-auto" (click)="error = ''"></button>
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th (click)="sort('id')" class="cursor-pointer">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span>ID</span>
|
||||||
|
<ng-icon [name]="getSortIcon('id')" class="ms-1 fs-12"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</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>Périodicité</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
<th (click)="sort('startDate')" class="cursor-pointer">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span>Date début</span>
|
||||||
|
<ng-icon [name]="getSortIcon('startDate')" class="ms-1 fs-12"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>Prochain paiement</th>
|
||||||
|
<th width="80">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (subscription of subscriptions; track subscription.id) {
|
||||||
|
<tr>
|
||||||
|
<td class="font-monospace small">{{ subscription.id }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-secondary">
|
||||||
|
Merchant {{ subscription.merchantPartnerId }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text-success">
|
||||||
|
{{ formatCurrency(subscription.amount, subscription.currency) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span [class]="getPeriodicityBadgeClass(subscription.periodicity)" class="badge">
|
||||||
|
{{ getPeriodicityDisplayName(subscription.periodicity) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span [class]="getStatusBadgeClass(subscription.status)" class="badge">
|
||||||
|
<ng-icon [name]="getStatusIcon(subscription.status)" class="me-1"></ng-icon>
|
||||||
|
{{ getStatusDisplayName(subscription.status) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="small text-muted">
|
||||||
|
{{ formatDate(subscription.startDate) }}
|
||||||
|
</td>
|
||||||
|
<td class="small text-muted">
|
||||||
|
@if (subscription.nextPaymentDate) {
|
||||||
|
{{ formatDate(subscription.nextPaymentDate) }}
|
||||||
|
} @else {
|
||||||
|
-
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-primary"
|
||||||
|
(click)="viewSubscriptionDetails(subscription.id)"
|
||||||
|
ngbTooltip="Voir les détails"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideEye"></ng-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center py-4">
|
||||||
|
<ng-icon name="lucideRepeat" class="text-muted fs-1 mb-2 d-block"></ng-icon>
|
||||||
|
<p class="text-muted mb-3">Aucun abonnement trouvé</p>
|
||||||
|
<button class="btn btn-primary" (click)="onClearFilters()">
|
||||||
|
Réinitialiser les filtres
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Subscriptions Table -->
|
|
||||||
@if (!loading && !error) {
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover table-striped">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<!-- Colonne Merchant pour les admins -->
|
|
||||||
@if (showMerchantColumn) {
|
|
||||||
<th>Merchant</th>
|
|
||||||
}
|
|
||||||
<!-- Colonne Customer pour les admins -->
|
|
||||||
@if (showCustomerColumn) {
|
|
||||||
<th>Client</th>
|
|
||||||
}
|
|
||||||
<th (click)="sort('id')" class="cursor-pointer">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<span>ID</span>
|
|
||||||
<ng-icon [name]="getSortIcon('id')" class="ms-1 fs-12"></ng-icon>
|
|
||||||
</div>
|
|
||||||
</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>Périodicité</th>
|
|
||||||
<th (click)="sort('status')" class="cursor-pointer">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<span>Statut</span>
|
|
||||||
<ng-icon [name]="getSortIcon('status')" class="ms-1 fs-12"></ng-icon>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th (click)="sort('startDate')" class="cursor-pointer">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<span>Date début</span>
|
|
||||||
<ng-icon [name]="getSortIcon('startDate')" class="ms-1 fs-12"></ng-icon>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th width="120">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (subscription of displayedSubscriptions; track subscription.id) {
|
|
||||||
<tr>
|
|
||||||
<!-- Colonne Merchant pour les admins -->
|
|
||||||
@if (showMerchantColumn) {
|
|
||||||
<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">
|
|
||||||
#{{ subscription.merchantPartnerId }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
}
|
|
||||||
<!-- Colonne Customer pour les admins -->
|
|
||||||
@if (showCustomerColumn) {
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="avatar-sm bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
|
||||||
<ng-icon name="lucideUser" class="text-info fs-12"></ng-icon>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<small class="text-muted font-monospace">
|
|
||||||
#{{ subscription.customerId }}
|
|
||||||
</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">
|
|
||||||
<ng-icon name="lucideRepeat" class="text-primary fs-12"></ng-icon>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong class="d-block">#{{ subscription.id }}</strong>
|
|
||||||
<small class="text-muted" [title]="subscription.token">
|
|
||||||
{{ subscription.token.substring(0, 12) }}...
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<strong class="d-block">{{ formatAmount(subscription.amount, subscription.currency) }}</strong>
|
|
||||||
<small class="text-muted">{{ subscription.currency }}</small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-light text-dark d-flex align-items-center">
|
|
||||||
<ng-icon name="lucideCalendar" class="me-1" size="14"></ng-icon>
|
|
||||||
{{ getPeriodicityDisplayName(subscription.periodicity) }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge d-flex align-items-center" [ngClass]="getStatusBadgeClass(subscription.status)">
|
|
||||||
<ng-icon
|
|
||||||
[name]="subscription.status === SubscriptionStatus.ACTIVE ? 'lucideCheckCircle' :
|
|
||||||
subscription.status === SubscriptionStatus.SUSPENDED ? 'lucidePauseCircle' :
|
|
||||||
subscription.status === SubscriptionStatus.CANCELLED ? 'lucideXCircle' : 'lucideClock'"
|
|
||||||
class="me-1"
|
|
||||||
size="14"
|
|
||||||
></ng-icon>
|
|
||||||
{{ getStatusDisplayName(subscription.status) }}
|
|
||||||
</span>
|
|
||||||
@if (isExpiringSoon(subscription)) {
|
|
||||||
<div class="mt-1">
|
|
||||||
<small class="text-warning">
|
|
||||||
<ng-icon name="lucideAlertTriangle" class="me-1" size="12"></ng-icon>
|
|
||||||
Expire bientôt
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div>
|
|
||||||
<small class="text-muted d-block">
|
|
||||||
Début: {{ formatDate(subscription.startDate) }}
|
|
||||||
</small>
|
|
||||||
<small class="text-muted d-block">
|
|
||||||
Prochain: {{ formatDate(subscription.nextPaymentDate) }}
|
|
||||||
</small>
|
|
||||||
@if (subscription.endDate) {
|
|
||||||
<small class="text-muted d-block">
|
|
||||||
Fin: {{ formatDate(subscription.endDate) }}
|
|
||||||
</small>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-primary btn-sm"
|
|
||||||
(click)="viewSubscriptionDetails(subscription.id)"
|
|
||||||
title="Voir les détails"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideEye"></ng-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-info btn-sm"
|
|
||||||
(click)="viewSubscriptionPayments(subscription.id)"
|
|
||||||
title="Voir les paiements"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideCreditCard"></ng-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
@empty {
|
|
||||||
<tr>
|
|
||||||
<td [attr.colspan]="getColumnCount()" class="text-center py-4">
|
|
||||||
<div class="text-muted">
|
|
||||||
<ng-icon name="lucideRepeat" class="fs-1 mb-3 opacity-50"></ng-icon>
|
|
||||||
<h5 class="mb-2">{{ getEmptyStateTitle() }}</h5>
|
|
||||||
<p class="mb-3">{{ getEmptyStateDescription() }}</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
@if (totalPages > 1) {
|
@if (totalPages > 1) {
|
||||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Affichage de {{ getStartIndex() }} à {{ getEndIndex() }} sur {{ totalItems }} abonnements
|
Affichage de {{ (currentPage - 1) * itemsPerPage + 1 }} à
|
||||||
|
{{ (currentPage * itemsPerPage) > totalItems ? totalItems : (currentPage * itemsPerPage) }}
|
||||||
|
sur {{ totalItems }} abonnements
|
||||||
</div>
|
</div>
|
||||||
<nav>
|
<nav>
|
||||||
<ngb-pagination
|
<ngb-pagination
|
||||||
@ -323,39 +231,6 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Résumé des résultats -->
|
|
||||||
@if (displayedSubscriptions.length > 0) {
|
|
||||||
<div class="mt-3 pt-3 border-top">
|
|
||||||
<div class="row text-center">
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">
|
|
||||||
<strong>Total :</strong> {{ getTotalSubscriptionsCount() }} abonnements
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">
|
|
||||||
<strong>Actifs :</strong> {{ getActiveSubscriptionsCount() }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">
|
|
||||||
<strong>Revenu total :</strong> {{ formatAmount(getTotalRevenue(), Currency.XOF) }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">
|
|
||||||
<strong>Quotidiens :</strong> {{ getDailySubscriptionsCount() }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">
|
|
||||||
<strong>Mensuels :</strong> {{ getMonthlySubscriptionsCount() }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</div>
|
}
|
||||||
</app-ui-card>
|
</div>
|
||||||
@ -1,23 +1,19 @@
|
|||||||
import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, Input, OnDestroy } from '@angular/core';
|
import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbPaginationModule, NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Observable, Subject, map, of } from 'rxjs';
|
|
||||||
import { catchError, takeUntil } from 'rxjs/operators';
|
import { SubscriptionsService } from '../services/subscriptions.service';
|
||||||
|
import { SubscriptionAccessService, SubscriptionAccess } from '../services/subscription-access.service';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Subscription,
|
Subscription,
|
||||||
SubscriptionsResponse,
|
|
||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
SubscriptionPeriodicity,
|
SubscriptionPeriodicity,
|
||||||
Currency
|
Currency
|
||||||
} from '@core/models/dcb-bo-hub-subscription.model';
|
} from '@core/models/dcb-bo-hub-subscription.model';
|
||||||
|
|
||||||
import { SubscriptionsService } from '../subscriptions.service';
|
|
||||||
import { AuthService } from '@core/services/auth.service';
|
|
||||||
import { UiCard } from '@app/components/ui-card';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-subscriptions-list',
|
selector: 'app-subscriptions-list',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@ -25,53 +21,50 @@ import { UiCard } from '@app/components/ui-card';
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
NgIcon,
|
NgIcon,
|
||||||
UiCard,
|
NgbPaginationModule,
|
||||||
NgbPaginationModule
|
NgbDropdownModule,
|
||||||
|
NgbTooltipModule
|
||||||
],
|
],
|
||||||
templateUrl: './subscriptions-list.html',
|
templateUrl: './subscriptions-list.html'
|
||||||
})
|
})
|
||||||
export class SubscriptionsList implements OnInit, OnDestroy {
|
export class SubscriptionsList implements OnInit {
|
||||||
private authService = inject(AuthService);
|
|
||||||
private subscriptionsService = inject(SubscriptionsService);
|
private subscriptionsService = inject(SubscriptionsService);
|
||||||
|
private accessService = inject(SubscriptionAccessService);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
private destroy$ = new Subject<void>();
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
readonly SubscriptionStatus = SubscriptionStatus;
|
|
||||||
readonly SubscriptionPeriodicity = SubscriptionPeriodicity;
|
|
||||||
readonly Currency = Currency;
|
|
||||||
|
|
||||||
// Outputs
|
|
||||||
@Output() subscriptionSelected = new EventEmitter<string>();
|
@Output() subscriptionSelected = new EventEmitter<string>();
|
||||||
@Output() viewPaymentsRequested = new EventEmitter<string>();
|
@Output() viewPaymentsRequested = new EventEmitter<string>();
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
access!: SubscriptionAccess;
|
||||||
|
currentUserRole = '';
|
||||||
|
currentMerchantId: number = 0;
|
||||||
|
|
||||||
// Données
|
// Données
|
||||||
allSubscriptions: Subscription[] = [];
|
subscriptions: Subscription[] = [];
|
||||||
filteredSubscriptions: Subscription[] = [];
|
allSubscriptions: Subscription[] = []; // Stocker toutes les données pour filtrage client
|
||||||
displayedSubscriptions: Subscription[] = [];
|
|
||||||
|
|
||||||
// États
|
// États
|
||||||
loading = false;
|
loading = false;
|
||||||
error = '';
|
error = '';
|
||||||
|
|
||||||
// Recherche et filtres
|
// Filtres et recherche
|
||||||
searchTerm = '';
|
searchTerm = '';
|
||||||
statusFilter: SubscriptionStatus | 'all' = 'all';
|
statusFilter: SubscriptionStatus | 'all' = 'all';
|
||||||
periodicityFilter: SubscriptionPeriodicity | 'all' = 'all';
|
periodicityFilter: SubscriptionPeriodicity | 'all' = 'all';
|
||||||
merchantFilter: number | 'all' = 'all';
|
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
itemsPerPage = 10;
|
itemsPerPage = 20;
|
||||||
totalItems = 0;
|
totalItems = 0;
|
||||||
totalPages = 0;
|
totalPages = 0;
|
||||||
|
|
||||||
// Tri
|
// Tri
|
||||||
sortField: keyof Subscription = 'startDate';
|
sortField: string = 'startDate';
|
||||||
sortDirection: 'asc' | 'desc' = 'desc';
|
sortDirection: 'asc' | 'desc' = 'desc';
|
||||||
|
|
||||||
// Filtres disponibles
|
// Options de filtre
|
||||||
availableStatuses: { value: SubscriptionStatus | 'all'; label: string }[] = [
|
statusOptions = [
|
||||||
{ value: 'all', label: 'Tous les statuts' },
|
{ value: 'all', label: 'Tous les statuts' },
|
||||||
{ value: SubscriptionStatus.ACTIVE, label: 'Actif' },
|
{ value: SubscriptionStatus.ACTIVE, label: 'Actif' },
|
||||||
{ value: SubscriptionStatus.SUSPENDED, label: 'Suspendu' },
|
{ value: SubscriptionStatus.SUSPENDED, label: 'Suspendu' },
|
||||||
@ -80,7 +73,7 @@ export class SubscriptionsList implements OnInit, OnDestroy {
|
|||||||
{ value: SubscriptionStatus.PENDING, label: 'En attente' }
|
{ value: SubscriptionStatus.PENDING, label: 'En attente' }
|
||||||
];
|
];
|
||||||
|
|
||||||
availablePeriodicities: { value: SubscriptionPeriodicity | 'all'; label: string }[] = [
|
periodicityOptions = [
|
||||||
{ value: 'all', label: 'Toutes les périodicités' },
|
{ value: 'all', label: 'Toutes les périodicités' },
|
||||||
{ value: SubscriptionPeriodicity.DAILY, label: 'Quotidien' },
|
{ value: SubscriptionPeriodicity.DAILY, label: 'Quotidien' },
|
||||||
{ value: SubscriptionPeriodicity.WEEKLY, label: 'Hebdomadaire' },
|
{ value: SubscriptionPeriodicity.WEEKLY, label: 'Hebdomadaire' },
|
||||||
@ -88,203 +81,178 @@ export class SubscriptionsList implements OnInit, OnDestroy {
|
|||||||
{ value: SubscriptionPeriodicity.YEARLY, label: 'Annuel' }
|
{ value: SubscriptionPeriodicity.YEARLY, label: 'Annuel' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// ID du merchant partner courant et permissions
|
|
||||||
currentMerchantPartnerId: string = '';
|
|
||||||
currentUserRole: string | null = null;
|
|
||||||
canViewAllMerchants = false;
|
|
||||||
|
|
||||||
// Merchants disponibles pour le filtre
|
|
||||||
availableMerchants: { value: number | 'all'; label: string }[] = [];
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadCurrentUserPermissions();
|
this.initializePermissions();
|
||||||
|
this.loadAllSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
private initializePermissions() {
|
||||||
this.destroy$.next();
|
this.access = this.accessService.getSubscriptionAccess();
|
||||||
this.destroy$.complete();
|
this.currentUserRole = this.access.userRoleLabel;
|
||||||
}
|
this.currentMerchantId = this.access.merchantId || 0;
|
||||||
|
|
||||||
private loadCurrentUserPermissions() {
|
if (!this.access.canViewSubscriptions) {
|
||||||
this.authService.getUserProfile()
|
this.error = 'Vous n\'avez pas la permission de voir les abonnements';
|
||||||
.pipe(takeUntil(this.destroy$))
|
return;
|
||||||
.subscribe({
|
}
|
||||||
next: (user) => {
|
|
||||||
this.currentUserRole = this.extractUserRole(user);
|
if (!this.access.isHubUser && this.currentMerchantId === 0) {
|
||||||
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
|
this.error = 'Merchant ID non disponible';
|
||||||
|
return;
|
||||||
console.log('Subscriptions Context Loaded:', {
|
|
||||||
role: this.currentUserRole,
|
|
||||||
merchantPartnerId: this.currentMerchantPartnerId,
|
|
||||||
canViewAllMerchants: this.canViewAllMerchants
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loadSubscriptions();
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Error loading current user permissions:', error);
|
|
||||||
this.fallbackPermissions();
|
|
||||||
this.loadSubscriptions();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractUserRole(user: any): string | null {
|
|
||||||
const userRoles = this.authService.getCurrentUserRoles();
|
|
||||||
if (userRoles && userRoles.length > 0) {
|
|
||||||
return userRoles[0];
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private canViewAllMerchantsCheck(role: string | null): boolean {
|
loadAllSubscriptions() {
|
||||||
if (!role) return false;
|
if (!this.access.canViewSubscriptions) return;
|
||||||
|
|
||||||
const canViewAllRoles = [
|
|
||||||
'DCB_ADMIN',
|
|
||||||
'DCB_SUPPORT',
|
|
||||||
'DCB_PARTNER_ADMIN'
|
|
||||||
];
|
|
||||||
|
|
||||||
return canViewAllRoles.includes(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
private fallbackPermissions(): void {
|
|
||||||
this.currentUserRole = this.authService.getCurrentUserRole();
|
|
||||||
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSubscriptions() {
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
|
|
||||||
let subscriptionsObservable: Observable<SubscriptionsResponse>;
|
// Déterminer quelle API appeler
|
||||||
|
let subscriptionObservable;
|
||||||
|
|
||||||
if (this.canViewAllMerchants) {
|
if (this.access.canViewAllSubscriptions) {
|
||||||
// Admin/Support - tous les abonnements
|
subscriptionObservable = this.subscriptionsService.getSubscriptions({});
|
||||||
subscriptionsObservable = this.subscriptionsService.getSubscriptions({
|
} else if (this.currentMerchantId > 0) {
|
||||||
page: this.currentPage,
|
subscriptionObservable = this.subscriptionsService.getSubscriptionsByMerchant(this.currentMerchantId, {});
|
||||||
limit: this.itemsPerPage
|
|
||||||
});
|
|
||||||
} else if (this.currentMerchantPartnerId) {
|
|
||||||
// Merchant régulier - ses abonnements
|
|
||||||
subscriptionsObservable = this.subscriptionsService.getSubscriptionsByMerchant(
|
|
||||||
parseInt(this.currentMerchantPartnerId),
|
|
||||||
{ page: this.currentPage, limit: this.itemsPerPage }
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback - abonnements généraux
|
this.error = 'Configuration invalide';
|
||||||
subscriptionsObservable = this.subscriptionsService.getSubscriptions({
|
this.loading = false;
|
||||||
page: this.currentPage,
|
return;
|
||||||
limit: this.itemsPerPage
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptionsObservable
|
subscriptionObservable.subscribe({
|
||||||
.pipe(
|
next: (response) => {
|
||||||
takeUntil(this.destroy$),
|
this.allSubscriptions = response.subscriptions || [];
|
||||||
catchError(error => {
|
this.applyFiltersAndPagination();
|
||||||
console.error('Error loading subscriptions:', error);
|
this.loading = false;
|
||||||
this.error = 'Erreur lors du chargement des abonnements';
|
this.cdRef.detectChanges();
|
||||||
return of({ subscriptions: [], statistics: { total: 0, active: 0, totalRevenue: 0, averageAmount: 0 } } as SubscriptionsResponse);
|
},
|
||||||
})
|
error: (error) => {
|
||||||
)
|
this.error = 'Erreur lors du chargement des abonnements';
|
||||||
.subscribe({
|
this.loading = false;
|
||||||
next: (response) => {
|
this.cdRef.detectChanges();
|
||||||
this.allSubscriptions = response.subscriptions || [];
|
}
|
||||||
console.log(`✅ Loaded ${this.allSubscriptions.length} subscriptions`);
|
});
|
||||||
this.applyFiltersAndPagination();
|
}
|
||||||
this.loading = false;
|
|
||||||
this.cdRef.detectChanges();
|
private applyFiltersAndPagination() {
|
||||||
},
|
// 1. Appliquer les filtres
|
||||||
error: () => {
|
let filteredSubscriptions = this.filterSubscriptions(this.allSubscriptions);
|
||||||
this.error = 'Erreur lors du chargement des abonnements';
|
|
||||||
this.loading = false;
|
// 2. Appliquer le tri
|
||||||
this.allSubscriptions = [];
|
filteredSubscriptions = this.sortSubscriptions(filteredSubscriptions);
|
||||||
this.filteredSubscriptions = [];
|
|
||||||
this.displayedSubscriptions = [];
|
// 3. Calculer la pagination
|
||||||
this.cdRef.detectChanges();
|
this.totalItems = filteredSubscriptions.length;
|
||||||
|
this.totalPages = Math.ceil(this.totalItems / this.itemsPerPage);
|
||||||
|
|
||||||
|
// Ajuster la page courante si nécessaire
|
||||||
|
if (this.currentPage > this.totalPages) {
|
||||||
|
this.currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Extraire les éléments de la page courante
|
||||||
|
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
||||||
|
const endIndex = Math.min(startIndex + this.itemsPerPage, this.totalItems);
|
||||||
|
this.subscriptions = filteredSubscriptions.slice(startIndex, endIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterSubscriptions(subscriptions: Subscription[]): Subscription[] {
|
||||||
|
let filtered = subscriptions;
|
||||||
|
|
||||||
|
// Filtre par statut
|
||||||
|
if (this.statusFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(sub => sub.status === this.statusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par périodicité
|
||||||
|
if (this.periodicityFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(sub => sub.periodicity === this.periodicityFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par recherche
|
||||||
|
if (this.searchTerm.trim()) {
|
||||||
|
const term = this.searchTerm.toLowerCase().trim();
|
||||||
|
filtered = filtered.filter(sub =>
|
||||||
|
(sub.startDate && sub.startDate.toLowerCase().includes(term)) ||
|
||||||
|
(sub.endDate && sub.endDate.toLowerCase().includes(term)) ||
|
||||||
|
(sub.amount && sub.amount.toString().toLowerCase().includes(term)) ||
|
||||||
|
(sub.periodicity && sub.periodicity.toLowerCase().includes(term)) ||
|
||||||
|
(sub.status && sub.status.toLowerCase().includes(term))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sortSubscriptions(subscriptions: Subscription[]): Subscription[] {
|
||||||
|
return [...subscriptions].sort((a: any, b: any) => {
|
||||||
|
const aValue = a[this.sortField];
|
||||||
|
const bValue = b[this.sortField];
|
||||||
|
|
||||||
|
if (aValue == null && bValue == null) return 0;
|
||||||
|
if (aValue == null) return 1;
|
||||||
|
if (bValue == null) return -1;
|
||||||
|
|
||||||
|
let comparison = 0;
|
||||||
|
|
||||||
|
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||||
|
comparison = aValue.localeCompare(bValue);
|
||||||
|
} else if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||||||
|
comparison = aValue - bValue;
|
||||||
|
} else {
|
||||||
|
const aDate = new Date(aValue);
|
||||||
|
const bDate = new Date(bValue);
|
||||||
|
if (!isNaN(aDate.getTime()) && !isNaN(bDate.getTime())) {
|
||||||
|
comparison = aDate.getTime() - bDate.getTime();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
return this.sortDirection === 'asc' ? comparison : -comparison;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recherche et filtres
|
// Recherche et filtres
|
||||||
onSearch() {
|
onSearch() {
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.applyFiltersAndPagination();
|
this.applyFiltersAndPagination();
|
||||||
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
onClearFilters() {
|
onClearFilters() {
|
||||||
this.searchTerm = '';
|
this.searchTerm = '';
|
||||||
this.statusFilter = 'all';
|
this.statusFilter = 'all';
|
||||||
this.periodicityFilter = 'all';
|
this.periodicityFilter = 'all';
|
||||||
this.merchantFilter = 'all';
|
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.applyFiltersAndPagination();
|
this.applyFiltersAndPagination();
|
||||||
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
applyFiltersAndPagination() {
|
onStatusFilterChange(status: SubscriptionStatus | 'all') {
|
||||||
if (!this.allSubscriptions) {
|
this.statusFilter = status;
|
||||||
this.allSubscriptions = [];
|
this.currentPage = 1;
|
||||||
}
|
this.applyFiltersAndPagination();
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
// Appliquer les filtres
|
onPeriodicityFilterChange(periodicity: SubscriptionPeriodicity | 'all') {
|
||||||
this.filteredSubscriptions = this.allSubscriptions.filter(subscription => {
|
this.periodicityFilter = periodicity;
|
||||||
const matchesSearch = !this.searchTerm ||
|
this.currentPage = 1;
|
||||||
subscription.id.toString().includes(this.searchTerm) ||
|
this.applyFiltersAndPagination();
|
||||||
subscription.token.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
|
this.cdRef.detectChanges();
|
||||||
(subscription.externalReference && subscription.externalReference.toLowerCase().includes(this.searchTerm.toLowerCase()));
|
|
||||||
|
|
||||||
const matchesStatus = this.statusFilter === 'all' ||
|
|
||||||
subscription.status === this.statusFilter;
|
|
||||||
|
|
||||||
const matchesPeriodicity = this.periodicityFilter === 'all' ||
|
|
||||||
subscription.periodicity === this.periodicityFilter;
|
|
||||||
|
|
||||||
const matchesMerchant = this.merchantFilter === 'all' ||
|
|
||||||
subscription.merchantPartnerId === this.merchantFilter;
|
|
||||||
|
|
||||||
return matchesSearch && matchesStatus && matchesPeriodicity && matchesMerchant;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Appliquer le tri
|
|
||||||
this.filteredSubscriptions.sort((a, b) => {
|
|
||||||
const aValue = a[this.sortField];
|
|
||||||
const bValue = b[this.sortField];
|
|
||||||
|
|
||||||
if (aValue === bValue) return 0;
|
|
||||||
|
|
||||||
let comparison = 0;
|
|
||||||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
|
||||||
comparison = aValue.localeCompare(bValue);
|
|
||||||
} else if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
||||||
comparison = aValue - bValue;
|
|
||||||
} else if (aValue instanceof Date && bValue instanceof Date) {
|
|
||||||
comparison = aValue.getTime() - bValue.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.sortDirection === 'asc' ? comparison : -comparison;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculer la pagination
|
|
||||||
this.totalItems = this.filteredSubscriptions.length;
|
|
||||||
this.totalPages = Math.ceil(this.totalItems / this.itemsPerPage);
|
|
||||||
|
|
||||||
// Appliquer la pagination
|
|
||||||
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
|
||||||
const endIndex = startIndex + this.itemsPerPage;
|
|
||||||
this.displayedSubscriptions = this.filteredSubscriptions.slice(startIndex, endIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tri
|
// Tri
|
||||||
sort(field: keyof Subscription) {
|
sort(field: string) {
|
||||||
if (this.sortField === field) {
|
if (this.sortField === field) {
|
||||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||||
} else {
|
} else {
|
||||||
this.sortField = field;
|
this.sortField = field;
|
||||||
this.sortDirection = 'asc';
|
this.sortDirection = 'desc';
|
||||||
}
|
}
|
||||||
this.applyFiltersAndPagination();
|
this.applyFiltersAndPagination();
|
||||||
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortIcon(field: string): string {
|
getSortIcon(field: string): string {
|
||||||
@ -296,244 +264,131 @@ export class SubscriptionsList implements OnInit, OnDestroy {
|
|||||||
onPageChange(page: number) {
|
onPageChange(page: number) {
|
||||||
this.currentPage = page;
|
this.currentPage = page;
|
||||||
this.applyFiltersAndPagination();
|
this.applyFiltersAndPagination();
|
||||||
}
|
this.cdRef.detectChanges();
|
||||||
|
|
||||||
getStartIndex(): number {
|
|
||||||
return (this.currentPage - 1) * this.itemsPerPage + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEndIndex(): number {
|
|
||||||
return Math.min(this.currentPage * this.itemsPerPage, this.totalItems);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
viewSubscriptionDetails(subscriptionId: string | number) {
|
viewSubscriptionDetails(subscriptionId: string) {
|
||||||
this.subscriptionSelected.emit(subscriptionId.toString());
|
if (!subscriptionId) return;
|
||||||
|
this.subscriptionSelected.emit(subscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewSubscriptionPayments(subscriptionId: string | number) {
|
viewSubscriptionPayments(subscriptionId: string) {
|
||||||
this.viewPaymentsRequested.emit(subscriptionId.toString());
|
if (!subscriptionId) return;
|
||||||
|
|
||||||
|
this.accessService.canAccessSubscription(Number(subscriptionId)).subscribe(canAccess => {
|
||||||
|
if (canAccess) {
|
||||||
|
this.viewPaymentsRequested.emit(subscriptionId);
|
||||||
|
} else {
|
||||||
|
this.error = 'Vous n\'avez pas la permission de voir les paiements de cet abonnement';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utilitaires d'affichage
|
// Utilitaires d'affichage
|
||||||
getStatusBadgeClass(status: SubscriptionStatus): string {
|
getStatusBadgeClass(status: SubscriptionStatus): string {
|
||||||
const statusClasses = {
|
switch (status) {
|
||||||
[SubscriptionStatus.ACTIVE]: 'badge bg-success',
|
case SubscriptionStatus.ACTIVE: return 'badge bg-success';
|
||||||
[SubscriptionStatus.SUSPENDED]: 'badge bg-warning',
|
case SubscriptionStatus.SUSPENDED: return 'badge bg-warning';
|
||||||
[SubscriptionStatus.CANCELLED]: 'badge bg-danger',
|
case SubscriptionStatus.CANCELLED: return 'badge bg-danger';
|
||||||
[SubscriptionStatus.EXPIRED]: 'badge bg-secondary',
|
case SubscriptionStatus.EXPIRED: return 'badge bg-secondary';
|
||||||
[SubscriptionStatus.PENDING]: 'badge bg-info'
|
case SubscriptionStatus.PENDING: return 'badge bg-info';
|
||||||
};
|
default: return 'badge bg-secondary';
|
||||||
return statusClasses[status] || 'badge bg-secondary';
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatusDisplayName(status: SubscriptionStatus): string {
|
getStatusIcon(status: SubscriptionStatus): string {
|
||||||
const statusNames = {
|
switch (status) {
|
||||||
[SubscriptionStatus.ACTIVE]: 'Actif',
|
case SubscriptionStatus.ACTIVE: return 'lucideCheckCircle';
|
||||||
[SubscriptionStatus.SUSPENDED]: 'Suspendu',
|
case SubscriptionStatus.SUSPENDED: return 'lucidePauseCircle';
|
||||||
[SubscriptionStatus.CANCELLED]: 'Annulé',
|
case SubscriptionStatus.CANCELLED: return 'lucideXCircle';
|
||||||
[SubscriptionStatus.EXPIRED]: 'Expiré',
|
case SubscriptionStatus.EXPIRED: return 'lucideCalendarOff';
|
||||||
[SubscriptionStatus.PENDING]: 'En attente'
|
case SubscriptionStatus.PENDING: return 'lucideClock';
|
||||||
};
|
default: return 'lucideClock';
|
||||||
return statusNames[status] || status;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPeriodicityDisplayName(periodicity: SubscriptionPeriodicity): string {
|
getPeriodicityBadgeClass(periodicity: SubscriptionPeriodicity): string {
|
||||||
const periodicityNames = {
|
switch (periodicity) {
|
||||||
[SubscriptionPeriodicity.DAILY]: 'Quotidien',
|
case SubscriptionPeriodicity.DAILY: return 'badge bg-primary';
|
||||||
[SubscriptionPeriodicity.WEEKLY]: 'Hebdomadaire',
|
case SubscriptionPeriodicity.WEEKLY: return 'badge bg-info';
|
||||||
[SubscriptionPeriodicity.MONTHLY]: 'Mensuel',
|
case SubscriptionPeriodicity.MONTHLY: return 'badge bg-success';
|
||||||
[SubscriptionPeriodicity.YEARLY]: 'Annuel'
|
case SubscriptionPeriodicity.YEARLY: return 'badge bg-warning';
|
||||||
};
|
default: return 'badge bg-secondary';
|
||||||
return periodicityNames[periodicity] || periodicity;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthodes pour compter les abonnements par périodicité
|
formatCurrency(amount: number, currency: Currency = Currency.XOF): string {
|
||||||
getDailySubscriptionsCount(): number {
|
if (isNaN(amount)) return '-';
|
||||||
if (!this.allSubscriptions || this.allSubscriptions.length === 0) return 0;
|
|
||||||
return this.allSubscriptions.filter(s => s.periodicity === SubscriptionPeriodicity.DAILY).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getWeeklySubscriptionsCount(): number {
|
|
||||||
if (!this.allSubscriptions || this.allSubscriptions.length === 0) return 0;
|
|
||||||
return this.allSubscriptions.filter(s => s.periodicity === SubscriptionPeriodicity.WEEKLY).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getMonthlySubscriptionsCount(): number {
|
|
||||||
if (!this.allSubscriptions || this.allSubscriptions.length === 0) return 0;
|
|
||||||
return this.allSubscriptions.filter(s => s.periodicity === SubscriptionPeriodicity.MONTHLY).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getYearlySubscriptionsCount(): number {
|
|
||||||
if (!this.allSubscriptions || this.allSubscriptions.length === 0) return 0;
|
|
||||||
return this.allSubscriptions.filter(s => s.periodicity === SubscriptionPeriodicity.YEARLY).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
formatAmount(amount: number, currency: Currency): string {
|
|
||||||
return new Intl.NumberFormat('fr-FR', {
|
return new Intl.NumberFormat('fr-FR', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: currency
|
currency: currency
|
||||||
}).format(amount);
|
}).format(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
formatDate(dateString: string): string {
|
formatDate(date: string | Date): string {
|
||||||
return new Date(dateString).toLocaleDateString('fr-FR', {
|
if (!date) return '-';
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDateTime(dateString: string): string {
|
const dateObj = date instanceof Date ? date : new Date(date);
|
||||||
return new Date(dateString).toLocaleDateString('fr-FR', {
|
|
||||||
|
if (isNaN(dateObj.getTime())) {
|
||||||
|
return 'Date invalide';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
});
|
}).format(dateObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recharger les données
|
getStatusDisplayName(status: SubscriptionStatus): string {
|
||||||
refreshData() {
|
switch (status) {
|
||||||
this.loadSubscriptions();
|
case SubscriptionStatus.ACTIVE: return 'Actif';
|
||||||
|
case SubscriptionStatus.SUSPENDED: return 'Suspendu';
|
||||||
|
case SubscriptionStatus.CANCELLED: return 'Annulé';
|
||||||
|
case SubscriptionStatus.EXPIRED: return 'Expiré';
|
||||||
|
case SubscriptionStatus.PENDING: return 'En attente';
|
||||||
|
default: return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeriodicityDisplayName(periodicity: SubscriptionPeriodicity): string {
|
||||||
|
switch (periodicity) {
|
||||||
|
case SubscriptionPeriodicity.DAILY: return 'Quotidien';
|
||||||
|
case SubscriptionPeriodicity.WEEKLY: return 'Hebdomadaire';
|
||||||
|
case SubscriptionPeriodicity.MONTHLY: return 'Mensuel';
|
||||||
|
case SubscriptionPeriodicity.YEARLY: return 'Annuel';
|
||||||
|
default: return periodicity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthodes pour le template
|
// Méthodes pour le template
|
||||||
getCardTitle(): string {
|
getUserBadgeClass(): string {
|
||||||
return 'Abonnements';
|
return this.access.isHubUser ? 'bg-primary' : 'bg-success';
|
||||||
}
|
}
|
||||||
|
|
||||||
getHelperText(): string {
|
getUserBadgeIcon(): string {
|
||||||
return this.canViewAllMerchants
|
return this.access.isHubUser ? 'lucideShield' : 'lucideStore';
|
||||||
? 'Vue administrative - Tous les abonnements'
|
|
||||||
: 'Vos abonnements';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getHelperIcon(): string {
|
getUserBadgeText(): string {
|
||||||
return this.canViewAllMerchants ? 'lucideShield' : 'lucideRepeat';
|
return this.access.isHubUser ? 'Hub User' : 'Merchant User';
|
||||||
}
|
}
|
||||||
|
|
||||||
getLoadingText(): string {
|
// Méthodes utilitaires
|
||||||
return 'Chargement des abonnements...';
|
refreshData() {
|
||||||
|
this.loadAllSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
getEmptyStateTitle(): string {
|
getFilteredCount(): number {
|
||||||
return 'Aucun abonnement trouvé';
|
return this.subscriptions.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEmptyStateDescription(): string {
|
getTotalCount(): number {
|
||||||
return 'Aucun abonnement ne correspond à vos critères de recherche.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Statistiques
|
|
||||||
getTotalSubscriptionsCount(): number {
|
|
||||||
return this.allSubscriptions.length;
|
return this.allSubscriptions.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthodes pour exposer les valeurs d'énumération au template
|
|
||||||
getActiveStatus(): SubscriptionStatus {
|
|
||||||
return SubscriptionStatus.ACTIVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSuspendedStatus(): SubscriptionStatus {
|
|
||||||
return SubscriptionStatus.SUSPENDED;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCancelledStatus(): SubscriptionStatus {
|
|
||||||
return SubscriptionStatus.CANCELLED;
|
|
||||||
}
|
|
||||||
|
|
||||||
getExpiredStatus(): SubscriptionStatus {
|
|
||||||
return SubscriptionStatus.EXPIRED;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPendingStatus(): SubscriptionStatus {
|
|
||||||
return SubscriptionStatus.PENDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthodes pour compter les abonnements par statut
|
|
||||||
getActiveSubscriptionsCount(): number {
|
|
||||||
if (!this.allSubscriptions || this.allSubscriptions.length === 0) return 0;
|
|
||||||
return this.allSubscriptions.filter(s => s.status === SubscriptionStatus.ACTIVE).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSuspendedSubscriptionsCount(): number {
|
|
||||||
if (!this.allSubscriptions || this.allSubscriptions.length === 0) return 0;
|
|
||||||
return this.allSubscriptions.filter(s => s.status === SubscriptionStatus.SUSPENDED).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCancelledSubscriptionsCount(): number {
|
|
||||||
if (!this.allSubscriptions || this.allSubscriptions.length === 0) return 0;
|
|
||||||
return this.allSubscriptions.filter(s => s.status === SubscriptionStatus.CANCELLED).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getExpiredSubscriptionsCount(): number {
|
|
||||||
if (!this.allSubscriptions || this.allSubscriptions.length === 0) return 0;
|
|
||||||
return this.allSubscriptions.filter(s => s.status === SubscriptionStatus.EXPIRED).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPendingSubscriptionsCount(): number {
|
|
||||||
if (!this.allSubscriptions || this.allSubscriptions.length === 0) return 0;
|
|
||||||
return this.allSubscriptions.filter(s => s.status === SubscriptionStatus.PENDING).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotalRevenue(): number {
|
|
||||||
return this.allSubscriptions.reduce((sum, sub) => sum + sub.amount, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters pour la logique conditionnelle
|
|
||||||
get showMerchantColumn(): boolean {
|
|
||||||
return this.canViewAllMerchants;
|
|
||||||
}
|
|
||||||
|
|
||||||
get showCustomerColumn(): boolean {
|
|
||||||
return this.canViewAllMerchants;
|
|
||||||
}
|
|
||||||
|
|
||||||
getColumnCount(): number {
|
|
||||||
let count = 6; // ID, Montant, Périodicité, Statut, Date début, Actions
|
|
||||||
if (this.showMerchantColumn) count++;
|
|
||||||
if (this.showCustomerColumn) count++;
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrage rapide par statut
|
|
||||||
filterByStatus(status: SubscriptionStatus | 'all') {
|
|
||||||
this.statusFilter = status;
|
|
||||||
this.currentPage = 1;
|
|
||||||
this.applyFiltersAndPagination();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrage rapide par périodicité
|
|
||||||
filterByPeriodicity(periodicity: SubscriptionPeriodicity | 'all') {
|
|
||||||
this.periodicityFilter = periodicity;
|
|
||||||
this.currentPage = 1;
|
|
||||||
this.applyFiltersAndPagination();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculer les jours jusqu'au prochain paiement
|
|
||||||
getDaysUntilNextPayment(subscription: Subscription): number {
|
|
||||||
const nextPayment = new Date(subscription.nextPaymentDate);
|
|
||||||
const today = new Date();
|
|
||||||
const diffTime = nextPayment.getTime() - today.getTime();
|
|
||||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si un abonnement est sur le point d'expirer (dans les 7 jours)
|
|
||||||
isExpiringSoon(subscription: Subscription): boolean {
|
|
||||||
if (!subscription.endDate) return false;
|
|
||||||
const endDate = new Date(subscription.endDate);
|
|
||||||
const today = new Date();
|
|
||||||
const diffTime = endDate.getTime() - today.getTime();
|
|
||||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
||||||
return diffDays <= 7 && diffDays > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si un abonnement est expiré
|
|
||||||
isExpired(subscription: Subscription): boolean {
|
|
||||||
if (!subscription.endDate) return false;
|
|
||||||
return new Date(subscription.endDate) < new Date();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<app-page-title
|
<app-page-title
|
||||||
title="pageTitle"
|
title="Abonnements DCB"
|
||||||
subtitle="Consultez et gérez les abonnements et leurs paiements"
|
subTitle="Gestion et suivi des abonnements de paiement mobile"
|
||||||
[badge]="badge"
|
[badge]="{icon:'lucideRepeat', text:'Abonnements'}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Indicateur de permissions -->
|
<!-- Indicateur de permissions -->
|
||||||
@ -15,12 +15,13 @@
|
|||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<small>
|
<small>
|
||||||
<strong>Rôle actuel :</strong>
|
<strong>Rôle actuel :</strong>
|
||||||
<span class="badge" [ngClass]="getRoleBadgeClass()">
|
<span [class]="getUserBadgeClass()" class="badge">
|
||||||
{{ getRoleLabel() }}
|
<ng-icon [name]="getUserBadgeIcon()" class="me-1"></ng-icon>
|
||||||
|
{{ getUserBadgeText() }}
|
||||||
</span>
|
</span>
|
||||||
@if (currentMerchantPartnerId) {
|
@if (currentMerchantId) {
|
||||||
<span class="ms-2">
|
<span class="ms-2">
|
||||||
<strong>Merchant ID :</strong> {{ currentMerchantPartnerId }}
|
<strong>Merchant ID :</strong> {{ currentMerchantId }}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</small>
|
</small>
|
||||||
@ -31,52 +32,46 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Navigation par onglets -->
|
<!-- Navigation -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<ul
|
@if (activeView === 'list') {
|
||||||
ngbNav
|
<app-subscriptions-list
|
||||||
#subscriptionsNav="ngbNav"
|
(subscriptionSelected)="showDetailsView($event)"
|
||||||
[activeId]="activeTab"
|
(viewPaymentsRequested)="onViewPaymentsRequested($event)"
|
||||||
[destroyOnHide]="false"
|
/>
|
||||||
class="nav nav-tabs nav-justified nav-bordered nav-bordered-primary mb-3"
|
} @else if (activeView === 'details' && selectedSubscriptionId) {
|
||||||
>
|
<div class="d-flex align-items-center mb-3">
|
||||||
<li [ngbNavItem]="'list'">
|
<button class="btn btn-outline-secondary btn-sm me-2" (click)="showListView()">
|
||||||
<a ngbNavLink (click)="showTab('list')">
|
<ng-icon name="lucideArrowLeft" class="me-1"></ng-icon>
|
||||||
<ng-icon name="lucideList" class="fs-lg me-md-1 d-inline-flex align-middle" />
|
Retour à la liste
|
||||||
<span class="d-none d-md-inline-block align-middle">Liste des Abonnements</span>
|
</button>
|
||||||
</a>
|
<button class="btn btn-outline-primary btn-sm me-2"
|
||||||
<ng-template ngbNavContent>
|
(click)="showPaymentsView(selectedSubscriptionId)">
|
||||||
<app-subscriptions-list
|
<ng-icon name="lucideCreditCard" class="me-1"></ng-icon>
|
||||||
#subscriptionsList
|
Voir les paiements
|
||||||
(subscriptionSelected)="onSubscriptionSelected($event)"
|
</button>
|
||||||
(viewPaymentsRequested)="onViewPaymentsRequested($event)"
|
<h5 class="mb-0">Détails de l'abonnement</h5>
|
||||||
/>
|
</div>
|
||||||
</ng-template>
|
<app-subscription-details [subscriptionId]="selectedSubscriptionId" />
|
||||||
</li>
|
} @else if (activeView === 'payments' && selectedSubscriptionId) { <!-- AJOUTER -->
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
<li [ngbNavItem]="'payments'" [hidden]="!selectedSubscriptionId">
|
<button class="btn btn-outline-secondary btn-sm me-2" (click)="showListView()">
|
||||||
<a ngbNavLink (click)="showTab('payments')">
|
<ng-icon name="lucideArrowLeft" class="me-1"></ng-icon>
|
||||||
<ng-icon name="lucideCreditCard" class="fs-lg me-md-1 d-inline-flex align-middle" />
|
Retour à la liste
|
||||||
<span class="d-none d-md-inline-block align-middle">Paiements</span>
|
</button>
|
||||||
</a>
|
<button class="btn btn-outline-primary btn-sm me-2"
|
||||||
<ng-template ngbNavContent>
|
(click)="showDetailsView(selectedSubscriptionId)">
|
||||||
@if (selectedSubscriptionId) {
|
<ng-icon name="lucideEye" class="me-1"></ng-icon>
|
||||||
<app-subscription-payments
|
Voir les détails
|
||||||
[subscriptionId]="selectedSubscriptionId"
|
</button>
|
||||||
(back)="backToList()"
|
<h5 class="mb-0">Paiements de l'abonnement</h5>
|
||||||
/>
|
</div>
|
||||||
} @else {
|
<app-subscription-payments
|
||||||
<div class="alert alert-warning text-center">
|
[subscriptionId]="selectedSubscriptionId"
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
(back)="showListView()"
|
||||||
Aucun abonnement sélectionné
|
/>
|
||||||
</div>
|
}
|
||||||
}
|
|
||||||
</ng-template>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="tab-content" [ngbNavOutlet]="subscriptionsNav"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1,246 +1,103 @@
|
|||||||
import { Component, inject, OnInit, TemplateRef, ViewChild, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
import { catchError, map, of, Subject, takeUntil } from 'rxjs';
|
|
||||||
|
|
||||||
import { SubscriptionsService } from './subscriptions.service';
|
|
||||||
import { AuthService } from '@core/services/auth.service';
|
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
import { SubscriptionsList } from './subscriptions-list/subscriptions-list';
|
import { SubscriptionsList } from './subscriptions-list/subscriptions-list';
|
||||||
import { SubscriptionPayments } from './subscription-payments/subscription-payments';
|
import { SubscriptionDetails } from './subscription-details/subscription-details';
|
||||||
|
import { SubscriptionPayments } from './subscription-payments/subscription-payments'; // AJOUTER
|
||||||
import {
|
import { SubscriptionAccessService } from './services/subscription-access.service';
|
||||||
Subscription,
|
import { Subscription } from 'rxjs';
|
||||||
SubscriptionPayment,
|
|
||||||
SubscriptionStatus,
|
|
||||||
SubscriptionPeriodicity,
|
|
||||||
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-old.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-subscriptions',
|
selector: 'app-subscriptions',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
|
||||||
NgIcon,
|
|
||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbModalModule,
|
NgIcon,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
SubscriptionsList,
|
SubscriptionsList,
|
||||||
|
SubscriptionDetails,
|
||||||
SubscriptionPayments
|
SubscriptionPayments
|
||||||
],
|
],
|
||||||
templateUrl: './subscriptions.html',
|
templateUrl: './subscriptions.html',
|
||||||
})
|
})
|
||||||
export class SubscriptionsManagement implements OnInit, OnDestroy {
|
export class Subscriptions implements OnInit {
|
||||||
private authService = inject(AuthService);
|
private subscriptions: Subscription[] = [];
|
||||||
private subscriptionsService = inject(SubscriptionsService);
|
private accessService = inject(SubscriptionAccessService);
|
||||||
private roleService = inject(RoleManagementService);
|
|
||||||
|
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
activeView: 'list' | 'details' | 'payments' = 'list'; // AJOUTER 'payments'
|
||||||
private destroy$ = new Subject<void>();
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
readonly SubscriptionStatus = SubscriptionStatus;
|
|
||||||
readonly SubscriptionPeriodicity = SubscriptionPeriodicity;
|
|
||||||
readonly Currency = Currency;
|
|
||||||
|
|
||||||
// Propriétés de configuration
|
|
||||||
pageTitle: string = 'Gestion des Abonnements';
|
|
||||||
badge: any = { icon: 'lucideRepeat', text: 'Abonnements' };
|
|
||||||
|
|
||||||
// État de l'interface
|
|
||||||
activeTab: 'list' | 'payments' = 'list';
|
|
||||||
selectedSubscriptionId: string | null = null;
|
selectedSubscriptionId: string | null = null;
|
||||||
|
|
||||||
user: User | undefined;
|
// Permissions
|
||||||
|
canAccessModule = true;
|
||||||
|
accessDenied = false;
|
||||||
|
currentUserRole = '';
|
||||||
|
currentMerchantId?: number;
|
||||||
|
|
||||||
// Gestion des rôles (lecture seule)
|
subscriptionInitialized = false;
|
||||||
availableRoles: { value: UserRole; label: string; description: string }[] = [];
|
|
||||||
|
|
||||||
// Gestion des permissions
|
ngOnInit(): void {
|
||||||
currentUserRole: string | null = null;
|
console.log('🔍 Subscription: ngOnInit() appelé');
|
||||||
currentMerchantPartnerId: string = '';
|
|
||||||
|
|
||||||
// Données
|
// Attendre que le SubscriptionAccessService soit VRAIMENT prêt
|
||||||
subscriptionPayments: { [subscriptionId: string]: SubscriptionPayment[] } = {};
|
this.subscriptions.push(
|
||||||
selectedSubscriptionForPayments: Subscription | null = null;
|
this.accessService.waitForReady().subscribe({
|
||||||
|
next: () => {
|
||||||
// Références aux composants enfants
|
console.log('✅ Subscription: waitForReady() a émis - Initialisation...');
|
||||||
@ViewChild(SubscriptionsList) subscriptionsList!: SubscriptionsList;
|
this.subscriptionInitialized = true;
|
||||||
|
this.checkAccess();
|
||||||
ngOnInit() {
|
|
||||||
this.activeTab = 'list';
|
|
||||||
this.loadCurrentUserPermissions();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.destroy$.next();
|
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise les permissions de l'utilisateur courant
|
|
||||||
*/
|
|
||||||
private loadCurrentUserPermissions(): void {
|
|
||||||
this.authService.getUserProfile()
|
|
||||||
.pipe(takeUntil(this.destroy$))
|
|
||||||
.subscribe({
|
|
||||||
next: (user) => {
|
|
||||||
this.currentUserRole = this.extractUserRole(user);
|
|
||||||
|
|
||||||
console.log(`User ROLE: ${this.currentUserRole}`);
|
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (err) => {
|
||||||
console.error('Error loading user profile:', error);
|
console.error('❌ Subscription: Erreur dans waitForReady():', err);
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private checkAccess() {
|
||||||
* Extraire le rôle de l'utilisateur
|
const access = this.accessService.getSubscriptionAccess();
|
||||||
*/
|
this.canAccessModule = access.canViewSubscriptions;
|
||||||
private extractUserRole(user: any): string | null {
|
this.accessDenied = !access.canViewSubscriptions;
|
||||||
const userRoles = this.authService.getCurrentUserRoles();
|
this.currentUserRole = access.userRoleLabel;
|
||||||
if (userRoles && userRoles.length > 0) {
|
this.currentMerchantId = access.merchantId;
|
||||||
return userRoles[0];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== MÉTHODES D'INTERFACE ====================
|
showListView() {
|
||||||
|
this.activeView = 'list';
|
||||||
// Méthode pour changer d'onglet
|
|
||||||
showTab(tab: 'list' | 'payments', subscriptionId?: string) {
|
|
||||||
console.log(`Switching to tab: ${tab}`, subscriptionId ? `for subscription ${subscriptionId}` : '');
|
|
||||||
this.activeTab = tab;
|
|
||||||
|
|
||||||
if (subscriptionId) {
|
|
||||||
this.selectedSubscriptionId = subscriptionId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
backToList() {
|
|
||||||
console.log('🔙 Returning to list view');
|
|
||||||
this.activeTab = 'list';
|
|
||||||
this.selectedSubscriptionId = null;
|
this.selectedSubscriptionId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthodes de gestion des événements du composant enfant
|
showDetailsView(subscriptionId: string) {
|
||||||
onSubscriptionSelected(subscriptionId: string) {
|
this.activeView = 'details';
|
||||||
this.showTab('payments', subscriptionId);
|
this.selectedSubscriptionId = subscriptionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOUVELLE MÉTHODE
|
||||||
|
showPaymentsView(subscriptionId: string) {
|
||||||
|
this.activeView = 'payments';
|
||||||
|
this.selectedSubscriptionId = subscriptionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOUVELLE MÉTHODE
|
||||||
onViewPaymentsRequested(subscriptionId: string) {
|
onViewPaymentsRequested(subscriptionId: string) {
|
||||||
this.showTab('payments', subscriptionId);
|
this.showPaymentsView(subscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== MÉTHODES UTILITAIRES ====================
|
// Utilitaires pour le template
|
||||||
|
getUserBadgeClass(): string {
|
||||||
private refreshSubscriptionsList(): void {
|
const access = this.accessService.getSubscriptionAccess();
|
||||||
if (this.subscriptionsList && typeof this.subscriptionsList.refreshData === 'function') {
|
return access.isHubUser ? 'badge bg-primary' : 'badge bg-success';
|
||||||
console.log('🔄 Refreshing subscriptions list...');
|
|
||||||
this.subscriptionsList.refreshData();
|
|
||||||
} else {
|
|
||||||
console.warn('❌ SubscriptionsList component not available for refresh');
|
|
||||||
this.showTab('list');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthodes pour les templates
|
getUserBadgeIcon(): string {
|
||||||
getStatusDisplayName(status: SubscriptionStatus): string {
|
const access = this.accessService.getSubscriptionAccess();
|
||||||
const statusNames: { [key: string]: string } = {
|
return access.isHubUser ? 'lucideShield' : 'lucideStore';
|
||||||
[SubscriptionStatus.ACTIVE]: 'Actif',
|
|
||||||
[SubscriptionStatus.SUSPENDED]: 'Suspendu',
|
|
||||||
[SubscriptionStatus.CANCELLED]: 'Annulé',
|
|
||||||
[SubscriptionStatus.EXPIRED]: 'Expiré',
|
|
||||||
[SubscriptionStatus.PENDING]: 'En attente'
|
|
||||||
};
|
|
||||||
return statusNames[status] || status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPeriodicityDisplayName(periodicity: SubscriptionPeriodicity): string {
|
getUserBadgeText(): string {
|
||||||
const periodicityNames: { [key: string]: string } = {
|
return this.currentUserRole;
|
||||||
[SubscriptionPeriodicity.DAILY]: 'Quotidien',
|
|
||||||
[SubscriptionPeriodicity.WEEKLY]: 'Hebdomadaire',
|
|
||||||
[SubscriptionPeriodicity.MONTHLY]: 'Mensuel',
|
|
||||||
[SubscriptionPeriodicity.YEARLY]: 'Annuel'
|
|
||||||
};
|
|
||||||
return periodicityNames[periodicity] || periodicity;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPaymentStatusDisplayName(status: string): string {
|
|
||||||
const statusNames: { [key: string]: string } = {
|
|
||||||
'PENDING': 'En attente',
|
|
||||||
'SUCCESS': 'Réussi',
|
|
||||||
'FAILED': 'Échoué'
|
|
||||||
};
|
|
||||||
return statusNames[status] || status;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatAmount(amount: number, currency: Currency): string {
|
|
||||||
return new Intl.NumberFormat('fr-FR', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: currency
|
|
||||||
}).format(amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDate(dateString: string): string {
|
|
||||||
return new Date(dateString).toLocaleDateString('fr-FR');
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserInitials(): string {
|
|
||||||
if (!this.user) return 'U';
|
|
||||||
return (this.user.firstName?.charAt(0) || '') + (this.user.lastName?.charAt(0) || '') || 'U';
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserDisplayName(): string {
|
|
||||||
if (!this.user) return 'Utilisateur';
|
|
||||||
if (this.user.firstName && this.user.lastName) {
|
|
||||||
return `${this.user.firstName} ${this.user.lastName}`;
|
|
||||||
}
|
|
||||||
return this.user.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoleBadgeClass(): string {
|
|
||||||
if (!this.user?.role) return 'badge bg-secondary';
|
|
||||||
return this.roleService.getRoleBadgeClass(this.user.role);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoleLabel(): string {
|
|
||||||
if (!this.user?.role) return 'Aucun rôle';
|
|
||||||
return this.roleService.getRoleLabel(this.user.role);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoleDescription(): string {
|
|
||||||
if (!this.user?.role) return 'Description non disponible';
|
|
||||||
const roleInfo = this.availableRoles.find(r => r.value === this.user!.role);
|
|
||||||
return roleInfo?.description || this.roleService.getRoleDescription(this.user.role);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoleIcon(role: string | UserRole): string {
|
|
||||||
return this.roleService.getRoleIcon(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtenir le rôle (peut être string ou UserRole)
|
|
||||||
getUserRole(): string | UserRole | undefined {
|
|
||||||
return this.user?.role;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pour le template, retourner un tableau pour la boucle
|
|
||||||
getUserRoles(): (string | UserRole)[] {
|
|
||||||
const role = this.user?.role;
|
|
||||||
if (!role) return [];
|
|
||||||
return Array.isArray(role) ? role : [role];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Afficher le rôle
|
|
||||||
getUserRoleDisplay(): string {
|
|
||||||
if (!this.user?.role) return 'Aucun rôle';
|
|
||||||
return this.getRoleLabel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
22
src/app/modules/transactions/list/list.css
Normal file
22
src/app/modules/transactions/list/list.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/* Dans votre fichier CSS */
|
||||||
|
.ng-icon.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fs-12 {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive {
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@
|
|||||||
<ng-icon [name]="getUserBadgeIcon()" class="me-1"></ng-icon>
|
<ng-icon [name]="getUserBadgeIcon()" class="me-1"></ng-icon>
|
||||||
{{ getUserBadgeText() }}
|
{{ getUserBadgeText() }}
|
||||||
</span>
|
</span>
|
||||||
<span class="badge bg-info" *ngIf="currentMerchantId">
|
<span class="badge bg-info" *ngIf="currentMerchantId > 0">
|
||||||
<ng-icon name="lucideStore" class="me-1"></ng-icon>
|
<ng-icon name="lucideStore" class="me-1"></ng-icon>
|
||||||
Merchant {{ currentMerchantId }}
|
Merchant {{ currentMerchantId }}
|
||||||
</span>
|
</span>
|
||||||
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<!-- Refresh -->
|
<!-- Refresh -->
|
||||||
<button class="btn btn-outline-secondary" (click)="loadTransactions()" [disabled]="loading">
|
<button class="btn btn-outline-secondary" (click)="refreshData()" [disabled]="loading">
|
||||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading"></ng-icon>
|
<ng-icon name="lucideRefreshCw" [class.spin]="loading"></ng-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -41,45 +41,77 @@
|
|||||||
<ng-icon name="lucideLock" class="me-2"></ng-icon>
|
<ng-icon name="lucideLock" class="me-2"></ng-icon>
|
||||||
<div>
|
<div>
|
||||||
<strong>Accès refusé</strong>
|
<strong>Accès refusé</strong>
|
||||||
<p class="mb-0">Vous n'avez pas les permissions nécessaires pour accéder à cette section.</p>
|
<p class="mb-0">{{ error || 'Vous n\'avez pas les permissions nécessaires pour accéder à cette section.' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
|
|
||||||
<!-- Statistiques rapides -->
|
<!-- Statistiques rapides -->
|
||||||
@if (paginatedData?.stats) {
|
<div class="row mb-4">
|
||||||
<div class="row mb-4">
|
<div class="col-12">
|
||||||
<div class="col-12">
|
<div class="card bg-light">
|
||||||
<div class="card bg-light">
|
<div class="card-body py-3">
|
||||||
<div class="card-body py-3">
|
<div class="row text-center">
|
||||||
<div class="row text-center">
|
<div class="col">
|
||||||
<div class="col">
|
<small class="text-muted">Total</small>
|
||||||
<small class="text-muted">Total</small>
|
<div class="h5 mb-0">{{ getTotal() }}</div>
|
||||||
<div class="h5 mb-0">{{ getTotal() }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">Succès</small>
|
|
||||||
<div class="h5 mb-0 text-success">{{ getSuccessCount() }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">Échecs</small>
|
|
||||||
<div class="h5 mb-0 text-danger">{{ getFailedCount() }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">En attente</small>
|
|
||||||
<div class="h5 mb-0 text-warning">{{ getPendingCount() }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">Montant total</small>
|
|
||||||
<div class="h5 mb-0">{{ formatCurrency(getTotalAmount()) }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">Succès</small>
|
||||||
|
<div class="h5 mb-0 text-success">{{ getSuccessCount() }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">Échecs</small>
|
||||||
|
<div class="h5 mb-0 text-danger">{{ getFailedCount() }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">En attente</small>
|
||||||
|
<div class="h5 mb-0 text-warning">{{ getPendingCount() }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">Montant total</small>
|
||||||
|
<div class="h5 mb-0">{{ formatCurrency(getTotalAmount()) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">Taux de succès</small>
|
||||||
|
<div class="h5 mb-0 text-success">{{ getSuccessRate() }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
|
|
||||||
|
<!-- Information sur le scope -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info py-2">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideShield" class="me-2"></ng-icon>
|
||||||
|
<small>
|
||||||
|
<strong>Scope:</strong> {{ getScopeText() }} |
|
||||||
|
<strong>Filtres actifs:</strong>
|
||||||
|
<span class="badge bg-primary me-1" *ngIf="statusFilter !== 'all'">
|
||||||
|
Statut: {{ getStatusDisplayName(statusFilter) }}
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-primary me-1" *ngIf="startDateFilter">
|
||||||
|
Début: {{ formatDate(startDateFilter) }}
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-primary me-1" *ngIf="endDateFilter">
|
||||||
|
Fin: {{ formatDate(endDateFilter) }}
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-primary me-1" *ngIf="searchTerm">
|
||||||
|
Recherche: "{{ searchTerm }}"
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-secondary">
|
||||||
|
{{ getFilteredCount() }} / {{ getTotalCount() }} transactions
|
||||||
|
</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Barre de recherche et filtres -->
|
<!-- Barre de recherche et filtres -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
@ -91,39 +123,71 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Rechercher par periodicity, Type..."
|
placeholder="Rechercher par ID, référence, merchant..."
|
||||||
[(ngModel)]="searchTerm"
|
[(ngModel)]="searchTerm"
|
||||||
(keyup.enter)="onSearch()"
|
(keyup.enter)="onSearch()"
|
||||||
>
|
>
|
||||||
|
@if (searchTerm) {
|
||||||
|
<button class="btn btn-outline-secondary" type="button" (click)="clearSearch()">
|
||||||
|
<ng-icon name="lucideX"></ng-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
<!-- Filtre statut -->
|
<!-- Filtre statut -->
|
||||||
<select class="form-select" style="width: auto;" (change)="onStatusFilterChange($any($event.target).value)">
|
<div ngbDropdown class="dropdown">
|
||||||
<option value="all">Tous les statuts</option>
|
<button class="btn btn-outline-secondary dropdown-toggle" ngbDropdownToggle>
|
||||||
@for (status of statusOptions; track status) {
|
<ng-icon name="lucideFilter" class="me-1"></ng-icon>
|
||||||
<option [value]="status">{{ status }}</option>
|
Statut: {{ statusFilter === 'all' ? 'Tous' : getStatusDisplayName(statusFilter) }}
|
||||||
}
|
</button>
|
||||||
</select>
|
<div ngbDropdownMenu>
|
||||||
|
<button ngbDropdownItem (click)="onStatusFilterChange('all')">
|
||||||
|
Tous les statuts
|
||||||
|
</button>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
@for (status of statusOptions; track status) {
|
||||||
|
<button ngbDropdownItem (click)="onStatusFilterChange(status)">
|
||||||
|
<span [class]="getStatusBadgeClass(status)" class="badge me-2">{{ getStatusDisplayName(status) }}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Filtre opérateur -->
|
<!-- Filtre date début -->
|
||||||
<select class="form-select" style="width: auto;" (change)="onOperatorFilterChange($any($event.target).value)">
|
<div class="input-group" style="width: 200px;">
|
||||||
<option value="">Tous les opérateurs</option>
|
<span class="input-group-text">
|
||||||
@for (operator of operatorOptions; track operator) {
|
<ng-icon name="lucideCalendar"></ng-icon>
|
||||||
<option [value]="operator">{{ operator }}</option>
|
</span>
|
||||||
}
|
<input
|
||||||
</select>
|
type="date"
|
||||||
|
class="form-control"
|
||||||
|
[value]="startDateFilter ? startDateFilter.toISOString().split('T')[0] : ''"
|
||||||
|
(change)="handleStartDateChange($event)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtre date fin -->
|
||||||
|
<div class="input-group" style="width: 200px;">
|
||||||
|
<span class="input-group-text">à</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
class="form-control"
|
||||||
|
[value]="endDateFilter ? endDateFilter.toISOString().split('T')[0] : ''"
|
||||||
|
(change)="handleEndDateChange($event)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-outline-primary" (click)="onSearch()">
|
<button class="btn btn-outline-primary" (click)="onSearch()">
|
||||||
<ng-icon name="lucideFilter" class="me-1"></ng-icon>
|
<ng-icon name="lucideSearch" class="me-1"></ng-icon>
|
||||||
Filtrer
|
Appliquer
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-outline-secondary" (click)="onClearFilters()">
|
<button class="btn btn-outline-secondary" (click)="onClearFilters()">
|
||||||
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||||
Effacer
|
Tout effacer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -181,11 +245,11 @@
|
|||||||
<th>Statut</th>
|
<th>Statut</th>
|
||||||
<th (click)="sort('transactionDate')" class="cursor-pointer">
|
<th (click)="sort('transactionDate')" class="cursor-pointer">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span>Date début</span>
|
<span>Date transaction</span>
|
||||||
<ng-icon [name]="getSortIcon('transactionDate')" class="ms-1 fs-12"></ng-icon>
|
<ng-icon [name]="getSortIcon('transactionDate')" class="ms-1 fs-12"></ng-icon>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th>Prochain paiement</th>
|
<th>Date création</th>
|
||||||
<th width="120">Actions</th>
|
<th width="120">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -200,17 +264,27 @@
|
|||||||
(change)="toggleTransactionSelection(transaction.id)"
|
(change)="toggleTransactionSelection(transaction.id)"
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td class="font-monospace small">{{ transaction.id }}</td>
|
<td class="font-monospace small">
|
||||||
|
{{ transaction.id }}
|
||||||
|
@if (transaction.externalReference) {
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">Ref: {{ transaction.externalReference }}</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-light text-dark">
|
<span class="badge bg-light text-dark">
|
||||||
<ng-icon [name]="getTypeIcon(transaction.type)" class="me-1"></ng-icon>
|
<ng-icon [name]="getTypeIcon(transaction.type)" class="me-1"></ng-icon>
|
||||||
Abonnement
|
{{ getTypeDisplayName(transaction.type) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-secondary">
|
@if (transaction.merchantPartnerId) {
|
||||||
Merchant {{ transaction.merchantPartnerId }}
|
<span class="badge bg-secondary">
|
||||||
</span>
|
Merchant {{ transaction.merchantPartnerId }}
|
||||||
|
</span>
|
||||||
|
} @else {
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span [class]="getAmountColor(transaction.amount)">
|
<span [class]="getAmountColor(transaction.amount)">
|
||||||
@ -222,6 +296,8 @@
|
|||||||
<span [class]="getPeriodicityBadgeClass(transaction.periodicity)" class="badge">
|
<span [class]="getPeriodicityBadgeClass(transaction.periodicity)" class="badge">
|
||||||
{{ getPeriodicityDisplayName(transaction.periodicity) }}
|
{{ getPeriodicityDisplayName(transaction.periodicity) }}
|
||||||
</span>
|
</span>
|
||||||
|
} @else {
|
||||||
|
<span class="text-muted">-</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@ -234,11 +310,7 @@
|
|||||||
{{ formatDate(transaction.transactionDate) }}
|
{{ formatDate(transaction.transactionDate) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="small text-muted">
|
<td class="small text-muted">
|
||||||
@if (transaction.nextPaymentDate) {
|
{{ formatDate(transaction.createdAt) }}
|
||||||
{{ formatDate(transaction.nextPaymentDate) }}
|
|
||||||
} @else {
|
|
||||||
-
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
@ -249,6 +321,15 @@
|
|||||||
>
|
>
|
||||||
<ng-icon name="lucideEye"></ng-icon>
|
<ng-icon name="lucideEye"></ng-icon>
|
||||||
</button>
|
</button>
|
||||||
|
@if (access.canViewAllTransactions && transaction.status === 'SUCCESS') {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-warning"
|
||||||
|
(click)="openRefundModal.emit(transaction.id)"
|
||||||
|
ngbTooltip="Rembourser"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideUndo2"></ng-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -257,10 +338,18 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="10" class="text-center py-4">
|
<td colspan="10" class="text-center py-4">
|
||||||
<ng-icon name="lucideCreditCard" class="text-muted fs-1 mb-2 d-block"></ng-icon>
|
<ng-icon name="lucideCreditCard" class="text-muted fs-1 mb-2 d-block"></ng-icon>
|
||||||
<p class="text-muted mb-3">Aucune transaction trouvée</p>
|
<p class="text-muted mb-3">
|
||||||
<button class="btn btn-primary" (click)="onClearFilters()">
|
@if (allTransactions.length === 0) {
|
||||||
Réinitialiser les filtres
|
Aucune transaction disponible
|
||||||
</button>
|
} @else {
|
||||||
|
Aucune transaction ne correspond aux filtres
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
@if (allTransactions.length > 0) {
|
||||||
|
<button class="btn btn-primary" (click)="onClearFilters()">
|
||||||
|
Réinitialiser les filtres
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@ -271,18 +360,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
@if (paginatedData && paginatedData.totalPages >= 1) {
|
@if (totalPages >= 1) {
|
||||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Affichage de {{ (filters.page! - 1) * filters.limit! + 1 }} à
|
Affichage de
|
||||||
{{ (filters.page! * filters.limit!) > (paginatedData.total || 0) ? (paginatedData.total || 0) : (filters.page! * filters.limit!) }}
|
{{ (currentPage - 1) * itemsPerPage + 1 }} à
|
||||||
sur {{ paginatedData.total || 0 }} transactions
|
{{ getCurrentPageEnd() }}
|
||||||
|
sur {{ totalItems }} transactions
|
||||||
|
({{ getFilteredCount() }} filtrées / {{ getTotalCount() }} total)
|
||||||
</div>
|
</div>
|
||||||
<nav>
|
<nav>
|
||||||
<ngb-pagination
|
<ngb-pagination
|
||||||
[collectionSize]="paginatedData.total"
|
[collectionSize]="totalItems"
|
||||||
[page]="filters.page!"
|
[page]="currentPage"
|
||||||
[pageSize]="filters.limit!"
|
[pageSize]="itemsPerPage"
|
||||||
[maxSize]="5"
|
[maxSize]="5"
|
||||||
[rotate]="true"
|
[rotate]="true"
|
||||||
[boundaryLinks]="true"
|
[boundaryLinks]="true"
|
||||||
@ -292,6 +383,5 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -40,8 +40,7 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Currency,
|
Currency,
|
||||||
SubscriptionPeriodicity,
|
SubscriptionPeriodicity
|
||||||
SubscriptionUtils
|
|
||||||
} from '@core/models/dcb-bo-hub-subscription.model';
|
} from '@core/models/dcb-bo-hub-subscription.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -53,14 +52,15 @@ import {
|
|||||||
NgIcon,
|
NgIcon,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbTooltipModule
|
NgbTooltipModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
provideNgIconsConfig({
|
provideNgIconsConfig({
|
||||||
size: '1.25em'
|
size: '1.25em'
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
templateUrl: './list.html'
|
templateUrl: './list.html',
|
||||||
|
styleUrl: './list.css'
|
||||||
})
|
})
|
||||||
export class TransactionsList implements OnInit, OnDestroy {
|
export class TransactionsList implements OnInit, OnDestroy {
|
||||||
private transactionsService = inject(TransactionsService);
|
private transactionsService = inject(TransactionsService);
|
||||||
@ -72,12 +72,11 @@ export class TransactionsList implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
access!: TransactionAccess;
|
access!: TransactionAccess;
|
||||||
currentUserRole = '';
|
currentMerchantId: number = 0;
|
||||||
currentMerchantId?: number;
|
|
||||||
|
|
||||||
// Données
|
// Données
|
||||||
transactions: Transaction[] = [];
|
transactions: Transaction[] = [];
|
||||||
paginatedData: PaginatedTransactions | null = null;
|
allTransactions: Transaction[] = [];
|
||||||
|
|
||||||
// États
|
// États
|
||||||
loading = false;
|
loading = false;
|
||||||
@ -85,15 +84,9 @@ export class TransactionsList implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Filtres et recherche
|
// Filtres et recherche
|
||||||
searchTerm = '';
|
searchTerm = '';
|
||||||
filters: TransactionQuery = {
|
statusFilter: TransactionStatus | 'all' = 'all';
|
||||||
page: 1,
|
startDateFilter: Date | null = null;
|
||||||
limit: 20,
|
endDateFilter: Date | null = null;
|
||||||
status: undefined,
|
|
||||||
startDate: undefined,
|
|
||||||
endDate: undefined,
|
|
||||||
sortBy: 'transactionDate',
|
|
||||||
sortOrder: 'desc'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Options de filtre
|
// Options de filtre
|
||||||
statusOptions: TransactionStatus[] = ['PENDING', 'SUCCESS', 'FAILED', 'CANCELLED'];
|
statusOptions: TransactionStatus[] = ['PENDING', 'SUCCESS', 'FAILED', 'CANCELLED'];
|
||||||
@ -102,9 +95,12 @@ export class TransactionsList implements OnInit, OnDestroy {
|
|||||||
TransactionType.SUBSCRIPTION_RENEWAL,
|
TransactionType.SUBSCRIPTION_RENEWAL,
|
||||||
TransactionType.ONE_TIME_PAYMENT
|
TransactionType.ONE_TIME_PAYMENT
|
||||||
];
|
];
|
||||||
periodicityOptions = Object.values(SubscriptionPeriodicity);
|
|
||||||
operatorOptions: string[] = ['Orange'];
|
// Pagination
|
||||||
countryOptions: string[] = ['SN'];
|
currentPage = 1;
|
||||||
|
itemsPerPage = 20;
|
||||||
|
totalItems = 0;
|
||||||
|
totalPages = 0;
|
||||||
|
|
||||||
// Tri
|
// Tri
|
||||||
sortField: string = 'transactionDate';
|
sortField: string = 'transactionDate';
|
||||||
@ -114,62 +110,74 @@ export class TransactionsList implements OnInit, OnDestroy {
|
|||||||
selectedTransactions: Set<string> = new Set();
|
selectedTransactions: Set<string> = new Set();
|
||||||
selectAll = false;
|
selectAll = false;
|
||||||
|
|
||||||
|
// Cache
|
||||||
|
private lastLoadTime: number = 0;
|
||||||
|
private readonly CACHE_TTL = 30000; // 30 secondes comme le service
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.initializePermissions();
|
this.initializePermissions();
|
||||||
this.loadTransactions();
|
this.loadAllTransactions();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
// Nettoyage si nécessaire
|
// Nettoyer le cache si nécessaire
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializePermissions() {
|
private initializePermissions() {
|
||||||
this.access = this.accessService.getTransactionAccess();
|
this.access = this.accessService.getTransactionAccess();
|
||||||
|
|
||||||
// IMPORTANT: Toujours filtrer par merchant pour les merchant users
|
|
||||||
if (this.access.isMerchantUser && this.access.allowedMerchantIds.length > 0) {
|
|
||||||
this.filters.merchantPartnerId = this.access.allowedMerchantIds[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Définir le rôle pour l'affichage
|
|
||||||
this.currentUserRole = this.access.userRoleLabel;
|
|
||||||
this.currentMerchantId = this.access.merchantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadTransactions() {
|
|
||||||
if (!this.access.canViewTransactions) {
|
if (!this.access.canViewTransactions) {
|
||||||
this.error = 'Vous n\'avez pas la permission de voir les transactions';
|
this.error = 'Vous n\'avez pas la permission de voir les transactions';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.access.isMerchantUser) {
|
||||||
|
this.currentMerchantId = this.access.merchantId || 0;
|
||||||
|
if (this.currentMerchantId === 0) {
|
||||||
|
this.error = 'Merchant ID non disponible pour l\'utilisateur merchant';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadAllTransactions() {
|
||||||
|
if (!this.access.canViewTransactions) return;
|
||||||
|
|
||||||
|
// Vérifier si on peut utiliser le cache
|
||||||
|
const now = Date.now();
|
||||||
|
const shouldUseCache = now - this.lastLoadTime < this.CACHE_TTL && this.allTransactions.length > 0;
|
||||||
|
|
||||||
|
if (shouldUseCache) {
|
||||||
|
console.log('🔄 Utilisation du cache pour les transactions');
|
||||||
|
this.applyFiltersAndPagination();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
|
|
||||||
// Préparer les filtres pour l'API
|
// Préparer la requête avec merchantPartnerId si nécessaire
|
||||||
const apiFilters = this.prepareFiltersForApi();
|
const query: TransactionQuery = {
|
||||||
|
page: 1,
|
||||||
|
limit: 1000, // Charger beaucoup d'éléments pour le filtrage client
|
||||||
|
sortBy: 'createdAt',
|
||||||
|
sortOrder: 'desc'
|
||||||
|
};
|
||||||
|
|
||||||
console.log('Chargement transactions avec filtres:', apiFilters);
|
// Ajouter le filtre merchant uniquement pour les merchant users
|
||||||
|
if (this.access.isMerchantUser && this.currentMerchantId > 0) {
|
||||||
|
query.merchantPartnerId = this.currentMerchantId;
|
||||||
|
}
|
||||||
|
|
||||||
this.transactionsService.getTransactions(apiFilters).subscribe({
|
this.transactionsService.getTransactions(query).subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
this.paginatedData = data;
|
// Stocker TOUTES les transactions pour le filtrage client
|
||||||
this.transactions = data.data;
|
this.allTransactions = data.data || [];
|
||||||
|
this.lastLoadTime = Date.now();
|
||||||
|
|
||||||
|
console.log(`✅ ${this.allTransactions.length} transactions chargées`);
|
||||||
|
this.applyFiltersAndPagination();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
// Log pour debug
|
|
||||||
if (data.data.length > 0) {
|
|
||||||
console.log(`Chargement réussi: ${data.data.length} transactions`);
|
|
||||||
if (this.access.isMerchantUser && this.currentMerchantId) {
|
|
||||||
// Vérifier que toutes les transactions appartiennent bien au marchand
|
|
||||||
const wrongMerchantTx = data.data.filter(tx =>
|
|
||||||
tx.merchantPartnerId !== this.currentMerchantId
|
|
||||||
);
|
|
||||||
if (wrongMerchantTx.length > 0) {
|
|
||||||
console.warn('ATTENTION: certaines transactions ne sont pas du bon marchand:', wrongMerchantTx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
@ -181,32 +189,118 @@ export class TransactionsList implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private prepareFiltersForApi(): TransactionQuery {
|
private applyFiltersAndPagination() {
|
||||||
const apiFilters: TransactionQuery = { ...this.filters };
|
// 1. Appliquer les filtres
|
||||||
|
let filteredTransactions = this.filterTransactions(this.allTransactions);
|
||||||
|
|
||||||
// Ajouter la recherche si présente
|
// 2. Appliquer le tri
|
||||||
if (this.searchTerm) {
|
filteredTransactions = this.sortTransactions(filteredTransactions);
|
||||||
apiFilters.search = this.searchTerm;
|
|
||||||
|
// 3. Calculer la pagination
|
||||||
|
this.totalItems = filteredTransactions.length;
|
||||||
|
this.totalPages = Math.ceil(this.totalItems / this.itemsPerPage);
|
||||||
|
|
||||||
|
// Ajuster la page courante si nécessaire
|
||||||
|
if (this.currentPage > this.totalPages) {
|
||||||
|
this.currentPage = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appliquer le tri
|
// 4. Extraire les éléments de la page courante
|
||||||
apiFilters.sortBy = this.sortField;
|
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
||||||
apiFilters.sortOrder = this.sortDirection;
|
const endIndex = Math.min(startIndex + this.itemsPerPage, this.totalItems);
|
||||||
|
this.transactions = filteredTransactions.slice(startIndex, endIndex);
|
||||||
|
|
||||||
// Nettoyer les filtres (enlever les undefined)
|
console.log(`📊 Filtrage: ${this.allTransactions.length} → ${filteredTransactions.length} → ${this.transactions.length} (page ${this.currentPage}/${this.totalPages})`);
|
||||||
Object.keys(apiFilters).forEach(key => {
|
}
|
||||||
if (apiFilters[key as keyof TransactionQuery] === undefined) {
|
|
||||||
delete apiFilters[key as keyof TransactionQuery];
|
private filterTransactions(transactions: Transaction[]): Transaction[] {
|
||||||
|
let filtered = [...transactions];
|
||||||
|
|
||||||
|
// Filtre par statut
|
||||||
|
if (this.statusFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(tx => tx.status === this.statusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par date de début
|
||||||
|
if (this.startDateFilter) {
|
||||||
|
const startDate = new Date(this.startDateFilter);
|
||||||
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
filtered = filtered.filter(tx => {
|
||||||
|
const txDate = new Date(tx.transactionDate);
|
||||||
|
return txDate >= startDate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par date de fin
|
||||||
|
if (this.endDateFilter) {
|
||||||
|
const endDate = new Date(this.endDateFilter);
|
||||||
|
endDate.setHours(23, 59, 59, 999);
|
||||||
|
filtered = filtered.filter(tx => {
|
||||||
|
const txDate = new Date(tx.transactionDate);
|
||||||
|
return txDate <= endDate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par recherche
|
||||||
|
if (this.searchTerm.trim()) {
|
||||||
|
const term = this.searchTerm.toLowerCase().trim();
|
||||||
|
filtered = filtered.filter(tx => {
|
||||||
|
// Recherche dans tous les champs textuels
|
||||||
|
return (
|
||||||
|
(tx.id && tx.id.toString().toLowerCase().includes(term)) ||
|
||||||
|
(tx.externalReference && tx.externalReference.toLowerCase().includes(term)) ||
|
||||||
|
(tx.subscriptionId && tx.subscriptionId.toString().toLowerCase().includes(term)) ||
|
||||||
|
(tx.productName && tx.productName.toLowerCase().includes(term)) ||
|
||||||
|
(tx.merchantPartnerId && tx.merchantPartnerId.toString().includes(term))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sortTransactions(transactions: Transaction[]): Transaction[] {
|
||||||
|
return [...transactions].sort((a: any, b: any) => {
|
||||||
|
const aValue = a[this.sortField];
|
||||||
|
const bValue = b[this.sortField];
|
||||||
|
|
||||||
|
if (aValue == null && bValue == null) return 0;
|
||||||
|
if (aValue == null) return 1;
|
||||||
|
if (bValue == null) return -1;
|
||||||
|
|
||||||
|
let comparison = 0;
|
||||||
|
|
||||||
|
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||||
|
comparison = aValue.localeCompare(bValue);
|
||||||
|
} else if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||||||
|
comparison = aValue - bValue;
|
||||||
|
} else {
|
||||||
|
// Traitement spécial pour les dates
|
||||||
|
const aDate = this.parseDate(aValue);
|
||||||
|
const bDate = this.parseDate(bValue);
|
||||||
|
if (aDate && bDate) {
|
||||||
|
comparison = aDate.getTime() - bDate.getTime();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return apiFilters;
|
return this.sortDirection === 'asc' ? comparison : -comparison;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseDate(value: any): Date | null {
|
||||||
|
if (value instanceof Date) return value;
|
||||||
|
if (typeof value === 'string' || typeof value === 'number') {
|
||||||
|
const date = new Date(value);
|
||||||
|
return !isNaN(date.getTime()) ? date : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recherche et filtres
|
// Recherche et filtres
|
||||||
onSearch() {
|
onSearch() {
|
||||||
this.filters.page = 1;
|
this.currentPage = 1;
|
||||||
this.loadTransactions();
|
this.applyFiltersAndPagination();
|
||||||
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSearch() {
|
clearSearch() {
|
||||||
@ -216,52 +310,33 @@ export class TransactionsList implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
onClearFilters() {
|
onClearFilters() {
|
||||||
this.searchTerm = '';
|
this.searchTerm = '';
|
||||||
this.filters = {
|
this.statusFilter = 'all';
|
||||||
page: 1,
|
this.startDateFilter = null;
|
||||||
limit: 20,
|
this.endDateFilter = null;
|
||||||
status: undefined,
|
this.currentPage = 1;
|
||||||
startDate: undefined,
|
this.applyFiltersAndPagination();
|
||||||
endDate: undefined,
|
this.cdRef.detectChanges();
|
||||||
sortBy: 'transactionDate',
|
|
||||||
sortOrder: 'desc'
|
|
||||||
};
|
|
||||||
|
|
||||||
// IMPORTANT: Toujours réappliquer le filtrage par marchand pour les merchant users
|
|
||||||
if (this.access.isMerchantUser && this.access.allowedMerchantIds.length > 0) {
|
|
||||||
this.filters.merchantPartnerId = this.access.allowedMerchantIds[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadTransactions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStatusFilterChange(status: TransactionStatus | 'all') {
|
onStatusFilterChange(status: TransactionStatus | 'all') {
|
||||||
this.filters.status = status === 'all' ? undefined : status;
|
this.statusFilter = status;
|
||||||
this.filters.page = 1;
|
this.currentPage = 1;
|
||||||
this.loadTransactions();
|
this.applyFiltersAndPagination();
|
||||||
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
onOperatorFilterChange(operator: string) {
|
onStartDateChange(date: Date | null) {
|
||||||
this.filters.page = 1;
|
this.startDateFilter = date;
|
||||||
this.loadTransactions();
|
this.currentPage = 1;
|
||||||
|
this.applyFiltersAndPagination();
|
||||||
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDateRangeChange(start: Date | null, end: Date | null) {
|
onEndDateChange(date: Date | null) {
|
||||||
this.filters.startDate = start || undefined;
|
this.endDateFilter = date;
|
||||||
this.filters.endDate = end || undefined;
|
this.currentPage = 1;
|
||||||
this.filters.page = 1;
|
this.applyFiltersAndPagination();
|
||||||
this.loadTransactions();
|
this.cdRef.detectChanges();
|
||||||
}
|
|
||||||
|
|
||||||
// Permissions pour les filtres
|
|
||||||
canUseMerchantFilter(): boolean {
|
|
||||||
// Uniquement pour les hub users avec plusieurs merchants autorisés
|
|
||||||
return this.access.canFilterByMerchant &&
|
|
||||||
this.access.isHubUser &&
|
|
||||||
this.access.allowedMerchantIds.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
canUseAllFilters(): boolean {
|
|
||||||
return this.access.canViewAllTransactions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tri
|
// Tri
|
||||||
@ -272,8 +347,8 @@ export class TransactionsList implements OnInit, OnDestroy {
|
|||||||
this.sortField = field;
|
this.sortField = field;
|
||||||
this.sortDirection = 'desc';
|
this.sortDirection = 'desc';
|
||||||
}
|
}
|
||||||
this.filters.page = 1;
|
this.applyFiltersAndPagination();
|
||||||
this.loadTransactions();
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortIcon(field: string): string {
|
getSortIcon(field: string): string {
|
||||||
@ -283,12 +358,14 @@ export class TransactionsList implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
onPageChange(page: number) {
|
onPageChange(page: number) {
|
||||||
this.filters.page = page;
|
this.currentPage = page;
|
||||||
this.loadTransactions();
|
this.applyFiltersAndPagination();
|
||||||
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
viewTransactionDetails(transactionId: string) {
|
viewTransactionDetails(transactionId: string) {
|
||||||
|
if (!transactionId) return;
|
||||||
this.transactionSelected.emit(transactionId);
|
this.transactionSelected.emit(transactionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,6 +393,26 @@ export class TransactionsList implements OnInit, OnDestroy {
|
|||||||
this.selectedTransactions.size === this.transactions.length;
|
this.selectedTransactions.size === this.transactions.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Méthode pour charger les paiements d'un abonnement spécifique
|
||||||
|
loadSubscriptionPayments(subscriptionId: number) {
|
||||||
|
if (this.currentMerchantId === 0) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.transactionsService.getTransactionPayments(this.currentMerchantId, subscriptionId).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.allTransactions = data.data;
|
||||||
|
this.applyFiltersAndPagination();
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.error = 'Erreur lors du chargement des paiements';
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Utilitaires d'affichage
|
// Utilitaires d'affichage
|
||||||
getStatusBadgeClass(status: TransactionStatus): string {
|
getStatusBadgeClass(status: TransactionStatus): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@ -347,20 +444,15 @@ export class TransactionsList implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPeriodicityBadgeClass(periodicity?: string): string {
|
getPeriodicityBadgeClass(periodicity?: string): string {
|
||||||
if (!periodicity) return 'badge bg-secondary';
|
if (!periodicity) return 'badge bg-secondary';
|
||||||
|
|
||||||
switch (periodicity.toLowerCase()) {
|
switch (periodicity.toLowerCase()) {
|
||||||
case 'daily':
|
case 'daily': return 'badge bg-primary';
|
||||||
return 'badge bg-primary';
|
case 'weekly': return 'badge bg-info';
|
||||||
case 'weekly':
|
case 'monthly': return 'badge bg-success';
|
||||||
return 'badge bg-info';
|
case 'yearly': return 'badge bg-warning';
|
||||||
case 'monthly':
|
default: return 'badge bg-secondary';
|
||||||
return 'badge bg-success';
|
|
||||||
case 'yearly':
|
|
||||||
return 'badge bg-warning';
|
|
||||||
default:
|
|
||||||
return 'badge bg-secondary';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -399,6 +491,24 @@ export class TransactionsList implements OnInit, OnDestroy {
|
|||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Méthodes pour gérer les changements de date
|
||||||
|
handleStartDateChange(event: Event): void {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const value = input.value;
|
||||||
|
this.onStartDateChange(value ? new Date(value) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEndDateChange(event: Event): void {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const value = input.value;
|
||||||
|
this.onEndDateChange(value ? new Date(value) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour calculer la fin de la page courante
|
||||||
|
getCurrentPageEnd(): number {
|
||||||
|
const end = this.currentPage * this.itemsPerPage;
|
||||||
|
return end > this.totalItems ? this.totalItems : end;
|
||||||
|
}
|
||||||
|
|
||||||
getAmountColor(amount: number): string {
|
getAmountColor(amount: number): string {
|
||||||
if (amount >= 10000) return 'text-danger fw-bold';
|
if (amount >= 10000) return 'text-danger fw-bold';
|
||||||
@ -414,25 +524,41 @@ export class TransactionsList implements OnInit, OnDestroy {
|
|||||||
return TransactionUtils.getTypeDisplayName(type);
|
return TransactionUtils.getTypeDisplayName(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Méthodes pour calculer les stats côté client
|
||||||
|
calculateStats() {
|
||||||
|
const filtered = this.filterTransactions(this.allTransactions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: filtered.length,
|
||||||
|
successCount: filtered.filter(tx => tx.status === 'SUCCESS').length,
|
||||||
|
failedCount: filtered.filter(tx => tx.status === 'FAILED').length,
|
||||||
|
pendingCount: filtered.filter(tx => tx.status === 'PENDING').length,
|
||||||
|
cancelledCount: filtered.filter(tx => tx.status === 'CANCELLED').length,
|
||||||
|
totalAmount: filtered.reduce((sum, tx) => sum + (tx.amount || 0), 0),
|
||||||
|
averageAmount: filtered.length > 0 ?
|
||||||
|
filtered.reduce((sum, tx) => sum + (tx.amount || 0), 0) / filtered.length : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Méthodes pour sécuriser l'accès aux stats
|
// Méthodes pour sécuriser l'accès aux stats
|
||||||
getTotal(): number {
|
getTotal(): number {
|
||||||
return this.paginatedData?.stats?.total || 0;
|
return this.calculateStats().total;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSuccessCount(): number {
|
getSuccessCount(): number {
|
||||||
return this.paginatedData?.stats?.successCount || 0;
|
return this.calculateStats().successCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFailedCount(): number {
|
getFailedCount(): number {
|
||||||
return this.paginatedData?.stats?.failedCount || 0;
|
return this.calculateStats().failedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPendingCount(): number {
|
getPendingCount(): number {
|
||||||
return this.paginatedData?.stats?.pendingCount || 0;
|
return this.calculateStats().pendingCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotalAmount(): number {
|
getTotalAmount(): number {
|
||||||
return this.paginatedData?.stats?.totalAmount || 0;
|
return this.calculateStats().totalAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthodes pour le template
|
// Méthodes pour le template
|
||||||
@ -449,29 +575,36 @@ export class TransactionsList implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getScopeText(): string {
|
getScopeText(): string {
|
||||||
if (this.access.isMerchantUser && this.currentMerchantId) {
|
if (this.access.isMerchantUser && this.currentMerchantId > 0) {
|
||||||
return `Marchand ${this.currentMerchantId}`;
|
return `Marchand ${this.currentMerchantId}`;
|
||||||
} else if (this.access.isHubUser && this.filters.merchantPartnerId) {
|
} else if (this.access.isHubUser) {
|
||||||
return `Marchand ${this.filters.merchantPartnerId}`;
|
return 'Tous les marchands';
|
||||||
}
|
}
|
||||||
return 'Tous les marchands';
|
return 'Aucun scope défini';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthode pour recharger les données
|
// Méthode pour recharger les données
|
||||||
refreshData() {
|
refreshData() {
|
||||||
this.loadTransactions();
|
this.lastLoadTime = 0; // Invalider le cache
|
||||||
|
this.loadAllTransactions();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug
|
// Méthodes utilitaires
|
||||||
showDebugInfo() {
|
getFilteredCount(): number {
|
||||||
console.log('=== DEBUG INFO ===');
|
return this.transactions.length;
|
||||||
console.log('Permissions:', {
|
|
||||||
isMerchantUser: this.access.isMerchantUser,
|
|
||||||
isHubUser: this.access.isHubUser,
|
|
||||||
merchantId: this.currentMerchantId,
|
|
||||||
allowedMerchantIds: this.access.allowedMerchantIds,
|
|
||||||
filters: this.filters
|
|
||||||
});
|
|
||||||
console.log('Transactions chargées:', this.transactions.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTotalCount(): number {
|
||||||
|
return this.allTransactions.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSuccessRate(): number {
|
||||||
|
const stats = this.calculateStats();
|
||||||
|
return stats.total > 0 ? Math.round((stats.successCount / stats.total) * 100) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
canUseMerchantFilter(): boolean {
|
||||||
|
return this.access.isHubUser && this.access.canViewAllTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -61,7 +61,7 @@ export class TransactionAccessService {
|
|||||||
|
|
||||||
// Informations utilisateur
|
// Informations utilisateur
|
||||||
userRole,
|
userRole,
|
||||||
userRoleLabel: this.roleService.getRoleLabel() || 'Utilisateur',
|
userRoleLabel: this.roleService.getRoleLabel(userRole) || 'Utilisateur',
|
||||||
merchantId
|
merchantId
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
localServiceTestApiUrl: "https://backoffice.dcb.pixpay.sn/api/v1",
|
localServiceTestApiUrl: "http://localhost:4200/api/v1",
|
||||||
iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1",
|
iamApiUrl: "http://localhost:3000/api/v1",
|
||||||
configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1',
|
configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1',
|
||||||
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
||||||
reportingApiUrl: 'https://api-reporting-service.dcb.pixpay.sn/api/v1/',
|
reportingApiUrl: 'https://api-reporting-service.dcb.pixpay.sn/api/v1/',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user