diff --git a/src/app/modules/transactions/details/details.ts b/src/app/modules/transactions/details/details.ts index 311662e..e3753a7 100644 --- a/src/app/modules/transactions/details/details.ts +++ b/src/app/modules/transactions/details/details.ts @@ -200,19 +200,6 @@ export class TransactionDetails implements OnInit { if (diffDays < 7) return `Il y a ${diffDays} j`; return this.formatDate(date); } - - canRefund(): boolean { - return this.access.canRefund && this.transaction?.status === 'SUCCESS'; - } - - canRetry(): boolean { - return this.access.canRetry && this.transaction?.status === 'FAILED'; - } - - canCancel(): boolean { - return this.access.canCancel && this.transaction?.status === 'PENDING'; - } - // Méthodes pour le template getUserBadgeClass(): string { return this.access.isHubUser ? 'bg-primary' : 'bg-success'; diff --git a/src/app/modules/transactions/list/list.ts b/src/app/modules/transactions/list/list.ts index ec35092..cfd0b15 100644 --- a/src/app/modules/transactions/list/list.ts +++ b/src/app/modules/transactions/list/list.ts @@ -21,7 +21,8 @@ import { lucideStore, lucideCalendar, lucideRepeat, - lucideCreditCard + lucideCreditCard, + lucideInfo } from '@ng-icons/lucide'; import { NgbPaginationModule, NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; @@ -125,12 +126,9 @@ export class TransactionsList implements OnInit, OnDestroy { private initializePermissions() { this.access = this.accessService.getTransactionAccess(); - // Ajouter le merchant ID aux filtres si nécessaire + // 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]; - } else if (this.access.isHubUser && this.access.merchantId) { - // Pour les hub users qui veulent voir un merchant spécifique - this.filters.merchantPartnerId = this.access.merchantId; } // Définir le rôle pour l'affichage @@ -147,27 +145,31 @@ export class TransactionsList implements OnInit, OnDestroy { this.loading = true; this.error = ''; - // Mettre à jour les filtres avec la recherche - if (this.searchTerm) { - this.filters.search = this.searchTerm; - } else { - delete this.filters.search; - } - - // Appliquer le tri - this.filters.sortBy = this.sortField; - this.filters.sortOrder = this.sortDirection; + // Préparer les filtres pour l'API + const apiFilters = this.prepareFiltersForApi(); - // Appliquer les restrictions de merchant pour les non-admin hub users - if (!this.access.canManageAll && this.access.allowedMerchantIds.length > 0) { - this.filters.merchantPartnerId = this.access.allowedMerchantIds[0]; - } + console.log('Chargement transactions avec filtres:', apiFilters); - this.transactionsService.getTransactions(this.filters).subscribe({ + this.transactionsService.getTransactions(apiFilters).subscribe({ next: (data) => { this.paginatedData = data; this.transactions = data.data; 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) => { @@ -179,12 +181,39 @@ export class TransactionsList implements OnInit, OnDestroy { }); } + private prepareFiltersForApi(): TransactionQuery { + const apiFilters: TransactionQuery = { ...this.filters }; + + // Ajouter la recherche si présente + if (this.searchTerm) { + apiFilters.search = this.searchTerm; + } + + // Appliquer le tri + apiFilters.sortBy = this.sortField; + apiFilters.sortOrder = this.sortDirection; + + // Nettoyer les filtres (enlever les undefined) + Object.keys(apiFilters).forEach(key => { + if (apiFilters[key as keyof TransactionQuery] === undefined) { + delete apiFilters[key as keyof TransactionQuery]; + } + }); + + return apiFilters; + } + // Recherche et filtres onSearch() { this.filters.page = 1; this.loadTransactions(); } + clearSearch() { + this.searchTerm = ''; + this.onSearch(); + } + onClearFilters() { this.searchTerm = ''; this.filters = { @@ -197,7 +226,7 @@ export class TransactionsList implements OnInit, OnDestroy { sortOrder: 'desc' }; - // Réappliquer les restrictions de merchant + // 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]; } @@ -225,7 +254,10 @@ export class TransactionsList implements OnInit, OnDestroy { // Permissions pour les filtres canUseMerchantFilter(): boolean { - return this.access.canFilterByMerchant && this.access.allowedMerchantIds.length > 1; + // Uniquement pour les hub users avec plusieurs merchants autorisés + return this.access.canFilterByMerchant && + this.access.isHubUser && + this.access.allowedMerchantIds.length > 0; } canUseAllFilters(): boolean { @@ -240,6 +272,7 @@ export class TransactionsList implements OnInit, OnDestroy { this.sortField = field; this.sortDirection = 'desc'; } + this.filters.page = 1; this.loadTransactions(); } @@ -256,15 +289,7 @@ export class TransactionsList implements OnInit, OnDestroy { // Actions viewTransactionDetails(transactionId: string) { - // Vérifier les permissions avant d'afficher - this.accessService.canAccessTransaction().subscribe(canAccess => { - if (canAccess) { - this.transactionSelected.emit(transactionId); - } else { - this.error = 'Vous n\'avez pas la permission de voir les détails de cette transaction'; - this.cdRef.detectChanges(); - } - }); + this.transactionSelected.emit(transactionId); } // Sélection multiple @@ -283,6 +308,7 @@ export class TransactionsList implements OnInit, OnDestroy { } else { this.selectedTransactions.clear(); } + this.updateSelectAllState(); } updateSelectAllState() { @@ -290,7 +316,7 @@ export class TransactionsList implements OnInit, OnDestroy { this.selectedTransactions.size === this.transactions.length; } - // Utilitaires d'affichage MIS À JOUR + // Utilitaires d'affichage getStatusBadgeClass(status: TransactionStatus): string { switch (status) { case 'SUCCESS': return 'badge bg-success'; @@ -321,7 +347,7 @@ export class TransactionsList implements OnInit, OnDestroy { } } - getPeriodicityBadgeClass(periodicity?: string): string { + getPeriodicityBadgeClass(periodicity?: string): string { if (!periodicity) return 'badge bg-secondary'; switch (periodicity.toLowerCase()) { @@ -338,101 +364,6 @@ export class TransactionsList implements OnInit, OnDestroy { } } - formatCurrency(amount: number, currency: Currency = Currency.XOF): string { - return TransactionUtils.formatAmount(amount, currency); - } - - formatDate(date: Date | string | undefined | null): string { - // Si la date est null/undefined, retourner une chaîne vide - if (!date) { - return '-'; - } - - // Si c'est déjà une Date valide - if (date instanceof Date) { - // Vérifier si la Date est valide - if (isNaN(date.getTime())) { - return 'Date invalide'; - } - return new Intl.DateTimeFormat('fr-FR', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }).format(date); - } - - // Si c'est une chaîne, essayer de la convertir - if (typeof date === 'string') { - const dateObj = new Date(date); - - // Vérifier si la conversion a réussi - if (isNaN(dateObj.getTime())) { - // Essayer d'autres formats - const alternativeDate = this.parseDateString(date); - if (alternativeDate && !isNaN(alternativeDate.getTime())) { - return new Intl.DateTimeFormat('fr-FR', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }).format(alternativeDate); - } - return 'Date invalide'; - } - - return new Intl.DateTimeFormat('fr-FR', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }).format(dateObj); - } - - // Pour tout autre type, retourner '-' - return '-'; - } - - private parseDateString(dateString: string): Date | null { - try { - // Essayer différents formats de date - const formats = [ - dateString, // Format ISO original - dateString.replace(' ', 'T'), // Remplacer espace par T - dateString.split('.')[0], // Enlever les millisecondes - ]; - - for (const format of formats) { - const date = new Date(format); - if (!isNaN(date.getTime())) { - return date; - } - } - - return null; - } catch { - return null; - } - } - - getAmountColor(amount: number): string { - if (amount >= 10000) return 'text-danger fw-bold'; - if (amount >= 5000) return 'text-warning fw-semibold'; - return 'text-success'; - } - - // utilitaires pour les abonnements - getStatusDisplayName(status: TransactionStatus): string { - return TransactionUtils.getStatusDisplayName(status); - } - - getTypeDisplayName(type: TransactionType): string { - return TransactionUtils.getTypeDisplayName(type); - } - getPeriodicityDisplayName(periodicity?: string): string { if (!periodicity) return ''; @@ -445,6 +376,43 @@ export class TransactionsList implements OnInit, OnDestroy { return periodicityNames[periodicity.toLowerCase()] || periodicity; } + + formatCurrency(amount: number, currency: Currency = Currency.XOF): string { + return TransactionUtils.formatAmount(amount, currency); + } + + formatDate(date: Date | string | undefined | null): string { + if (!date) return '-'; + + try { + 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); + } catch { + return '-'; + } + } + + getAmountColor(amount: number): string { + if (amount >= 10000) return 'text-danger fw-bold'; + if (amount >= 5000) return 'text-warning fw-semibold'; + return 'text-success'; + } + + getStatusDisplayName(status: TransactionStatus): string { + return TransactionUtils.getStatusDisplayName(status); + } + + getTypeDisplayName(type: TransactionType): string { + return TransactionUtils.getTypeDisplayName(type); + } // Méthodes pour sécuriser l'accès aux stats getTotal(): number { @@ -482,8 +450,28 @@ export class TransactionsList implements OnInit, OnDestroy { getScopeText(): string { if (this.access.isMerchantUser && this.currentMerchantId) { - return `Merchant ${this.currentMerchantId}`; + return `Marchand ${this.currentMerchantId}`; + } else if (this.access.isHubUser && this.filters.merchantPartnerId) { + return `Marchand ${this.filters.merchantPartnerId}`; } - return this.access.canManageAll ? 'Tous les merchants' : 'Merchants autorisés'; + return 'Tous les marchands'; + } + + // Méthode pour recharger les données + refreshData() { + this.loadTransactions(); + } + + // 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); } } \ No newline at end of file diff --git a/src/app/modules/transactions/services/transaction-access.service.ts b/src/app/modules/transactions/services/transaction-access.service.ts index 441c109..fc20ae4 100644 --- a/src/app/modules/transactions/services/transaction-access.service.ts +++ b/src/app/modules/transactions/services/transaction-access.service.ts @@ -1,4 +1,3 @@ -// [file name]: transactions/services/transaction-access.service.ts import { Injectable, Injector, inject } from '@angular/core'; import { Observable, of } from 'rxjs'; import { RoleManagementService, UserRole } from '@core/services/hub-users-roles-management.service'; @@ -10,12 +9,6 @@ export interface TransactionAccess { canViewAllTransactions: boolean; // Toutes vs seulement les siennes canViewDetails: boolean; - // Permissions d'actions - canRefund: boolean; - canRetry: boolean; - canCancel: boolean; - canExport: boolean; - // Permissions administratives canManageAll: boolean; // Toutes les transactions canFilterByMerchant: boolean; @@ -41,7 +34,6 @@ export class TransactionAccessService { private roleService: RoleManagementService ) {} - getTransactionAccess(): TransactionAccess { if (this.accessCache) { return this.accessCache; @@ -57,19 +49,13 @@ export class TransactionAccessService { canViewAllTransactions: this.canViewAllTransactions(userRole, isHubUser), canViewDetails: this.canViewDetails(userRole, isHubUser), - // Actions selon le rôle - canRefund: this.canPerformRefund(userRole, isHubUser), - canRetry: this.canPerformRetry(userRole, isHubUser), - canCancel: this.canPerformCancel(userRole, isHubUser), - canExport: this.canExport(userRole, isHubUser), - // Permissions administratives canManageAll: this.canManageAll(userRole, isHubUser), canFilterByMerchant: this.canFilterByMerchant(userRole, isHubUser), canViewSensitiveData: this.canViewSensitiveData(userRole, isHubUser), // Scope - allowedMerchantIds: this.getAllowedMerchantIds(isHubUser, merchantId), + allowedMerchantIds: this.getAllowedMerchantIds(isHubUser, merchantId, userRole), isHubUser, isMerchantUser: !isHubUser, @@ -86,13 +72,26 @@ export class TransactionAccessService { // === MÉTHODES DE DÉTERMINATION DES PERMISSIONS === private canViewTransactions(userRole: UserRole, isHubUser: boolean): boolean { - // Tous les rôles peuvent voir les transactions + // Tous les rôles peuvent voir les transactions (marchands et hub) return true; } private canViewAllTransactions(userRole: UserRole, isHubUser: boolean): boolean { // Hub users et DCB_PARTNER_ADMIN peuvent voir toutes les transactions - return isHubUser || userRole === UserRole.DCB_PARTNER_ADMIN; + if (isHubUser) { + return true; // Les utilisateurs hub voient toutes les transactions + } + + // Pour les utilisateurs marchands : + switch (userRole) { + case UserRole.DCB_PARTNER_ADMIN: + case UserRole.DCB_PARTNER_MANAGER: + case UserRole.DCB_PARTNER_SUPPORT: + // Les admins/support du marchand voient toutes les transactions de leur marchand + return true; + default: + return false; + } } private canViewDetails(userRole: UserRole, isHubUser: boolean): boolean { @@ -100,32 +99,6 @@ export class TransactionAccessService { return true; } - private canPerformRefund(userRole: UserRole, isHubUser: boolean): boolean { - // DCB_ADMIN: peut rembourser toutes les transactions - // DCB_SUPPORT: peut rembourser les transactions de son périmètre - // DCB_PARTNER_ADMIN: peut rembourser les transactions de son merchant - return isHubUser - ? userRole === UserRole.DCB_ADMIN || userRole === UserRole.DCB_SUPPORT - : userRole === UserRole.DCB_PARTNER_ADMIN; - } - - private canPerformRetry(userRole: UserRole, isHubUser: boolean): boolean { - // Mêmes permissions que le remboursement - return this.canPerformRefund(userRole, isHubUser); - } - - private canPerformCancel(userRole: UserRole, isHubUser: boolean): boolean { - // Plus restrictif - seulement les admins peuvent annuler - return isHubUser - ? userRole === UserRole.DCB_ADMIN - : userRole === UserRole.DCB_PARTNER_ADMIN; - } - - private canExport(userRole: UserRole, isHubUser: boolean): boolean { - // Tous peuvent exporter leurs propres données - return true; - } - private canManageAll(userRole: UserRole, isHubUser: boolean): boolean { // Seulement DCB_ADMIN hub peut tout gérer return isHubUser && userRole === UserRole.DCB_ADMIN; @@ -137,8 +110,13 @@ export class TransactionAccessService { } private canViewSensitiveData(userRole: UserRole, isHubUser: boolean): boolean { - // Hub users et DCB_PARTNER_ADMIN peuvent voir les données sensibles - return isHubUser || userRole === UserRole.DCB_PARTNER_ADMIN; + if (isHubUser) { + // Tous les utilisateurs hub peuvent voir les données sensibles + return true; + } else { + // Pour les marchands, seulement les rôles avec autorisation + return userRole === UserRole.DCB_PARTNER_ADMIN || userRole === UserRole.DCB_PARTNER_SUPPORT; + } } // === GESTION DU SCOPE === @@ -156,16 +134,33 @@ export class TransactionAccessService { return isNaN(merchantId) ? undefined : merchantId; } - private getAllowedMerchantIds(isHubUser: boolean, merchantId?: number): number[] { + private getAllowedMerchantIds(isHubUser: boolean, merchantId?: number, userRole?: UserRole): number[] { if (isHubUser) { // Hub users peuvent voir tous les merchants return []; // Tableau vide = tous les merchants } else { // Merchant users: seulement leur merchant - return merchantId ? [merchantId] : []; + // Mais seulement si l'utilisateur a accès aux transactions + if (merchantId && this.hasMerchantAccess(userRole)) { + return [merchantId]; + } + return []; } } + private hasMerchantAccess(userRole?: UserRole): boolean { + if (!userRole) return false; + + // Rôles marchands qui ont accès aux transactions + const merchantRolesWithAccess = [ + UserRole.DCB_PARTNER_ADMIN, + UserRole.DCB_PARTNER_MANAGER, + UserRole.DCB_PARTNER_SUPPORT + ]; + + return merchantRolesWithAccess.includes(userRole); + } + // === MÉTHODES PUBLIQUES === // Vérifie si l'utilisateur peut accéder à une transaction spécifique @@ -184,6 +179,7 @@ export class TransactionAccessService { // Merchant users: seulement leur merchant if (access.isMerchantUser) { + // Vérifier si l'utilisateur marchand a accès au merchant de la transaction return of(access.allowedMerchantIds.includes(transactionMerchantId)); } @@ -195,6 +191,22 @@ export class TransactionAccessService { return of(access.allowedMerchantIds.includes(transactionMerchantId)); } + // Obtenir le scope des transactions pour les requêtes API + getTransactionScope(): { merchantIds?: number[] } { + const access = this.getTransactionAccess(); + + if (access.isHubUser) { + // Hub users peuvent voir tous les merchants + return {}; + } else { + // Merchant users: seulement leur(s) merchant(s) + if (access.allowedMerchantIds.length > 0) { + return { merchantIds: access.allowedMerchantIds }; + } + return {}; + } + } + // Nettoyer le cache clearCache(): void { this.accessCache = null; @@ -204,4 +216,10 @@ export class TransactionAccessService { refreshAccess(): void { this.clearCache(); } + + // Méthode utilitaire pour afficher les permissions (debug) + logAccessInfo(): void { + const access = this.getTransactionAccess(); + console.log('Transaction Access Info:', access); + } } \ No newline at end of file