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 ===
|
||||
export interface Subscription {
|
||||
id: number;
|
||||
id: string;
|
||||
externalReference?: string | null;
|
||||
periodicity: SubscriptionPeriodicity;
|
||||
startDate: string;
|
||||
|
||||
@ -152,14 +152,27 @@ export interface RoleOperationResponse {
|
||||
// === SEARCH ===
|
||||
export interface SearchUsersParams {
|
||||
query?: string;
|
||||
role?: UserRole;
|
||||
role?: string;
|
||||
enabled?: boolean;
|
||||
userType?: UserType;
|
||||
merchantPartnerId?: string;
|
||||
searchTerm?: string;
|
||||
status?: 'all' | 'enabled' | 'disabled';
|
||||
emailVerified?: 'all' | 'verified' | 'not-verified';
|
||||
sortField?: keyof User;
|
||||
sortDirection?: 'asc' | 'desc';
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface UserStats {
|
||||
total: number;
|
||||
enabled: number;
|
||||
disabled: number;
|
||||
emailVerified: number;
|
||||
roleCounts: Record<string, number>;
|
||||
}
|
||||
|
||||
// === UTILITAIRES ===
|
||||
export class UserUtils {
|
||||
static isHubUser(user: User): boolean {
|
||||
|
||||
@ -111,6 +111,7 @@ export interface ApiMerchantUser {
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
merchantPartnerId?: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
@ -129,6 +130,10 @@ export interface ApiMerchant {
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface MerchantUserWithMerchant extends MerchantUser {
|
||||
merchant: Merchant;
|
||||
}
|
||||
|
||||
// === DTOs CRUD ===
|
||||
export interface CreateMerchantDto {
|
||||
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 { Router } from '@angular/router';
|
||||
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 { DashboardAccessService } from '@modules/dcb-dashboard/services/dashboard-access.service';
|
||||
|
||||
import {
|
||||
User,
|
||||
UserType,
|
||||
UserRole,
|
||||
} from '@core/models/dcb-bo-hub-user.model';
|
||||
import { TransactionAccessService } from '@modules/transactions/services/transaction-access.service';
|
||||
|
||||
// === INTERFACES DTO AUTH ===
|
||||
export interface LoginDto {
|
||||
@ -66,18 +62,32 @@ export class AuthService {
|
||||
private userProfile$ = new BehaviorSubject<User | null>(null);
|
||||
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 ===
|
||||
|
||||
/**
|
||||
* Initialise l'authentification au démarrage de l'application
|
||||
*/
|
||||
|
||||
/**
|
||||
* Initialise l'authentification et charge le profil
|
||||
*/
|
||||
async initialize(): Promise<boolean> {
|
||||
const token = this.getAccessToken();
|
||||
|
||||
// Pas de token → pas authentifié
|
||||
if (!token) {
|
||||
this.profileLoaded$.next(true);
|
||||
this.initialized$.next(true);
|
||||
return false;
|
||||
}
|
||||
@ -85,6 +95,7 @@ export class AuthService {
|
||||
// Token expiré → tenter refresh
|
||||
if (this.isTokenExpired(token)) {
|
||||
const ok = await this.tryRefreshToken();
|
||||
this.profileLoaded$.next(true);
|
||||
this.initialized$.next(true);
|
||||
return ok;
|
||||
}
|
||||
@ -93,15 +104,50 @@ export class AuthService {
|
||||
try {
|
||||
await firstValueFrom(this.loadUserProfile());
|
||||
this.authState$.next(true);
|
||||
this.profileLoaded$.next(true);
|
||||
this.initialized$.next(true);
|
||||
return true;
|
||||
} catch {
|
||||
this.clearAuthData();
|
||||
this.profileLoaded$.next(true);
|
||||
this.initialized$.next(true);
|
||||
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
|
||||
*/
|
||||
@ -171,21 +217,16 @@ export class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Déconnexion utilisateur
|
||||
*/
|
||||
/**
|
||||
* Déconnexion utilisateur avec nettoyage complet
|
||||
*/
|
||||
logout(): Observable<LogoutResponseDto> {
|
||||
const token = this.getAccessToken();
|
||||
|
||||
// Si pas de token, nettoyer et retourner un observable complet
|
||||
if (!token) {
|
||||
this.clearAuthData();
|
||||
this.performLogoutCleanup(); // Nettoyage local
|
||||
return of({ message: 'Already logged out' });
|
||||
}
|
||||
|
||||
// Ajouter le token dans le header si nécessaire
|
||||
const headers = new HttpHeaders({
|
||||
'Authorization': `Bearer ${token}`
|
||||
});
|
||||
@ -196,48 +237,44 @@ export class AuthService {
|
||||
{ headers }
|
||||
).pipe(
|
||||
tap(() => {
|
||||
this.clearAuthData();
|
||||
|
||||
this.transactionAccessService.clearCache();
|
||||
this.clearAllStorage(); // Nettoyer tout le storage
|
||||
this.performLogoutCleanup();
|
||||
}),
|
||||
catchError(error => {
|
||||
// Même en cas d'erreur, nettoyer tout
|
||||
this.clearAuthData();
|
||||
|
||||
this.transactionAccessService.clearCache();
|
||||
this.clearAllStorage();
|
||||
this.performLogoutCleanup();
|
||||
console.warn('Logout API error, but local data cleared:', error);
|
||||
// Retourner un succès simulé pour permettre la navigation
|
||||
return of({ message: 'Local session cleared' });
|
||||
}),
|
||||
finalize(() => {
|
||||
// Garantir le nettoyage dans tous les cas
|
||||
this.clearAuthData();
|
||||
|
||||
this.transactionAccessService.clearCache();
|
||||
this.performLogoutCleanup();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Déconnexion forcée sans appel API
|
||||
*/
|
||||
forceLogout(): void {
|
||||
this.clearAuthData();
|
||||
private performLogoutCleanup(): void {
|
||||
// 1. Nettoyer les données d'authentification
|
||||
localStorage.removeItem(this.tokenKey);
|
||||
localStorage.removeItem(this.refreshTokenKey);
|
||||
localStorage.removeItem('user_profile');
|
||||
sessionStorage.clear();
|
||||
|
||||
this.transactionAccessService.clearCache();
|
||||
this.clearAllStorage();
|
||||
// 2. Réinitialiser les BehaviorSubjects
|
||||
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
|
||||
*/
|
||||
private clearAuthData(): void {
|
||||
|
||||
|
||||
this.transactionAccessService.clearCache();
|
||||
|
||||
// Supprimer tous les tokens et données utilisateur
|
||||
localStorage.removeItem(this.tokenKey);
|
||||
localStorage.removeItem(this.refreshTokenKey);
|
||||
@ -250,6 +287,7 @@ export class AuthService {
|
||||
// Réinitialiser les BehaviorSubjects
|
||||
this.authState$.next(false);
|
||||
this.userProfile$.next(null);
|
||||
this.authStateChanged$.next(false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -307,18 +345,28 @@ export class AuthService {
|
||||
*/
|
||||
private determineUserType(apiUser: any): UserType {
|
||||
|
||||
const hubRoles = [UserRole.DCB_ADMIN || UserRole.DCB_SUPPORT];
|
||||
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN || UserRole.DCB_PARTNER_MANAGER || UserRole.DCB_PARTNER_SUPPORT];
|
||||
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
|
||||
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
|
||||
|
||||
// Logique pour déterminer le type d'utilisateur
|
||||
if (apiUser.clientRoles?.[0].includes(merchantRoles)) {
|
||||
return UserType.MERCHANT_PARTNER;
|
||||
} else if (apiUser.clientRoles?.[0].includes(hubRoles)) {
|
||||
return UserType.HUB;
|
||||
} else {
|
||||
console.warn('Type d\'utilisateur non reconnu, rôle:', apiUser.clientRoles?.[0]);
|
||||
return UserType.HUB; // Fallback
|
||||
// Récupérer le rôle depuis l'API
|
||||
const clientRole = apiUser.clientRoles?.[0];
|
||||
|
||||
if (!clientRole) {
|
||||
console.warn('Aucun rôle trouvé dans le profil');
|
||||
throw new Error(`Type d'utilisateur non reconnu: ${clientRole}`);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@ -374,6 +422,9 @@ export class AuthService {
|
||||
return this.userProfile$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer le profil utilisateur (synchrone, après chargement)
|
||||
*/
|
||||
getCurrentUserProfile(): User | null {
|
||||
return this.userProfile$.value;
|
||||
}
|
||||
@ -557,6 +608,8 @@ export class AuthService {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("Merchant Partner ID : " + profile.merchantPartnerId)
|
||||
|
||||
const merchantId = profile.merchantPartnerId
|
||||
|
||||
if (merchantId === null || merchantId === undefined) {
|
||||
|
||||
@ -20,6 +20,88 @@ export enum UserRole {
|
||||
|
||||
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' })
|
||||
export class RoleManagementService {
|
||||
private currentRole: UserRole | null = null;
|
||||
@ -198,14 +280,27 @@ export class RoleManagementService {
|
||||
|
||||
// === UTILITAIRES ===
|
||||
|
||||
getRoleLabel(role?: UserRole): string {
|
||||
const targetRole = role || this.currentRole;
|
||||
return targetRole ? this.roleLabels[targetRole] || targetRole : '';
|
||||
/**
|
||||
* Méthodes d'utilité pour les rôles
|
||||
*/
|
||||
getRoleLabel(role: string): string {
|
||||
const userRole = role as UserRole;
|
||||
return ROLE_CONFIG[userRole]?.label || role;
|
||||
}
|
||||
|
||||
getRoleIcon(role?: UserRole): string {
|
||||
const targetRole = role || this.currentRole;
|
||||
return targetRole ? this.roleIcons[targetRole] || 'user' : 'user';
|
||||
getRoleDescription(role: string | UserRole): string {
|
||||
const userRole = role as UserRole;
|
||||
return ROLE_CONFIG[userRole]?.description || 'Description non disponible';
|
||||
}
|
||||
|
||||
getRoleBadgeClass(role: string): string {
|
||||
const userRole = role as UserRole;
|
||||
return ROLE_CONFIG[userRole]?.badgeClass || 'bg-secondary';
|
||||
}
|
||||
|
||||
getRoleIcon(role: string): string {
|
||||
const userRole = role as UserRole;
|
||||
return ROLE_CONFIG[userRole]?.icon || 'lucideUser';
|
||||
}
|
||||
|
||||
getAllRoles(): UserRole[] {
|
||||
|
||||
@ -77,33 +77,9 @@ export class MenuService {
|
||||
icon: 'lucideCreditCard',
|
||||
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: '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 },
|
||||
{
|
||||
@ -119,8 +95,6 @@ export class MenuService {
|
||||
|
||||
{ label: 'Configurations', isTitle: true },
|
||||
{ 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', icon: 'lucideLifeBuoy', url: '/support' },
|
||||
|
||||
@ -33,25 +33,7 @@ export class PermissionsService {
|
||||
module: 'merchant-users-management',
|
||||
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
|
||||
{
|
||||
module: 'settings',
|
||||
@ -63,16 +45,6 @@ export class PermissionsService {
|
||||
module: 'subscriptions',
|
||||
roles: this.allRoles
|
||||
},
|
||||
{
|
||||
module: 'subscriptions-merchant',
|
||||
roles: this.allRoles
|
||||
},
|
||||
|
||||
// Payments
|
||||
{
|
||||
module: 'subscriptions-payments',
|
||||
roles: this.allRoles
|
||||
},
|
||||
|
||||
// Settings - Tout le monde
|
||||
{
|
||||
@ -80,11 +52,6 @@ export class PermissionsService {
|
||||
roles: this.allRoles
|
||||
},
|
||||
|
||||
// Integrations - Admin seulement
|
||||
{
|
||||
module: 'integrations',
|
||||
roles: [UserRole.DCB_ADMIN]
|
||||
},
|
||||
// Modules publics - Tout le monde
|
||||
{
|
||||
module: 'support',
|
||||
|
||||
@ -59,49 +59,11 @@ export const menuItems: MenuItemType[] = [
|
||||
icon: 'lucideCreditCard',
|
||||
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: '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' },
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// Utilisateurs & Sécurité
|
||||
// ---------------------------
|
||||
@ -128,7 +90,6 @@ export const menuItems: MenuItemType[] = [
|
||||
{ label: 'Configurations', isTitle: true },
|
||||
{ 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' },
|
||||
|
||||
// ---------------------------
|
||||
// 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 { AuthService } from '@core/services/auth.service';
|
||||
import { PageTitle } from '@app/components/page-title/page-title';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
// ============ TYPES ET INTERFACES ============
|
||||
|
||||
@ -138,7 +139,9 @@ interface SubscriptionStats {
|
||||
FormsModule,
|
||||
NgIconComponent,
|
||||
NgbDropdownModule,
|
||||
PageTitle],
|
||||
PageTitle,
|
||||
RouterModule
|
||||
],
|
||||
providers: [
|
||||
provideIcons({
|
||||
lucideActivity, lucideAlertCircle, lucideCheckCircle2, lucideRefreshCw,
|
||||
@ -309,103 +312,100 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
Chart.register(...registerables);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
dashboardInitialized = false;
|
||||
|
||||
// ============ INITIALISATION ============
|
||||
|
||||
private initializeAccess(): void {
|
||||
// Attendre que l'accès soit prêt
|
||||
ngOnInit(): void {
|
||||
console.log('🔍 Dashboard: ngOnInit() appelé');
|
||||
|
||||
// Attendre que le DashboardAccessService soit VRAIMENT prêt
|
||||
this.subscriptions.push(
|
||||
this.accessService.waitForAccess().subscribe(() => {
|
||||
this.access = this.accessService.getDashboardAccess();
|
||||
this.currentRoleLabel = this.access.roleLabel;
|
||||
this.currentRoleIcon = this.access.roleIcon;
|
||||
|
||||
console.log('✅ Dashboard initialisé avec:', {
|
||||
access: this.access,
|
||||
merchantId: this.access.merchantId,
|
||||
isHubUser: this.access.isHubUser,
|
||||
});
|
||||
|
||||
// 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é)');
|
||||
}
|
||||
this.accessService.waitForReady().subscribe({
|
||||
next: () => {
|
||||
console.log('✅ Dashboard: waitForReady() a émis - Initialisation...');
|
||||
this.dashboardInitialized = true;
|
||||
this.initializeDashboard();
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('❌ Dashboard: Erreur dans waitForReady():', err);
|
||||
// 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');
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private initializeDashboard(): void {
|
||||
console.log('🚀 Dashboard: initializeDashboard() appelé');
|
||||
|
||||
isValidMerchantId(id: any): boolean {
|
||||
if (id === null || id === undefined) {
|
||||
return false;
|
||||
try {
|
||||
// 1. Récupérer l'accès
|
||||
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);
|
||||
return !isNaN(numId) && Number.isInteger(numId) && numId > 0;
|
||||
private loadData(): void {
|
||||
if (this.access.isHubUser && this.isViewingGlobalData) {
|
||||
this.loadGlobalData();
|
||||
} else {
|
||||
this.loadMerchantData(this.merchantId);
|
||||
}
|
||||
}
|
||||
|
||||
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 ============
|
||||
private loadGlobalData(): void {
|
||||
if (!this.access.isHubUser) return;
|
||||
@ -485,25 +461,11 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
private loadMerchantData(merchantId: number | undefined): void {
|
||||
console.log('Chargement des données pour merchant:', merchantId);
|
||||
|
||||
// Vérification plus robuste
|
||||
if (!merchantId || merchantId <= 0 || isNaN(merchantId)) {
|
||||
console.error('Merchant ID invalide ou manquant:', merchantId);
|
||||
this.addAlert('warning', 'Merchant non spécifié',
|
||||
'Veuillez sélectionner un merchant valide', 'Maintenant');
|
||||
|
||||
// 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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading.merchantData = true;
|
||||
@ -1507,6 +1469,15 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
// ============ 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 {
|
||||
return this.isViewingGlobalData;
|
||||
}
|
||||
@ -1525,7 +1496,6 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
private getCurrentMerchantPartnerId(): string | null {
|
||||
// Utiliser une valeur par défaut sécurisée
|
||||
return this.access?.merchantId?.toString() || null;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
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 { MerchantConfigService } from '@modules/merchant-config/merchant-config.service';
|
||||
import { AuthService } from '@core/services/auth.service';
|
||||
@ -8,8 +8,6 @@ import { AuthService } from '@core/services/auth.service';
|
||||
export interface DashboardAccess {
|
||||
isHubUser: boolean;
|
||||
isMerchantUser: boolean;
|
||||
roleLabel: string;
|
||||
roleIcon: string;
|
||||
userRole: UserRole;
|
||||
merchantId?: number;
|
||||
}
|
||||
@ -24,130 +22,124 @@ export class DashboardAccessService {
|
||||
private accessCache: DashboardAccess | null = null;
|
||||
private merchantsCache: AllowedMerchant[] | null = null;
|
||||
private currentMerchantId: number | null = null;
|
||||
private accessReady$ = new BehaviorSubject<boolean>(false);
|
||||
private ready$ = new BehaviorSubject<boolean>(false);
|
||||
private profileLoaded = false;
|
||||
|
||||
constructor(
|
||||
private roleService: RoleManagementService,
|
||||
private merchantService: MerchantConfigService,
|
||||
private authService: AuthService
|
||||
) {
|
||||
// S'abonner aux changements du profil utilisateur
|
||||
this.initializeProfileSubscription();
|
||||
// Initialisation simple
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise la surveillance du profil utilisateur
|
||||
*/
|
||||
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
|
||||
});
|
||||
private initialize(): void {
|
||||
console.log('🚀 DashboardAccessService: Initialisation');
|
||||
|
||||
this.clearCache();
|
||||
this.accessReady$.next(true);
|
||||
// S'abonner aux changements de profil
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attend que l'accès soit prêt
|
||||
*/
|
||||
waitForAccess(): Observable<boolean> {
|
||||
return this.accessReady$.pipe(
|
||||
filter(ready => ready),
|
||||
take(1)
|
||||
// Attendre que le service soit prêt AVEC PROFIL CHARGÉ
|
||||
waitForReady(): Observable<boolean> {
|
||||
return this.ready$.pipe(
|
||||
filter(ready => ready && this.profileLoaded),
|
||||
take(1),
|
||||
tap(() => {
|
||||
console.log('✅ DashboardAccessService: waitForReady() - Service vraiment prêt');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient l'accès dashboard (version synchrone)
|
||||
*/
|
||||
// Obtenir l'accès dashboard
|
||||
getDashboardAccess(): DashboardAccess {
|
||||
if (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 isHubUser = this.roleService.isHubUser();
|
||||
|
||||
let merchantId: number | undefined = undefined;
|
||||
|
||||
if (!isHubUser) {
|
||||
merchantId = this.getMerchantIdForCurrentUser();
|
||||
if (!isHubUser && profile.merchantPartnerId) {
|
||||
merchantId = Number(profile.merchantPartnerId);
|
||||
if (isNaN(merchantId) || merchantId <= 0) {
|
||||
console.warn(`⚠️ DashboardAccessService: merchantPartnerId invalide: ${profile.merchantPartnerId}`);
|
||||
merchantId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const access: DashboardAccess = {
|
||||
this.accessCache = {
|
||||
isHubUser,
|
||||
isMerchantUser: !isHubUser,
|
||||
roleLabel: this.roleService.getRoleLabel(),
|
||||
roleIcon: this.roleService.getRoleIcon(),
|
||||
userRole: userRole || UserRole.DCB_SUPPORT,
|
||||
merchantId
|
||||
};
|
||||
|
||||
console.log('📊 DashboardAccess créé:', {
|
||||
...access,
|
||||
merchantId,
|
||||
userRoleLabel: userRole
|
||||
});
|
||||
console.log('🎯 DashboardAccessService: Accès créé:', this.accessCache);
|
||||
|
||||
this.accessCache = access;
|
||||
return access;
|
||||
return this.accessCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient l'accès dashboard (version asynchrone)
|
||||
*/
|
||||
// Obtenir l'accès dashboard avec attente
|
||||
getDashboardAccessAsync(): Observable<DashboardAccess> {
|
||||
return this.waitForAccess().pipe(
|
||||
return this.waitForReady().pipe(
|
||||
map(() => this.getDashboardAccess())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
// Obtenir les marchands disponibles
|
||||
getAvailableMerchants(): Observable<AllowedMerchant[]> {
|
||||
return this.waitForAccess().pipe(
|
||||
return this.waitForReady().pipe(
|
||||
switchMap(() => {
|
||||
if (this.merchantsCache) {
|
||||
return of(this.merchantsCache);
|
||||
@ -155,162 +147,71 @@ export class DashboardAccessService {
|
||||
|
||||
const access = this.getDashboardAccess();
|
||||
|
||||
console.log('📊 getAvailableMerchants pour:', {
|
||||
isHubUser: access.isHubUser,
|
||||
merchantId: access.merchantId,
|
||||
role: access.userRole
|
||||
});
|
||||
|
||||
if (access.isHubUser) {
|
||||
// Hub users: tous les merchants + option globale
|
||||
return this.loadAllMerchantsForHubUser();
|
||||
return this.merchantService.getAllMerchants().pipe(
|
||||
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 {
|
||||
// Merchant users: seulement leur merchant
|
||||
return this.loadSingleMerchantForUser(access.merchantId);
|
||||
// Merchant user: seulement son merchant
|
||||
const merchantId = access.merchantId;
|
||||
if (merchantId) {
|
||||
const merchants = [{ id: merchantId, name: `🏪 Merchant ${merchantId}` }];
|
||||
this.merchantsCache = merchants;
|
||||
return of(merchants);
|
||||
}
|
||||
return of([]);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
// Définir le marchand sélectionné (hub users seulement)
|
||||
setSelectedMerchantId(merchantId: number): void {
|
||||
if (this.getDashboardAccess().isHubUser) {
|
||||
this.currentMerchantId = merchantId;
|
||||
console.log(`📌 Merchant sélectionné: ${merchantId}`);
|
||||
} else {
|
||||
console.warn('⚠️ Seuls les hub users peuvent sélectionner un merchant');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient le merchant sélectionné
|
||||
*/
|
||||
// Obtenir le marchand sélectionné
|
||||
getSelectedMerchantId(): number | null {
|
||||
const access = this.getDashboardAccess();
|
||||
|
||||
if (access.isMerchantUser) {
|
||||
// Merchant users: toujours leur merchant
|
||||
return access.merchantId || null;
|
||||
}
|
||||
|
||||
// Hub users: le merchant sélectionné
|
||||
return this.currentMerchantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
// Nettoyer le cache
|
||||
clearCache(): void {
|
||||
this.accessCache = null;
|
||||
this.merchantsCache = null;
|
||||
this.currentMerchantId = null;
|
||||
console.log('🗑️ DashboardAccessService: Cache nettoyé');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ============
|
||||
// ============ MÉTHODES UTILITAIRES SIMPLES ============
|
||||
|
||||
shouldShowSystemHealth(): boolean {
|
||||
return this.getDashboardAccess().isHubUser;
|
||||
}
|
||||
|
||||
shouldShowAllTransactions(): boolean {
|
||||
return this.getDashboardAccess().isHubUser;
|
||||
shouldShowAlerts(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
canTriggerSync(): boolean {
|
||||
@ -323,36 +224,10 @@ export class DashboardAccessService {
|
||||
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 {
|
||||
return this.getDashboardAccess().isHubUser;
|
||||
}
|
||||
|
||||
shouldShowMerchantId(): boolean {
|
||||
const access = this.getDashboardAccess();
|
||||
return access.isMerchantUser ||
|
||||
(access.isHubUser && this.getSelectedMerchantId() !== null);
|
||||
}
|
||||
|
||||
canEditMerchantFilter(): boolean {
|
||||
const access = this.getDashboardAccess();
|
||||
if (access.isHubUser) {
|
||||
@ -361,18 +236,16 @@ export class DashboardAccessService {
|
||||
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 {
|
||||
return this.getDashboardAccess().isHubUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur est en mode "données globales"
|
||||
*/
|
||||
isViewingGlobalData(): boolean {
|
||||
const access = this.getDashboardAccess();
|
||||
if (access.isHubUser) {
|
||||
@ -381,9 +254,6 @@ export class DashboardAccessService {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le nom du merchant courant
|
||||
*/
|
||||
getCurrentMerchantName(): string {
|
||||
const access = this.getDashboardAccess();
|
||||
|
||||
@ -401,14 +271,4 @@ export class DashboardAccessService {
|
||||
|
||||
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';
|
||||
|
||||
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 { UiCard } from '@app/components/ui-card';
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
} from '@core/models/dcb-bo-hub-user.model';
|
||||
|
||||
import { HubUsersService } from '../hub-users.service';
|
||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||
import { AuthService } from '@core/services/auth.service';
|
||||
|
||||
@Component({
|
||||
@ -214,12 +214,6 @@ export class HubUserProfile implements OnInit, OnDestroy {
|
||||
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
|
||||
if (!this.isValidHubRole(newRole)) {
|
||||
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
|
||||
return this.roleService.canEditUsers(this.currentUserRole);
|
||||
|
||||
return this.roleService.isAnyAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -318,7 +313,8 @@ export class HubUserProfile implements OnInit, OnDestroy {
|
||||
*/
|
||||
canManageRoles(): boolean {
|
||||
// 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
|
||||
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
|
||||
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
|
||||
return this.roleService.canDeleteUsers(this.currentUserRole);
|
||||
|
||||
return this.roleService.isAnyAdmin();
|
||||
}
|
||||
|
||||
// ==================== UTILITAIRES D'AFFICHAGE ====================
|
||||
@ -526,8 +525,7 @@ export class HubUserProfile implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
getAssignableRoles(): UserRole[] {
|
||||
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
|
||||
return hubRoles.filter(role => this.roleService.canAssignRole(this.currentUserRole, role));
|
||||
return [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
|
||||
}
|
||||
|
||||
// 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 { 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 { MerchantSyncService } from './merchant-sync-orchestrator.service';
|
||||
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}`);
|
||||
|
||||
if (this.currentUserRole) {
|
||||
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);
|
||||
this.roleService.setCurrentRole(this.currentUserRole);
|
||||
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
|
||||
*/
|
||||
private fallbackPermissions(): void {
|
||||
this.currentUserRole = this.authService.getCurrentUserRole();
|
||||
|
||||
if (this.currentUserRole) {
|
||||
this.canCreateUsers = this.roleService.canCreateUsers(this.currentUserRole);
|
||||
this.canDeleteUsers = this.roleService.canDeleteUsers(this.currentUserRole);
|
||||
this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole);
|
||||
}
|
||||
private fallbackPermissions(): boolean {
|
||||
return this.roleService.isAnyAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -541,7 +529,7 @@ export class HubUsersManagement implements OnInit, OnDestroy {
|
||||
* Vérifie si l'utilisateur peut attribuer un rôle spécifique
|
||||
*/
|
||||
canAssignRole(targetRole: UserRole): boolean {
|
||||
return this.roleService.canAssignRole(this.currentUserRole, targetRole);
|
||||
return this.roleService.isAnyAdmin();
|
||||
}
|
||||
|
||||
// Réinitialiser le mot de passe
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
[class.active]="roleFilter === 'all'"
|
||||
(click)="filterByRole('all')"
|
||||
>
|
||||
Tous ({{ getTotalUsersCount() }})
|
||||
Tous ({{ userStats?.total }})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -94,7 +94,7 @@
|
||||
</div>
|
||||
|
||||
<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="enabled">Activés seulement</option>
|
||||
<option value="disabled">Désactivés seulement</option>
|
||||
@ -102,7 +102,7 @@
|
||||
</div>
|
||||
|
||||
<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="verified">Email vérifié</option>
|
||||
<option value="not-verified">Email non vérifié</option>
|
||||
@ -110,14 +110,13 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@for (role of availableRoles; track role.value) {
|
||||
<option [value]="role.value">{{ role.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>
|
||||
@ -313,10 +312,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if (totalPages > 1) {
|
||||
@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 }} utilisateurs
|
||||
Affichage de {{ getStartIndex() }}
|
||||
à {{ getEndIndex() }}
|
||||
sur {{ totalItems }} utilisateurs
|
||||
</div>
|
||||
<nav>
|
||||
<ngb-pagination
|
||||
@ -338,7 +339,7 @@
|
||||
<div class="row text-center">
|
||||
<div class="col">
|
||||
<small class="text-muted">
|
||||
<strong>Total :</strong> {{ allUsers.length }} utilisateurs
|
||||
<strong>Total :</strong> {{ userStats?.total }} utilisateurs
|
||||
</small>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
@ -3,7 +3,7 @@ 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 { Observable, Subject, forkJoin, map, of } from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
@ -11,13 +11,17 @@ import {
|
||||
PaginatedUserResponse,
|
||||
UserRole,
|
||||
UserType,
|
||||
UserUtils
|
||||
UserUtils,
|
||||
SearchUsersParams,
|
||||
UserStats
|
||||
} from '@core/models/dcb-bo-hub-user.model';
|
||||
|
||||
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 { UiCard } from '@app/components/ui-card';
|
||||
import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service';
|
||||
import { MerchantUser } from '@core/models/merchant-config.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-merchant-users-list',
|
||||
@ -34,6 +38,7 @@ import { UiCard } from '@app/components/ui-card';
|
||||
export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
private authService = inject(AuthService);
|
||||
private merchantUsersService = inject(MerchantUsersService);
|
||||
private merchantConfigService = inject(MerchantConfigService);
|
||||
protected roleService = inject(RoleManagementService);
|
||||
private cdRef = inject(ChangeDetectorRef);
|
||||
private destroy$ = new Subject<void>();
|
||||
@ -85,11 +90,13 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
// ID du merchant partner courant et permissions
|
||||
currentMerchantPartnerId: string = '';
|
||||
currentUserRole: UserRole | null = null;
|
||||
canViewAllMerchants = false;
|
||||
canViewAllMerchantsUsers = false;
|
||||
canViewOwnMerchantUsers = false;
|
||||
userStats: UserStats | undefined;
|
||||
|
||||
// Getters pour la logique conditionnelle
|
||||
get showMerchantPartnerColumn(): boolean {
|
||||
return this.canViewAllMerchants;
|
||||
return this.canViewAllMerchantsUsers;
|
||||
}
|
||||
|
||||
get showCreateButton(): boolean {
|
||||
@ -116,14 +123,15 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
.subscribe({
|
||||
next: (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) => {
|
||||
console.error('Error loading current user permissions:', error);
|
||||
this.fallbackPermissions();
|
||||
this.loadUsers();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -136,21 +144,12 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
return null;
|
||||
}
|
||||
|
||||
private canViewAllMerchantsCheck(role: UserRole | null): boolean {
|
||||
if (!role) return false;
|
||||
|
||||
const canViewAllRoles = [
|
||||
UserRole.DCB_ADMIN,
|
||||
UserRole.DCB_SUPPORT,
|
||||
UserRole.DCB_PARTNER_ADMIN
|
||||
];
|
||||
|
||||
return canViewAllRoles.includes(role);
|
||||
private canViewAllMerchantsUsersCheck(): boolean {
|
||||
return this.roleService.isHubUser()
|
||||
}
|
||||
|
||||
private fallbackPermissions(): void {
|
||||
this.currentUserRole = this.authService.getCurrentUserRole();
|
||||
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
|
||||
private canViewOwnMerchantUsersCheck(): boolean {
|
||||
return !this.roleService.isHubUser()
|
||||
}
|
||||
|
||||
private initializeAvailableRoles() {
|
||||
@ -162,49 +161,86 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
];
|
||||
}
|
||||
|
||||
loadUsers() {
|
||||
|
||||
private loadData() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
const usersObservable: Observable<User[]> = this.canViewAllMerchants
|
||||
? this.merchantUsersService
|
||||
.getMerchantUsers(this.currentPage, this.itemsPerPage)
|
||||
.pipe(map((response: PaginatedUserResponse) => response.users))
|
||||
: of([]); // fallback propre
|
||||
// Marquer pour détection de changement
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
const filters: SearchUsersParams = {
|
||||
searchTerm: this.searchTerm,
|
||||
status: this.statusFilter,
|
||||
emailVerified: this.emailVerifiedFilter,
|
||||
role: this.roleFilter,
|
||||
sortField: this.sortField,
|
||||
sortDirection: this.sortDirection
|
||||
};
|
||||
|
||||
usersObservable
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
catchError(error => {
|
||||
console.error('Error loading merchant users:', error);
|
||||
this.error = 'Erreur lors du chargement des utilisateurs marchands';
|
||||
return of([] as User[]);
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: (users) => {
|
||||
this.allUsers = users || [];
|
||||
console.log(`✅ Loaded ${this.allUsers.length} merchant users`);
|
||||
this.applyFiltersAndPagination();
|
||||
this.loading = false;
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: () => {
|
||||
this.error = 'Erreur lors du chargement des utilisateurs marchands';
|
||||
this.loading = false;
|
||||
this.allUsers = [];
|
||||
this.filteredUsers = [];
|
||||
this.displayedUsers = [];
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
});
|
||||
// Charger les données et les statistiques en parallèle
|
||||
forkJoin({
|
||||
users: this.merchantUsersService.getMerchantUsers(this.currentPage, this.itemsPerPage, filters),
|
||||
stats: this.merchantUsersService.getMerchantUsersStats(filters)
|
||||
})
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
catchError(error => {
|
||||
console.error('Error loading data:', error);
|
||||
this.error = 'Erreur lors du chargement des données';
|
||||
this.loading = false;
|
||||
this.cdRef.markForCheck();
|
||||
return of({
|
||||
users: {
|
||||
users: [],
|
||||
total: 0,
|
||||
page: this.currentPage,
|
||||
limit: this.itemsPerPage,
|
||||
totalPages: 0
|
||||
} as PaginatedUserResponse,
|
||||
stats: {
|
||||
total: 0,
|
||||
enabled: 0,
|
||||
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() {
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
this.loadData(); // Recharger avec les nouveaux filtres
|
||||
}
|
||||
|
||||
onFilterChange() {
|
||||
this.currentPage = 1;
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
onClearFilters() {
|
||||
@ -212,64 +248,10 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
this.statusFilter = 'all';
|
||||
this.emailVerifiedFilter = 'all';
|
||||
this.roleFilter = 'all';
|
||||
this.sortField = 'username';
|
||||
this.sortDirection = 'asc';
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
}
|
||||
|
||||
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);
|
||||
this.loadData(); // Recharger avec filtres réinitialisés
|
||||
}
|
||||
|
||||
// Tri
|
||||
@ -280,26 +262,25 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
this.sortField = field;
|
||||
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 {
|
||||
if (this.sortField !== field) return 'lucideArrowUpDown';
|
||||
return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown';
|
||||
}
|
||||
|
||||
// Pagination
|
||||
onPageChange(page: number) {
|
||||
this.currentPage = page;
|
||||
this.applyFiltersAndPagination();
|
||||
getEndIndex(): number {
|
||||
return Math.min(this.currentPage * this.itemsPerPage, this.totalItems);
|
||||
}
|
||||
|
||||
getStartIndex(): number {
|
||||
return (this.currentPage - 1) * this.itemsPerPage + 1;
|
||||
}
|
||||
|
||||
getEndIndex(): number {
|
||||
return Math.min(this.currentPage * this.itemsPerPage, this.totalItems);
|
||||
return this.totalItems > 0 ? (this.currentPage - 1) * this.itemsPerPage + 1 : 0;
|
||||
}
|
||||
|
||||
// Actions
|
||||
@ -324,7 +305,7 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
if (index !== -1) {
|
||||
this.allUsers[index] = updatedUser;
|
||||
}
|
||||
this.applyFiltersAndPagination();
|
||||
this.loadData();
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: (error) => {
|
||||
@ -344,7 +325,7 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
if (index !== -1) {
|
||||
this.allUsers[index] = updatedUser;
|
||||
}
|
||||
this.applyFiltersAndPagination();
|
||||
this.loadData();
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: (error) => {
|
||||
@ -407,14 +388,6 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
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 {
|
||||
return UserUtils.hasRole(user, role);
|
||||
}
|
||||
@ -423,12 +396,12 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
filterByRole(role: UserRole | 'all') {
|
||||
this.roleFilter = role;
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
// Recharger les données
|
||||
refreshData() {
|
||||
this.loadUsers();
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
// Méthodes pour le template
|
||||
@ -437,22 +410,13 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
getHelperText(): string {
|
||||
return this.canViewAllMerchants
|
||||
return this.canViewAllMerchantsUsers
|
||||
? 'Vue administrative - Tous les utilisateurs marchands'
|
||||
: 'Votre équipe marchande';
|
||||
}
|
||||
|
||||
getHelperIcon(): string {
|
||||
return this.canViewAllMerchants ? '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;
|
||||
return this.canViewAllMerchantsUsers ? 'lucideShield' : 'lucideUsers';
|
||||
}
|
||||
|
||||
getLoadingText(): string {
|
||||
@ -476,19 +440,34 @@ export class MerchantUsersList implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
showMerchantPartnerId(): boolean {
|
||||
return !this.canViewAllMerchants;
|
||||
return !this.canViewAllMerchantsUsers;
|
||||
}
|
||||
|
||||
// Statistiques
|
||||
getTotalUsersCount(): number {
|
||||
return this.allUsers.length;
|
||||
return this.userStats?.total || 0;
|
||||
}
|
||||
|
||||
getActiveUsersCount(): number {
|
||||
return this.allUsers.filter(user => user.enabled).length;
|
||||
return this.userStats?.enabled || 0;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
@Component({
|
||||
@ -210,12 +210,6 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
||||
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
|
||||
if (!this.isValidMerchantRole(newRole)) {
|
||||
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
|
||||
return this.roleService.canEditUsers(this.currentUserRole);
|
||||
return this.roleService.isAnyAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -314,7 +308,8 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
||||
*/
|
||||
canManageRoles(): boolean {
|
||||
// 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
|
||||
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
|
||||
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
|
||||
return this.roleService.canDeleteUsers(this.currentUserRole);
|
||||
|
||||
return this.roleService.isAnyAdmin();
|
||||
}
|
||||
|
||||
// ==================== UTILITAIRES D'AFFICHAGE ====================
|
||||
@ -522,8 +520,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
getAssignableRoles(): UserRole[] {
|
||||
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
|
||||
return merchantRoles.filter(role => this.roleService.canAssignRole(this.currentUserRole, role));
|
||||
return [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
|
||||
}
|
||||
|
||||
// Méthodes pour les actions spécifiques
|
||||
|
||||
@ -136,6 +136,49 @@
|
||||
|
||||
<form (ngSubmit)="createUser()" #userForm="ngForm">
|
||||
<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 -->
|
||||
<div class="col-md-6">
|
||||
@ -286,12 +329,8 @@
|
||||
@for (role of availableRoles; track role.value) {
|
||||
<option
|
||||
[value]="role.value"
|
||||
[disabled]="!canAssignRole(role.value)"
|
||||
>
|
||||
{{ role.label }} - {{ role.description }}
|
||||
@if (!canAssignRole(role.value)) {
|
||||
(Non autorisé)
|
||||
}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
@ -13,7 +13,8 @@ import {
|
||||
SearchUsersParams,
|
||||
UserRole,
|
||||
UserType,
|
||||
UserUtils
|
||||
UserUtils,
|
||||
UserStats
|
||||
} from '@core/models/dcb-bo-hub-user.model';
|
||||
|
||||
// Interfaces pour les nouvelles réponses
|
||||
@ -87,7 +88,8 @@ export class MerchantUsersService {
|
||||
role: createUserDto.role,
|
||||
enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : 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(
|
||||
@ -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> {
|
||||
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 => {
|
||||
console.error('Error loading merchant users:', 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> {
|
||||
const payload: any = {
|
||||
firstName: updateUserDto.firstName,
|
||||
@ -280,50 +365,4 @@ export class MerchantUsersService {
|
||||
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 { NgIcon } from '@ng-icons/core';
|
||||
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 { 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 { PageTitle } from '@app/components/page-title/page-title';
|
||||
import { MerchantUsersList } from './merchant-users-list/merchant-users-list';
|
||||
@ -15,11 +15,12 @@ import { MerchantUserProfile } from './merchant-users-profile/merchant-users-pro
|
||||
import {
|
||||
PaginatedUserResponse,
|
||||
ResetPasswordDto,
|
||||
User,
|
||||
UserRole,
|
||||
UserType
|
||||
} 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({
|
||||
selector: 'app-merchant-users',
|
||||
@ -40,13 +41,14 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
private modalService = inject(NgbModal);
|
||||
private authService = inject(AuthService);
|
||||
private merchantUsersService = inject(MerchantUsersService);
|
||||
private hubUsersService = inject(HubUsersService);
|
||||
private merchantConfigService = inject(MerchantConfigService);
|
||||
protected roleService = inject(RoleManagementService);
|
||||
private cdRef = inject(ChangeDetectorRef);
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
// Configuration
|
||||
readonly UserRole = UserRole;
|
||||
readonly UserType = UserType;
|
||||
|
||||
// Propriétés de configuration
|
||||
pageTitle: string = 'Gestion des Utilisateurs Marchands';
|
||||
@ -76,6 +78,7 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
enabled: boolean;
|
||||
emailVerified: boolean;
|
||||
userType: UserType;
|
||||
merchantPartnerId?: string;
|
||||
} = this.getDefaultUserForm();
|
||||
|
||||
// États des opérations
|
||||
@ -110,22 +113,49 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
availableRoles: { value: UserRole; label: string; description: string }[] = [];
|
||||
assignableRoles: UserRole[] = [];
|
||||
|
||||
merchantPartners: User[] = [];
|
||||
merchantPartners: Merchant[] = [];
|
||||
merchantPartner: Merchant | undefined;
|
||||
loadingMerchantPartners = false;
|
||||
merchantPartnersError = '';
|
||||
selectedMerchantPartnerId: string = '';
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
this.activeTab = 'list';
|
||||
this.loadCurrentUserPermissions();
|
||||
this.loadAvailableRoles();
|
||||
|
||||
// Charger les données selon le type d'utilisateur
|
||||
this.loadUserSpecificData();
|
||||
|
||||
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() {}
|
||||
|
||||
private updateNewUserMerchantId() {
|
||||
if (this.selectedMerchantPartnerId) {
|
||||
this.newUser.merchantPartnerId = this.selectedMerchantPartnerId;
|
||||
}
|
||||
}
|
||||
|
||||
onMerchantSelected(merchantId: string) {
|
||||
this.selectedMerchantPartnerId = merchantId;
|
||||
this.updateNewUserMerchantId();
|
||||
}
|
||||
|
||||
onRoleSelectionChange(selectedRole: UserRole) {
|
||||
this.newUser.role = selectedRole;
|
||||
}
|
||||
@ -157,14 +187,10 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
|
||||
this.currentUserType = this.extractUserType(user);
|
||||
|
||||
if (this.currentUserRole) {
|
||||
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.canCreateUsers = this.roleService.isAnyAdmin();
|
||||
|
||||
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() {
|
||||
return {
|
||||
const defaultForm = {
|
||||
username: '',
|
||||
email: '',
|
||||
firstName: '',
|
||||
@ -224,8 +250,12 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
role: UserRole.DCB_PARTNER_SUPPORT,
|
||||
enabled: true,
|
||||
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 ====================
|
||||
@ -279,6 +309,71 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
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() {
|
||||
console.log('🔙 Returning to list view');
|
||||
@ -324,17 +419,17 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private resetUserForm() {
|
||||
this.newUser = {
|
||||
username: '',
|
||||
email: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
password: '',
|
||||
role: UserRole.DCB_PARTNER_SUPPORT,
|
||||
enabled: true,
|
||||
emailVerified: false,
|
||||
userType: UserType.MERCHANT_PARTNER,
|
||||
};
|
||||
this.newUser = this.getDefaultUserForm();
|
||||
|
||||
// Si l'utilisateur est un Merchant, utiliser son merchantPartnerId
|
||||
if (this.isMerchantUser && this.currentUserType === UserType.MERCHANT_PARTNER && this.merchantPartner) {
|
||||
const currentUserMerchant = this.merchantPartner;
|
||||
if (currentUserMerchant?.id) {
|
||||
this.newUser.merchantPartnerId = currentUserMerchant.id.toString();
|
||||
console.log('🔄 Form reset with current user merchant:', this.newUser.merchantPartnerId);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🔄 Merchant user form reset');
|
||||
}
|
||||
|
||||
@ -401,9 +496,62 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier la permission pour attribuer le rôle sélectionné
|
||||
if (!this.canAssignRole(this.newUser.role)) {
|
||||
this.createUserError = `Vous n'avez pas la permission d'attribuer le rôle: ${this.getRoleLabel(this.newUser.role)}`;
|
||||
// Vérifications pour les utilisateurs Merchant
|
||||
if (this.isMerchantRole(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;
|
||||
}
|
||||
|
||||
@ -412,18 +560,60 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
|
||||
console.log('📤 Creating merchant user with data:', this.newUser);
|
||||
|
||||
this.merchantUsersService.createMerchantUser(this.newUser)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
// 1. Créer l'utilisateur dans Keycloak
|
||||
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({
|
||||
next: (createdUser) => {
|
||||
console.log('✅ Merchant user created successfully:', createdUser);
|
||||
next: (result) => {
|
||||
console.log('✅ Complete user creation successful:', result);
|
||||
this.creatingUser = false;
|
||||
this.modalService.dismissAll();
|
||||
this.refreshUsersList();
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('❌ Error creating merchant user:', error);
|
||||
console.error('❌ Error in user creation process:', error);
|
||||
this.creatingUser = false;
|
||||
this.createUserError = this.getErrorMessage(error);
|
||||
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
|
||||
*/
|
||||
@ -444,13 +678,6 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
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
|
||||
confirmResetPassword() {
|
||||
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 {
|
||||
return this.roleService.getRoleLabel(role);
|
||||
}
|
||||
@ -550,6 +772,10 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
return this.roleService.getRoleIcon(role);
|
||||
}
|
||||
|
||||
getRoleBadgeClass(role: UserRole): string {
|
||||
return this.roleService.getRoleBadgeClass(role);
|
||||
}
|
||||
|
||||
getRoleDescription(role: UserRole): string {
|
||||
const roleInfo = this.availableRoles.find(r => r.value === role);
|
||||
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.email?.trim(), name: 'Email' },
|
||||
{ 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) {
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
} from '@core/models/merchant-config.model';
|
||||
|
||||
import { MerchantConfigService } from '../merchant-config.service';
|
||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||
import { AuthService } from '@core/services/auth.service';
|
||||
import { UiCard } from '@app/components/ui-card';
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ import {
|
||||
|
||||
import { MerchantConfigService } from '../merchant-config.service';
|
||||
import { MerchantDataAdapter } from '../merchant-data-adapter.service';
|
||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||
import { AuthService } from '@core/services/auth.service';
|
||||
import { UserRole } from '@core/models/dcb-bo-hub-user.model';
|
||||
|
||||
|
||||
@ -19,7 +19,8 @@ import {
|
||||
ApiMerchant,
|
||||
ApiMerchantConfig,
|
||||
ApiTechnicalContact,
|
||||
ApiMerchantUser
|
||||
ApiMerchantUser,
|
||||
MerchantUserWithMerchant
|
||||
} from '@core/models/merchant-config.model';
|
||||
|
||||
// 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> {
|
||||
//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 { 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 { MerchantSyncService } from '../hub-users-management/merchant-sync-orchestrator.service';
|
||||
import { PageTitle } from '@app/components/page-title/page-title';
|
||||
@ -421,7 +421,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
this.currentMerchantConfigId = this.extractMerchantConfigId(user);
|
||||
|
||||
if (this.currentUserRole) {
|
||||
this.userPermissions = this.roleService.getPermissionsForRole(this.currentUserRole);
|
||||
this.canCreateMerchants = this.canManageMerchant();
|
||||
this.canDeleteMerchants = this.canManageMerchant();
|
||||
this.canManageMerchants = this.canManageMerchant();
|
||||
@ -448,7 +447,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
// ==================== PERMISSIONS SPÉCIFIQUES MARCHAND ====================
|
||||
|
||||
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 { Help } from '@modules/help/help';
|
||||
import { About } from '@modules/about/about';
|
||||
import { SubscriptionsManagement } from './subscriptions/subscriptions';
|
||||
import { SubscriptionPayments } from './subscriptions/subscription-payments/subscription-payments';
|
||||
import { Subscriptions } from './subscriptions/subscriptions';
|
||||
import { SubscriptionDetails } from './subscriptions/subscription-details/subscription-details';
|
||||
import { MerchantConfigManagement } from './merchant-config/merchant-config';
|
||||
|
||||
const routes: Routes = [
|
||||
@ -93,7 +93,7 @@ const routes: Routes = [
|
||||
// ---------------------------
|
||||
{
|
||||
path: 'subscriptions',
|
||||
component: SubscriptionsManagement,
|
||||
component: Subscriptions,
|
||||
canActivate: [authGuard, roleGuard],
|
||||
data: {
|
||||
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
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
|
||||
import { HubUsersService } from '@modules/hub-users-management/hub-users.service';
|
||||
import { MerchantUsersService } from '@modules/hub-users-management/merchant-users.service';
|
||||
import { RoleManagementService } from '@core/services/hub-users-roles-management-old.service';
|
||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||
import { AuthService } from '@core/services/auth.service';
|
||||
|
||||
@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
|
||||
} from '@core/models/dcb-bo-hub-subscription.model';
|
||||
|
||||
import { SubscriptionsService } from '../subscriptions.service';
|
||||
import { AuthService } from '@core/services/auth.service';
|
||||
import { SubscriptionsService } from '../services/subscriptions.service';
|
||||
import { SubscriptionAccessService, SubscriptionAccess } from '../services/subscription-access.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subscription-payments',
|
||||
@ -33,7 +33,7 @@ import { AuthService } from '@core/services/auth.service';
|
||||
})
|
||||
export class SubscriptionPayments implements OnInit, OnDestroy {
|
||||
private subscriptionsService = inject(SubscriptionsService);
|
||||
private authService = inject(AuthService);
|
||||
private accessService = inject(SubscriptionAccessService); // CHANGÉ
|
||||
private cdRef = inject(ChangeDetectorRef);
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@ -51,8 +51,9 @@ export class SubscriptionPayments implements OnInit, OnDestroy {
|
||||
error = '';
|
||||
success = '';
|
||||
|
||||
// Gestion des permissions
|
||||
currentUserRole: string | null = null;
|
||||
// Gestion des permissions MIS À JOUR
|
||||
access!: SubscriptionAccess;
|
||||
accessDenied = false;
|
||||
|
||||
merchantPartnerId: number | undefined;
|
||||
|
||||
@ -62,7 +63,7 @@ export class SubscriptionPayments implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit() {
|
||||
if (this.subscriptionId) {
|
||||
this.loadCurrentUserPermissions();
|
||||
this.initializePermissions(); // NOUVELLE MÉTHODE
|
||||
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 {
|
||||
this.authService.getUserProfile()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (profile) => {
|
||||
this.currentUserRole = this.authService.getCurrentUserRole();
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading user permissions:', error);
|
||||
}
|
||||
});
|
||||
private initializePermissions(): void {
|
||||
this.access = this.accessService.getSubscriptionAccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les détails de l'abonnement puis les paiements
|
||||
*/
|
||||
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.error = '';
|
||||
this.accessDenied = false;
|
||||
|
||||
this.subscriptionsService.getSubscriptionById(this.subscriptionId)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (subscription) => {
|
||||
console.log('Subscription loaded:', {
|
||||
id: subscription.id,
|
||||
merchantPartnerId: subscription.merchantPartnerId,
|
||||
hasMerchantId: !!subscription.merchantPartnerId
|
||||
});
|
||||
// Vérifier l'accès à cet abonnement spécifique
|
||||
this.accessService.canAccessSubscription(subscription.merchantPartnerId)
|
||||
.subscribe(canAccess => {
|
||||
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.subscription = subscription;
|
||||
this.loading = false;
|
||||
this.cdRef.detectChanges();
|
||||
|
||||
// Passer explicitement le merchantPartnerId à loadPayments
|
||||
this.loadPayments(subscription.merchantPartnerId);
|
||||
this.loadPayments(subscription.merchantPartnerId);
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
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) {
|
||||
this.loadingPayments = true;
|
||||
|
||||
console.log("loadPayments " + merchantPartnerId);
|
||||
|
||||
// Utiliser le merchantPartnerId passé en paramètre ou celui de l'abonnement
|
||||
const merchantId = merchantPartnerId;
|
||||
|
||||
// Vérifier que nous avons les IDs nécessaires
|
||||
if (!merchantId) {
|
||||
console.error('MerchantPartnerId manquant pour charger les paiements');
|
||||
this.error = 'Impossible de charger les paiements : Merchant ID manquant';
|
||||
@ -146,12 +143,6 @@ export class SubscriptionPayments implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Loading payments with:', {
|
||||
merchantId: merchantId,
|
||||
subscriptionId: this.subscriptionId,
|
||||
merchantIdType: typeof merchantId
|
||||
});
|
||||
|
||||
this.subscriptionsService.getSubscriptionPayments(merchantId, this.subscriptionId)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
@ -163,7 +154,6 @@ export class SubscriptionPayments implements OnInit, OnDestroy {
|
||||
error: (error) => {
|
||||
console.error('Error loading subscription payments:', error);
|
||||
|
||||
// Message d'erreur plus spécifique
|
||||
if (error.status === 400) {
|
||||
this.error = 'Données invalides pour charger les paiements';
|
||||
} else if (error.status === 404) {
|
||||
@ -177,11 +167,12 @@ export class SubscriptionPayments implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique les filtres aux paiements
|
||||
*/
|
||||
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];
|
||||
}
|
||||
|
||||
// Gestion des erreurs
|
||||
private getErrorMessage(error: any): string {
|
||||
if (error.error?.message) {
|
||||
return error.error.message;
|
||||
}
|
||||
if (error.status === 400) {
|
||||
return 'Données invalides.';
|
||||
}
|
||||
if (error.status === 403) {
|
||||
return 'Vous n\'avez pas les permissions pour accéder à ces informations.';
|
||||
}
|
||||
if (error.status === 404) {
|
||||
return 'Abonnement ou paiements non trouvés.';
|
||||
}
|
||||
return 'Erreur lors de l\'opération. Veuillez réessayer.';
|
||||
// NOUVELLES 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';
|
||||
}
|
||||
}
|
||||
@ -1,72 +1,54 @@
|
||||
<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 ({{ 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 class="transactions-container">
|
||||
<!-- En-tête avec actions -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h4 class="mb-1">Gestion des Abonnements</h4>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="javascript:void(0)" class="text-decoration-none">DCB Abonnements</a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<span [class]="getUserBadgeClass()" class="badge">
|
||||
<ng-icon [name]="getUserBadgeIcon()" class="me-1"></ng-icon>
|
||||
{{ getUserBadgeText() }}
|
||||
</span>
|
||||
@if (currentMerchantId) {
|
||||
<span class="badge bg-info">
|
||||
<ng-icon name="lucideStore" class="me-1"></ng-icon>
|
||||
Merchant {{ currentMerchantId }}
|
||||
</span>
|
||||
}
|
||||
</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
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-outline-secondary" (click)="loadAllSubscriptions()" [disabled]="loading">
|
||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading"></ng-icon>
|
||||
</button>
|
||||
</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="col-md-4">
|
||||
<div class="input-group">
|
||||
@ -78,237 +60,163 @@
|
||||
class="form-control"
|
||||
placeholder="Rechercher par ID, token..."
|
||||
[(ngModel)]="searchTerm"
|
||||
(input)="onSearch()"
|
||||
[disabled]="loading"
|
||||
(keyup.enter)="onSearch()"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<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-8">
|
||||
<div class="d-flex gap-2">
|
||||
<!-- Filtre statut -->
|
||||
<select class="form-select" style="width: auto;" (change)="onStatusFilterChange($any($event.target).value)">
|
||||
@for (status of statusOptions; track status.value) {
|
||||
<option [value]="status.value">{{ status.label }}</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" [(ngModel)]="periodicityFilter" (change)="applyFiltersAndPagination()">
|
||||
@for (periodicity of availablePeriodicities; track periodicity.value) {
|
||||
<option [value]="periodicity.value">{{ periodicity.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<!-- Filtre périodicité -->
|
||||
<select class="form-select" style="width: auto;" (change)="onPeriodicityFilterChange($any($event.target).value)">
|
||||
@for (periodicity of periodicityOptions; track periodicity.value) {
|
||||
<option [value]="periodicity.value">{{ periodicity.label }}</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" [(ngModel)]="merchantFilter" (change)="applyFiltersAndPagination()">
|
||||
<option value="all">Tous les merchants</option>
|
||||
<!-- Les options merchants seraient dynamiquement chargées si nécessaire -->
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-outline-primary" (click)="onSearch()">
|
||||
<ng-icon name="lucideFilter" class="me-1"></ng-icon>
|
||||
Filtrer
|
||||
</button>
|
||||
|
||||
<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>
|
||||
<button class="btn btn-outline-secondary" (click)="onClearFilters()">
|
||||
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||
Effacer
|
||||
</button>
|
||||
</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) {
|
||||
<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>
|
||||
<p class="mt-2 text-muted">Chargement des abonnements...</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>
|
||||
<!-- Tableau des abonnements -->
|
||||
@if (!loading) {
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<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>
|
||||
}
|
||||
|
||||
<!-- 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 -->
|
||||
@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 }} abonnements
|
||||
Affichage de {{ (currentPage - 1) * itemsPerPage + 1 }} à
|
||||
{{ (currentPage * itemsPerPage) > totalItems ? totalItems : (currentPage * itemsPerPage) }}
|
||||
sur {{ totalItems }} abonnements
|
||||
</div>
|
||||
<nav>
|
||||
<ngb-pagination
|
||||
@ -323,39 +231,6 @@
|
||||
</nav>
|
||||
</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 { 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 { NgbPaginationModule, NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { SubscriptionsService } from '../services/subscriptions.service';
|
||||
import { SubscriptionAccessService, SubscriptionAccess } from '../services/subscription-access.service';
|
||||
|
||||
import {
|
||||
Subscription,
|
||||
SubscriptionsResponse,
|
||||
SubscriptionStatus,
|
||||
SubscriptionPeriodicity,
|
||||
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-subscriptions-list',
|
||||
standalone: true,
|
||||
@ -25,53 +21,50 @@ import { UiCard } from '@app/components/ui-card';
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
NgIcon,
|
||||
UiCard,
|
||||
NgbPaginationModule
|
||||
NgbPaginationModule,
|
||||
NgbDropdownModule,
|
||||
NgbTooltipModule
|
||||
],
|
||||
templateUrl: './subscriptions-list.html',
|
||||
templateUrl: './subscriptions-list.html'
|
||||
})
|
||||
export class SubscriptionsList implements OnInit, OnDestroy {
|
||||
private authService = inject(AuthService);
|
||||
export class SubscriptionsList implements OnInit {
|
||||
private subscriptionsService = inject(SubscriptionsService);
|
||||
private accessService = inject(SubscriptionAccessService);
|
||||
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() viewPaymentsRequested = new EventEmitter<string>();
|
||||
|
||||
// Permissions
|
||||
access!: SubscriptionAccess;
|
||||
currentUserRole = '';
|
||||
currentMerchantId: number = 0;
|
||||
|
||||
// Données
|
||||
allSubscriptions: Subscription[] = [];
|
||||
filteredSubscriptions: Subscription[] = [];
|
||||
displayedSubscriptions: Subscription[] = [];
|
||||
subscriptions: Subscription[] = [];
|
||||
allSubscriptions: Subscription[] = []; // Stocker toutes les données pour filtrage client
|
||||
|
||||
// États
|
||||
loading = false;
|
||||
error = '';
|
||||
|
||||
// Recherche et filtres
|
||||
// Filtres et recherche
|
||||
searchTerm = '';
|
||||
statusFilter: SubscriptionStatus | 'all' = 'all';
|
||||
periodicityFilter: SubscriptionPeriodicity | 'all' = 'all';
|
||||
merchantFilter: number | 'all' = 'all';
|
||||
|
||||
// Pagination
|
||||
currentPage = 1;
|
||||
itemsPerPage = 10;
|
||||
itemsPerPage = 20;
|
||||
totalItems = 0;
|
||||
totalPages = 0;
|
||||
|
||||
// Tri
|
||||
sortField: keyof Subscription = 'startDate';
|
||||
sortField: string = 'startDate';
|
||||
sortDirection: 'asc' | 'desc' = 'desc';
|
||||
|
||||
// Filtres disponibles
|
||||
availableStatuses: { value: SubscriptionStatus | 'all'; label: string }[] = [
|
||||
// Options de filtre
|
||||
statusOptions = [
|
||||
{ value: 'all', label: 'Tous les statuts' },
|
||||
{ value: SubscriptionStatus.ACTIVE, label: 'Actif' },
|
||||
{ value: SubscriptionStatus.SUSPENDED, label: 'Suspendu' },
|
||||
@ -80,7 +73,7 @@ export class SubscriptionsList implements OnInit, OnDestroy {
|
||||
{ value: SubscriptionStatus.PENDING, label: 'En attente' }
|
||||
];
|
||||
|
||||
availablePeriodicities: { value: SubscriptionPeriodicity | 'all'; label: string }[] = [
|
||||
periodicityOptions = [
|
||||
{ value: 'all', label: 'Toutes les périodicités' },
|
||||
{ value: SubscriptionPeriodicity.DAILY, label: 'Quotidien' },
|
||||
{ value: SubscriptionPeriodicity.WEEKLY, label: 'Hebdomadaire' },
|
||||
@ -88,203 +81,178 @@ export class SubscriptionsList implements OnInit, OnDestroy {
|
||||
{ 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() {
|
||||
this.loadCurrentUserPermissions();
|
||||
this.initializePermissions();
|
||||
this.loadAllSubscriptions();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
private initializePermissions() {
|
||||
this.access = this.accessService.getSubscriptionAccess();
|
||||
this.currentUserRole = this.access.userRoleLabel;
|
||||
this.currentMerchantId = this.access.merchantId || 0;
|
||||
|
||||
private loadCurrentUserPermissions() {
|
||||
this.authService.getUserProfile()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (user) => {
|
||||
this.currentUserRole = this.extractUserRole(user);
|
||||
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
|
||||
|
||||
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];
|
||||
if (!this.access.canViewSubscriptions) {
|
||||
this.error = 'Vous n\'avez pas la permission de voir les abonnements';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.access.isHubUser && this.currentMerchantId === 0) {
|
||||
this.error = 'Merchant ID non disponible';
|
||||
return;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private canViewAllMerchantsCheck(role: string | null): boolean {
|
||||
if (!role) return false;
|
||||
loadAllSubscriptions() {
|
||||
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.error = '';
|
||||
|
||||
let subscriptionsObservable: Observable<SubscriptionsResponse>;
|
||||
// Déterminer quelle API appeler
|
||||
let subscriptionObservable;
|
||||
|
||||
if (this.canViewAllMerchants) {
|
||||
// Admin/Support - tous les abonnements
|
||||
subscriptionsObservable = this.subscriptionsService.getSubscriptions({
|
||||
page: this.currentPage,
|
||||
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 }
|
||||
);
|
||||
if (this.access.canViewAllSubscriptions) {
|
||||
subscriptionObservable = this.subscriptionsService.getSubscriptions({});
|
||||
} else if (this.currentMerchantId > 0) {
|
||||
subscriptionObservable = this.subscriptionsService.getSubscriptionsByMerchant(this.currentMerchantId, {});
|
||||
} else {
|
||||
// Fallback - abonnements généraux
|
||||
subscriptionsObservable = this.subscriptionsService.getSubscriptions({
|
||||
page: this.currentPage,
|
||||
limit: this.itemsPerPage
|
||||
});
|
||||
this.error = 'Configuration invalide';
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
subscriptionsObservable
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
catchError(error => {
|
||||
console.error('Error loading subscriptions:', error);
|
||||
this.error = 'Erreur lors du chargement des abonnements';
|
||||
return of({ subscriptions: [], statistics: { total: 0, active: 0, totalRevenue: 0, averageAmount: 0 } } as SubscriptionsResponse);
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.allSubscriptions = response.subscriptions || [];
|
||||
console.log(`✅ Loaded ${this.allSubscriptions.length} subscriptions`);
|
||||
this.applyFiltersAndPagination();
|
||||
this.loading = false;
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: () => {
|
||||
this.error = 'Erreur lors du chargement des abonnements';
|
||||
this.loading = false;
|
||||
this.allSubscriptions = [];
|
||||
this.filteredSubscriptions = [];
|
||||
this.displayedSubscriptions = [];
|
||||
this.cdRef.detectChanges();
|
||||
subscriptionObservable.subscribe({
|
||||
next: (response) => {
|
||||
this.allSubscriptions = response.subscriptions || [];
|
||||
this.applyFiltersAndPagination();
|
||||
this.loading = false;
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: (error) => {
|
||||
this.error = 'Erreur lors du chargement des abonnements';
|
||||
this.loading = false;
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private applyFiltersAndPagination() {
|
||||
// 1. Appliquer les filtres
|
||||
let filteredSubscriptions = this.filterSubscriptions(this.allSubscriptions);
|
||||
|
||||
// 2. Appliquer le tri
|
||||
filteredSubscriptions = this.sortSubscriptions(filteredSubscriptions);
|
||||
|
||||
// 3. Calculer la pagination
|
||||
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
|
||||
onSearch() {
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
onClearFilters() {
|
||||
this.searchTerm = '';
|
||||
this.statusFilter = 'all';
|
||||
this.periodicityFilter = 'all';
|
||||
this.merchantFilter = 'all';
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
applyFiltersAndPagination() {
|
||||
if (!this.allSubscriptions) {
|
||||
this.allSubscriptions = [];
|
||||
}
|
||||
onStatusFilterChange(status: SubscriptionStatus | 'all') {
|
||||
this.statusFilter = status;
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
// Appliquer les filtres
|
||||
this.filteredSubscriptions = this.allSubscriptions.filter(subscription => {
|
||||
const matchesSearch = !this.searchTerm ||
|
||||
subscription.id.toString().includes(this.searchTerm) ||
|
||||
subscription.token.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
|
||||
(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);
|
||||
onPeriodicityFilterChange(periodicity: SubscriptionPeriodicity | 'all') {
|
||||
this.periodicityFilter = periodicity;
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
// Tri
|
||||
sort(field: keyof Subscription) {
|
||||
sort(field: string) {
|
||||
if (this.sortField === field) {
|
||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.sortField = field;
|
||||
this.sortDirection = 'asc';
|
||||
this.sortDirection = 'desc';
|
||||
}
|
||||
this.applyFiltersAndPagination();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
getSortIcon(field: string): string {
|
||||
@ -296,244 +264,131 @@ export class SubscriptionsList implements OnInit, OnDestroy {
|
||||
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);
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
// Actions
|
||||
viewSubscriptionDetails(subscriptionId: string | number) {
|
||||
this.subscriptionSelected.emit(subscriptionId.toString());
|
||||
viewSubscriptionDetails(subscriptionId: string) {
|
||||
if (!subscriptionId) return;
|
||||
this.subscriptionSelected.emit(subscriptionId);
|
||||
}
|
||||
|
||||
viewSubscriptionPayments(subscriptionId: string | number) {
|
||||
this.viewPaymentsRequested.emit(subscriptionId.toString());
|
||||
viewSubscriptionPayments(subscriptionId: string) {
|
||||
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
|
||||
getStatusBadgeClass(status: SubscriptionStatus): string {
|
||||
const statusClasses = {
|
||||
[SubscriptionStatus.ACTIVE]: 'badge bg-success',
|
||||
[SubscriptionStatus.SUSPENDED]: 'badge bg-warning',
|
||||
[SubscriptionStatus.CANCELLED]: 'badge bg-danger',
|
||||
[SubscriptionStatus.EXPIRED]: 'badge bg-secondary',
|
||||
[SubscriptionStatus.PENDING]: 'badge bg-info'
|
||||
};
|
||||
return statusClasses[status] || 'badge bg-secondary';
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
getPeriodicityDisplayName(periodicity: SubscriptionPeriodicity): string {
|
||||
const periodicityNames = {
|
||||
[SubscriptionPeriodicity.DAILY]: 'Quotidien',
|
||||
[SubscriptionPeriodicity.WEEKLY]: 'Hebdomadaire',
|
||||
[SubscriptionPeriodicity.MONTHLY]: 'Mensuel',
|
||||
[SubscriptionPeriodicity.YEARLY]: 'Annuel'
|
||||
};
|
||||
return periodicityNames[periodicity] || periodicity;
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
// Méthodes pour compter les abonnements par périodicité
|
||||
getDailySubscriptionsCount(): number {
|
||||
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 {
|
||||
formatCurrency(amount: number, currency: Currency = Currency.XOF): string {
|
||||
if (isNaN(amount)) return '-';
|
||||
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'
|
||||
});
|
||||
}
|
||||
formatDate(date: string | Date): string {
|
||||
if (!date) return '-';
|
||||
|
||||
formatDateTime(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('fr-FR', {
|
||||
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',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}).format(dateObj);
|
||||
}
|
||||
|
||||
// Recharger les données
|
||||
refreshData() {
|
||||
this.loadSubscriptions();
|
||||
getStatusDisplayName(status: SubscriptionStatus): string {
|
||||
switch (status) {
|
||||
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
|
||||
getCardTitle(): string {
|
||||
return 'Abonnements';
|
||||
getUserBadgeClass(): string {
|
||||
return this.access.isHubUser ? 'bg-primary' : 'bg-success';
|
||||
}
|
||||
|
||||
getHelperText(): string {
|
||||
return this.canViewAllMerchants
|
||||
? 'Vue administrative - Tous les abonnements'
|
||||
: 'Vos abonnements';
|
||||
getUserBadgeIcon(): string {
|
||||
return this.access.isHubUser ? 'lucideShield' : 'lucideStore';
|
||||
}
|
||||
|
||||
getHelperIcon(): string {
|
||||
return this.canViewAllMerchants ? 'lucideShield' : 'lucideRepeat';
|
||||
getUserBadgeText(): string {
|
||||
return this.access.isHubUser ? 'Hub User' : 'Merchant User';
|
||||
}
|
||||
|
||||
getLoadingText(): string {
|
||||
return 'Chargement des abonnements...';
|
||||
// Méthodes utilitaires
|
||||
refreshData() {
|
||||
this.loadAllSubscriptions();
|
||||
}
|
||||
|
||||
getEmptyStateTitle(): string {
|
||||
return 'Aucun abonnement trouvé';
|
||||
getFilteredCount(): number {
|
||||
return this.subscriptions.length;
|
||||
}
|
||||
|
||||
getEmptyStateDescription(): string {
|
||||
return 'Aucun abonnement ne correspond à vos critères de recherche.';
|
||||
}
|
||||
|
||||
// Statistiques
|
||||
getTotalSubscriptionsCount(): number {
|
||||
getTotalCount(): number {
|
||||
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">
|
||||
<app-page-title
|
||||
title="pageTitle"
|
||||
subtitle="Consultez et gérez les abonnements et leurs paiements"
|
||||
[badge]="badge"
|
||||
title="Abonnements DCB"
|
||||
subTitle="Gestion et suivi des abonnements de paiement mobile"
|
||||
[badge]="{icon:'lucideRepeat', text:'Abonnements'}"
|
||||
/>
|
||||
|
||||
<!-- Indicateur de permissions -->
|
||||
@ -15,12 +15,13 @@
|
||||
<div class="flex-grow-1">
|
||||
<small>
|
||||
<strong>Rôle actuel :</strong>
|
||||
<span class="badge" [ngClass]="getRoleBadgeClass()">
|
||||
{{ getRoleLabel() }}
|
||||
<span [class]="getUserBadgeClass()" class="badge">
|
||||
<ng-icon [name]="getUserBadgeIcon()" class="me-1"></ng-icon>
|
||||
{{ getUserBadgeText() }}
|
||||
</span>
|
||||
@if (currentMerchantPartnerId) {
|
||||
@if (currentMerchantId) {
|
||||
<span class="ms-2">
|
||||
<strong>Merchant ID :</strong> {{ currentMerchantPartnerId }}
|
||||
<strong>Merchant ID :</strong> {{ currentMerchantId }}
|
||||
</span>
|
||||
}
|
||||
</small>
|
||||
@ -31,52 +32,46 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Navigation par onglets -->
|
||||
<!-- Navigation -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<ul
|
||||
ngbNav
|
||||
#subscriptionsNav="ngbNav"
|
||||
[activeId]="activeTab"
|
||||
[destroyOnHide]="false"
|
||||
class="nav nav-tabs nav-justified nav-bordered nav-bordered-primary mb-3"
|
||||
>
|
||||
<li [ngbNavItem]="'list'">
|
||||
<a ngbNavLink (click)="showTab('list')">
|
||||
<ng-icon name="lucideList" class="fs-lg me-md-1 d-inline-flex align-middle" />
|
||||
<span class="d-none d-md-inline-block align-middle">Liste des Abonnements</span>
|
||||
</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-subscriptions-list
|
||||
#subscriptionsList
|
||||
(subscriptionSelected)="onSubscriptionSelected($event)"
|
||||
(viewPaymentsRequested)="onViewPaymentsRequested($event)"
|
||||
/>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="'payments'" [hidden]="!selectedSubscriptionId">
|
||||
<a ngbNavLink (click)="showTab('payments')">
|
||||
<ng-icon name="lucideCreditCard" class="fs-lg me-md-1 d-inline-flex align-middle" />
|
||||
<span class="d-none d-md-inline-block align-middle">Paiements</span>
|
||||
</a>
|
||||
<ng-template ngbNavContent>
|
||||
@if (selectedSubscriptionId) {
|
||||
<app-subscription-payments
|
||||
[subscriptionId]="selectedSubscriptionId"
|
||||
(back)="backToList()"
|
||||
/>
|
||||
} @else {
|
||||
<div class="alert alert-warning text-center">
|
||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||
Aucun abonnement sélectionné
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" [ngbNavOutlet]="subscriptionsNav"></div>
|
||||
@if (activeView === 'list') {
|
||||
<app-subscriptions-list
|
||||
(subscriptionSelected)="showDetailsView($event)"
|
||||
(viewPaymentsRequested)="onViewPaymentsRequested($event)"
|
||||
/>
|
||||
} @else if (activeView === 'details' && selectedSubscriptionId) {
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm me-2" (click)="showListView()">
|
||||
<ng-icon name="lucideArrowLeft" class="me-1"></ng-icon>
|
||||
Retour à la liste
|
||||
</button>
|
||||
<button class="btn btn-outline-primary btn-sm me-2"
|
||||
(click)="showPaymentsView(selectedSubscriptionId)">
|
||||
<ng-icon name="lucideCreditCard" class="me-1"></ng-icon>
|
||||
Voir les paiements
|
||||
</button>
|
||||
<h5 class="mb-0">Détails de l'abonnement</h5>
|
||||
</div>
|
||||
<app-subscription-details [subscriptionId]="selectedSubscriptionId" />
|
||||
} @else if (activeView === 'payments' && selectedSubscriptionId) { <!-- AJOUTER -->
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm me-2" (click)="showListView()">
|
||||
<ng-icon name="lucideArrowLeft" class="me-1"></ng-icon>
|
||||
Retour à la liste
|
||||
</button>
|
||||
<button class="btn btn-outline-primary btn-sm me-2"
|
||||
(click)="showDetailsView(selectedSubscriptionId)">
|
||||
<ng-icon name="lucideEye" class="me-1"></ng-icon>
|
||||
Voir les détails
|
||||
</button>
|
||||
<h5 class="mb-0">Paiements de l'abonnement</h5>
|
||||
</div>
|
||||
<app-subscription-payments
|
||||
[subscriptionId]="selectedSubscriptionId"
|
||||
(back)="showListView()"
|
||||
/>
|
||||
}
|
||||
</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 { FormsModule } from '@angular/forms';
|
||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
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 { SubscriptionsList } from './subscriptions-list/subscriptions-list';
|
||||
import { SubscriptionPayments } from './subscription-payments/subscription-payments';
|
||||
|
||||
import {
|
||||
Subscription,
|
||||
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';
|
||||
import { SubscriptionDetails } from './subscription-details/subscription-details';
|
||||
import { SubscriptionPayments } from './subscription-payments/subscription-payments'; // AJOUTER
|
||||
import { SubscriptionAccessService } from './services/subscription-access.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subscriptions',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
NgIcon,
|
||||
NgbNavModule,
|
||||
NgbModalModule,
|
||||
NgIcon,
|
||||
PageTitle,
|
||||
SubscriptionsList,
|
||||
SubscriptionDetails,
|
||||
SubscriptionPayments
|
||||
],
|
||||
templateUrl: './subscriptions.html',
|
||||
})
|
||||
export class SubscriptionsManagement implements OnInit, OnDestroy {
|
||||
private authService = inject(AuthService);
|
||||
private subscriptionsService = inject(SubscriptionsService);
|
||||
private roleService = inject(RoleManagementService);
|
||||
export class Subscriptions implements OnInit {
|
||||
private subscriptions: Subscription[] = [];
|
||||
private accessService = inject(SubscriptionAccessService);
|
||||
|
||||
private cdRef = inject(ChangeDetectorRef);
|
||||
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';
|
||||
activeView: 'list' | 'details' | 'payments' = 'list'; // AJOUTER 'payments'
|
||||
selectedSubscriptionId: string | null = null;
|
||||
|
||||
user: User | undefined;
|
||||
// Permissions
|
||||
canAccessModule = true;
|
||||
accessDenied = false;
|
||||
currentUserRole = '';
|
||||
currentMerchantId?: number;
|
||||
|
||||
// Gestion des rôles (lecture seule)
|
||||
availableRoles: { value: UserRole; label: string; description: string }[] = [];
|
||||
subscriptionInitialized = false;
|
||||
|
||||
// Gestion des permissions
|
||||
currentUserRole: string | null = null;
|
||||
currentMerchantPartnerId: string = '';
|
||||
ngOnInit(): void {
|
||||
console.log('🔍 Subscription: ngOnInit() appelé');
|
||||
|
||||
// Données
|
||||
subscriptionPayments: { [subscriptionId: string]: SubscriptionPayment[] } = {};
|
||||
selectedSubscriptionForPayments: Subscription | null = null;
|
||||
|
||||
// Références aux composants enfants
|
||||
@ViewChild(SubscriptionsList) subscriptionsList!: SubscriptionsList;
|
||||
|
||||
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}`);
|
||||
// Attendre que le SubscriptionAccessService soit VRAIMENT prêt
|
||||
this.subscriptions.push(
|
||||
this.accessService.waitForReady().subscribe({
|
||||
next: () => {
|
||||
console.log('✅ Subscription: waitForReady() a émis - Initialisation...');
|
||||
this.subscriptionInitialized = true;
|
||||
this.checkAccess();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading user profile:', error);
|
||||
error: (err) => {
|
||||
console.error('❌ Subscription: Erreur dans waitForReady():', err);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extraire le rôle de l'utilisateur
|
||||
*/
|
||||
private extractUserRole(user: any): string | null {
|
||||
const userRoles = this.authService.getCurrentUserRoles();
|
||||
if (userRoles && userRoles.length > 0) {
|
||||
return userRoles[0];
|
||||
}
|
||||
return null;
|
||||
private checkAccess() {
|
||||
const access = this.accessService.getSubscriptionAccess();
|
||||
this.canAccessModule = access.canViewSubscriptions;
|
||||
this.accessDenied = !access.canViewSubscriptions;
|
||||
this.currentUserRole = access.userRoleLabel;
|
||||
this.currentMerchantId = access.merchantId;
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES D'INTERFACE ====================
|
||||
|
||||
// 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';
|
||||
showListView() {
|
||||
this.activeView = 'list';
|
||||
this.selectedSubscriptionId = null;
|
||||
}
|
||||
|
||||
// Méthodes de gestion des événements du composant enfant
|
||||
onSubscriptionSelected(subscriptionId: string) {
|
||||
this.showTab('payments', subscriptionId);
|
||||
showDetailsView(subscriptionId: string) {
|
||||
this.activeView = 'details';
|
||||
this.selectedSubscriptionId = subscriptionId;
|
||||
}
|
||||
|
||||
// NOUVELLE MÉTHODE
|
||||
showPaymentsView(subscriptionId: string) {
|
||||
this.activeView = 'payments';
|
||||
this.selectedSubscriptionId = subscriptionId;
|
||||
}
|
||||
|
||||
// NOUVELLE MÉTHODE
|
||||
onViewPaymentsRequested(subscriptionId: string) {
|
||||
this.showTab('payments', subscriptionId);
|
||||
this.showPaymentsView(subscriptionId);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES UTILITAIRES ====================
|
||||
|
||||
private refreshSubscriptionsList(): void {
|
||||
if (this.subscriptionsList && typeof this.subscriptionsList.refreshData === 'function') {
|
||||
console.log('🔄 Refreshing subscriptions list...');
|
||||
this.subscriptionsList.refreshData();
|
||||
} else {
|
||||
console.warn('❌ SubscriptionsList component not available for refresh');
|
||||
this.showTab('list');
|
||||
}
|
||||
// Utilitaires pour le template
|
||||
getUserBadgeClass(): string {
|
||||
const access = this.accessService.getSubscriptionAccess();
|
||||
return access.isHubUser ? 'badge bg-primary' : 'badge bg-success';
|
||||
}
|
||||
|
||||
// Méthodes pour les templates
|
||||
getStatusDisplayName(status: SubscriptionStatus): string {
|
||||
const statusNames: { [key: string]: string } = {
|
||||
[SubscriptionStatus.ACTIVE]: 'Actif',
|
||||
[SubscriptionStatus.SUSPENDED]: 'Suspendu',
|
||||
[SubscriptionStatus.CANCELLED]: 'Annulé',
|
||||
[SubscriptionStatus.EXPIRED]: 'Expiré',
|
||||
[SubscriptionStatus.PENDING]: 'En attente'
|
||||
};
|
||||
return statusNames[status] || status;
|
||||
getUserBadgeIcon(): string {
|
||||
const access = this.accessService.getSubscriptionAccess();
|
||||
return access.isHubUser ? 'lucideShield' : 'lucideStore';
|
||||
}
|
||||
|
||||
getPeriodicityDisplayName(periodicity: SubscriptionPeriodicity): string {
|
||||
const periodicityNames: { [key: string]: string } = {
|
||||
[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();
|
||||
getUserBadgeText(): string {
|
||||
return this.currentUserRole;
|
||||
}
|
||||
}
|
||||
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>
|
||||
{{ getUserBadgeText() }}
|
||||
</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>
|
||||
Merchant {{ currentMerchantId }}
|
||||
</span>
|
||||
@ -26,7 +26,7 @@
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<!-- 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>
|
||||
</button>
|
||||
</div>
|
||||
@ -41,45 +41,77 @@
|
||||
<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>
|
||||
<p class="mb-0">{{ error || 'Vous n\'avez pas les permissions nécessaires pour accéder à cette section.' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
|
||||
<!-- Statistiques rapides -->
|
||||
@if (paginatedData?.stats) {
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body py-3">
|
||||
<div class="row text-center">
|
||||
<div class="col">
|
||||
<small class="text-muted">Total</small>
|
||||
<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>
|
||||
<!-- Statistiques rapides -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body py-3">
|
||||
<div class="row text-center">
|
||||
<div class="col">
|
||||
<small class="text-muted">Total</small>
|
||||
<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 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>
|
||||
|
||||
<!-- 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 -->
|
||||
<div class="row mb-3">
|
||||
@ -91,39 +123,71 @@
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Rechercher par periodicity, Type..."
|
||||
placeholder="Rechercher par ID, référence, merchant..."
|
||||
[(ngModel)]="searchTerm"
|
||||
(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 class="col-md-8">
|
||||
<div class="d-flex gap-2">
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<!-- Filtre statut -->
|
||||
<select class="form-select" style="width: auto;" (change)="onStatusFilterChange($any($event.target).value)">
|
||||
<option value="all">Tous les statuts</option>
|
||||
@for (status of statusOptions; track status) {
|
||||
<option [value]="status">{{ status }}</option>
|
||||
}
|
||||
</select>
|
||||
<div ngbDropdown class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" ngbDropdownToggle>
|
||||
<ng-icon name="lucideFilter" class="me-1"></ng-icon>
|
||||
Statut: {{ statusFilter === 'all' ? 'Tous' : getStatusDisplayName(statusFilter) }}
|
||||
</button>
|
||||
<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 -->
|
||||
<select class="form-select" style="width: auto;" (change)="onOperatorFilterChange($any($event.target).value)">
|
||||
<option value="">Tous les opérateurs</option>
|
||||
@for (operator of operatorOptions; track operator) {
|
||||
<option [value]="operator">{{ operator }}</option>
|
||||
}
|
||||
</select>
|
||||
<!-- Filtre date début -->
|
||||
<div class="input-group" style="width: 200px;">
|
||||
<span class="input-group-text">
|
||||
<ng-icon name="lucideCalendar"></ng-icon>
|
||||
</span>
|
||||
<input
|
||||
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()">
|
||||
<ng-icon name="lucideFilter" class="me-1"></ng-icon>
|
||||
Filtrer
|
||||
<ng-icon name="lucideSearch" class="me-1"></ng-icon>
|
||||
Appliquer
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-secondary" (click)="onClearFilters()">
|
||||
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||
Effacer
|
||||
Tout effacer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -181,11 +245,11 @@
|
||||
<th>Statut</th>
|
||||
<th (click)="sort('transactionDate')" class="cursor-pointer">
|
||||
<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>
|
||||
</div>
|
||||
</th>
|
||||
<th>Prochain paiement</th>
|
||||
<th>Date création</th>
|
||||
<th width="120">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -200,17 +264,27 @@
|
||||
(change)="toggleTransactionSelection(transaction.id)"
|
||||
>
|
||||
</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>
|
||||
<span class="badge bg-light text-dark">
|
||||
<ng-icon [name]="getTypeIcon(transaction.type)" class="me-1"></ng-icon>
|
||||
Abonnement
|
||||
{{ getTypeDisplayName(transaction.type) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">
|
||||
Merchant {{ transaction.merchantPartnerId }}
|
||||
</span>
|
||||
@if (transaction.merchantPartnerId) {
|
||||
<span class="badge bg-secondary">
|
||||
Merchant {{ transaction.merchantPartnerId }}
|
||||
</span>
|
||||
} @else {
|
||||
<span class="text-muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<span [class]="getAmountColor(transaction.amount)">
|
||||
@ -222,6 +296,8 @@
|
||||
<span [class]="getPeriodicityBadgeClass(transaction.periodicity)" class="badge">
|
||||
{{ getPeriodicityDisplayName(transaction.periodicity) }}
|
||||
</span>
|
||||
} @else {
|
||||
<span class="text-muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@ -234,11 +310,7 @@
|
||||
{{ formatDate(transaction.transactionDate) }}
|
||||
</td>
|
||||
<td class="small text-muted">
|
||||
@if (transaction.nextPaymentDate) {
|
||||
{{ formatDate(transaction.nextPaymentDate) }}
|
||||
} @else {
|
||||
-
|
||||
}
|
||||
{{ formatDate(transaction.createdAt) }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
@ -249,6 +321,15 @@
|
||||
>
|
||||
<ng-icon name="lucideEye"></ng-icon>
|
||||
</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>
|
||||
</td>
|
||||
</tr>
|
||||
@ -257,10 +338,18 @@
|
||||
<tr>
|
||||
<td colspan="10" class="text-center py-4">
|
||||
<ng-icon name="lucideCreditCard" class="text-muted fs-1 mb-2 d-block"></ng-icon>
|
||||
<p class="text-muted mb-3">Aucune transaction trouvée</p>
|
||||
<button class="btn btn-primary" (click)="onClearFilters()">
|
||||
Réinitialiser les filtres
|
||||
</button>
|
||||
<p class="text-muted mb-3">
|
||||
@if (allTransactions.length === 0) {
|
||||
Aucune transaction disponible
|
||||
} @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>
|
||||
</tr>
|
||||
}
|
||||
@ -271,18 +360,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if (paginatedData && paginatedData.totalPages >= 1) {
|
||||
@if (totalPages >= 1) {
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<div class="text-muted">
|
||||
Affichage de {{ (filters.page! - 1) * filters.limit! + 1 }} à
|
||||
{{ (filters.page! * filters.limit!) > (paginatedData.total || 0) ? (paginatedData.total || 0) : (filters.page! * filters.limit!) }}
|
||||
sur {{ paginatedData.total || 0 }} transactions
|
||||
Affichage de
|
||||
{{ (currentPage - 1) * itemsPerPage + 1 }} à
|
||||
{{ getCurrentPageEnd() }}
|
||||
sur {{ totalItems }} transactions
|
||||
({{ getFilteredCount() }} filtrées / {{ getTotalCount() }} total)
|
||||
</div>
|
||||
<nav>
|
||||
<ngb-pagination
|
||||
[collectionSize]="paginatedData.total"
|
||||
[page]="filters.page!"
|
||||
[pageSize]="filters.limit!"
|
||||
[collectionSize]="totalItems"
|
||||
[page]="currentPage"
|
||||
[pageSize]="itemsPerPage"
|
||||
[maxSize]="5"
|
||||
[rotate]="true"
|
||||
[boundaryLinks]="true"
|
||||
@ -292,6 +383,5 @@
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</div>
|
||||
@ -40,8 +40,7 @@ import {
|
||||
|
||||
import {
|
||||
Currency,
|
||||
SubscriptionPeriodicity,
|
||||
SubscriptionUtils
|
||||
SubscriptionPeriodicity
|
||||
} from '@core/models/dcb-bo-hub-subscription.model';
|
||||
|
||||
@Component({
|
||||
@ -53,14 +52,15 @@ import {
|
||||
NgIcon,
|
||||
NgbPaginationModule,
|
||||
NgbDropdownModule,
|
||||
NgbTooltipModule
|
||||
NgbTooltipModule,
|
||||
],
|
||||
providers: [
|
||||
provideNgIconsConfig({
|
||||
size: '1.25em'
|
||||
})
|
||||
],
|
||||
templateUrl: './list.html'
|
||||
templateUrl: './list.html',
|
||||
styleUrl: './list.css'
|
||||
})
|
||||
export class TransactionsList implements OnInit, OnDestroy {
|
||||
private transactionsService = inject(TransactionsService);
|
||||
@ -72,12 +72,11 @@ export class TransactionsList implements OnInit, OnDestroy {
|
||||
|
||||
// Permissions
|
||||
access!: TransactionAccess;
|
||||
currentUserRole = '';
|
||||
currentMerchantId?: number;
|
||||
currentMerchantId: number = 0;
|
||||
|
||||
// Données
|
||||
transactions: Transaction[] = [];
|
||||
paginatedData: PaginatedTransactions | null = null;
|
||||
allTransactions: Transaction[] = [];
|
||||
|
||||
// États
|
||||
loading = false;
|
||||
@ -85,15 +84,9 @@ export class TransactionsList implements OnInit, OnDestroy {
|
||||
|
||||
// Filtres et recherche
|
||||
searchTerm = '';
|
||||
filters: TransactionQuery = {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
status: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
sortBy: 'transactionDate',
|
||||
sortOrder: 'desc'
|
||||
};
|
||||
statusFilter: TransactionStatus | 'all' = 'all';
|
||||
startDateFilter: Date | null = null;
|
||||
endDateFilter: Date | null = null;
|
||||
|
||||
// Options de filtre
|
||||
statusOptions: TransactionStatus[] = ['PENDING', 'SUCCESS', 'FAILED', 'CANCELLED'];
|
||||
@ -102,9 +95,12 @@ export class TransactionsList implements OnInit, OnDestroy {
|
||||
TransactionType.SUBSCRIPTION_RENEWAL,
|
||||
TransactionType.ONE_TIME_PAYMENT
|
||||
];
|
||||
periodicityOptions = Object.values(SubscriptionPeriodicity);
|
||||
operatorOptions: string[] = ['Orange'];
|
||||
countryOptions: string[] = ['SN'];
|
||||
|
||||
// Pagination
|
||||
currentPage = 1;
|
||||
itemsPerPage = 20;
|
||||
totalItems = 0;
|
||||
totalPages = 0;
|
||||
|
||||
// Tri
|
||||
sortField: string = 'transactionDate';
|
||||
@ -114,62 +110,74 @@ export class TransactionsList implements OnInit, OnDestroy {
|
||||
selectedTransactions: Set<string> = new Set();
|
||||
selectAll = false;
|
||||
|
||||
// Cache
|
||||
private lastLoadTime: number = 0;
|
||||
private readonly CACHE_TTL = 30000; // 30 secondes comme le service
|
||||
|
||||
ngOnInit() {
|
||||
this.initializePermissions();
|
||||
this.loadTransactions();
|
||||
this.loadAllTransactions();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Nettoyage si nécessaire
|
||||
// Nettoyer le cache si nécessaire
|
||||
}
|
||||
|
||||
private initializePermissions() {
|
||||
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) {
|
||||
this.error = 'Vous n\'avez pas la permission de voir les transactions';
|
||||
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.error = '';
|
||||
|
||||
// Préparer les filtres pour l'API
|
||||
const apiFilters = this.prepareFiltersForApi();
|
||||
// Préparer la requête avec merchantPartnerId si nécessaire
|
||||
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) => {
|
||||
this.paginatedData = data;
|
||||
this.transactions = data.data;
|
||||
// Stocker TOUTES les transactions pour le filtrage client
|
||||
this.allTransactions = data.data || [];
|
||||
this.lastLoadTime = Date.now();
|
||||
|
||||
console.log(`✅ ${this.allTransactions.length} transactions chargées`);
|
||||
this.applyFiltersAndPagination();
|
||||
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();
|
||||
},
|
||||
error: (error) => {
|
||||
@ -181,32 +189,118 @@ export class TransactionsList implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
private prepareFiltersForApi(): TransactionQuery {
|
||||
const apiFilters: TransactionQuery = { ...this.filters };
|
||||
private applyFiltersAndPagination() {
|
||||
// 1. Appliquer les filtres
|
||||
let filteredTransactions = this.filterTransactions(this.allTransactions);
|
||||
|
||||
// Ajouter la recherche si présente
|
||||
if (this.searchTerm) {
|
||||
apiFilters.search = this.searchTerm;
|
||||
// 2. Appliquer le tri
|
||||
filteredTransactions = this.sortTransactions(filteredTransactions);
|
||||
|
||||
// 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
|
||||
apiFilters.sortBy = this.sortField;
|
||||
apiFilters.sortOrder = this.sortDirection;
|
||||
// 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.transactions = filteredTransactions.slice(startIndex, endIndex);
|
||||
|
||||
// Nettoyer les filtres (enlever les undefined)
|
||||
Object.keys(apiFilters).forEach(key => {
|
||||
if (apiFilters[key as keyof TransactionQuery] === undefined) {
|
||||
delete apiFilters[key as keyof TransactionQuery];
|
||||
console.log(`📊 Filtrage: ${this.allTransactions.length} → ${filteredTransactions.length} → ${this.transactions.length} (page ${this.currentPage}/${this.totalPages})`);
|
||||
}
|
||||
|
||||
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
|
||||
onSearch() {
|
||||
this.filters.page = 1;
|
||||
this.loadTransactions();
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
clearSearch() {
|
||||
@ -216,52 +310,33 @@ export class TransactionsList implements OnInit, OnDestroy {
|
||||
|
||||
onClearFilters() {
|
||||
this.searchTerm = '';
|
||||
this.filters = {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
status: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
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();
|
||||
this.statusFilter = 'all';
|
||||
this.startDateFilter = null;
|
||||
this.endDateFilter = null;
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
onStatusFilterChange(status: TransactionStatus | 'all') {
|
||||
this.filters.status = status === 'all' ? undefined : status;
|
||||
this.filters.page = 1;
|
||||
this.loadTransactions();
|
||||
this.statusFilter = status;
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
onOperatorFilterChange(operator: string) {
|
||||
this.filters.page = 1;
|
||||
this.loadTransactions();
|
||||
onStartDateChange(date: Date | null) {
|
||||
this.startDateFilter = date;
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
onDateRangeChange(start: Date | null, end: Date | null) {
|
||||
this.filters.startDate = start || undefined;
|
||||
this.filters.endDate = end || undefined;
|
||||
this.filters.page = 1;
|
||||
this.loadTransactions();
|
||||
}
|
||||
|
||||
// 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;
|
||||
onEndDateChange(date: Date | null) {
|
||||
this.endDateFilter = date;
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
// Tri
|
||||
@ -272,8 +347,8 @@ export class TransactionsList implements OnInit, OnDestroy {
|
||||
this.sortField = field;
|
||||
this.sortDirection = 'desc';
|
||||
}
|
||||
this.filters.page = 1;
|
||||
this.loadTransactions();
|
||||
this.applyFiltersAndPagination();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
getSortIcon(field: string): string {
|
||||
@ -283,12 +358,14 @@ export class TransactionsList implements OnInit, OnDestroy {
|
||||
|
||||
// Pagination
|
||||
onPageChange(page: number) {
|
||||
this.filters.page = page;
|
||||
this.loadTransactions();
|
||||
this.currentPage = page;
|
||||
this.applyFiltersAndPagination();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
// Actions
|
||||
viewTransactionDetails(transactionId: string) {
|
||||
if (!transactionId) return;
|
||||
this.transactionSelected.emit(transactionId);
|
||||
}
|
||||
|
||||
@ -316,6 +393,26 @@ export class TransactionsList implements OnInit, OnDestroy {
|
||||
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
|
||||
getStatusBadgeClass(status: TransactionStatus): string {
|
||||
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';
|
||||
|
||||
switch (periodicity.toLowerCase()) {
|
||||
case 'daily':
|
||||
return 'badge bg-primary';
|
||||
case 'weekly':
|
||||
return 'badge bg-info';
|
||||
case 'monthly':
|
||||
return 'badge bg-success';
|
||||
case 'yearly':
|
||||
return 'badge bg-warning';
|
||||
default:
|
||||
return 'badge bg-secondary';
|
||||
case 'daily': return 'badge bg-primary';
|
||||
case 'weekly': return 'badge bg-info';
|
||||
case 'monthly': return 'badge bg-success';
|
||||
case 'yearly': return 'badge bg-warning';
|
||||
default: return 'badge bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
@ -399,6 +491,24 @@ export class TransactionsList implements OnInit, OnDestroy {
|
||||
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 {
|
||||
if (amount >= 10000) return 'text-danger fw-bold';
|
||||
@ -414,25 +524,41 @@ export class TransactionsList implements OnInit, OnDestroy {
|
||||
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
|
||||
getTotal(): number {
|
||||
return this.paginatedData?.stats?.total || 0;
|
||||
return this.calculateStats().total;
|
||||
}
|
||||
|
||||
getSuccessCount(): number {
|
||||
return this.paginatedData?.stats?.successCount || 0;
|
||||
return this.calculateStats().successCount;
|
||||
}
|
||||
|
||||
getFailedCount(): number {
|
||||
return this.paginatedData?.stats?.failedCount || 0;
|
||||
return this.calculateStats().failedCount;
|
||||
}
|
||||
|
||||
getPendingCount(): number {
|
||||
return this.paginatedData?.stats?.pendingCount || 0;
|
||||
return this.calculateStats().pendingCount;
|
||||
}
|
||||
|
||||
getTotalAmount(): number {
|
||||
return this.paginatedData?.stats?.totalAmount || 0;
|
||||
return this.calculateStats().totalAmount;
|
||||
}
|
||||
|
||||
// Méthodes pour le template
|
||||
@ -449,29 +575,36 @@ export class TransactionsList implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
getScopeText(): string {
|
||||
if (this.access.isMerchantUser && this.currentMerchantId) {
|
||||
if (this.access.isMerchantUser && this.currentMerchantId > 0) {
|
||||
return `Marchand ${this.currentMerchantId}`;
|
||||
} else if (this.access.isHubUser && this.filters.merchantPartnerId) {
|
||||
return `Marchand ${this.filters.merchantPartnerId}`;
|
||||
} else if (this.access.isHubUser) {
|
||||
return 'Tous les marchands';
|
||||
}
|
||||
return 'Tous les marchands';
|
||||
return 'Aucun scope défini';
|
||||
}
|
||||
|
||||
// Méthode pour recharger les données
|
||||
refreshData() {
|
||||
this.loadTransactions();
|
||||
this.lastLoadTime = 0; // Invalider le cache
|
||||
this.loadAllTransactions();
|
||||
}
|
||||
|
||||
// Debug
|
||||
showDebugInfo() {
|
||||
console.log('=== DEBUG INFO ===');
|
||||
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);
|
||||
// Méthodes utilitaires
|
||||
getFilteredCount(): number {
|
||||
return 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
|
||||
userRole,
|
||||
userRoleLabel: this.roleService.getRoleLabel() || 'Utilisateur',
|
||||
userRoleLabel: this.roleService.getRoleLabel(userRole) || 'Utilisateur',
|
||||
merchantId
|
||||
};
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
localServiceTestApiUrl: "https://backoffice.dcb.pixpay.sn/api/v1",
|
||||
iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1",
|
||||
localServiceTestApiUrl: "http://localhost:4200/api/v1",
|
||||
iamApiUrl: "http://localhost:3000/api/v1",
|
||||
configApiUrl: 'https://api-merchant-config-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/',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user