feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
658559deac
commit
6433da55a9
142
src/app/core/models/dcb-bo-hub-subscription.model.ts
Normal file
142
src/app/core/models/dcb-bo-hub-subscription.model.ts
Normal file
@ -0,0 +1,142 @@
|
||||
// === ENUMS COHÉRENTS ===
|
||||
export enum SubscriptionPeriodicity {
|
||||
DAILY = 'Daily',
|
||||
WEEKLY = 'Weekly',
|
||||
MONTHLY = 'Monthly',
|
||||
YEARLY = 'Yearly'
|
||||
}
|
||||
|
||||
export enum SubscriptionStatus {
|
||||
ACTIVE = 'ACTIVE',
|
||||
SUSPENDED = 'SUSPENDED',
|
||||
CANCELLED = 'CANCELLED',
|
||||
EXPIRED = 'EXPIRED',
|
||||
PENDING = 'PENDING'
|
||||
}
|
||||
|
||||
export enum Currency {
|
||||
XOF = 'XOF',
|
||||
EUR = 'EUR',
|
||||
USD = 'USD'
|
||||
}
|
||||
|
||||
// === MODÈLE SUBSCRIPTION PRINCIPAL ===
|
||||
export interface Subscription {
|
||||
id: number;
|
||||
externalReference?: string | null;
|
||||
periodicity: SubscriptionPeriodicity;
|
||||
startDate: string;
|
||||
endDate?: string | null;
|
||||
amount: number;
|
||||
currency: Currency;
|
||||
token: string;
|
||||
status: SubscriptionStatus;
|
||||
nextPaymentDate: string;
|
||||
suspendedAt?: string | null;
|
||||
merchantPartnerId: number;
|
||||
customerId: number;
|
||||
planId: number;
|
||||
serviceId: number;
|
||||
failureCount: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
metadata?: {
|
||||
note?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// === RÉPONSES API ===
|
||||
export interface SubscriptionsResponse {
|
||||
subscriptions: Subscription[];
|
||||
}
|
||||
|
||||
export interface MerchantSubscriptionsResponse {
|
||||
merchantId: number;
|
||||
subscriptions: Subscription[];
|
||||
statistics: {
|
||||
total: number;
|
||||
active: number;
|
||||
totalRevenue: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Interface pour les paramètres de recherche
|
||||
export interface SearchSubscriptionsParams {
|
||||
merchantId?: number;
|
||||
status?: SubscriptionStatus;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
periodicity?: SubscriptionPeriodicity;
|
||||
customerId?: number;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: 'startDate' | 'amount' | 'nextPaymentDate' | 'createdAt';
|
||||
sortOrder?: 'ASC' | 'DESC';
|
||||
}
|
||||
|
||||
// === UTILITAIRES ===
|
||||
export class SubscriptionUtils {
|
||||
static 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;
|
||||
}
|
||||
|
||||
static getPeriodicityDisplayName(periodicity: SubscriptionPeriodicity): string {
|
||||
const periodicityNames = {
|
||||
[SubscriptionPeriodicity.DAILY]: 'Quotidien',
|
||||
[SubscriptionPeriodicity.WEEKLY]: 'Hebdomadaire',
|
||||
[SubscriptionPeriodicity.MONTHLY]: 'Mensuel',
|
||||
[SubscriptionPeriodicity.YEARLY]: 'Annuel'
|
||||
};
|
||||
return periodicityNames[periodicity] || periodicity;
|
||||
}
|
||||
|
||||
static formatAmount(amount: number, currency: Currency): string {
|
||||
return new Intl.NumberFormat('fr-FR', {
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}).format(amount);
|
||||
}
|
||||
}
|
||||
|
||||
// === MODÈLE PAIEMENT ===
|
||||
export interface SubscriptionPayment {
|
||||
id: number;
|
||||
subscriptionId: string;
|
||||
amount: number;
|
||||
currency: Currency;
|
||||
status: 'PENDING' | 'SUCCESS' | 'FAILED';
|
||||
reference: string;
|
||||
description: string;
|
||||
metadata: {
|
||||
internatRef: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
partnerId: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// === RÉPONSE LISTE DES PAIEMENTS ===
|
||||
export interface SubscriptionPaymentsResponse {
|
||||
subscriptionId: string;
|
||||
payments: SubscriptionPayment[];
|
||||
}
|
||||
|
||||
// === UTILITAIRES ===
|
||||
export class PaymentUtils {
|
||||
static getStatusDisplayName(status: 'PENDING' | 'SUCCESS' | 'FAILED' | string): string {
|
||||
const statusNames: Record<string, string> = {
|
||||
'PENDING': 'En attente',
|
||||
'SUCCESS': 'Réussi',
|
||||
'FAILED': 'Échoué'
|
||||
};
|
||||
return statusNames[status] || status;
|
||||
}
|
||||
}
|
||||
@ -97,6 +97,13 @@ export class MenuService {
|
||||
],
|
||||
},
|
||||
|
||||
{ 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 },
|
||||
{
|
||||
label: 'Utilisateurs Hub',
|
||||
|
||||
@ -58,6 +58,22 @@ export class PermissionsService {
|
||||
roles: this.allRoles
|
||||
},
|
||||
|
||||
// Subscriptions
|
||||
{
|
||||
module: 'subscriptions',
|
||||
roles: this.allRoles
|
||||
},
|
||||
{
|
||||
module: 'subscriptions-merchant',
|
||||
roles: this.allRoles
|
||||
},
|
||||
|
||||
// Payments
|
||||
{
|
||||
module: 'subscriptions-payments',
|
||||
roles: this.allRoles
|
||||
},
|
||||
|
||||
// Settings - Tout le monde
|
||||
{
|
||||
module: 'merchant-configs',
|
||||
|
||||
@ -94,6 +94,14 @@ export const menuItems: MenuItemType[] = [
|
||||
],
|
||||
},
|
||||
|
||||
{ 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é
|
||||
// ---------------------------
|
||||
|
||||
@ -11,7 +11,6 @@ import { UserProfile } from '@layouts/components/topbar/components/user-profile/
|
||||
import { NotificationDropdown } from '@layouts/components/topbar/components/notification-dropdown/notification-dropdown'
|
||||
import { ThemeDropdown } from '@layouts/components/topbar/components/theme-dropdown/theme-dropdown'
|
||||
import {
|
||||
NgbActiveOffcanvas,
|
||||
NgbDropdownModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
@ -21,11 +20,9 @@ import {
|
||||
NgIcon,
|
||||
RouterLink,
|
||||
NgbDropdownModule,
|
||||
LanguageDropdown,
|
||||
CustomizerToggler,
|
||||
ThemeToggler,
|
||||
UserProfile,
|
||||
NotificationDropdown,
|
||||
ThemeDropdown,
|
||||
],
|
||||
templateUrl: './topbar.html',
|
||||
|
||||
@ -21,6 +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';
|
||||
|
||||
const routes: Routes = [
|
||||
// ---------------------------
|
||||
@ -86,7 +88,83 @@ const routes: Routes = [
|
||||
},
|
||||
|
||||
// ---------------------------
|
||||
// Partners
|
||||
// Subscriptions
|
||||
// ---------------------------
|
||||
{
|
||||
path: 'subscriptions',
|
||||
component: SubscriptionsManagement,
|
||||
canActivate: [authGuard, roleGuard],
|
||||
data: {
|
||||
title: 'Gestion des Abonnements',
|
||||
module: 'subscriptions',
|
||||
requiredRoles: [
|
||||
'dcb-admin',
|
||||
'dcb-support',
|
||||
'dcb-partner',
|
||||
'dcb-partner-admin',
|
||||
'dcb-partner-manager',
|
||||
'dcb-partner-support',
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
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 (existant - gardé pour référence)
|
||||
// ---------------------------
|
||||
{
|
||||
path: 'merchant-users-management',
|
||||
@ -102,11 +180,10 @@ const routes: Routes = [
|
||||
'dcb-partner',
|
||||
'dcb-partner-admin',
|
||||
'dcb-partner-manager',
|
||||
'dcb-partner-suport',
|
||||
'dcb-partner-support',
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// ---------------------------
|
||||
// Operators (Admin seulement)
|
||||
// ---------------------------
|
||||
|
||||
@ -0,0 +1,332 @@
|
||||
<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>
|
||||
@ -0,0 +1,491 @@
|
||||
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);
|
||||
this.currentMerchantPartnerId = this.extractMerchantPartnerId(user);
|
||||
|
||||
console.log('Payments Context Loaded:', {
|
||||
role: this.currentUserRole,
|
||||
merchantPartnerId: this.currentMerchantPartnerId
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading current user permissions:', error);
|
||||
this.fallbackPermissions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private extractUserRole(user: any): string | null {
|
||||
const userRoles = this.authService.getCurrentUserRoles();
|
||||
if (userRoles && userRoles.length > 0) {
|
||||
return userRoles[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private extractMerchantPartnerId(user: any): string {
|
||||
if (user?.merchantPartnerId) {
|
||||
return user.merchantPartnerId;
|
||||
}
|
||||
return this.authService.getCurrentMerchantPartnerId() || '';
|
||||
}
|
||||
|
||||
private fallbackPermissions(): void {
|
||||
this.currentUserRole = this.authService.getCurrentUserRole();
|
||||
this.currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId() || '';
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,408 @@
|
||||
<div class="container-fluid">
|
||||
<!-- En-tête avec navigation -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h4 class="mb-1">
|
||||
@if (subscription) {
|
||||
Paiements - Abonnement #{{ subscription.id }}
|
||||
} @else {
|
||||
Paiements de l'Abonnement
|
||||
}
|
||||
</h4>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="javascript:void(0)" (click)="goBack()" class="text-decoration-none cursor-pointer">
|
||||
Abonnements
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
@if (subscription) {
|
||||
Paiements #{{ subscription.id }}
|
||||
} @else {
|
||||
Paiements
|
||||
}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<!-- Bouton d'actualisation -->
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
(click)="refresh()"
|
||||
[disabled]="loading || loadingPayments"
|
||||
>
|
||||
<ng-icon name="lucideRefreshCw" class="me-1" [class.spin]="loading || loadingPayments"></ng-icon>
|
||||
Actualiser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages d'alerte -->
|
||||
@if (error) {
|
||||
<div class="alert alert-danger">
|
||||
<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)="clearMessages()"></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (success) {
|
||||
<div class="alert alert-success">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||
<div>{{ success }}</div>
|
||||
<button class="btn-close ms-auto" (click)="clearMessages()"></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<!-- Loading State -->
|
||||
@if (loading) {
|
||||
<div class="col-12 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>
|
||||
}
|
||||
|
||||
<!-- Subscription Details & Payments -->
|
||||
@if (subscription && !loading) {
|
||||
<!-- Colonne de gauche - Informations de l'abonnement -->
|
||||
<div class="col-xl-4 col-lg-5">
|
||||
<!-- Carte abonnement -->
|
||||
<div class="card">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="card-title mb-0">Détails de l'Abonnement</h5>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<!-- Icône abonnement -->
|
||||
<div class="avatar-lg mx-auto mb-3">
|
||||
<div class="avatar-title bg-primary bg-opacity-10 rounded-circle text-primary fs-24">
|
||||
<ng-icon name="lucideRepeat"></ng-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Abonnement #{{ subscription.id }}</h5>
|
||||
<p class="text-muted mb-2">{{ subscription.token.substring(0, 20) }}...</p>
|
||||
|
||||
<!-- Statut -->
|
||||
<span [class]="getStatusBadgeClass(subscription.status)" class="mb-3 d-block">
|
||||
<ng-icon
|
||||
[name]="subscription.status === SubscriptionStatus.ACTIVE ? 'lucideCheckCircle' :
|
||||
subscription.status === SubscriptionStatus.SUSPENDED ? 'lucidePauseCircle' :
|
||||
subscription.status === SubscriptionStatus.CANCELLED ? 'lucideXCircle' : 'lucideClock'"
|
||||
class="me-1"
|
||||
></ng-icon>
|
||||
{{ getStatusDisplayName(subscription.status) }}
|
||||
</span>
|
||||
|
||||
<!-- Indicateur de santé -->
|
||||
<div class="mb-3">
|
||||
<small class="d-flex align-items-center justify-content-center" [ngClass]="getSubscriptionHealthColor()">
|
||||
<ng-icon [name]="getSubscriptionHealthIcon()" class="me-1"></ng-icon>
|
||||
@switch (getSubscriptionHealth()) {
|
||||
@case ('good') { Bon état }
|
||||
@case ('warning') { Attention nécessaire }
|
||||
@case ('danger') { Problème détecté }
|
||||
}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Informations rapides -->
|
||||
<div class="mt-4 text-start">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<ng-icon name="lucideDollarSign" class="me-2 text-muted"></ng-icon>
|
||||
<small>{{ formatAmount(subscription.amount, subscription.currency) }}</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
||||
<small>{{ getPeriodicityDisplayName(subscription.periodicity) }}</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<ng-icon name="lucidePlay" class="me-2 text-muted"></ng-icon>
|
||||
<small>Début : {{ formatDate(subscription.startDate) }}</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
||||
<small>Prochain : {{ formatDate(subscription.nextPaymentDate) }}</small>
|
||||
</div>
|
||||
@if (subscription.endDate) {
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideFlag" class="me-2 text-muted"></ng-icon>
|
||||
<small>Fin : {{ formatDate(subscription.endDate) }}</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Carte statistiques -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="card-title mb-0">Statistiques des Paiements</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6 mb-3">
|
||||
<div class="text-primary fw-bold fs-4">{{ getTotalPayments() }}</div>
|
||||
<small class="text-muted">Total</small>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<div class="text-success fw-bold fs-4">{{ getSuccessfulPayments() }}</div>
|
||||
<small class="text-muted">Réussis</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-danger fw-bold fs-4">{{ getFailedPayments() }}</div>
|
||||
<small class="text-muted">Échoués</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-warning fw-bold fs-4">{{ getPendingPayments() }}</div>
|
||||
<small class="text-muted">En attente</small>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="text-center">
|
||||
<div class="fw-bold fs-5">{{ formatAmount(getTotalAmount(), subscription.currency) }}</div>
|
||||
<small class="text-muted">Montant total collecté</small>
|
||||
</div>
|
||||
<div class="text-center mt-2">
|
||||
<div class="fw-bold fs-6">{{ getSuccessRate().toFixed(1) }}%</div>
|
||||
<small class="text-muted">Taux de réussite</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informations techniques -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="card-title mb-0">Informations Techniques</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-2 small">
|
||||
<div class="col-12">
|
||||
<strong>ID Abonnement :</strong>
|
||||
<div class="text-muted font-monospace">{{ subscription.id }}</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<strong>Token :</strong>
|
||||
<div class="text-muted font-monospace text-truncate">{{ subscription.token }}</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<strong>Merchant ID :</strong>
|
||||
<div class="text-muted">{{ subscription.merchantPartnerId }}</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<strong>Client ID :</strong>
|
||||
<div class="text-muted">{{ subscription.customerId }}</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<strong>Échecs :</strong>
|
||||
<div class="text-muted">{{ subscription.failureCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colonne de droite - Historique des paiements -->
|
||||
<div class="col-xl-8 col-lg-7">
|
||||
<!-- Filtres -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="card-title mb-0">Filtres des Paiements</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Statut</label>
|
||||
<select class="form-select" [(ngModel)]="statusFilter" (change)="applyFilters()">
|
||||
<option value="all">Tous les statuts</option>
|
||||
<option value="SUCCESS">Réussis</option>
|
||||
<option value="FAILED">Échoués</option>
|
||||
<option value="PENDING">En attente</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Période</label>
|
||||
<select class="form-select" [(ngModel)]="dateFilter" (change)="applyFilters()">
|
||||
<option value="all">Toute période</option>
|
||||
<option value="week">7 derniers jours</option>
|
||||
<option value="month">30 derniers jours</option>
|
||||
<option value="quarter">3 derniers mois</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="btn btn-outline-secondary btn-sm" (click)="clearFilters()">
|
||||
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||
Réinitialiser les filtres
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liste des paiements -->
|
||||
<div class="card">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<ng-icon name="lucideCreditCard" class="me-2"></ng-icon>
|
||||
Historique des Paiements
|
||||
<span class="badge bg-primary ms-2">{{ getSortedPayments().length }}</span>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Loading State Payments -->
|
||||
@if (loadingPayments) {
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
<p class="mt-2 text-muted">Chargement des paiements...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Payments List -->
|
||||
@if (!loadingPayments) {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Référence</th>
|
||||
<th>Date</th>
|
||||
<th>Montant</th>
|
||||
<th>Statut</th>
|
||||
<th>Description</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (payment of getSortedPayments(); track payment.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<code class="small" [title]="payment.reference">
|
||||
{{ payment.reference.substring(0, 12) }}...
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
<small>{{ formatDateTime(payment.createdAt) }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ formatAmount(payment.amount, payment.currency) }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge d-flex align-items-center" [ngClass]="getPaymentStatusBadgeClass(payment.status)">
|
||||
<ng-icon
|
||||
[name]="payment.status === 'SUCCESS' ? 'lucideCheckCircle' :
|
||||
payment.status === 'FAILED' ? 'lucideXCircle' : 'lucideClock'"
|
||||
class="me-1"
|
||||
size="12"
|
||||
></ng-icon>
|
||||
{{ getPaymentStatusDisplayName(payment.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">{{ payment.description }}</small>
|
||||
@if (payment.metadata.internatRef) {
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
Ref: {{ payment.metadata.internatRef }}
|
||||
</small>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-primary btn-sm" title="Voir détails">
|
||||
<ng-icon name="lucideEye"></ng-icon>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@empty {
|
||||
<tr>
|
||||
<td colspan="6" 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">Aucun paiement trouvé</h5>
|
||||
<p class="mb-3">Aucun paiement ne correspond à vos critères de filtrage.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Résumé des résultats filtrés -->
|
||||
@if (getSortedPayments().length > 0) {
|
||||
<div class="mt-3 pt-3 border-top">
|
||||
<div class="row text-center small">
|
||||
<div class="col">
|
||||
<span class="text-muted">
|
||||
Affichage de {{ getSortedPayments().length }} paiement(s)
|
||||
</span>
|
||||
</div>
|
||||
@if (statusFilter !== 'all') {
|
||||
<div class="col">
|
||||
<span class="text-muted">
|
||||
Filtre : {{
|
||||
statusFilter === 'SUCCESS' ? 'Réussis' :
|
||||
statusFilter === 'FAILED' ? 'Échoués' : 'En attente'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alertes et indicateurs -->
|
||||
@if (isExpiringSoon()) {
|
||||
<div class="alert alert-warning mt-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideAlertTriangle" class="me-2"></ng-icon>
|
||||
<div>
|
||||
<strong>Attention :</strong> Cet abonnement expire bientôt
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isExpired()) {
|
||||
<div class="alert alert-danger mt-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideXCircle" class="me-2"></ng-icon>
|
||||
<div>
|
||||
<strong>Expiré :</strong> Cet abonnement a expiré
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (getDaysUntilNextPayment() <= 3 && subscription.status === SubscriptionStatus.ACTIVE) {
|
||||
<div class="alert alert-info mt-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideCalendar" class="me-2"></ng-icon>
|
||||
<div>
|
||||
<strong>Prochain paiement :</strong> Dans {{ getDaysUntilNextPayment() }} jour(s)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,439 @@
|
||||
import { Component, inject, OnInit, Input, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
import {
|
||||
Subscription,
|
||||
SubscriptionPayment,
|
||||
SubscriptionStatus,
|
||||
SubscriptionPeriodicity,
|
||||
Currency
|
||||
} from '@core/models/dcb-bo-hub-subscription.model';
|
||||
|
||||
import { SubscriptionsService } from '../subscriptions.service';
|
||||
import { AuthService } from '@core/services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subscription-payments',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule],
|
||||
templateUrl: './subscription-payments.html',
|
||||
styles: [`
|
||||
.avatar-lg {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.fs-24 {
|
||||
font-size: 24px;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class SubscriptionPayments implements OnInit, OnDestroy {
|
||||
private subscriptionsService = inject(SubscriptionsService);
|
||||
private authService = inject(AuthService);
|
||||
private cdRef = inject(ChangeDetectorRef);
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
readonly SubscriptionStatus = SubscriptionStatus;
|
||||
readonly SubscriptionPeriodicity = SubscriptionPeriodicity;
|
||||
readonly Currency = Currency;
|
||||
|
||||
@Input() subscriptionId!: string;
|
||||
@Output() back = new EventEmitter<void>();
|
||||
|
||||
subscription: Subscription | null = null;
|
||||
payments: SubscriptionPayment[] = [];
|
||||
loading = false;
|
||||
loadingPayments = false;
|
||||
error = '';
|
||||
success = '';
|
||||
|
||||
// Gestion des permissions
|
||||
currentUserRole: string | null = null;
|
||||
|
||||
merchantPartnerId: number | undefined;
|
||||
|
||||
// Filtres
|
||||
statusFilter: 'all' | 'SUCCESS' | 'FAILED' | 'PENDING' = 'all';
|
||||
dateFilter: 'all' | 'week' | 'month' | 'quarter' = 'all';
|
||||
|
||||
ngOnInit() {
|
||||
if (this.subscriptionId) {
|
||||
this.loadCurrentUserPermissions();
|
||||
this.loadSubscriptionDetails();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les permissions de l'utilisateur courant
|
||||
*/
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les détails de l'abonnement puis les paiements
|
||||
*/
|
||||
loadSubscriptionDetails() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
this.merchantPartnerId = subscription.merchantPartnerId,
|
||||
|
||||
console.log("loadSubscriptionDetails " + this.merchantPartnerId);
|
||||
|
||||
this.subscription = subscription;
|
||||
this.loading = false;
|
||||
this.cdRef.detectChanges();
|
||||
|
||||
// Passer explicitement le merchantPartnerId à loadPayments
|
||||
this.loadPayments(subscription.merchantPartnerId);
|
||||
},
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les paiements de l'abonnement
|
||||
*/
|
||||
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';
|
||||
this.loadingPayments = false;
|
||||
this.cdRef.detectChanges();
|
||||
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({
|
||||
next: (response) => {
|
||||
this.payments = response.payments || [];
|
||||
this.loadingPayments = false;
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
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) {
|
||||
this.error = 'Aucun paiement trouvé pour cet abonnement';
|
||||
} else {
|
||||
this.error = 'Erreur lors du chargement des paiements';
|
||||
}
|
||||
|
||||
this.loadingPayments = false;
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Applique les filtres aux paiements
|
||||
*/
|
||||
applyFilters() {
|
||||
this.loadPayments(this.merchantPartnerId); // Recharger avec les filtres actuels
|
||||
}
|
||||
|
||||
/**
|
||||
* Réinitialise les filtres
|
||||
*/
|
||||
clearFilters() {
|
||||
this.statusFilter = 'all';
|
||||
this.dateFilter = 'all';
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
// ==================== 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';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
getPeriodicityDisplayName(periodicity: SubscriptionPeriodicity): string {
|
||||
const periodicityNames = {
|
||||
[SubscriptionPeriodicity.DAILY]: 'Quotidien',
|
||||
[SubscriptionPeriodicity.WEEKLY]: 'Hebdomadaire',
|
||||
[SubscriptionPeriodicity.MONTHLY]: 'Mensuel',
|
||||
[SubscriptionPeriodicity.YEARLY]: 'Annuel'
|
||||
};
|
||||
return periodicityNames[periodicity] || periodicity;
|
||||
}
|
||||
|
||||
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 ====================
|
||||
|
||||
getTotalPayments(): number {
|
||||
return this.payments.length;
|
||||
}
|
||||
|
||||
getSuccessfulPayments(): number {
|
||||
return this.payments.filter(p => p.status === 'SUCCESS').length;
|
||||
}
|
||||
|
||||
getFailedPayments(): number {
|
||||
return this.payments.filter(p => p.status === 'FAILED').length;
|
||||
}
|
||||
|
||||
getPendingPayments(): number {
|
||||
return this.payments.filter(p => p.status === 'PENDING').length;
|
||||
}
|
||||
|
||||
getTotalAmount(): number {
|
||||
return this.payments
|
||||
.filter(p => p.status === 'SUCCESS')
|
||||
.reduce((sum, p) => sum + p.amount, 0);
|
||||
}
|
||||
|
||||
getSuccessRate(): number {
|
||||
if (this.payments.length === 0) return 0;
|
||||
return (this.getSuccessfulPayments() / this.payments.length) * 100;
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES DE NAVIGATION ====================
|
||||
|
||||
goBack() {
|
||||
this.back.emit();
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES UTILITAIRES ====================
|
||||
|
||||
refresh() {
|
||||
this.loadSubscriptionDetails();
|
||||
this.loadPayments(this.merchantPartnerId);
|
||||
}
|
||||
|
||||
clearMessages() {
|
||||
this.error = '';
|
||||
this.success = '';
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
// Méthodes pour le template
|
||||
getPageTitle(): string {
|
||||
return `Paiements - Abonnement #${this.subscription?.id || '...'}`;
|
||||
}
|
||||
|
||||
getContextDescription(): string {
|
||||
return 'Historique des paiements pour cet abonnement';
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Obtenir les paiements filtrés
|
||||
getFilteredPayments(): SubscriptionPayment[] {
|
||||
let filtered = this.payments;
|
||||
|
||||
// Filtrer par statut
|
||||
if (this.statusFilter !== 'all') {
|
||||
filtered = filtered.filter(p => p.status === this.statusFilter);
|
||||
}
|
||||
|
||||
// Filtrer par date
|
||||
if (this.dateFilter !== 'all') {
|
||||
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;
|
||||
}
|
||||
|
||||
filtered = filtered.filter(p => new Date(p.createdAt) >= startDate);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// Trier les paiements par date (plus récents en premier)
|
||||
getSortedPayments(): SubscriptionPayment[] {
|
||||
return this.getFilteredPayments().sort((a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
// Méthodes pour les indicateurs visuels
|
||||
getSubscriptionHealth(): 'good' | 'warning' | 'danger' {
|
||||
if (!this.subscription) return 'good';
|
||||
|
||||
if (this.subscription.failureCount > 3) return 'danger';
|
||||
if (this.subscription.failureCount > 0 || this.isExpiringSoon()) return 'warning';
|
||||
return 'good';
|
||||
}
|
||||
|
||||
getSubscriptionHealthIcon(): string {
|
||||
const health = this.getSubscriptionHealth();
|
||||
const icons = {
|
||||
'good': 'lucideCheckCircle',
|
||||
'warning': 'lucideAlertTriangle',
|
||||
'danger': 'lucideXCircle'
|
||||
};
|
||||
return icons[health];
|
||||
}
|
||||
|
||||
getSubscriptionHealthColor(): string {
|
||||
const health = this.getSubscriptionHealth();
|
||||
const colors = {
|
||||
'good': 'text-success',
|
||||
'warning': 'text-warning',
|
||||
'danger': 'text-danger'
|
||||
};
|
||||
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.';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,361 @@
|
||||
<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>
|
||||
</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 ID, token..."
|
||||
[(ngModel)]="searchTerm"
|
||||
(input)="onSearch()"
|
||||
[disabled]="loading"
|
||||
>
|
||||
</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-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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
}
|
||||
|
||||
<!-- 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
|
||||
</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 (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>
|
||||
@ -0,0 +1,548 @@
|
||||
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 {
|
||||
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,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
NgIcon,
|
||||
UiCard,
|
||||
NgbPaginationModule
|
||||
],
|
||||
templateUrl: './subscriptions-list.html',
|
||||
})
|
||||
export class SubscriptionsList implements OnInit, OnDestroy {
|
||||
private authService = inject(AuthService);
|
||||
private subscriptionsService = inject(SubscriptionsService);
|
||||
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>();
|
||||
|
||||
// Données
|
||||
allSubscriptions: Subscription[] = [];
|
||||
filteredSubscriptions: Subscription[] = [];
|
||||
displayedSubscriptions: Subscription[] = [];
|
||||
|
||||
// États
|
||||
loading = false;
|
||||
error = '';
|
||||
|
||||
// Recherche et filtres
|
||||
searchTerm = '';
|
||||
statusFilter: SubscriptionStatus | 'all' = 'all';
|
||||
periodicityFilter: SubscriptionPeriodicity | 'all' = 'all';
|
||||
merchantFilter: number | 'all' = 'all';
|
||||
|
||||
// Pagination
|
||||
currentPage = 1;
|
||||
itemsPerPage = 10;
|
||||
totalItems = 0;
|
||||
totalPages = 0;
|
||||
|
||||
// Tri
|
||||
sortField: keyof Subscription = 'startDate';
|
||||
sortDirection: 'asc' | 'desc' = 'desc';
|
||||
|
||||
// Filtres disponibles
|
||||
availableStatuses: { value: SubscriptionStatus | 'all'; label: string }[] = [
|
||||
{ value: 'all', label: 'Tous les statuts' },
|
||||
{ value: SubscriptionStatus.ACTIVE, label: 'Actif' },
|
||||
{ value: SubscriptionStatus.SUSPENDED, label: 'Suspendu' },
|
||||
{ value: SubscriptionStatus.CANCELLED, label: 'Annulé' },
|
||||
{ value: SubscriptionStatus.EXPIRED, label: 'Expiré' },
|
||||
{ value: SubscriptionStatus.PENDING, label: 'En attente' }
|
||||
];
|
||||
|
||||
availablePeriodicities: { value: SubscriptionPeriodicity | 'all'; label: string }[] = [
|
||||
{ value: 'all', label: 'Toutes les périodicités' },
|
||||
{ value: SubscriptionPeriodicity.DAILY, label: 'Quotidien' },
|
||||
{ value: SubscriptionPeriodicity.WEEKLY, label: 'Hebdomadaire' },
|
||||
{ value: SubscriptionPeriodicity.MONTHLY, label: 'Mensuel' },
|
||||
{ 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();
|
||||
}
|
||||
|
||||
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);
|
||||
this.currentMerchantPartnerId = this.extractMerchantPartnerId(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];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private extractMerchantPartnerId(user: any): string {
|
||||
if (user?.merchantPartnerId) {
|
||||
return user.merchantPartnerId;
|
||||
}
|
||||
return this.authService.getCurrentMerchantPartnerId() || '';
|
||||
}
|
||||
|
||||
private canViewAllMerchantsCheck(role: string | null): boolean {
|
||||
if (!role) return false;
|
||||
|
||||
const canViewAllRoles = [
|
||||
'DCB_ADMIN',
|
||||
'DCB_SUPPORT',
|
||||
'DCB_PARTNER_ADMIN'
|
||||
];
|
||||
|
||||
return canViewAllRoles.includes(role);
|
||||
}
|
||||
|
||||
private fallbackPermissions(): void {
|
||||
this.currentUserRole = this.authService.getCurrentUserRole();
|
||||
this.currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId() || '';
|
||||
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
|
||||
}
|
||||
|
||||
loadSubscriptions() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
let subscriptionsObservable: Observable<SubscriptionsResponse>;
|
||||
|
||||
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 }
|
||||
);
|
||||
} else {
|
||||
// Fallback - abonnements généraux
|
||||
subscriptionsObservable = this.subscriptionsService.getSubscriptions({
|
||||
page: this.currentPage,
|
||||
limit: this.itemsPerPage
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Recherche et filtres
|
||||
onSearch() {
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
}
|
||||
|
||||
onClearFilters() {
|
||||
this.searchTerm = '';
|
||||
this.statusFilter = 'all';
|
||||
this.periodicityFilter = 'all';
|
||||
this.merchantFilter = 'all';
|
||||
this.currentPage = 1;
|
||||
this.applyFiltersAndPagination();
|
||||
}
|
||||
|
||||
applyFiltersAndPagination() {
|
||||
if (!this.allSubscriptions) {
|
||||
this.allSubscriptions = [];
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Tri
|
||||
sort(field: keyof Subscription) {
|
||||
if (this.sortField === field) {
|
||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.sortField = field;
|
||||
this.sortDirection = 'asc';
|
||||
}
|
||||
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
|
||||
viewSubscriptionDetails(subscriptionId: string | number) {
|
||||
this.subscriptionSelected.emit(subscriptionId.toString());
|
||||
}
|
||||
|
||||
viewSubscriptionPayments(subscriptionId: string | number) {
|
||||
this.viewPaymentsRequested.emit(subscriptionId.toString());
|
||||
}
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
// Recharger les données
|
||||
refreshData() {
|
||||
this.loadSubscriptions();
|
||||
}
|
||||
|
||||
// Méthodes pour le template
|
||||
getCardTitle(): string {
|
||||
return 'Abonnements';
|
||||
}
|
||||
|
||||
getHelperText(): string {
|
||||
return this.canViewAllMerchants
|
||||
? 'Vue administrative - Tous les abonnements'
|
||||
: 'Vos abonnements';
|
||||
}
|
||||
|
||||
getHelperIcon(): string {
|
||||
return this.canViewAllMerchants ? 'lucideShield' : 'lucideRepeat';
|
||||
}
|
||||
|
||||
getLoadingText(): string {
|
||||
return 'Chargement des abonnements...';
|
||||
}
|
||||
|
||||
getEmptyStateTitle(): string {
|
||||
return 'Aucun abonnement trouvé';
|
||||
}
|
||||
|
||||
getEmptyStateDescription(): string {
|
||||
return 'Aucun abonnement ne correspond à vos critères de recherche.';
|
||||
}
|
||||
|
||||
// Statistiques
|
||||
getTotalSubscriptionsCount(): 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();
|
||||
}
|
||||
}
|
||||
82
src/app/modules/subscriptions/subscriptions.html
Normal file
82
src/app/modules/subscriptions/subscriptions.html
Normal file
@ -0,0 +1,82 @@
|
||||
<div class="container-fluid">
|
||||
<app-page-title
|
||||
title="pageTitle"
|
||||
subtitle="Consultez et gérez les abonnements et leurs paiements"
|
||||
[badge]="badge"
|
||||
/>
|
||||
|
||||
<!-- Indicateur de permissions -->
|
||||
@if (currentUserRole) {
|
||||
<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="lucideInfo" class="me-2"></ng-icon>
|
||||
<div class="flex-grow-1">
|
||||
<small>
|
||||
<strong>Rôle actuel :</strong>
|
||||
<span class="badge" [ngClass]="getRoleBadgeClass()">
|
||||
{{ getRoleLabel() }}
|
||||
</span>
|
||||
@if (currentMerchantPartnerId) {
|
||||
<span class="ms-2">
|
||||
<strong>Merchant ID :</strong> {{ currentMerchantPartnerId }}
|
||||
</span>
|
||||
}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Navigation par onglets -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
259
src/app/modules/subscriptions/subscriptions.service.ts
Normal file
259
src/app/modules/subscriptions/subscriptions.service.ts
Normal file
@ -0,0 +1,259 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { environment } from '@environments/environment';
|
||||
import { Observable, map, catchError, throwError } from 'rxjs';
|
||||
|
||||
import {
|
||||
Subscription,
|
||||
SubscriptionPayment,
|
||||
SubscriptionsResponse,
|
||||
SubscriptionPaymentsResponse,
|
||||
SearchSubscriptionsParams,
|
||||
SubscriptionStatus,
|
||||
SubscriptionPeriodicity,
|
||||
Currency
|
||||
} from '@core/models/dcb-bo-hub-subscription.model';
|
||||
|
||||
// Interfaces pour les réponses API
|
||||
export interface MessageResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ===== SERVICE SUBSCRIPTIONS =====
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SubscriptionsService {
|
||||
private http = inject(HttpClient);
|
||||
private subcriptionBaseApiUrl = `${environment.apiCoreUrl}/subscriptions`;
|
||||
private paymentBaseApiUrl = `${environment.apiCoreUrl}/payments`;
|
||||
|
||||
// === MÉTHODES SUBSCRIPTIONS ===
|
||||
|
||||
/**
|
||||
* Récupère la liste des abonnements
|
||||
*/
|
||||
getSubscriptions(params?: SearchSubscriptionsParams): Observable<SubscriptionsResponse> {
|
||||
let httpParams = new HttpParams();
|
||||
|
||||
if (params) {
|
||||
Object.keys(params).forEach(key => {
|
||||
const value = params[key as keyof SearchSubscriptionsParams];
|
||||
if (value !== undefined && value !== null) {
|
||||
httpParams = httpParams.set(key, value.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.http.get<Subscription[]>(this.subcriptionBaseApiUrl, { params: httpParams }).pipe(
|
||||
map(subscriptions => ({
|
||||
subscriptions: subscriptions.map(sub => this.mapToSubscriptionModel(sub)),
|
||||
statistics: this.calculateSubscriptionStats(subscriptions)
|
||||
})),
|
||||
catchError(error => {
|
||||
console.error('Error loading subscriptions:', error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les abonnements par merchant
|
||||
*/
|
||||
getSubscriptionsByMerchant(merchantId: number, params?: Omit<SearchSubscriptionsParams, 'merchantId'>): Observable<SubscriptionsResponse> {
|
||||
let httpParams = new HttpParams();
|
||||
|
||||
if (params) {
|
||||
for (const key in params) {
|
||||
if (params.hasOwnProperty(key)) {
|
||||
const value = params[key as keyof typeof params];
|
||||
if (value !== undefined && value !== null) {
|
||||
httpParams = httpParams.set(key, value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.http.get<Subscription[]>(`${this.subcriptionBaseApiUrl}/merchant/${merchantId}`, {
|
||||
params: httpParams
|
||||
}).pipe(
|
||||
map(subscriptions => ({
|
||||
subscriptions: subscriptions.map(sub => this.mapToSubscriptionModel(sub)),
|
||||
statistics: this.calculateSubscriptionStats(subscriptions)
|
||||
})),
|
||||
catchError(error => {
|
||||
console.error(`Error loading subscriptions for merchant ${merchantId}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un abonnement par son ID
|
||||
*/
|
||||
getSubscriptionById(subscriptionId: string): Observable<Subscription> {
|
||||
return this.http.get<Subscription>(`${this.subcriptionBaseApiUrl}/${subscriptionId}`).pipe(
|
||||
map(subscription => this.mapToSubscriptionModel(subscription)),
|
||||
catchError(error => {
|
||||
console.error(`Error loading subscription ${subscriptionId}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les paiements d'un abonnement
|
||||
*/
|
||||
getSubscriptionPayments(merchantId: string | number, subscriptionId: string | number): Observable<SubscriptionPaymentsResponse> {
|
||||
// Convertir en number si c'est une string numérique, sinon utiliser tel quel
|
||||
const merchantIdNum = typeof merchantId === 'string' ? parseInt(merchantId, 10) : merchantId;
|
||||
const subscriptionIdNum = typeof subscriptionId === 'string' ? parseInt(subscriptionId, 10) : subscriptionId;
|
||||
|
||||
return this.http.get<SubscriptionPayment[]>(
|
||||
`${this.paymentBaseApiUrl}/merchant/${merchantIdNum}/subscription/${subscriptionIdNum}`
|
||||
).pipe(
|
||||
map(payments => ({
|
||||
merchantId: merchantIdNum.toString(),
|
||||
subscriptionId: subscriptionIdNum.toString(),
|
||||
payments: payments.map(payment => this.mapToPaymentModel(payment))
|
||||
})),
|
||||
catchError(error => {
|
||||
console.error(`Error loading payments for subscription ${subscriptionId} for merchant ${merchantId}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche des abonnements avec filtres
|
||||
*/
|
||||
searchSubscriptions(params: SearchSubscriptionsParams): Observable<Subscription[]> {
|
||||
return this.getSubscriptions(params).pipe(
|
||||
map(response => response.subscriptions)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les abonnements par statut
|
||||
*/
|
||||
getSubscriptionsByStatus(status: SubscriptionStatus): Observable<Subscription[]> {
|
||||
return this.searchSubscriptions({ status });
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les abonnements par périodicité
|
||||
*/
|
||||
getSubscriptionsByPeriodicity(periodicity: SubscriptionPeriodicity): Observable<Subscription[]> {
|
||||
return this.searchSubscriptions({ periodicity });
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les abonnements actifs
|
||||
*/
|
||||
getActiveSubscriptions(): Observable<Subscription[]> {
|
||||
return this.getSubscriptionsByStatus(SubscriptionStatus.ACTIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les abonnements suspendus
|
||||
*/
|
||||
getSuspendedSubscriptions(): Observable<Subscription[]> {
|
||||
return this.getSubscriptionsByStatus(SubscriptionStatus.SUSPENDED);
|
||||
}
|
||||
|
||||
// === MÉTHODES UTILITAIRES ===
|
||||
|
||||
/**
|
||||
* Calcule les statistiques des abonnements
|
||||
*/
|
||||
private calculateSubscriptionStats(subscriptions: any[]): {
|
||||
total: number;
|
||||
active: number;
|
||||
suspended: number;
|
||||
cancelled: number;
|
||||
totalRevenue: number;
|
||||
averageAmount: number;
|
||||
} {
|
||||
const total = subscriptions.length;
|
||||
const active = subscriptions.filter(sub => sub.status === SubscriptionStatus.ACTIVE).length;
|
||||
const suspended = subscriptions.filter(sub => sub.status === SubscriptionStatus.SUSPENDED).length;
|
||||
const cancelled = subscriptions.filter(sub => sub.status === SubscriptionStatus.CANCELLED).length;
|
||||
const totalRevenue = subscriptions.reduce((sum, sub) => sum + sub.amount, 0);
|
||||
const averageAmount = total > 0 ? totalRevenue / total : 0;
|
||||
|
||||
return {
|
||||
total,
|
||||
active,
|
||||
suspended,
|
||||
cancelled,
|
||||
totalRevenue,
|
||||
averageAmount
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappe la réponse API vers le modèle Subscription
|
||||
*/
|
||||
private mapToSubscriptionModel(apiSubscription: any): Subscription {
|
||||
return {
|
||||
id: apiSubscription.id,
|
||||
externalReference: apiSubscription.externalReference,
|
||||
periodicity: apiSubscription.periodicity as SubscriptionPeriodicity,
|
||||
startDate: apiSubscription.startDate,
|
||||
endDate: apiSubscription.endDate,
|
||||
amount: apiSubscription.amount,
|
||||
currency: apiSubscription.currency as Currency,
|
||||
token: apiSubscription.token,
|
||||
status: apiSubscription.status as SubscriptionStatus,
|
||||
nextPaymentDate: apiSubscription.nextPaymentDate,
|
||||
suspendedAt: apiSubscription.suspendedAt,
|
||||
merchantPartnerId: apiSubscription.merchantPartnerId,
|
||||
customerId: apiSubscription.customerId,
|
||||
planId: apiSubscription.planId,
|
||||
serviceId: apiSubscription.serviceId,
|
||||
failureCount: apiSubscription.failureCount,
|
||||
createdAt: apiSubscription.createdAt,
|
||||
updatedAt: apiSubscription.updatedAt,
|
||||
metadata: apiSubscription.metadata || {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappe la réponse API vers le modèle Payment
|
||||
*/
|
||||
private mapToPaymentModel(apiPayment: any): SubscriptionPayment {
|
||||
return {
|
||||
id: apiPayment.id,
|
||||
subscriptionId: apiPayment.subscriptionId,
|
||||
amount: apiPayment.amount,
|
||||
currency: apiPayment.currency as Currency,
|
||||
status: apiPayment.status as 'PENDING' | 'SUCCESS' | 'FAILED',
|
||||
reference: apiPayment.reference,
|
||||
description: apiPayment.description,
|
||||
metadata: apiPayment.metadata || { internatRef: '' },
|
||||
partnerId: apiPayment.partnerId,
|
||||
createdAt: apiPayment.createdAt,
|
||||
updatedAt: apiPayment.updatedAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide si un statut est valide
|
||||
*/
|
||||
isValidSubscriptionStatus(status: string): boolean {
|
||||
return Object.values(SubscriptionStatus).includes(status as SubscriptionStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide si une périodicité est valide
|
||||
*/
|
||||
isValidPeriodicity(periodicity: string): boolean {
|
||||
return Object.values(SubscriptionPeriodicity).includes(periodicity as SubscriptionPeriodicity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide si une devise est valide
|
||||
*/
|
||||
isValidCurrency(currency: string): boolean {
|
||||
return Object.values(Currency).includes(currency as Currency);
|
||||
}
|
||||
}
|
||||
267
src/app/modules/subscriptions/subscriptions.ts
Normal file
267
src/app/modules/subscriptions/subscriptions.ts
Normal file
@ -0,0 +1,267 @@
|
||||
import { Component, inject, OnInit, TemplateRef, ViewChild, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||
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 { 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.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subscriptions',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
NgIcon,
|
||||
NgbNavModule,
|
||||
NgbModalModule,
|
||||
PageTitle,
|
||||
SubscriptionsList,
|
||||
SubscriptionPayments
|
||||
],
|
||||
templateUrl: './subscriptions.html',
|
||||
})
|
||||
export class SubscriptionsManagement implements OnInit, OnDestroy {
|
||||
private authService = inject(AuthService);
|
||||
private subscriptionsService = inject(SubscriptionsService);
|
||||
private roleService = inject(RoleManagementService);
|
||||
|
||||
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';
|
||||
selectedSubscriptionId: string | null = null;
|
||||
|
||||
user: User | undefined;
|
||||
|
||||
// Gestion des rôles (lecture seule)
|
||||
availableRoles: { value: UserRole; label: string; description: string }[] = [];
|
||||
|
||||
// Gestion des permissions
|
||||
currentUserRole: string | null = null;
|
||||
currentMerchantPartnerId: string = '';
|
||||
|
||||
// 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);
|
||||
this.currentMerchantPartnerId = this.extractMerchantPartnerId(user);
|
||||
|
||||
console.log(`User ROLE: ${this.currentUserRole}`);
|
||||
console.log(`Merchant Partner ID: ${this.currentMerchantPartnerId}`);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading user profile:', error);
|
||||
this.fallbackPermissions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extraire le merchantPartnerId
|
||||
*/
|
||||
private extractMerchantPartnerId(user: any): string {
|
||||
if (user?.merchantPartnerId) {
|
||||
return user.merchantPartnerId;
|
||||
}
|
||||
return this.authService.getCurrentMerchantPartnerId() || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback en cas d'erreur de chargement du profil
|
||||
*/
|
||||
private fallbackPermissions(): void {
|
||||
this.currentUserRole = this.authService.getCurrentUserRole();
|
||||
this.currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId() || '';
|
||||
}
|
||||
|
||||
// ==================== 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';
|
||||
this.selectedSubscriptionId = null;
|
||||
}
|
||||
|
||||
// Méthodes de gestion des événements du composant enfant
|
||||
onSubscriptionSelected(subscriptionId: string) {
|
||||
this.showTab('payments', subscriptionId);
|
||||
}
|
||||
|
||||
onViewPaymentsRequested(subscriptionId: string) {
|
||||
this.showTab('payments', 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');
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -2,90 +2,5 @@ export const environment = {
|
||||
production: false,
|
||||
localServiceTestApiUrl: "http://localhost:4200/api/v1",
|
||||
iamApiUrl: "http://localhost:3000/api/v1",
|
||||
dcbApiUrl: 'https://api.paymenthub.com/v2',
|
||||
|
||||
// Configuration DCB
|
||||
dcb: {
|
||||
// Opérateurs supportés
|
||||
operators: {
|
||||
orange: {
|
||||
endpoint: 'https://api.orange.com/dcb/v2',
|
||||
timeout: 30000,
|
||||
retryAttempts: 3,
|
||||
countries: ['CIV', 'SEN', 'CMR', 'MLI', 'BFA', 'GIN']
|
||||
},
|
||||
mtn: {
|
||||
endpoint: 'https://api.mtn.com/dcb/v2',
|
||||
timeout: 25000,
|
||||
retryAttempts: 3,
|
||||
countries: ['CIV', 'GHA', 'NGA', 'CMR', 'RWA']
|
||||
},
|
||||
airtel: {
|
||||
endpoint: 'https://api.airtel.com/dcb/v2',
|
||||
timeout: 30000,
|
||||
retryAttempts: 3,
|
||||
countries: ['COD', 'TZN', 'KEN', 'UGA', 'RWA']
|
||||
},
|
||||
moov: {
|
||||
endpoint: 'https://api.moov.com/dcb/v2',
|
||||
timeout: 25000,
|
||||
retryAttempts: 3,
|
||||
countries: ['CIV', 'BEN', 'TGO', 'NER', 'BFA']
|
||||
}
|
||||
},
|
||||
|
||||
// Limitations
|
||||
limits: {
|
||||
maxAmount: 50,
|
||||
minAmount: 0.5,
|
||||
dailyLimit: 100,
|
||||
monthlyLimit: 1000
|
||||
},
|
||||
|
||||
// Sécurité
|
||||
security: {
|
||||
webhookSecret: 'dcb_wh_secret_2024',
|
||||
encryptionKey: 'dcb_enc_key_2024',
|
||||
jwtExpiry: '24h'
|
||||
},
|
||||
|
||||
// Monitoring
|
||||
monitoring: {
|
||||
healthCheckInterval: 60000,
|
||||
alertThreshold: 0.1, // 10% d'erreur
|
||||
performanceThreshold: 5000 // 5 secondes
|
||||
}
|
||||
},
|
||||
|
||||
// Configuration Partners
|
||||
merchants: {
|
||||
onboarding: {
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
allowedFileTypes: ['pdf', 'jpg', 'jpeg', 'png'],
|
||||
autoApproveThreshold: 1000
|
||||
},
|
||||
payouts: {
|
||||
defaultSchedule: 'monthly',
|
||||
processingDays: [1, 15],
|
||||
minPayoutAmount: 50,
|
||||
fees: {
|
||||
bankTransfer: 1.5,
|
||||
mobileMoney: 2.0
|
||||
}
|
||||
},
|
||||
kyc: {
|
||||
requiredDocuments: ['registration_certificate', 'tax_certificate', 'id_document'],
|
||||
autoExpireDays: 365
|
||||
}
|
||||
},
|
||||
|
||||
// Configuration générale
|
||||
app: {
|
||||
name: 'Payment Aggregation Hub',
|
||||
version: '2.0.0',
|
||||
supportEmail: 'support@paymenthub.com',
|
||||
defaultLanguage: 'fr',
|
||||
currencies: ['XOF', 'XAF', 'USD', 'EUR', 'TND'],
|
||||
countries: ['CIV', 'SEN', 'CMR', 'COD', 'TUN', 'BFA', 'MLI', 'GIN', 'NGA', 'GHA']
|
||||
}
|
||||
};
|
||||
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user