feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature

This commit is contained in:
diallolatoile 2025-12-02 00:33:12 +00:00
parent e5b0368e8c
commit ef0ba8237d
20 changed files with 2193 additions and 2836 deletions

34
api_results.txt Normal file
View File

@ -0,0 +1,34 @@
===========================================================================
RESULTATS DES TESTS API
===========================================================================
Date: 01/12/2025
Heure: 17:01:04,29
===========================================================================
*** ENDPOINT !i! : Transactions journalieres (global) ***
{"type":"transaction","period":"daily","startDate":"","endDate":"","totalAmount":600,"totalCount":38,"items":[{"period":"2025-11-14","totalAmount":600,"totalTax":600,"count":38,"successCount":9,"failedCount":29,"pendingCount":0}],"summary":{"avgAmount":15.79,"minAmount":600,"maxAmount":600},"generatedAt":"2025-12-01T17:01:03.406Z"}
*** ENDPOINT !i! : Transactions journalieres (merchant 4) ***
{"type":"transaction","period":"daily","startDate":"","endDate":"","merchantPartnerId":4,"totalAmount":600,"totalCount":38,"items":[{"period":"2025-11-14","totalAmount":600,"totalTax":600,"count":38,"successCount":9,"failedCount":29,"pendingCount":0,"merchantPartnerId":4}],"summary":{"avgAmount":15.79,"minAmount":600,"maxAmount":600},"generatedAt":"2025-12-01T17:01:03.885Z"}
*** ENDPOINT !i! : Transactions hebdomadaires ***
{"type":"transaction","period":"weekly","startDate":"","endDate":"","totalAmount":600,"totalCount":38,"items":[{"period":"2025-W46","totalAmount":600,"totalTax":600,"count":38,"successCount":9,"failedCount":29,"pendingCount":0}],"summary":{"avgAmount":15.79,"minAmount":600,"maxAmount":600},"generatedAt":"2025-12-01T17:01:04.313Z"}
*** ENDPOINT !i! : Transactions mensuelles ***
{"type":"transaction","period":"monthly","startDate":"","endDate":"","totalAmount":600,"totalCount":38,"items":[{"period":"2025-11","totalAmount":600,"totalTax":600,"count":38,"successCount":9,"failedCount":29,"pendingCount":0}],"summary":{"avgAmount":15.79,"minAmount":600,"maxAmount":600},"generatedAt":"2025-12-01T17:01:04.782Z"}
*** ENDPOINT !i! : Transactions avec dates (01-30 nov 2024) ***
{"type":"transaction","period":"daily","startDate":"2024-11-01","endDate":"2024-11-30","totalAmount":0,"totalCount":0,"items":[],"summary":{"avgAmount":0,"minAmount":0,"maxAmount":0},"generatedAt":"2025-12-01T17:01:05.230Z"}
*** ENDPOINT !i! : Subscriptions journalieres ***
{"type":"subscription","period":"daily","startDate":"","endDate":"","totalAmount":180,"totalCount":9,"items":[{"period":"2025-11-14","totalAmount":180,"count":9,"activeCount":9,"cancelledCount":0}],"summary":{"avgAmount":20,"minAmount":180,"maxAmount":180},"generatedAt":"2025-12-01T17:01:05.732Z"}
*** ENDPOINT !i! : Subscriptions mensuelles (merchant 4) ***
{"type":"subscription","period":"monthly","startDate":"","endDate":"","merchantPartnerId":4,"totalAmount":180,"totalCount":9,"items":[{"period":"2025-11","totalAmount":180,"count":9,"activeCount":9,"cancelledCount":0,"merchantPartnerId":4}],"summary":{"avgAmount":20,"minAmount":180,"maxAmount":180},"generatedAt":"2025-12-01T17:01:06.233Z"}
*** ENDPOINT !i! : Synchronisation manuelle (POST) ***
{"message":"Full sync completed successfully","timestamp":"2025-12-01T17:01:07.034Z"}
===========================================================================
TESTS TERMINES - !i! endpoints
===========================================================================

8
package-lock.json generated
View File

@ -23,7 +23,7 @@
"@fullcalendar/list": "^6.1.19",
"@fullcalendar/timegrid": "^6.1.19",
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
"@ng-icons/core": "^32.2.0",
"@ng-icons/core": "^32.5.0",
"@ng-icons/lucide": "^32.2.0",
"@ng-icons/tabler-icons": "^32.2.0",
"@popperjs/core": "^2.11.8",
@ -3108,9 +3108,9 @@
}
},
"node_modules/@ng-icons/core": {
"version": "32.2.0",
"resolved": "https://registry.npmjs.org/@ng-icons/core/-/core-32.2.0.tgz",
"integrity": "sha512-42S9QFH+FaigjXQp0QtWLHyJz8G8EaqJqcrK3qfZH4OyH86o32s3pkkf+lWnEtp2tR+07qNPSLYKCIgDE64Tug==",
"version": "32.5.0",
"resolved": "https://registry.npmjs.org/@ng-icons/core/-/core-32.5.0.tgz",
"integrity": "sha512-6zAXQ5vryaclOWEVzprFJjJAW6NSOl0eBm+I6BwmcMk+vR+1vHU82DNpNTbUE9Wn4CGXEP1yd+S+pTKIaRTXjg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"

View File

@ -26,7 +26,7 @@
"@fullcalendar/list": "^6.1.19",
"@fullcalendar/timegrid": "^6.1.19",
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
"@ng-icons/core": "^32.2.0",
"@ng-icons/core": "^32.5.0",
"@ng-icons/lucide": "^32.2.0",
"@ng-icons/tabler-icons": "^32.2.0",
"@popperjs/core": "^2.11.8",

View File

@ -1,192 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgIconComponent } from '@ng-icons/core';
import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap';
import { CountUpModule } from 'ngx-countup';
import { DcbReportingService, SubscriptionItem } from '../services/dcb-reporting.service';
import { catchError, finalize } from 'rxjs/operators';
import { of, Subscription } from 'rxjs';
@Component({
selector: 'app-active-subscriptions',
imports: [CommonModule, NgIconComponent, NgbProgressbarModule, CountUpModule],
template: `
<div class="card card-h-100">
<div class="card-header bg-light">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Abonnements Actifs</h5>
<button class="btn btn-sm btn-outline-primary" (click)="refresh()" [disabled]="loading">
<ng-icon name="lucideRefreshCw" [class.spin]="loading" class="me-1"></ng-icon>
</button>
</div>
</div>
<div class="card-body">
<!-- État de chargement -->
<div *ngIf="loading" class="text-center py-3">
<div class="spinner-border text-primary spinner-border-sm" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div>
<!-- Contenu principal -->
<div *ngIf="!loading">
<div class="d-flex justify-content-between align-items-start">
<div>
<h5 class="text-uppercase text-muted mb-3">Total actifs</h5>
<h3 class="mb-0 fw-normal">
<span [countUp]="totalSubscriptions" [options]="{duration: 1}">
{{ formatNumber(totalSubscriptions) }}
</span>
</h3>
</div>
<div>
<ng-icon name="lucideUsers" class="text-info fs-24 svg-sw-10" />
</div>
</div>
<!-- Barre de progression pour le taux d'activité -->
<div class="mt-4">
<div class="d-flex justify-content-between mb-1">
<small class="text-muted">Taux d'activité</small>
<small class="text-muted">{{ activePercentage | number:'1.1-1' }}%</small>
</div>
<ngb-progressbar [value]="activePercentage" class="progress-lg mb-3" />
</div>
<div class="d-flex justify-content-between">
<div>
<span class="text-muted">Nouveaux</span>
<h5 class="mb-0">{{ newSubscriptionsToday }}</h5>
</div>
<div class="text-end">
<span class="text-muted">Annulés</span>
<h5 class="mb-0">{{ cancelledSubscriptions }}</h5>
</div>
</div>
</div>
<!-- Message d'erreur -->
<div *ngIf="error && !loading" class="mt-3">
<div class="alert alert-warning alert-sm mb-0">
<ng-icon name="lucideAlertTriangle" class="me-2 fs-12"></ng-icon>
{{ error }}
</div>
</div>
</div>
<div *ngIf="!loading && !error" class="card-footer text-muted text-center py-2">
<small>
<ng-icon name="lucideClock" class="me-1 fs-12"></ng-icon>
Mise à jour: {{ lastUpdated | date:'HH:mm' }}
</small>
</div>
</div>
`,
styles: [`
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.alert-sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
`]
})
export class ActiveSubscriptions implements OnInit, OnDestroy {
loading = false;
error: string | null = null;
lastUpdated = new Date();
// Données d'abonnements
totalSubscriptions = 0;
newSubscriptionsToday = 0;
cancelledSubscriptions = 0;
activePercentage = 0;
private apiSubscription?: Subscription;
constructor(private reportingService: DcbReportingService) {}
ngOnInit() {
this.loadSubscriptionData();
}
ngOnDestroy() {
if (this.apiSubscription) {
this.apiSubscription.unsubscribe();
}
}
loadSubscriptionData() {
this.loading = true;
this.error = null;
const today = new Date();
const startDate = this.reportingService.formatDate(today);
console.log('ActiveSubscriptions - Loading data for date:', startDate);
this.apiSubscription = this.reportingService.getDailySubscriptions(startDate, startDate)
.pipe(
catchError(err => {
console.error('ActiveSubscriptions - API error:', err);
this.error = 'Impossible de charger les données';
return of([]);
}),
finalize(() => {
this.loading = false;
this.lastUpdated = new Date();
})
)
.subscribe({
next: (subscriptions: SubscriptionItem[]) => {
console.log('ActiveSubscriptions - Received data:', subscriptions);
this.processSubscriptionData(subscriptions);
}
});
}
processSubscriptionData(subscriptions: SubscriptionItem[]) {
if (!subscriptions || subscriptions.length === 0) {
console.warn('ActiveSubscriptions - No data available');
return;
}
// Prendre les données du jour (ou la période la plus récente)
const latestData = subscriptions[subscriptions.length - 1];
console.log('ActiveSubscriptions - Latest data:', latestData);
// Utiliser les données brutes de l'API
// Note: Votre API retourne activeCount et cancelledCount
this.totalSubscriptions = latestData.activeCount || 0;
this.cancelledSubscriptions = latestData.cancelledCount || 0;
// Pour les nouveaux abonnements aujourd'hui
// Si l'API ne fournit pas cette donnée, on peut estimer ou utiliser 0
this.newSubscriptionsToday = 0; // À adapter selon votre logique
// Calculer le pourcentage d'activité
const totalCount = latestData.count || 0;
this.activePercentage = totalCount > 0 ?
((latestData.activeCount || 0) / totalCount) * 100 : 0;
}
formatNumber(value: number): string {
return new Intl.NumberFormat('fr-FR').format(value);
}
refresh() {
console.log('ActiveSubscriptions - Refreshing data');
// Essayer de vider le cache si disponible
if (this.reportingService.clearCache) {
this.reportingService.clearCache();
}
this.loadSubscriptionData();
}
}

View File

@ -1,617 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Chartjs } from '@app/components/chartjs';
import { ChartConfiguration } from 'chart.js';
import { getColor } from '@/app/utils/color-utils';
import { NgbProgressbarModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { NgIconComponent } from '@ng-icons/core';
import { CountUpModule } from 'ngx-countup';
import { DcbReportingService, TransactionItem, SubscriptionItem } from '../services/dcb-reporting.service';
import { catchError } from 'rxjs/operators';
import { of, Subscription } from 'rxjs';
@Component({
selector: 'app-dashboard-report',
imports: [
CommonModule,
Chartjs,
NgbProgressbarModule,
NgbDropdownModule, // Ajoutez ce module
NgIconComponent,
CountUpModule
],
template: `
<div class="container-fluid">
<!-- Titre principal -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h1 class="h3 mb-0">Dashboard Reporting</h1>
<button class="btn btn-outline-primary" (click)="refreshAll()">
<ng-icon name="lucideRefreshCw" class="me-2"></ng-icon>
Actualiser
</button>
</div>
</div>
</div>
<!-- Cartes de statistiques -->
<div class="row g-3 mb-4">
<!-- Transactions Journalières -->
<div class="col-md-6 col-xl-3">
<div class="card border-primary border-2">
<div class="card-body text-center p-3">
<ng-icon name="lucideCalendar" class="text-primary fs-20 mb-2" />
<h6 class="text-primary mb-2">Transactions Journalières</h6>
<h4 class="fw-bold mb-1">
<span [countUp]="dailyTransactions.total" [options]="{duration: 1}">
{{ dailyTransactions.total }}
</span>
</h4>
<p class="text-muted mb-1">Transactions</p>
<div class="d-flex justify-content-between small">
<span>{{ formatCurrency(dailyTransactions.revenue) }}</span>
<span [ngClass]="dailyTransactions.successRate >= 95 ? 'text-success' : 'text-warning'">
<ng-icon name="lucideCheckCircle" class="me-1 fs-12" />
{{ dailyTransactions.successRate | number:'1.1-1' }}%
</span>
</div>
</div>
</div>
</div>
<!-- Transactions Hebdomadaires -->
<div class="col-md-6 col-xl-3">
<div class="card border-info border-2">
<div class="card-body text-center p-3">
<ng-icon name="lucideCalendarDays" class="text-info fs-20 mb-2" />
<h6 class="text-info mb-2">Transactions Hebdomadaires</h6>
<h4 class="fw-bold mb-1">
<span [countUp]="weeklyTransactions.total" [options]="{duration: 1}">
{{ weeklyTransactions.total }}
</span>
</h4>
<p class="text-muted mb-1">Transactions</p>
<div class="d-flex justify-content-between small">
<span>{{ formatCurrency(weeklyTransactions.revenue) }}</span>
<span [ngClass]="weeklyTransactions.successRate >= 95 ? 'text-success' : 'text-warning'">
<ng-icon name="lucideCheckCircle" class="me-1 fs-12" />
{{ weeklyTransactions.successRate | number:'1.1-1' }}%
</span>
</div>
</div>
</div>
</div>
<!-- Transactions Mensuelles -->
<div class="col-md-6 col-xl-3">
<div class="card border-success border-2">
<div class="card-body text-center p-3">
<ng-icon name="lucideCalendarRange" class="text-success fs-20 mb-2" />
<h6 class="text-success mb-2">Transactions Mensuelles</h6>
<h4 class="fw-bold mb-1">
<span [countUp]="monthlyTransactions.total" [options]="{duration: 1}">
{{ monthlyTransactions.total }}
</span>
</h4>
<p class="text-muted mb-1">Transactions</p>
<div class="d-flex justify-content-between small">
<span>{{ formatCurrency(monthlyTransactions.revenue) }}</span>
<span [ngClass]="monthlyTransactions.successRate >= 95 ? 'text-success' : 'text-warning'">
<ng-icon name="lucideCheckCircle" class="me-1 fs-12" />
{{ monthlyTransactions.successRate | number:'1.1-1' }}%
</span>
</div>
</div>
</div>
</div>
<!-- Taux global -->
<div class="col-md-6 col-xl-3">
<div class="card border-warning border-2">
<div class="card-body text-center p-3">
<ng-icon name="lucideActivity" class="text-warning fs-20 mb-2" />
<h6 class="text-warning mb-2">Taux global</h6>
<h4 class="fw-bold mb-1">
<span [countUp]="overallSuccessRate" [options]="{duration: 1, decimalPlaces: 1}">
{{ overallSuccessRate | number:'1.1-1' }}
</span>%
</h4>
<p class="text-muted mb-1">Succès (30 jours)</p>
<div class="d-flex justify-content-between small">
<span>Période: 30j</span>
<span [ngClass]="overallSuccessRate >= 95 ? 'text-success' : overallSuccessRate >= 90 ? 'text-warning' : 'text-danger'">
{{ getPerformanceLabel(overallSuccessRate) }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Graphique et Abonnements -->
<div class="row g-3 mb-4">
<!-- Graphique des transactions mensuelles -->
<div class="col-xl-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Revenue Mensuel</h5>
</div>
<div class="card-body">
<app-chartjs [getOptions]="revenueChart" [height]="300" />
</div>
</div>
</div>
<!-- Abonnements -->
<div class="col-xl-4">
<div class="card h-100">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Abonnements</h5>
<div ngbDropdown>
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" ngbDropdownToggle>
{{ getSubscriptionPeriodLabel() }}
</button>
<div ngbDropdownMenu>
<button ngbDropdownItem (click)="showDailySubscriptions()">Journalier</button>
<button ngbDropdownItem (click)="showWeeklySubscriptions()">Hebdomadaire</button>
<button ngbDropdownItem (click)="showMonthlySubscriptions()">Mensuel</button>
</div>
</div>
</div>
</div>
<div class="card-body">
<!-- Affichage selon la période sélectionnée -->
<div *ngIf="subscriptionPeriod === 'daily'">
<h6 class="text-muted mb-2">Journaliers</h6>
<div class="d-flex align-items-center mb-3">
<ng-icon name="lucideUsers" class="text-primary me-2"></ng-icon>
<div class="flex-grow-1">
<h4 class="mb-0">{{ dailySubscriptions.active }}</h4>
<small class="text-muted">Actifs</small>
</div>
<div class="text-end">
<small class="text-muted">Total: {{ dailySubscriptions.total }}</small><br>
<small class="text-danger">Annulés: {{ dailySubscriptions.cancelled }}</small>
</div>
</div>
</div>
<div *ngIf="subscriptionPeriod === 'weekly'">
<h6 class="text-muted mb-2">Hebdomadaires</h6>
<div class="d-flex align-items-center mb-3">
<ng-icon name="lucideUsers" class="text-info me-2"></ng-icon>
<div class="flex-grow-1">
<h4 class="mb-0">{{ weeklySubscriptions.active }}</h4>
<small class="text-muted">Actifs</small>
</div>
<div class="text-end">
<small class="text-muted">Total: {{ weeklySubscriptions.total }}</small><br>
<small class="text-danger">Annulés: {{ weeklySubscriptions.cancelled }}</small>
</div>
</div>
</div>
<div *ngIf="subscriptionPeriod === 'monthly'">
<h6 class="text-muted mb-2">Mensuels</h6>
<div class="d-flex align-items-center mb-3">
<ng-icon name="lucideUsers" class="text-success me-2"></ng-icon>
<div class="flex-grow-1">
<h4 class="mb-0">{{ monthlySubscriptions.active }}</h4>
<small class="text-muted">Actifs</small>
</div>
<div class="text-end">
<small class="text-muted">Total: {{ monthlySubscriptions.total }}</small><br>
<small class="text-danger">Annulés: {{ monthlySubscriptions.cancelled }}</small>
</div>
</div>
</div>
<!-- Taux d'activité -->
<div class="mt-4 pt-3 border-top">
<div class="d-flex justify-content-between mb-1">
<small class="text-muted">Taux d'activité</small>
<small class="text-muted">{{ getCurrentActivityRate() | number:'1.1-1' }}%</small>
</div>
<ngb-progressbar [value]="getCurrentActivityRate()" class="progress-sm" />
<small class="text-muted mt-1 d-block">
{{ getCurrentActiveSubscriptions() }} actifs sur {{ getCurrentTotalSubscriptions() }} total
</small>
</div>
</div>
</div>
</div>
</div>
<!-- Tableau des transactions récentes -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Transactions Récentes</h5>
<div ngbDropdown>
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" ngbDropdownToggle>
{{ getTransactionPeriodLabel() }}
</button>
<div ngbDropdownMenu>
<button ngbDropdownItem (click)="showDailyTransactions()">Journalières</button>
<button ngbDropdownItem (click)="showWeeklyTransactions()">Hebdomadaires</button>
<button ngbDropdownItem (click)="showMonthlyTransactions()">Mensuelles</button>
</div>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Période</th>
<th>Transactions</th>
<th>Montant Total</th>
<th>Réussies</th>
<th>Échouées</th>
<th>En attente</th>
<th>Taux de succès</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of getCurrentTransactions()">
<td>{{ item.period }}</td>
<td>{{ item.count }}</td>
<td>{{ formatCurrency(item.totalAmount) }}</td>
<td class="text-success">{{ item.successCount }}</td>
<td class="text-danger">{{ item.failedCount }}</td>
<td class="text-warning">{{ item.pendingCount }}</td>
<td>
<span [ngClass]="getSuccessRate(item) >= 95 ? 'text-success' : getSuccessRate(item) >= 90 ? 'text-warning' : 'text-danger'">
{{ getSuccessRate(item) | number:'1.1-1' }}%
</span>
</td>
</tr>
<tr *ngIf="getCurrentTransactions().length === 0">
<td colspan="7" class="text-center text-muted py-3">
Aucune donnée de transaction disponible
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
`,
styles: [`
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`]
})
export class DashboardReport implements OnInit, OnDestroy {
// Périodes d'affichage
subscriptionPeriod: 'daily' | 'weekly' | 'monthly' = 'daily';
transactionPeriod: 'daily' | 'weekly' | 'monthly' = 'daily';
// Données des transactions
dailyTransactions = { total: 0, revenue: 0, successRate: 0 };
weeklyTransactions = { total: 0, revenue: 0, successRate: 0 };
monthlyTransactions = { total: 0, revenue: 0, successRate: 0 };
overallSuccessRate = 0;
// Données brutes pour les tableaux
dailyTransactionItems: TransactionItem[] = [];
weeklyTransactionItems: TransactionItem[] = [];
monthlyTransactionItems: TransactionItem[] = [];
// Données des abonnements
dailySubscriptions = { total: 0, active: 0, cancelled: 0 };
weeklySubscriptions = { total: 0, active: 0, cancelled: 0 };
monthlySubscriptions = { total: 0, active: 0, cancelled: 0 };
// Données brutes pour les abonnements
dailySubscriptionItems: SubscriptionItem[] = [];
weeklySubscriptionItems: SubscriptionItem[] = [];
monthlySubscriptionItems: SubscriptionItem[] = [];
// Données pour le graphique
chartData: { date: string; revenue: number }[] = [];
// Abonnements aux API
private subscriptions: Subscription[] = [];
constructor(private reportingService: DcbReportingService) {}
ngOnInit() {
}
ngAfterViewInit() {
// Charger les données après que la vue soit initialisée
this.loadAllData();
}
ngOnDestroy() {
this.cleanupSubscriptions();
}
private cleanupSubscriptions() {
this.subscriptions.forEach(sub => sub.unsubscribe());
this.subscriptions = [];
}
loadAllData() {
this.cleanupSubscriptions();
// Charger toutes les données en parallèle
this.subscriptions.push(
this.reportingService.getDailyTransactions()
.pipe(catchError(() => of([])))
.subscribe(data => {
this.dailyTransactionItems = data;
this.dailyTransactions = this.calculateTransactionStats(data);
})
);
this.subscriptions.push(
this.reportingService.getWeeklyTransactions()
.pipe(catchError(() => of([])))
.subscribe(data => {
this.weeklyTransactionItems = data;
this.weeklyTransactions = this.calculateTransactionStats(data);
})
);
this.subscriptions.push(
this.reportingService.getMonthlyTransactions()
.pipe(catchError(() => of([])))
.subscribe(data => {
this.monthlyTransactionItems = data;
this.monthlyTransactions = this.calculateTransactionStats(data);
this.overallSuccessRate = this.monthlyTransactions.successRate;
this.prepareChartData(data);
})
);
this.subscriptions.push(
this.reportingService.getDailySubscriptions()
.pipe(catchError(() => of([])))
.subscribe(data => {
this.dailySubscriptionItems = data;
this.dailySubscriptions = this.calculateSubscriptionStats(data);
})
);
this.subscriptions.push(
this.reportingService.getWeeklySubscriptions()
.pipe(catchError(() => of([])))
.subscribe(data => {
this.weeklySubscriptionItems = data;
this.weeklySubscriptions = this.calculateSubscriptionStats(data);
})
);
this.subscriptions.push(
this.reportingService.getMonthlySubscriptions()
.pipe(catchError(() => of([])))
.subscribe(data => {
this.monthlySubscriptionItems = data;
this.monthlySubscriptions = this.calculateSubscriptionStats(data);
})
);
}
// Méthodes pour changer la période d'affichage
showDailySubscriptions() {
this.subscriptionPeriod = 'daily';
}
showWeeklySubscriptions() {
this.subscriptionPeriod = 'weekly';
}
showMonthlySubscriptions() {
this.subscriptionPeriod = 'monthly';
}
showDailyTransactions() {
this.transactionPeriod = 'daily';
}
showWeeklyTransactions() {
this.transactionPeriod = 'weekly';
}
showMonthlyTransactions() {
this.transactionPeriod = 'monthly';
}
// Méthodes utilitaires
calculateTransactionStats(data: TransactionItem[]): { total: number; revenue: number; successRate: number } {
if (!data || data.length === 0) {
return { total: 0, revenue: 0, successRate: 0 };
}
const total = data.reduce((sum, item) => sum + (item.count || 0), 0);
const revenue = data.reduce((sum, item) => sum + (item.totalAmount || 0), 0);
const successful = data.reduce((sum, item) => sum + (item.successCount || 0), 0);
const successRate = total > 0 ? (successful / total) * 100 : 0;
return { total, revenue, successRate };
}
calculateSubscriptionStats(data: SubscriptionItem[]): { total: number; active: number; cancelled: number } {
if (!data || data.length === 0) {
return { total: 0, active: 0, cancelled: 0 };
}
// Prendre la dernière période
const latest = data[data.length - 1];
return {
total: latest.count || 0,
active: latest.activeCount || 0,
cancelled: latest.cancelledCount || 0
};
}
getSubscriptionPeriodLabel(): string {
switch (this.subscriptionPeriod) {
case 'daily': return 'Journalier';
case 'weekly': return 'Hebdomadaire';
case 'monthly': return 'Mensuel';
default: return 'Période';
}
}
getTransactionPeriodLabel(): string {
switch (this.transactionPeriod) {
case 'daily': return 'Journalières';
case 'weekly': return 'Hebdomadaires';
case 'monthly': return 'Mensuelles';
default: return 'Période';
}
}
prepareChartData(data: TransactionItem[]) {
if (!data || data.length === 0) {
this.chartData = [];
return;
}
// Prendre les 6 derniers mois pour le graphique
this.chartData = data
.slice(-6)
.map(item => ({
date: item.period, // Ex: "2025-11"
revenue: item.totalAmount || 0
}));
}
getCurrentTransactions(): TransactionItem[] {
switch (this.transactionPeriod) {
case 'daily': return this.dailyTransactionItems;
case 'weekly': return this.weeklyTransactionItems;
case 'monthly': return this.monthlyTransactionItems;
default: return this.dailyTransactionItems;
}
}
getCurrentTotalSubscriptions(): number {
switch (this.subscriptionPeriod) {
case 'daily': return this.dailySubscriptions.total;
case 'weekly': return this.weeklySubscriptions.total;
case 'monthly': return this.monthlySubscriptions.total;
default: return this.dailySubscriptions.total;
}
}
getCurrentActiveSubscriptions(): number {
switch (this.subscriptionPeriod) {
case 'daily': return this.dailySubscriptions.active;
case 'weekly': return this.weeklySubscriptions.active;
case 'monthly': return this.monthlySubscriptions.active;
default: return this.dailySubscriptions.active;
}
}
getCurrentActivityRate(): number {
const total = this.getCurrentTotalSubscriptions();
const active = this.getCurrentActiveSubscriptions();
return total > 0 ? (active / total) * 100 : 0;
}
getSuccessRate(item: TransactionItem): number {
const count = item.count || 0;
const success = item.successCount || 0;
return count > 0 ? (success / count) * 100 : 0;
}
getPerformanceLabel(successRate: number): string {
if (successRate >= 95) return 'Excellent';
if (successRate >= 90) return 'Bon';
if (successRate >= 80) return 'Moyen';
if (successRate >= 70) return 'Passable';
return 'À améliorer';
}
formatCurrency(amount: number): string {
if (amount >= 1000000) {
return `${(amount / 1000000).toFixed(1)}M XOF`;
} else if (amount >= 1000) {
return `${(amount / 1000).toFixed(0)}K XOF`;
}
return `${Math.round(amount)} XOF`;
}
revenueChart = (): ChartConfiguration => ({
type: 'bar',
data: {
labels: this.chartData.map(item => {
// Gérer différents formats de période
if (item.date.includes('-')) {
const [year, month] = item.date.split('-');
const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'];
const monthIndex = parseInt(month) - 1;
if (monthIndex >= 0 && monthIndex < months.length) {
return `${months[monthIndex]} ${year}`;
}
}
return item.date;
}),
datasets: [
{
label: 'Revenue (XOF)',
data: this.chartData.map(item => Math.round(item.revenue / 1000)), // En milliers
backgroundColor: this.chartData.map((_, index) =>
getColor(index === this.chartData.length - 1 ? 'chart-primary' : 'chart-secondary')
),
borderRadius: 6,
borderSkipped: false,
}
]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (context) => {
const value = context.raw as number;
return `Revenue: ${this.formatCurrency(value * 1000)}`;
}
}
}
},
scales: {
x: {
grid: { display: false }
},
y: {
beginAtZero: true,
ticks: {
callback: (value) => `${value}K XOF`
},
title: {
display: true,
text: 'Revenue (en milliers XOF)'
}
}
}
}
});
refreshAll() {
this.loadAllData();
}
}

View File

@ -0,0 +1,212 @@
/* Styles pour le dashboard DCB Reporting */
.dashboard-container {
min-height: 100vh;
background-color: #f8f9fa;
padding: 1.5rem;
}
/* Header */
.dashboard-header {
background: white;
border-radius: 1rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
/* Barre d'état */
.status-bar .bg-light {
background-color: #f8f9fa !important;
border: 1px solid #e9ecef;
}
/* Cartes KPI */
.kpi-card {
border: none;
border-radius: 0.75rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
transition: transform 0.2s, box-shadow 0.2s;
}
.kpi-card:hover {
transform: translateY(-2px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
}
.kpi-card .avatar-sm {
width: 40px;
height: 40px;
}
.kpi-card .border-start {
border-left-width: 4px !important;
}
/* Graphiques */
.card {
border: none;
border-radius: 1rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 1rem;
}
.card-header {
background-color: white;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 1.25rem 1.5rem;
}
.card-body {
padding: 1.5rem;
}
/* Tables */
.table-responsive {
border-radius: 0.75rem;
overflow: hidden;
}
.table {
margin-bottom: 0;
}
.table thead {
background-color: #f8f9fa;
}
.table tbody tr {
transition: background-color 0.15s;
}
.table tbody tr:hover {
background-color: rgba(13, 110, 253, 0.05);
}
/* Alertes */
.alert-list {
max-height: 300px;
overflow-y: auto;
}
.alert-item {
transition: background-color 0.15s;
}
.alert-item:hover {
background-color: rgba(0, 0, 0, 0.02);
}
/* Indicateurs de santé */
.health-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
.health-indicator.indicator-success {
background-color: #51cf66;
box-shadow: 0 0 0 2px rgba(81, 207, 102, 0.2);
}
.health-indicator.indicator-warning {
background-color: #ff922b;
box-shadow: 0 0 0 2px rgba(255, 146, 43, 0.2);
}
.health-indicator.indicator-danger {
background-color: #ff6b6b;
box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.2);
}
/* Spinners */
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Badges */
.badge {
padding: 0.35em 0.65em;
font-weight: 500;
}
/* Dropdown */
.dropdown-menu {
border: none;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
border-radius: 0.75rem;
padding: 0.5rem 0;
}
.dropdown-item {
padding: 0.5rem 1rem;
transition: background-color 0.15s;
}
.dropdown-item:hover {
background-color: rgba(13, 110, 253, 0.1);
}
/* Form controls */
.input-group-sm {
max-width: 300px;
}
.form-control {
border-color: #e9ecef;
}
.form-control:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
/* Boutons */
.btn {
border-radius: 0.5rem;
font-weight: 500;
}
.btn-outline-primary {
border-color: #0d6efd;
color: #0d6efd;
}
.btn-outline-primary:hover {
background-color: #0d6efd;
border-color: #0d6efd;
}
/* Dashboard footer */
.dashboard-footer .card {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 0.75rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.dashboard-container {
padding: 1rem;
}
.dashboard-header {
padding: 1rem;
}
.status-bar .col-auto {
margin-bottom: 0.5rem;
}
.filters-card .input-group {
max-width: 100%;
}
}

View File

@ -0,0 +1,601 @@
<div class="container-fluid dashboard-container">
<!-- Header avec navigation -->
<div class="dashboard-header">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0 text-primary">
<ng-icon name="lucideLayoutDashboard" class="me-2"></ng-icon>
Dashboard Reporting DCB
</h1>
<p class="text-muted mb-0">Surveillance en temps réel des transactions et abonnements</p>
</div>
<div class="d-flex gap-2 align-items-center">
<!-- Contrôles rapides -->
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-primary btn-sm"
(click)="loadAllData()"
[disabled]="loading.all">
<ng-icon name="lucideRefreshCw" [class.spin]="loading.all" class="me-1"></ng-icon>
{{ loading.all ? 'Chargement...' : 'Actualiser' }}
</button>
<button class="btn btn-outline-danger btn-sm"
(click)="triggerSync()"
[disabled]="loading.sync">
<ng-icon name="lucideRefreshCcw" [class.spin]="loading.sync" class="me-1"></ng-icon>
Sync
</button>
</div>
<!-- Filtres -->
<div class="filters-card">
<div class="d-flex align-items-center gap-2">
<div class="input-group input-group-sm">
<span class="input-group-text bg-light">
<ng-icon name="lucideStore"></ng-icon>
</span>
<input type="number" class="form-control form-control-sm"
[(ngModel)]="merchantId" min="1"
placeholder="Merchant ID"
(change)="refreshMerchantData()"
style="width: 100px;">
</div>
<div class="input-group input-group-sm">
<span class="input-group-text bg-light">
<ng-icon name="lucideCalendar"></ng-icon>
</span>
<input type="date" class="form-control form-control-sm"
[(ngModel)]="startDate"
style="width: 110px;">
<input type="date" class="form-control form-control-sm"
[(ngModel)]="endDate"
style="width: 110px;">
<button class="btn btn-outline-secondary btn-sm"
(click)="refreshWithDates()"
title="Appliquer les dates">
<ng-icon name="lucideFilter"></ng-icon>
</button>
</div>
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false">
<ng-icon name="lucideSettings" class="me-1"></ng-icon>
Options
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" (click)="checkSystemHealth()">
<ng-icon name="lucideHeartPulse" class="me-2"></ng-icon>
Vérifier la santé
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" (click)="loadAllData()">
<ng-icon name="lucideRefreshCw" class="me-2"></ng-icon>
Rafraîchir tout
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Barre d'état rapide -->
<div class="status-bar mb-4">
<div class="row g-2">
<div class="col-auto">
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
<ng-icon name="lucideClock" class="text-primary"></ng-icon>
<small>Mis à jour: {{ lastUpdated | date:'HH:mm:ss' }}</small>
</div>
</div>
<div class="col-auto">
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
<ng-icon name="lucideCpu" class="text-success"></ng-icon>
<small>Services: {{ stats.onlineServices }}/{{ stats.totalServices }} en ligne</small>
</div>
</div>
<div class="col-auto">
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
<ng-icon name="lucidePhone" class="text-warning"></ng-icon>
<small>Opérateur: Orange</small>
</div>
</div>
</div>
</div>
<!-- Message de sync -->
<div *ngIf="syncResponse" class="alert alert-success alert-dismissible fade show mb-4">
<div class="d-flex align-items-center">
<ng-icon name="lucideCheckCircle" class="me-2 fs-5"></ng-icon>
<div class="flex-grow-1">
<strong>{{ syncResponse.message }}</strong>
<div class="text-muted small">Synchronisée à {{ formatDate(syncResponse.timestamp) }}</div>
</div>
<button type="button" class="btn-close" (click)="syncResponse = null"></button>
</div>
</div>
<!-- ==================== SECTION DES KPIs HORIZONTAUX ==================== -->
<div class="kpi-section mb-4">
<div class="row g-3">
<!-- Transactions Journalières -->
<div class="col-xl-2 col-md-4 col-sm-6">
<div class="card kpi-card border-start border-primary border-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h6 class="text-uppercase text-muted mb-1">Transactions</h6>
<h4 class="fw-bold mb-0">{{ formatNumber(getPaymentStats().daily.transactions) }}</h4>
<small class="text-muted">Journalier</small>
</div>
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<ng-icon name="lucideStore" class="text-primary fs-5"></ng-icon>
</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted small">{{ formatCurrency(getPaymentStats().daily.revenue) }}</span>
<span class="badge bg-primary bg-opacity-25 text-primary">
<ng-icon name="lucideArrowUpRight" class="me-1"></ng-icon>
{{ getPaymentStats().daily.successRate | number:'1.0-0' }}%
</span>
</div>
</div>
</div>
</div>
<!-- Transactions Hebdomadaires -->
<div class="col-xl-2 col-md-4 col-sm-6">
<div class="card kpi-card border-start border-info border-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h6 class="text-uppercase text-muted mb-1">Transactions</h6>
<h4 class="fw-bold mb-0">{{ formatNumber(getPaymentStats().weekly.transactions) }}</h4>
<small class="text-muted">Hebdomadaire</small>
</div>
<div class="avatar-sm bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<ng-icon name="lucideCalendar" class="text-info fs-5"></ng-icon>
</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted small">{{ formatCurrency(getPaymentStats().weekly.revenue) }}</span>
<span class="badge bg-info bg-opacity-25 text-info">
<ng-icon name="lucideArrowUpRight" class="me-1"></ng-icon>
{{ getPaymentStats().weekly.successRate | number:'1.0-0' }}%
</span>
</div>
</div>
</div>
</div>
<!-- Transactions Mensuelles -->
<div class="col-xl-2 col-md-4 col-sm-6">
<div class="card kpi-card border-start border-success border-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h6 class="text-uppercase text-muted mb-1">Transactions</h6>
<h4 class="fw-bold mb-0">{{ formatNumber(getPaymentStats().monthly.transactions) }}</h4>
<small class="text-muted">Mensuel</small>
</div>
<div class="avatar-sm bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<ng-icon name="lucideCalendar" class="text-success fs-5"></ng-icon>
</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted small">{{ formatCurrency(getPaymentStats().monthly.revenue) }}</span>
<span class="badge bg-success bg-opacity-25 text-success">
<ng-icon name="lucideArrowUpRight" class="me-1"></ng-icon>
{{ getPaymentStats().monthly.successRate | number:'1.0-0' }}%
</span>
</div>
</div>
</div>
</div>
<!-- Revenue Annuel -->
<div class="col-xl-2 col-md-4 col-sm-6">
<div class="card kpi-card border-start border-purple border-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h6 class="text-uppercase text-muted mb-1">Revenue {{ currentYear }}</h6>
<h4 class="fw-bold mb-0">{{ formatCurrency(stats.yearlyRevenue) }}</h4>
<small class="text-muted">Annuel</small>
</div>
<div class="avatar-sm bg-purple bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<ng-icon name="lucideTrophy" class="text-purple fs-5"></ng-icon>
</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted small">{{ formatNumber(stats.yearlyTransactions) }} transactions</span>
<span class="badge bg-purple bg-opacity-25 text-purple">
<ng-icon name="lucideCalendar" class="me-1"></ng-icon>
{{ currentYear }}
</span>
</div>
</div>
</div>
</div>
<!-- Abonnements Actifs -->
<div class="col-xl-2 col-md-4 col-sm-6">
<div class="card kpi-card border-start border-warning border-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h6 class="text-uppercase text-muted mb-1">Abonnements</h6>
<h4 class="fw-bold mb-0">{{ formatNumber(getSubscriptionStats().active) }}</h4>
<small class="text-muted">Actifs</small>
</div>
<div class="avatar-sm bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<ng-icon name="lucideUsers" class="text-warning fs-5"></ng-icon>
</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted small">Total: {{ formatNumber(getSubscriptionStats().total) }}</span>
<span class="badge bg-warning bg-opacity-25 text-warning">
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
+{{ getSubscriptionStats().newToday }}
</span>
</div>
</div>
</div>
</div>
<!-- Taux de Succès -->
<div class="col-xl-2 col-md-4 col-sm-6">
<div class="card kpi-card border-start border-danger border-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h6 class="text-uppercase text-muted mb-1">Taux de succès</h6>
<h4 class="fw-bold mb-0">{{ stats.successRate | number:'1.1-1' }}%</h4>
<small class="text-muted">Global</small>
</div>
<div class="avatar-sm bg-danger bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<ng-icon name="lucideCheckCircle" class="text-danger fs-5"></ng-icon>
</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted small">{{ getPerformanceLabel(stats.successRate) }}</span>
<span class="badge" [ngClass]="getSuccessRateClass(stats.successRate)">
<ng-icon name="lucideTrendingUp" class="me-1"></ng-icon>
{{ stats.avgSuccessRate | number:'1.0-0' }}% cible
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== SECTION DES GRAPHIQUES ==================== -->
<div class="charts-section mb-4">
<div class="row g-4">
<!-- Revenue Chart -->
<div class="col-xl-8">
<div class="card h-100">
<div class="card-header bg-transparent border-bottom-0">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title mb-0">
<ng-icon name="lucideTrendingUp" class="text-primary me-2"></ng-icon>
Évolution du Revenue
</h5>
<p class="text-muted small mb-0">Courbe des revenus par période</p>
</div>
<div class="d-flex gap-2 align-items-center">
<button class="btn btn-sm btn-outline-primary"
(click)="refreshDailyTransactions()"
[disabled]="loading.dailyTransactions">
<ng-icon name="lucideRefreshCw" [class.spin]="loading.dailyTransactions" class="me-1"></ng-icon>
Actualiser
</button>
<div class="dropdown">
<select class="form-select form-select-sm"
[(ngModel)]="selectedPeriod"
(change)="onPeriodChange()"
style="width: 120px;">
<option value="7days">7 jours</option>
<option value="30days">30 jours</option>
<option value="90days">90 jours</option>
</select>
</div>
</div>
</div>
</div>
<div class="card-body pt-0">
<div *ngIf="loading.dailyTransactions" class="text-center py-5">
<div class="spinner-border text-primary"></div>
<p class="mt-2 text-muted">Chargement des données...</p>
</div>
<div *ngIf="!loading.dailyTransactions && revenueChartData.length === 0"
class="text-center py-5 text-muted">
<ng-icon name="lucideBarChart3" class="fs-1 opacity-25"></ng-icon>
<p class="mt-2">Aucune donnée disponible</p>
</div>
<div *ngIf="!loading.dailyTransactions && revenueChartData.length > 0"
class="position-relative" style="height: 300px;">
<canvas #revenueChart></canvas>
</div>
</div>
</div>
</div>
<!-- Performance & Santé -->
<div class="col-xl-4">
<div class="row h-100 g-4">
<!-- Taux de succès -->
<div class="col-12">
<div class="card h-100">
<div class="card-header bg-transparent border-bottom-0">
<h5 class="card-title mb-0">
<ng-icon name="lucideActivity" class="text-success me-2"></ng-icon>
Performance
</h5>
</div>
<div class="card-body">
<div class="text-center mb-4">
<div class="position-relative d-inline-block">
<canvas #successRateChart width="120" height="120"></canvas>
<div class="position-absolute top-50 start-50 translate-middle">
<h3 class="fw-bold mb-0" [ngClass]="getSuccessRateClass(stats.successRate)">
{{ stats.successRate | number:'1.0-0' }}%
</h3>
<small class="text-muted">Taux de succès</small>
</div>
</div>
</div>
<div class="row text-center">
<div class="col-6">
<div class="border-end">
<div class="text-success fw-bold">
<ng-icon name="lucideCheckCircle" class="me-1"></ng-icon>
{{ formatNumber(dailyTransactions?.items?.[0]?.successCount || 0) }}
</div>
<small class="text-muted">Réussies</small>
</div>
</div>
<div class="col-6">
<div class="text-danger fw-bold">
<ng-icon name="lucideXCircle" class="me-1"></ng-icon>
{{ formatNumber(dailyTransactions?.items?.[0]?.failedCount || 0) }}
</div>
<small class="text-muted">Échouées</small>
</div>
</div>
</div>
</div>
</div>
<!-- Santé du système -->
<div class="col-12">
<div class="card h-100">
<div class="card-header bg-transparent border-bottom-0">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<ng-icon name="lucideHeartPulse" class="text-danger me-2"></ng-icon>
Santé du système
</h5>
<button class="btn btn-sm btn-outline-danger"
(click)="refreshHealthCheck()"
[disabled]="loading.healthCheck">
<ng-icon name="lucideRefreshCw" [class.spin]="loading.healthCheck"></ng-icon>
</button>
</div>
</div>
<div class="card-body">
<div class="system-health-list">
<div *ngFor="let service of systemHealth" class="health-item mb-2">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<div class="health-indicator me-2" [ngClass]="'indicator-' + service.color"></div>
<span class="small">{{ service.service }}</span>
</div>
<div>
<small class="text-muted me-2" *ngIf="service.responseTime">
{{ service.responseTime }}ms
</small>
<span class="badge" [ngClass]="'bg-' + service.color">
{{ service.status }}
</span>
</div>
</div>
</div>
</div>
<div class="mt-3 text-center">
<small class="text-muted">
<ng-icon name="lucideClock" class="me-1"></ng-icon>
Dernière vérification: {{ formatTimeAgo(systemHealth[0]?.lastChecked) }}
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== SECTION DES TABLEAUX ==================== -->
<div class="tables-section">
<div class="row g-4">
<!-- Transactions récentes -->
<div class="col-xl-6">
<div class="card h-100">
<div class="card-header bg-transparent border-bottom-0">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title mb-0">
<ng-icon name="lucideListChecks" class="text-primary me-2"></ng-icon>
Transactions récentes
</h5>
<p class="text-muted small mb-0">Dernières 24 heures</p>
</div>
<div>
<button class="btn btn-sm btn-outline-primary"
(click)="refreshDailyTransactions()"
[disabled]="loading.dailyTransactions">
<ng-icon name="lucideRefreshCw" [class.spin]="loading.dailyTransactions"></ng-icon>
</button>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="ps-3">Date/Heure</th>
<th class="text-end">Montant</th>
<th class="text-end">Statut</th>
<th class="text-end pe-3">Taux</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of (dailyTransactions?.items?.slice(0, 5) || [])">
<td class="ps-3">
<div>{{ item.period }}</div>
<small class="text-muted">{{ formatNumber(item.count) }} transactions</small>
</td>
<td class="text-end">
<div class="fw-medium">{{ formatCurrency(item.totalAmount) }}</div>
</td>
<td class="text-end">
<span class="badge bg-success bg-opacity-10 text-success">
{{ item.successCount || 0 }} ✓
</span>
<span class="badge bg-danger bg-opacity-10 text-danger ms-1">
{{ item.failedCount || 0 }} ✗
</span>
</td>
<td class="text-end pe-3">
<span [ngClass]="getSuccessRateClass((item.count > 0 ? (item.successCount / item.count) * 100 : 0))">
{{ (item.count > 0 ? (item.successCount / item.count) * 100 : 0) | number:'1.1-1' }}%
</span>
</td>
</tr>
<tr *ngIf="!(dailyTransactions?.items?.length)">
<td colspan="4" class="text-center text-muted py-5">
<ng-icon name="lucideDatabase" class="fs-1 opacity-25 mb-3 d-block"></ng-icon>
<p class="mb-0">Aucune transaction disponible</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card-footer bg-transparent border-top-0">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
<ng-icon name="lucideInfo" class="me-1"></ng-icon>
Affichage des {{ (dailyTransactions?.items?.slice(0, 5) || []).length }} dernières périodes
</small>
<a href="#" class="btn btn-sm btn-outline-primary">Voir tout</a>
</div>
</div>
</div>
</div>
<!-- Alertes système -->
<div class="col-xl-6">
<div class="card h-100">
<div class="card-header bg-transparent border-bottom-0">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title mb-0">
<ng-icon name="lucideBell" class="text-warning me-2"></ng-icon>
Alertes système
</h5>
<p class="text-muted small mb-0">Notifications en temps réel</p>
</div>
<div>
<span class="badge" [ngClass]="getAlertBadgeClass()">
{{ alerts.length }}
</span>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="alert-list">
<div *ngFor="let alert of alerts.slice(0, 5)" class="alert-item p-3 border-bottom">
<div class="d-flex align-items-start">
<div class="me-3">
<ng-icon *ngIf="alert.type === 'warning'"
name="lucideAlertTriangle"
class="text-warning fs-5"></ng-icon>
<ng-icon *ngIf="alert.type === 'info'"
name="lucideInfo"
class="text-info fs-5"></ng-icon>
<ng-icon *ngIf="alert.type === 'success'"
name="lucideCheckCircle"
class="text-success fs-5"></ng-icon>
<ng-icon *ngIf="alert.type === 'danger'"
name="lucideAlertCircle"
class="text-danger fs-5"></ng-icon>
</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start mb-1">
<h6 class="mb-0">{{ alert.title }}</h6>
<small class="text-muted">{{ formatTimeAgo(alert.timestamp) }}</small>
</div>
<p class="mb-0 text-muted small">{{ alert.description }}</p>
</div>
</div>
</div>
<div *ngIf="alerts.length === 0" class="text-center text-muted py-5">
<ng-icon name="lucideCheckCircle" class="text-success fs-1 opacity-25 mb-3 d-block"></ng-icon>
<p class="mb-0">Aucune alerte active</p>
<small class="text-muted">Tous les systèmes fonctionnent normalement</small>
</div>
</div>
</div>
<div class="card-footer bg-transparent border-top-0">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
<ng-icon name="lucideClock" class="me-1"></ng-icon>
Dernière vérification: {{ lastUpdated | date:'HH:mm:ss' }}
</small>
<button class="btn btn-sm btn-outline-warning" (click)="checkSystemHealth()">
<ng-icon name="lucideRefreshCw" class="me-1"></ng-icon>
Vérifier
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="dashboard-footer mt-4">
<div class="card bg-light">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted small">
<ng-icon name="lucideCode" class="me-1"></ng-icon>
Dashboard Reporting DCB v1.0
</div>
<div class="text-muted small">
<ng-icon name="lucideDatabase" class="me-1"></ng-icon>
Données extraites de l'API Reporting Service DCB
</div>
<div class="text-muted small">
<ng-icon name="lucideClock" class="me-1"></ng-icon>
Mise à jour: {{ lastUpdated | date:'dd/MM/yyyy HH:mm:ss' }}
</div>
</div>
</div>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -1,412 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgIconComponent } from '@ng-icons/core';
import { CountUpModule } from 'ngx-countup';
import { DcbReportingService, TransactionItem } from '../services/dcb-reporting.service';
import { catchError, finalize } from 'rxjs/operators';
import { of, Subscription } from 'rxjs';
@Component({
selector: 'app-payment-stats',
imports: [CommonModule, NgIconComponent, CountUpModule],
template: `
<div class="card">
<div class="card-header bg-light">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Statistiques des Paiements</h5>
<button class="btn btn-sm btn-outline-primary" (click)="refresh()" [disabled]="loading">
<ng-icon name="lucideRefreshCw" [class.spin]="loading" class="me-1"></ng-icon>
Actualiser
</button>
</div>
</div>
<div class="card-body">
<!-- État de chargement -->
<div *ngIf="loading" 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">Chargement des statistiques...</p>
</div>
<!-- Statistiques -->
<div *ngIf="!loading && !error" class="row g-3">
<!-- Journalier -->
<div class="col-md-6 col-xl-3">
<div class="card border-primary border-2">
<div class="card-body text-center p-3">
<ng-icon name="lucideCalendar" class="text-primary fs-20 mb-2" />
<h6 class="text-primary mb-2">Journalier</h6>
<h4 class="fw-bold mb-1">
<span [countUp]="dailyStats.transactions" [options]="{duration: 1}">
{{ dailyStats.transactions }}
</span>
</h4>
<p class="text-muted mb-1">Transactions</p>
<div class="d-flex justify-content-between small">
<span>{{ formatCurrency(dailyStats.revenue) }}</span>
<span [ngClass]="dailyStats.successRate >= 95 ? 'text-success' : 'text-warning'">
<ng-icon name="lucideCheckCircle" class="me-1 fs-12" />
{{ dailyStats.successRate | number:'1.1-1' }}%
</span>
</div>
</div>
</div>
</div>
<!-- Hebdomadaire -->
<div class="col-md-6 col-xl-3">
<div class="card border-info border-2">
<div class="card-body text-center p-3">
<ng-icon name="lucideCalendarDays" class="text-info fs-20 mb-2" />
<h6 class="text-info mb-2">Hebdomadaire</h6>
<h4 class="fw-bold mb-1">
<span [countUp]="weeklyStats.transactions" [options]="{duration: 1}">
{{ weeklyStats.transactions }}
</span>
</h4>
<p class="text-muted mb-1">Transactions</p>
<div class="d-flex justify-content-between small">
<span>{{ formatCurrency(weeklyStats.revenue) }}</span>
<span [ngClass]="weeklyStats.successRate >= 95 ? 'text-success' : 'text-warning'">
<ng-icon name="lucideCheckCircle" class="me-1 fs-12" />
{{ weeklyStats.successRate | number:'1.1-1' }}%
</span>
</div>
</div>
</div>
</div>
<!-- Mensuel -->
<div class="col-md-6 col-xl-3">
<div class="card border-success border-2">
<div class="card-body text-center p-3">
<ng-icon name="lucideCalendarRange" class="text-success fs-20 mb-2" />
<h6 class="text-success mb-2">Mensuel</h6>
<h4 class="fw-bold mb-1">
<span [countUp]="monthlyStats.transactions" [options]="{duration: 1}">
{{ monthlyStats.transactions }}
</span>
</h4>
<p class="text-muted mb-1">Transactions</p>
<div class="d-flex justify-content-between small">
<span>{{ formatCurrency(monthlyStats.revenue) }}</span>
<span [ngClass]="monthlyStats.successRate >= 95 ? 'text-success' : 'text-warning'">
<ng-icon name="lucideCheckCircle" class="me-1 fs-12" />
{{ monthlyStats.successRate | number:'1.1-1' }}%
</span>
</div>
</div>
</div>
</div>
<!-- Taux global -->
<div class="col-md-6 col-xl-3">
<div class="card border-warning border-2">
<div class="card-body text-center p-3">
<ng-icon name="lucideActivity" class="text-warning fs-20 mb-2" />
<h6 class="text-warning mb-2">Taux global</h6>
<h4 class="fw-bold mb-1">
<span [countUp]="overallSuccessRate" [options]="{duration: 1, decimalPlaces: 1}">
{{ overallSuccessRate | number:'1.1-1' }}
</span>%
</h4>
<p class="text-muted mb-1">Succès (30 jours)</p>
<div class="d-flex justify-content-between small">
<span>Période: 30j</span>
<span [ngClass]="overallSuccessRate >= 95 ? 'text-success' : overallSuccessRate >= 90 ? 'text-warning' : 'text-danger'">
{{ getPerformanceLabel(overallSuccessRate) }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Résumé global -->
<div *ngIf="!loading && !error" class="row mt-3">
<div class="col-12">
<div class="alert alert-light mb-0">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>Performance globale:</strong>
<span [ngClass]="overallSuccessRate >= 95 ? 'text-success' : overallSuccessRate >= 90 ? 'text-warning' : 'text-danger'">
{{ overallSuccessRate | number:'1.1-1' }}% de taux de succès
</span>
<span class="text-muted ms-2">
({{ dailyStats.transactions }} transactions aujourd'hui)
</span>
</div>
<div class="text-muted small">
<ng-icon name="lucideClock" class="me-1"></ng-icon>
Mise à jour: {{ lastUpdated | date:'HH:mm' }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`,
styles: [`
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`]
})
export class PaymentStats implements OnInit, OnDestroy {
loading = false;
error: string | null = null;
// Statistiques calculées à partir des données brutes
dailyStats = { transactions: 0, revenue: 0, successRate: 0 };
weeklyStats = { transactions: 0, revenue: 0, successRate: 0 };
monthlyStats = { transactions: 0, revenue: 0, successRate: 0 };
overallSuccessRate = 0;
lastUpdated = new Date();
// Données brutes de l'API
private dailyData: TransactionItem[] = [];
private weeklyData: TransactionItem[] = [];
private monthlyData: TransactionItem[] = [];
private dailySubscription?: Subscription;
private weeklySubscription?: Subscription;
private monthlySubscription?: Subscription;
constructor(private reportingService: DcbReportingService) {}
ngOnInit() {
this.loadStats();
}
ngOnDestroy() {
this.cleanupSubscriptions();
}
private cleanupSubscriptions() {
if (this.dailySubscription) {
this.dailySubscription.unsubscribe();
}
if (this.weeklySubscription) {
this.weeklySubscription.unsubscribe();
}
if (this.monthlySubscription) {
this.monthlySubscription.unsubscribe();
}
}
loadStats() {
console.log('PaymentStats - Starting loadStats()');
this.cleanupSubscriptions(); // Nettoyer les anciennes souscriptions
this.loading = true;
this.error = null;
const today = new Date();
const threeDaysAgo = new Date(today);
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
const twoWeeksAgo = new Date(today);
twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14);
const threeMonthsAgo = new Date(today);
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
console.log('PaymentStats - Date ranges:', {
threeDaysAgo: this.reportingService.formatDate(threeDaysAgo),
today: this.reportingService.formatDate(today),
twoWeeksAgo: this.reportingService.formatDate(twoWeeksAgo),
threeMonthsAgo: this.reportingService.formatDate(threeMonthsAgo)
});
// Charger les données séquentiellement
this.loadSequentialData(threeDaysAgo, twoWeeksAgo, threeMonthsAgo, today);
}
loadSequentialData(threeDaysAgo: Date, twoWeeksAgo: Date, threeMonthsAgo: Date, today: Date) {
// 1. Charger les données quotidiennes
this.dailySubscription = this.reportingService.getDailyTransactions(
this.reportingService.formatDate(threeDaysAgo),
this.reportingService.formatDate(today)
)
.pipe(
catchError(err => {
console.error('PaymentStats - Error loading daily data:', err);
return of([]);
})
)
.subscribe({
next: (dailyData) => {
console.log('PaymentStats - Daily data loaded:', dailyData.length);
this.dailyData = dailyData;
// 2. Après les données quotidiennes, charger les données hebdomadaires
this.weeklySubscription = this.reportingService.getWeeklyTransactions(
this.reportingService.formatDate(twoWeeksAgo),
this.reportingService.formatDate(today)
)
.pipe(
catchError(err => {
console.error('PaymentStats - Error loading weekly data:', err);
return of([]);
})
)
.subscribe({
next: (weeklyData) => {
console.log('PaymentStats - Weekly data loaded:', weeklyData.length);
this.weeklyData = weeklyData;
// 3. Après les données hebdomadaires, charger les données mensuelles
this.monthlySubscription = this.reportingService.getMonthlyTransactions(
this.reportingService.formatDate(threeMonthsAgo),
this.reportingService.formatDate(today)
)
.pipe(
catchError(err => {
console.error('PaymentStats - Error loading monthly data:', err);
return of([]);
}),
finalize(() => {
console.log('PaymentStats - All data loaded, processing...');
this.loading = false;
this.lastUpdated = new Date();
this.processAllData();
})
)
.subscribe({
next: (monthlyData) => {
console.log('PaymentStats - Monthly data loaded:', monthlyData.length);
this.monthlyData = monthlyData;
},
error: (error) => {
console.error('PaymentStats - Monthly subscription error:', error);
}
});
},
error: (error) => {
console.error('PaymentStats - Weekly subscription error:', error);
}
});
},
error: (error) => {
console.error('PaymentStats - Daily subscription error:', error);
}
});
}
processAllData() {
console.log('PaymentStats - Processing all data:', {
daily: this.dailyData.length,
weekly: this.weeklyData.length,
monthly: this.monthlyData.length
});
// Vérifier si nous avons des données
const hasAnyData = this.dailyData.length > 0 ||
this.weeklyData.length > 0 ||
this.monthlyData.length > 0;
if (!hasAnyData) {
console.warn('PaymentStats - No data available');
return;
}
// Calculer les statistiques avec les données disponibles
this.calculateStatsFromRawData();
}
calculateStatsFromRawData() {
// Calculer les statistiques pour chaque période à partir des données brutes
this.dailyStats = this.calculatePeriodStats(this.dailyData);
this.weeklyStats = this.calculatePeriodStats(this.weeklyData);
this.monthlyStats = this.calculatePeriodStats(this.monthlyData);
// Utiliser le taux de succès mensuel comme taux global
this.overallSuccessRate = this.monthlyStats.successRate;
// Si aucune donnée mensuelle, utiliser la meilleure période disponible
if (this.monthlyStats.transactions === 0) {
if (this.weeklyStats.transactions > 0) {
this.overallSuccessRate = this.weeklyStats.successRate;
} else if (this.dailyStats.transactions > 0) {
this.overallSuccessRate = this.dailyStats.successRate;
}
}
console.log('PaymentStats - Calculated stats from raw data:', {
daily: this.dailyStats,
weekly: this.weeklyStats,
monthly: this.monthlyStats,
overallSuccess: this.overallSuccessRate
});
// Si toutes les données sont vides, afficher un message
if (this.dailyStats.transactions === 0 &&
this.weeklyStats.transactions === 0 &&
this.monthlyStats.transactions === 0) {
this.error = 'Les données sont actuellement vides.';
}
}
calculatePeriodStats(data: TransactionItem[]): { transactions: number; revenue: number; successRate: number } {
if (!data || data.length === 0) {
return { transactions: 0, revenue: 0, successRate: 0 };
}
// Calculer les totaux sur tous les éléments
const totalTransactions = data.reduce((sum, item) => sum + (item.count || 0), 0);
const totalRevenue = data.reduce((sum, item) => sum + (item.totalAmount || 0), 0);
const totalSuccessful = data.reduce((sum, item) => sum + (item.successCount || 0), 0);
const successRate = totalTransactions > 0 ? (totalSuccessful / totalTransactions) * 100 : 0;
console.log('PaymentStats - Period stats calculation:', {
dataLength: data.length,
totalTransactions,
totalRevenue,
totalSuccessful,
successRate
});
return {
transactions: totalTransactions,
revenue: totalRevenue,
successRate
};
}
getPerformanceLabel(successRate: number): string {
if (successRate >= 95) return 'Excellent';
if (successRate >= 90) return 'Bon';
if (successRate >= 80) return 'Moyen';
if (successRate >= 70) return 'Passable';
return 'À améliorer';
}
formatCurrency(amount: number): string {
if (amount >= 1000000) {
return `${(amount / 1000000).toFixed(1)}M XOF`;
} else if (amount >= 1000) {
return `${(amount / 1000).toFixed(0)}K XOF`;
}
return `${Math.round(amount)} XOF`;
}
refresh() {
console.log('PaymentStats - Refreshing data');
// Essayer de vider le cache si disponible
if (this.reportingService.clearCache) {
this.reportingService.clearCache();
}
this.loadStats();
}
}

View File

@ -1,247 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SubscriptionsService } from '../../subscriptions/subscriptions.service';
import { Subscription} from '@core/models/dcb-bo-hub-subscription.model';
import { catchError, finalize } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'app-recent-transactions',
imports: [CommonModule],
template: `
<div class="card">
<div class="card-header border-dashed">
<div class="d-flex justify-content-between align-items-center">
<h4 class="card-title mb-0">Abonnements Récents</h4>
<div class="d-flex align-items-center gap-2">
<button class="btn btn-sm btn-outline-primary" (click)="refresh()" [disabled]="loading">
<i class="lucideRefreshCw" [class.spin]="loading"></i>
</button>
<a href="/subscriptions" class="btn btn-sm btn-light">Voir tout</a>
</div>
</div>
</div>
<div class="card-body p-0">
<!-- État de chargement -->
<div *ngIf="loading" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
<p class="mt-2 text-muted">Chargement des abonnements...</p>
</div>
<!-- Message d'erreur -->
<div *ngIf="error && !loading" class="text-center py-5">
<i class="lucideAlertTriangle text-warning fs-1"></i>
<p class="mt-2">{{ error }}</p>
<button class="btn btn-sm btn-primary mt-2" (click)="refresh()">
Réessayer
</button>
</div>
<!-- Liste des abonnements -->
<div *ngIf="!loading && !error" class="table-responsive">
<table class="table table-hover table-nowrap mb-0">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Date de création</th>
<th>Statut</th>
<th>Montant</th>
<th>Périodicité</th>
<th>Client</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let subscription of subscriptions"
[class.table-success]="subscription.status === 'ACTIVE'"
[class.table-warning]="subscription.status === 'PENDING'"
[class.table-danger]="subscription.status === 'EXPIRED' || subscription.status === 'CANCELLED'">
<td>
<div class="fw-semibold">#{{ subscription.id }}</div>
<small class="text-muted">{{ subscription.externalReference || 'N/A' }}</small>
</td>
<td>
<div>{{ subscription.createdAt | date:'dd/MM/yyyy' }}</div>
<small class="text-muted">{{ subscription.createdAt | date:'HH:mm' }}</small>
</td>
<td>
<span class="badge" [ngClass]="getStatusClass(subscription.status)">
<i class="lucideCircle me-1" [ngClass]="getStatusIcon(subscription.status)"></i>
{{ getStatusLabel(subscription.status) }}
</span>
</td>
<td>
<div class="fw-bold">{{ subscription.amount | currency:subscription.currency:'symbol':'1.0-0' }}</div>
<small class="text-muted">{{ subscription.currency }}</small>
</td>
<td>
<div>{{ getPeriodicityLabel(subscription.periodicity) }}</div>
<small class="text-muted">
Prochain: {{ subscription.nextPaymentDate | date:'dd/MM' }}
</small>
</td>
<td>
<div>Client #{{ subscription.customerId }}</div>
<small class="text-muted">Partenaire: {{ subscription.merchantPartnerId }}</small>
</td>
</tr>
<tr *ngIf="subscriptions.length === 0">
<td colspan="6" class="text-center py-4">
<div class="text-muted">
<i class="lucidePackageOpen fs-4"></i>
<p class="mt-2 mb-0">Aucun abonnement trouvé</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Statistiques -->
<div *ngIf="!loading && !error && subscriptions.length > 0" class="card-footer">
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted">
Affichage de <strong>{{ subscriptions.length }}</strong> abonnements récents
</div>
<div class="text-end">
<small class="text-muted">Total: {{ statistics?.total || 0 }} abonnements</small>
<div class="mt-1">
<span class="badge bg-success-subtle text-success me-2">
{{ statistics?.active || 0 }} actifs
</span>
<span class="badge bg-warning-subtle text-warning">
{{ statistics?.suspended || 0 }} suspendus
</span>
</div>
</div>
</div>
</div>
</div>
</div>
`,
styles: [``]
})
export class RecentTransactions implements OnInit {
loading = false;
error: string | null = null;
subscriptions: Subscription[] = [];
statistics: any = null;
constructor(private subscriptionsService: SubscriptionsService) {}
ngOnInit() {
this.loadRecentSubscriptions();
}
loadRecentSubscriptions() {
this.loading = true;
this.error = null;
this.subscriptionsService.getRecentSubscriptions(10)
.pipe(
catchError(err => {
console.error('Erreur lors du chargement des abonnements:', err);
this.error = 'Impossible de charger les abonnements. Veuillez réessayer.';
return of({
subscriptions: [],
statistics: {
total: 0,
active: 0,
suspended: 0,
cancelled: 0,
totalRevenue: 0,
averageAmount: 0
}
});
}),
finalize(() => {
this.loading = false;
})
)
.subscribe({
next: (response) => {
this.subscriptions = response.subscriptions;
}
});
}
getStatusClass(status: string): string {
switch(status?.toUpperCase()) {
case 'ACTIVE':
return 'bg-success-subtle text-success border border-success-subtle';
case 'PENDING':
return 'bg-warning-subtle text-warning border border-warning-subtle';
case 'SUSPENDED':
return 'bg-secondary-subtle text-secondary border border-secondary-subtle';
case 'CANCELLED':
return 'bg-danger-subtle text-danger border border-danger-subtle';
case 'FAILED':
return 'bg-danger-subtle text-danger border border-danger-subtle';
case 'EXPIRED':
return 'bg-info-subtle text-info border border-info-subtle';
default:
return 'bg-light text-dark border';
}
}
getStatusIcon(status: string): string {
switch(status?.toUpperCase()) {
case 'ACTIVE':
return 'lucideCheckCircle';
case 'PENDING':
return 'lucideClock';
case 'SUSPENDED':
return 'lucidePauseCircle';
case 'CANCELLED':
return 'lucideXCircle';
case 'FAILED':
return 'lucideAlertCircle';
case 'EXPIRED':
return 'lucideCalendarX';
default:
return 'lucideHelpCircle';
}
}
getStatusLabel(status: string): string {
switch(status?.toUpperCase()) {
case 'ACTIVE':
return 'Actif';
case 'PENDING':
return 'En attente';
case 'SUSPENDED':
return 'Suspendu';
case 'CANCELLED':
return 'Annulé';
case 'FAILED':
return 'Échoué';
case 'EXPIRED':
return 'Expiré';
default:
return status || 'Inconnu';
}
}
getPeriodicityLabel(periodicity: string): string {
switch(periodicity?.toUpperCase()) {
case 'DAILY':
return 'Quotidien';
case 'WEEKLY':
return 'Hebdomadaire';
case 'MONTHLY':
return 'Mensuel';
case 'YEARLY':
return 'Annuel';
default:
return periodicity || 'Non défini';
}
}
refresh() {
this.loadRecentSubscriptions();
}
}

View File

@ -1,307 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Chartjs } from '@app/components/chartjs';
import { ChartConfiguration } from 'chart.js';
import { getColor } from '@/app/utils/color-utils';
import { DcbReportingService, SubscriptionItem } from '../services/dcb-reporting.service';
import { catchError, finalize } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'app-subscription-overview',
imports: [CommonModule, Chartjs],
template: `
<div class="card">
<div class="card-header border-dashed">
<div class="d-flex justify-content-between align-items-center">
<h4 class="card-title mb-0">Aperçu des Abonnements</h4>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-primary-subtle text-primary">
<i class="lucideUsers me-1"></i>
{{ formatNumber(totalSubscriptions) }} total
</span>
<button class="btn btn-sm btn-outline-primary" (click)="refresh()" [disabled]="loading">
<i class="lucideRefreshCw" [class.spin]="loading"></i>
</button>
</div>
</div>
</div>
<div class="card-body">
<!-- État de chargement -->
<div *ngIf="loading" 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">Chargement des données...</p>
</div>
<!-- Contenu principal -->
<div *ngIf="!loading && !error">
<!-- Graphique circulaire -->
<div class="text-center mb-4" style="height: 200px;">
<app-chartjs [getOptions]="subscriptionChart" [height]="200" />
</div>
<!-- Statistiques détaillées -->
<div class="row">
<div class="col-6 mb-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="avatar-xs">
<div class="avatar-title bg-success-subtle rounded">
<i class="lucideCheckCircle text-success fs-12"></i>
</div>
</div>
</div>
<div class="flex-grow-1 ms-2">
<h6 class="mb-0">Actifs</h6>
<div class="d-flex justify-content-between">
<span class="text-muted">{{ formatNumber(activeSubscriptions) }}</span>
<small class="text-success" *ngIf="growthRate > 0">
<i class="lucideTrendingUp me-1"></i>
+{{ growthRate | number:'1.1-1' }}%
</small>
<small class="text-danger" *ngIf="growthRate < 0">
<i class="lucideTrendingDown me-1"></i>
{{ growthRate | number:'1.1-1' }}%
</small>
</div>
</div>
</div>
</div>
<div class="col-6 mb-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="avatar-xs">
<div class="avatar-title bg-warning-subtle rounded">
<i class="lucideClock text-warning fs-12"></i>
</div>
</div>
</div>
<div class="flex-grow-1 ms-2">
<h6 class="mb-0">En période d'essai</h6>
<div class="d-flex justify-content-between">
<span class="text-muted">{{ formatNumber(trialSubscriptions) }}</span>
<small class="text-warning">{{ trialPercentage | number:'1.1-1' }}%</small>
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="avatar-xs">
<div class="avatar-title bg-danger-subtle rounded">
<i class="lucideXCircle text-danger fs-12"></i>
</div>
</div>
</div>
<div class="flex-grow-1 ms-2">
<h6 class="mb-0">Annulés</h6>
<div class="d-flex justify-content-between">
<span class="text-muted">{{ formatNumber(cancelledSubscriptions) }}</span>
<small class="text-danger">{{ cancelledPercentage | number:'1.1-1' }}%</small>
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="avatar-xs">
<div class="avatar-title bg-info-subtle rounded">
<i class="lucideCalendar text-info fs-12"></i>
</div>
</div>
</div>
<div class="flex-grow-1 ms-2">
<h6 class="mb-0">Expirés</h6>
<div class="d-flex justify-content-between">
<span class="text-muted">{{ formatNumber(expiredSubscriptions) }}</span>
<small class="text-info">{{ expiredPercentage | number:'1.1-1' }}%</small>
</div>
</div>
</div>
</div>
</div>
<!-- Tendances mensuelles -->
<div class="mt-4 pt-3 border-top">
<h6 class="mb-3">Nouveaux abonnements (30 derniers jours)</h6>
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-success" role="progressbar"
[style.width]="newSubscriptionsPercentage + '%'"></div>
</div>
</div>
<div class="flex-shrink-0 ms-3">
<span class="fw-bold">{{ formatNumber(newSubscriptions) }}</span>
<small class="text-muted ms-1">nouveaux</small>
</div>
</div>
<div class="text-end mt-1">
<small class="text-muted">Dernière mise à jour: {{ lastUpdated | date:'HH:mm' }}</small>
</div>
</div>
</div>
</div>
</div>
`,
styles: [`
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`]
})
export class SubscriptionOverview implements OnInit {
loading = false;
error: string | null = null;
lastUpdated = new Date();
// Données d'abonnements
totalSubscriptions = 0;
activeSubscriptions = 0;
trialSubscriptions = 0;
cancelledSubscriptions = 0;
expiredSubscriptions = 0;
newSubscriptions = 0;
// Pourcentages
trialPercentage = 0;
cancelledPercentage = 0;
expiredPercentage = 0;
newSubscriptionsPercentage = 0;
growthRate = 0;
constructor(private reportingService: DcbReportingService) {}
ngOnInit() {
this.loadSubscriptionData();
}
loadSubscriptionData() {
this.loading = true;
this.error = null;
// Récupérer les 30 derniers jours
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 30);
const startDateStr = this.formatDate(startDate);
const endDateStr = this.formatDate(endDate);
console.log('SubscriptionOverview - Loading data for period:', startDateStr, 'to', endDateStr);
this.reportingService.getDailySubscriptions(startDateStr, endDateStr)
.pipe(
catchError(err => {
console.error('SubscriptionOverview - API error:', err);
this.error = 'Impossible de charger les données des abonnements';
return of([]); // Retourner un tableau vide
}),
finalize(() => {
this.loading = false;
this.lastUpdated = new Date();
})
)
.subscribe({
next: (subscriptions: SubscriptionItem[]) => {
console.log('SubscriptionOverview - Received data:', subscriptions);
this.processSubscriptionData(subscriptions);
}
});
}
processSubscriptionData(subscriptions: SubscriptionItem[]) {
if (!subscriptions || subscriptions.length === 0) {
console.warn('SubscriptionOverview - No subscription data');
return; // Afficher rien
}
// Utiliser la dernière période disponible
const latestData = subscriptions[subscriptions.length - 1];
// Note: Votre API retourne activeCount et cancelledCount, pas trial/expired/new
this.activeSubscriptions = latestData.activeCount || 0;
this.cancelledSubscriptions = latestData.cancelledCount || 0;
// Pour les champs manquants, mettre à 0 ou estimer selon votre logique métier
this.trialSubscriptions = 0;
this.expiredSubscriptions = 0;
this.newSubscriptions = 0;
this.totalSubscriptions = latestData.count || 0;
}
subscriptionChart = (): ChartConfiguration => ({
type: 'doughnut',
data: {
labels: ['Actifs', 'En essai', 'Annulés', 'Expirés'],
datasets: [{
data: [
this.activeSubscriptions,
this.trialSubscriptions,
this.cancelledSubscriptions,
this.expiredSubscriptions
],
backgroundColor: [
getColor('chart-success'),
getColor('chart-warning'),
getColor('chart-danger'),
getColor('chart-info')
],
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (context) => {
const label = context.label || '';
const value = context.raw as number;
const total = this.totalSubscriptions;
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
return `${label}: ${this.formatNumber(value)} (${percentage}%)`;
}
}
}
}
}
});
formatNumber(value: number): string {
if (value >= 1000000) {
return `${(value / 1000000).toFixed(1)}M`;
} else if (value >= 1000) {
return `${(value / 1000).toFixed(1)}K`;
}
return value.toLocaleString('fr-FR');
}
refresh() {
this.reportingService.clearCache?.();
this.loadSubscriptionData();
}
private formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
}

View File

@ -1,281 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Chartjs } from '@app/components/chartjs';
import { ChartConfiguration } from 'chart.js';
import { getColor } from '@/app/utils/color-utils';
import { DcbReportingService, TransactionItem } from '../services/dcb-reporting.service';
import { CommonModule } from '@angular/common';
import { catchError, finalize } from 'rxjs/operators';
@Component({
selector: 'app-success-rate-chart',
imports: [Chartjs, CommonModule],
template: `
<div class="card">
<div class="card-header border-dashed">
<h4 class="card-title mb-0">Taux de Succès des Transactions</h4>
</div>
<div class="card-body">
<!-- État de chargement -->
<div *ngIf="loading" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div>
<!-- Contenu principal -->
<div *ngIf="!loading">
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
<h2 class="mb-0">{{ overallSuccessRate | number:'1.1-1' }}%</h2>
<span class="text-muted">Taux global de succès</span>
</div>
<div class="avatar-sm">
<div class="avatar-title bg-success-subtle rounded">
<i class="lucideCheckCircle text-success fs-24"></i>
</div>
</div>
</div>
<app-chartjs [getOptions]="successRateChart" [height]="250" />
<!-- Statistiques détaillées -->
<div class="row mt-4">
<div class="col-6">
<div class="d-flex align-items-center mb-2">
<div class="flex-shrink-0">
<div class="avatar-xs">
<div class="avatar-title bg-success-subtle rounded">
<i class="lucideCheck text-success fs-12"></i>
</div>
</div>
</div>
<div class="flex-grow-1 ms-2">
<h6 class="mb-0">Transactions réussies</h6>
<p class="text-muted mb-0">{{ formatNumber(successfulTransactions) }}</p>
</div>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center mb-2">
<div class="flex-shrink-0">
<div class="avatar-xs">
<div class="avatar-title bg-danger-subtle rounded">
<i class="lucideX text-danger fs-12"></i>
</div>
</div>
</div>
<div class="flex-grow-1 ms-2">
<h6 class="mb-0">Transactions échouées</h6>
<p class="text-muted mb-0">{{ formatNumber(failedTransactions) }}</p>
</div>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="avatar-xs">
<div class="avatar-title bg-warning-subtle rounded">
<i class="lucideClock text-warning fs-12"></i>
</div>
</div>
</div>
<div class="flex-grow-1 ms-2">
<h6 class="mb-0">En attente</h6>
<p class="text-muted mb-0">{{ formatNumber(pendingTransactions) }}</p>
</div>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="avatar-xs">
<div class="avatar-title bg-info-subtle rounded">
<i class="lucideActivity text-info fs-12"></i>
</div>
</div>
</div>
<div class="flex-grow-1 ms-2">
<h6 class="mb-0">Total transactions</h6>
<p class="text-muted mb-0">{{ formatNumber(totalTransactions) }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`,
})
export class SuccessRateChart implements OnInit {
loading = false;
error: string | null = null;
// Données du graphique
chartLabels: string[] = [];
successRates: number[] = [];
// Statistiques
overallSuccessRate = 0;
successfulTransactions = 0;
failedTransactions = 0;
pendingTransactions = 0;
totalTransactions = 0;
constructor(private reportingService: DcbReportingService) {}
ngOnInit() {
this.loadChartData();
}
loadChartData() {
this.loading = true;
this.error = null;
// Récupérer les 30 derniers jours
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 30);
const startDateStr = this.formatDate(startDate);
const endDateStr = this.formatDate(endDate);
this.reportingService.getDailyTransactions(startDateStr, endDateStr)
.pipe(
catchError(err => {
console.error('Erreur API:', err);
this.error = 'Données non disponibles';
return [];
}),
finalize(() => {
this.loading = false;
})
)
.subscribe({
next: (transactions: TransactionItem[]) => {
this.processTransactionData(transactions);
}
});
}
processTransactionData(transactions: any[]) {
// Calculer les statistiques globales
let totalSuccess = 0;
let totalFailed = 0;
let totalPending = 0;
let totalAll = 0;
transactions.forEach(transaction => {
totalSuccess += transaction.successful || 0;
totalFailed += transaction.failed || 0;
totalPending += transaction.pending || 0;
totalAll += transaction.total || 0;
});
this.successfulTransactions = totalSuccess;
this.failedTransactions = totalFailed;
this.pendingTransactions = totalPending;
this.totalTransactions = totalAll;
// Calculer le taux de succès global
if (totalAll > 0) {
this.overallSuccessRate = (totalSuccess / totalAll) * 100;
}
// Préparer les données pour le graphique (dernières 7 jours)
const last7Days = transactions.slice(-7);
this.chartLabels = last7Days.map(t => {
const date = new Date(t.date);
return date.toLocaleDateString('fr-FR', { weekday: 'short', day: 'numeric' });
});
this.successRates = last7Days.map(t => {
const total = t.total || 0;
const success = t.successful || 0;
return total > 0 ? (success / total) * 100 : 0;
});
}
successRateChart = (): ChartConfiguration => ({
type: 'line',
data: {
labels: this.chartLabels,
datasets: [
{
label: 'Taux de succès (%)',
data: this.successRates,
borderColor: getColor('chart-success'),
backgroundColor: getColor('chart-success') + '20',
borderWidth: 2,
fill: true,
tension: 0.4,
pointBackgroundColor: getColor('chart-success'),
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: (context) => {
return `Taux de succès: ${context.raw}%`;
}
}
}
},
scales: {
x: {
grid: {
display: false
},
ticks: {
color: '#6c757d'
}
},
y: {
beginAtZero: false,
min: 80,
max: 100,
grid: {
tickBorderDash: [5, 5]
},
ticks: {
callback: (value) => `${value}%`,
color: '#6c757d'
},
title: {
display: true,
text: 'Taux de succès (%)'
}
}
}
}
});
formatNumber(value: number): string {
if (value >= 1000000) {
return `${(value / 1000000).toFixed(1)}M`;
} else if (value >= 1000) {
return `${(value / 1000).toFixed(0)}K`;
}
return value.toString();
}
private formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
}

View File

@ -7,7 +7,7 @@
<div class="row g-4 mb-4">
<div class="col-12">
<app-dashboard-report />
<app-dcb-reporting-dashboard></app-dcb-reporting-dashboard>
</div>
</div>
</div>

View File

@ -1,17 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { Dashboard } from './dcb-dashboard'
import { DcbDashboard } from './dcb-dashboard'
describe('Dashboard', () => {
let component: Dashboard
let fixture: ComponentFixture<Dashboard>
let component: DcbDashboard
let fixture: ComponentFixture<DcbDashboard>
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Dashboard],
imports: [DcbDashboard],
}).compileComponents()
fixture = TestBed.createComponent(Dashboard)
fixture = TestBed.createComponent(DcbDashboard)
component = fixture.componentInstance
fixture.detectChanges()
})

View File

@ -1,14 +1,14 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PageTitle } from '@app/components/page-title/page-title';
import { DashboardReport } from './components/dcb-dashboard-report';
import { DcbReportingDashboard } from './components/dcb-reporting-dashboard';
@Component({
selector: 'app-dcb-dashboard',
imports: [
CommonModule,
PageTitle,
DashboardReport,
DcbReportingDashboard,
],
templateUrl: './dcb-dashboard.html',
})

View File

@ -1,105 +0,0 @@
export interface KpiCardModel {
title: string;
value: number | string;
change?: number;
changeType?: 'positive' | 'negative' | 'neutral';
icon: string;
color: string;
format?: 'currency' | 'number' | 'percentage';
currency?: string;
}
export interface PaymentChartData {
period: string;
successful: number;
failed: number;
pending: number;
revenue: number;
}
export interface SubscriptionStatsModel {
total: number;
active: number;
trial: number;
cancelled: number;
expired: number;
growthRate: number;
}
export interface Alert {
id: string;
type: 'error' | 'warning' | 'info' | 'success';
title: string;
message: string;
timestamp: Date;
acknowledged: boolean;
priority: 'low' | 'medium' | 'high';
action?: {
label: string;
route?: string;
action?: () => void;
};
}
export interface DcbDashboardData {
kpis: KpiCardModel[];
paymentChart: PaymentChartData[];
subscriptionStats: SubscriptionStatsModel;
alerts: Alert[];
lastUpdated: Date;
}
// Interface pour la réponse brute de l'API
export interface ApiTransactionItem {
period: string;
totalAmount: number;
totalTax: number;
count: number;
successCount: number;
failedCount: number;
pendingCount: number;
merchantPartnerId?: number;
}
export interface ApiTransactionResponse {
type: string;
period: string;
startDate: string;
endDate: string;
merchantPartnerId?: number;
totalAmount: number;
totalCount: number;
items: ApiTransactionItem[];
summary: {
avgAmount: number;
minAmount: number;
maxAmount: number;
};
generatedAt: string;
}
export interface ApiSubscriptionItem {
period: string;
active: number;
trial: number;
cancelled: number;
expired: number;
new: number;
total: number;
merchantPartnerId?: number;
}
export interface ApiSubscriptionResponse {
type: string;
period: string;
startDate: string;
endDate: string;
merchantPartnerId?: number;
totalCount: number;
items: ApiSubscriptionItem[];
summary: {
avgActive: number;
avgNew: number;
};
generatedAt: string;
}

View File

@ -0,0 +1,68 @@
export interface TransactionReport {
type: 'transaction';
period: 'daily' | 'weekly' | 'monthly';
startDate: string;
endDate: string;
merchantPartnerId?: number;
totalAmount: number;
totalCount: number;
items: TransactionItem[];
summary: TransactionSummary;
generatedAt: string;
}
export interface TransactionItem {
period: string;
totalAmount: number;
totalTax: number;
count: number;
successCount: number;
failedCount: number;
pendingCount: number;
merchantPartnerId?: number;
}
export interface TransactionSummary {
avgAmount: number;
minAmount: number;
maxAmount: number;
}
export interface SubscriptionReport {
type: 'subscription';
period: 'daily' | 'weekly' | 'monthly';
startDate: string;
endDate: string;
merchantPartnerId?: number;
totalAmount: number;
totalCount: number;
items: SubscriptionItem[];
summary: SubscriptionSummary;
generatedAt: string;
}
export interface SubscriptionItem {
period: string;
totalAmount: number;
count: number;
activeCount: number;
cancelledCount: number;
merchantPartnerId?: number;
}
export interface SubscriptionSummary {
avgAmount: number;
minAmount: number;
maxAmount: number;
}
export interface SyncResponse {
message: string;
timestamp: string;
}
export interface ReportParams {
startDate?: string;
endDate?: string;
merchantPartnerId?: number;
}

View File

@ -1,459 +0,0 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, of, forkJoin } from 'rxjs';
import { map, catchError, shareReplay } from 'rxjs/operators';
import {
DcbDashboardData,
KpiCardModel,
PaymentChartData,
SubscriptionStatsModel,
Alert
} from '../models/dcb-dashboard.models';
import { environment } from '@environments/environment';
export interface TransactionReport {
date: string;
successful: number;
failed: number;
pending: number;
revenue: number;
total: number;
}
export interface SubscriptionReport {
date: string;
active: number;
trial: number;
cancelled: number;
expired: number;
new: number;
total: number;
}
@Injectable({ providedIn: 'root' })
export class DcbDashboardService {
private http = inject(HttpClient);
private reportingApiUrl = `${environment.reportingApiUrl}/reporting`;
private cache = new Map<string, Observable<any>>();
// Données mockées pour fallback
private mockData: DcbDashboardData = {
kpis: [
{
title: 'Revenue Mensuel',
value: 0,
change: 0,
changeType: 'positive',
icon: 'lucideTrendingUp',
color: 'success',
format: 'currency',
currency: 'XOF'
},
{
title: 'Transactions Journalières',
value: 0,
change: 0,
changeType: 'positive',
icon: 'lucideCreditCard',
color: 'primary',
format: 'number'
},
{
title: 'Taux de Succès',
value: 0,
change: 0,
changeType: 'positive',
icon: 'lucideCheckCircle',
color: 'info',
format: 'percentage'
},
{
title: 'Nouveaux Abonnements',
value: 0,
change: 0,
changeType: 'positive',
icon: 'lucideUsers',
color: 'warning',
format: 'number'
}
],
paymentChart: [],
subscriptionStats: {
total: 0,
active: 0,
trial: 0,
cancelled: 0,
expired: 0,
growthRate: 0
},
alerts: [],
lastUpdated: new Date()
};
/**
* Récupère toutes les données du dashboard
*/
getDcbDashboardData(): Observable<DcbDashboardData> {
const cacheKey = 'dashboard-full-data';
if (!this.cache.has(cacheKey)) {
const dashboardData$ = this.fetchAllDashboardData().pipe(
shareReplay({ bufferSize: 1, refCount: true }),
catchError(error => {
console.error('Erreur dashboard:', error);
return of(this.mockData);
})
);
this.cache.set(cacheKey, dashboardData$);
}
return this.cache.get(cacheKey)!;
}
/**
* Récupère toutes les données nécessaires
*/
private fetchAllDashboardData(): Observable<DcbDashboardData> {
/*if (!environment.production) {
return of(this.mockData);
}*/
// Calcul des dates (30 derniers jours)
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 30);
const startDateStr = this.formatDate(startDate);
const endDateStr = this.formatDate(endDate);
// Récupération des transactions mensuelles pour les KPIs
return this.getMonthlyTransactions(startDateStr, endDateStr).pipe(
map(monthlyData => this.transformDashboardData(monthlyData))
);
}
/**
* Récupère les transactions journalières (pour le graphique)
*/
getDailyTransactions(
startDate?: string,
endDate?: string,
merchantPartnerId?: number
): Observable<TransactionReport[]> {
let params = new HttpParams();
if (startDate) params = params.set('startDate', startDate);
if (endDate) params = params.set('endDate', endDate);
if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString());
const url = `${this.reportingApiUrl}/transactions/daily`;
return this.http.get<any>(url, { params }).pipe(
map(response => response.data || []),
catchError(() => of([]))
);
}
/**
* Récupère les transactions mensuelles (pour les KPIs)
*/
getMonthlyTransactions(
startDate?: string,
endDate?: string,
merchantPartnerId?: number
): Observable<TransactionReport[]> {
let params = new HttpParams();
if (startDate) params = params.set('startDate', startDate);
if (endDate) params = params.set('endDate', endDate);
if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString());
const url = `${this.reportingApiUrl}/transactions/monthly`;
return this.http.get<any>(url, { params }).pipe(
map(response => response.data || []),
catchError(() => of([]))
);
}
/**
* Récupère les abonnements journaliers
*/
getDailySubscriptions(
startDate?: string,
endDate?: string,
merchantPartnerId?: number
): Observable<SubscriptionReport[]> {
let params = new HttpParams();
if (startDate) params = params.set('startDate', startDate);
if (endDate) params = params.set('endDate', endDate);
if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString());
const url = `${this.reportingApiUrl}/subscriptions/daily`;
return this.http.get<any>(url, { params }).pipe(
map(response => response.data || []),
catchError(() => of([]))
);
}
/**
* Récupère les abonnements mensuels
*/
getMonthlySubscriptions(
startDate?: string,
endDate?: string,
merchantPartnerId?: number
): Observable<SubscriptionReport[]> {
let params = new HttpParams();
if (startDate) params = params.set('startDate', startDate);
if (endDate) params = params.set('endDate', endDate);
if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString());
const url = `${this.reportingApiUrl}/subscriptions/monthly`;
return this.http.get<any>(url, { params }).pipe(
map(response => response.data || []),
catchError(() => of([]))
);
}
/**
* Lance une synchronisation manuelle
*/
triggerManualSync(): Observable<{ success: boolean; message?: string }> {
const url = `${this.reportingApiUrl}/sync/full`;
return this.http.post<{ success: boolean; message?: string }>(url, {}).pipe(
catchError(error => of({
success: false,
message: error.message || 'Erreur lors de la synchronisation'
}))
);
}
/**
* Transforme les données d'API en format dashboard
*/
private transformDashboardData(monthlyTransactions: TransactionReport[]): DcbDashboardData {
// Calcul des KPIs à partir des transactions mensuelles
const currentMonth = this.getCurrentMonthData(monthlyTransactions);
const previousMonth = this.getPreviousMonthData(monthlyTransactions);
const totalRevenue = this.calculateTotalRevenue(monthlyTransactions);
const revenueChange = this.calculateRevenueChange(currentMonth, previousMonth);
const totalTransactions = this.calculateTotalTransactions(monthlyTransactions);
const transactionChange = this.calculateTransactionChange(currentMonth, previousMonth);
const successRate = this.calculateSuccessRate(monthlyTransactions);
const successRateChange = this.calculateSuccessRateChange(currentMonth, previousMonth);
// Récupération des abonnements pour le 4ème KPI
const today = this.formatDate(new Date());
const lastMonthDate = this.formatDate(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000));
return {
kpis: [
{
title: 'Revenue Mensuel',
value: totalRevenue,
change: revenueChange,
changeType: revenueChange >= 0 ? 'positive' : 'negative',
icon: 'lucideTrendingUp',
color: 'success',
format: 'currency',
currency: 'XOF'
},
{
title: 'Transactions Total',
value: totalTransactions,
change: transactionChange,
changeType: transactionChange >= 0 ? 'positive' : 'negative',
icon: 'lucideCreditCard',
color: 'primary',
format: 'number'
},
{
title: 'Taux de Succès',
value: successRate,
change: successRateChange,
changeType: successRateChange >= 0 ? 'positive' : 'negative',
icon: 'lucideCheckCircle',
color: 'info',
format: 'percentage'
},
{
title: 'Nouveaux Abonnements',
value: 0, // À calculer via getDailySubscriptions
change: 0,
changeType: 'positive',
icon: 'lucideUsers',
color: 'warning',
format: 'number'
}
],
paymentChart: this.preparePaymentChartData(monthlyTransactions),
subscriptionStats: {
total: 0, // À calculer via getMonthlySubscriptions
active: 0,
trial: 0,
cancelled: 0,
expired: 0,
growthRate: 0
},
alerts: [],
lastUpdated: new Date()
};
}
/**
* Calcule le revenue total des dernières transactions
*/
private calculateTotalRevenue(transactions: TransactionReport[]): number {
if (!transactions || transactions.length === 0) return 0;
return transactions.reduce((sum, transaction) => sum + (transaction.revenue || 0), 0);
}
/**
* Calcule le changement de revenue
*/
private calculateRevenueChange(currentMonth: TransactionReport[], previousMonth: TransactionReport[]): number {
const currentRevenue = this.calculateTotalRevenue(currentMonth);
const previousRevenue = this.calculateTotalRevenue(previousMonth);
if (previousRevenue === 0) return currentRevenue > 0 ? 100 : 0;
return ((currentRevenue - previousRevenue) / previousRevenue) * 100;
}
/**
* Calcule le total des transactions
*/
private calculateTotalTransactions(transactions: TransactionReport[]): number {
if (!transactions || transactions.length === 0) return 0;
return transactions.reduce((sum, transaction) => sum + (transaction.total || 0), 0);
}
/**
* Calcule le changement de transactions
*/
private calculateTransactionChange(currentMonth: TransactionReport[], previousMonth: TransactionReport[]): number {
const currentTransactions = this.calculateTotalTransactions(currentMonth);
const previousTransactions = this.calculateTotalTransactions(previousMonth);
if (previousTransactions === 0) return currentTransactions > 0 ? 100 : 0;
return ((currentTransactions - previousTransactions) / previousTransactions) * 100;
}
/**
* Calcule le taux de succès moyen
*/
private calculateSuccessRate(transactions: TransactionReport[]): number {
if (!transactions || transactions.length === 0) return 0;
let totalSuccess = 0;
let totalTransactions = 0;
transactions.forEach(transaction => {
totalSuccess += transaction.successful || 0;
totalTransactions += transaction.total || 0;
});
if (totalTransactions === 0) return 0;
return (totalSuccess / totalTransactions) * 100;
}
/**
* Calcule le changement du taux de succès
*/
private calculateSuccessRateChange(currentMonth: TransactionReport[], previousMonth: TransactionReport[]): number {
const currentRate = this.calculateSuccessRate(currentMonth);
const previousRate = this.calculateSuccessRate(previousMonth);
if (previousRate === 0) return currentRate > 0 ? 100 : 0;
return currentRate - previousRate;
}
/**
* Prépare les données pour le graphique
*/
private preparePaymentChartData(transactions: TransactionReport[]): PaymentChartData[] {
if (!transactions || transactions.length === 0) return [];
// Limiter aux 6 derniers mois pour le graphique
const recentTransactions = transactions.slice(-6);
return recentTransactions.map(transaction => ({
period: this.formatMonthName(transaction.date),
successful: transaction.successful || 0,
failed: transaction.failed || 0,
pending: transaction.pending || 0,
revenue: transaction.revenue || 0
}));
}
/**
* Récupère les données du mois courant
*/
private getCurrentMonthData(transactions: TransactionReport[]): TransactionReport[] {
const currentMonth = new Date().getMonth();
const currentYear = new Date().getFullYear();
return transactions.filter(transaction => {
const transactionDate = new Date(transaction.date);
return transactionDate.getMonth() === currentMonth &&
transactionDate.getFullYear() === currentYear;
});
}
/**
* Récupère les données du mois précédent
*/
private getPreviousMonthData(transactions: TransactionReport[]): TransactionReport[] {
const now = new Date();
const previousMonth = now.getMonth() === 0 ? 11 : now.getMonth() - 1;
const year = now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear();
return transactions.filter(transaction => {
const transactionDate = new Date(transaction.date);
return transactionDate.getMonth() === previousMonth &&
transactionDate.getFullYear() === year;
});
}
/**
* Format une date au format YYYY-MM-DD
*/
private formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
/**
* Format le nom du mois à partir d'une date
*/
private formatMonthName(dateString: string): string {
const date = new Date(dateString);
const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'];
return months[date.getMonth()];
}
/**
* Vide le cache
*/
clearCache(): void {
this.cache.clear();
}
/**
* Rafraîchit les données
*/
refreshData(): Observable<DcbDashboardData> {
this.clearCache();
return this.getDcbDashboardData();
}
}

View File

@ -1,10 +1,23 @@
import { Injectable, inject } from '@angular/core';
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { map, timeout } from 'rxjs/operators';
import { environment } from '@environments/environment';
// Interfaces pour les réponses de l'API
// ============ INTERFACES EXPORTÉES ============
export interface TransactionReport {
type: 'transaction';
period: 'daily' | 'weekly' | 'monthly';
startDate: string;
endDate: string;
merchantPartnerId?: number;
totalAmount: number;
totalCount: number;
items: TransactionItem[];
summary: TransactionSummary;
generatedAt: string;
}
export interface TransactionItem {
period: string;
totalAmount: number;
@ -16,20 +29,22 @@ export interface TransactionItem {
merchantPartnerId?: number;
}
export interface TransactionResponse {
type: string;
period: string;
export interface TransactionSummary {
avgAmount: number;
minAmount: number;
maxAmount: number;
}
export interface SubscriptionReport {
type: 'subscription';
period: 'daily' | 'weekly' | 'monthly';
startDate: string;
endDate: string;
merchantPartnerId?: number;
totalAmount: number;
totalCount: number;
items: TransactionItem[];
summary: {
avgAmount: number;
minAmount: number;
maxAmount: number;
};
items: SubscriptionItem[];
summary: SubscriptionSummary;
generatedAt: string;
}
@ -39,218 +54,115 @@ export interface SubscriptionItem {
count: number;
activeCount: number;
cancelledCount: number;
merchantPartnerId?: number;
}
export interface SubscriptionResponse {
type: string;
period: string;
startDate: string;
endDate: string;
merchantPartnerId?: number;
totalAmount: number;
totalCount: number;
items: SubscriptionItem[];
summary: {
export interface SubscriptionSummary {
avgAmount: number;
minAmount: number;
maxAmount: number;
};
generatedAt: string;
}
@Injectable({ providedIn: 'root' })
export interface SyncResponse {
message: string;
timestamp: string;
}
export interface ReportParams {
startDate?: string;
endDate?: string;
merchantPartnerId?: number;
}
@Injectable({
providedIn: 'root'
})
export class DcbReportingService {
private http = inject(HttpClient);
private reportingApiUrl = `${environment.reportingApiUrl}/reporting`;
private baseUrl = environment.reportingApiUrl + '/reporting';
/**
* Récupère les transactions journalières
*/
getDailyTransactions(
startDate?: string,
endDate?: string,
merchantPartnerId?: number
): Observable<TransactionItem[]> {
let params = new HttpParams();
constructor(private http: HttpClient) {}
if (startDate) params = params.set('startDate', startDate);
if (endDate) params = params.set('endDate', endDate);
if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString());
const url = `${this.reportingApiUrl}/transactions/daily`;
console.log('Fetching daily transactions from:', url, 'with params:', params.toString());
return this.http.get<TransactionResponse>(url, { params }).pipe(
map(response => {
console.log('Daily transactions raw response:', response);
return response?.items || [];
}),
catchError((error) => {
console.error('Error fetching daily transactions:', error);
return of([]);
})
healthCheck(endpoint: string): Observable<any> {
return this.http.get(`${this.baseUrl}/${endpoint}`, {
observe: 'response',
responseType: 'text'
}).pipe(
timeout(5000),
map(response => ({
status: 'success',
statusCode: response.status
}))
);
}
/**
* Récupère les transactions hebdomadaires
*/
getWeeklyTransactions(
startDate?: string,
endDate?: string,
merchantPartnerId?: number
): Observable<TransactionItem[]> {
let params = new HttpParams();
if (startDate) params = params.set('startDate', startDate);
if (endDate) params = params.set('endDate', endDate);
if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString());
const url = `${this.reportingApiUrl}/transactions/weekly`;
console.log('Fetching weekly transactions from:', url);
return this.http.get<TransactionResponse>(url, { params }).pipe(
map(response => {
console.log('Weekly transactions raw response:', response);
return response?.items || [];
}),
catchError((error) => {
console.error('Error fetching weekly transactions:', error);
return of([]);
})
syncHealthCheck(): Observable<any> {
return this.http.post(`${this.baseUrl}/sync/full`, {}, {
observe: 'response',
responseType: 'text'
}).pipe(
timeout(5000),
map(response => ({
status: 'success',
statusCode: response.status,
service: 'Synchronisation'
}))
);
}
/**
* Récupère les transactions mensuelles
*/
getMonthlyTransactions(
startDate?: string,
endDate?: string,
merchantPartnerId?: number
): Observable<TransactionItem[]> {
let params = new HttpParams();
// === TRANSACTIONS ===
getDailyTransactions(params?: ReportParams): Observable<TransactionReport> {
let httpParams = new HttpParams();
if (startDate) params = params.set('startDate', startDate);
if (endDate) params = params.set('endDate', endDate);
if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString());
const url = `${this.reportingApiUrl}/transactions/monthly`;
console.log('Fetching monthly transactions from:', url);
return this.http.get<TransactionResponse>(url, { params }).pipe(
map(response => {
console.log('Monthly transactions raw response:', response);
return response?.items || [];
}),
catchError((error) => {
console.error('Error fetching monthly transactions:', error);
return of([]);
})
);
if (params?.startDate) {
httpParams = httpParams.set('startDate', params.startDate);
}
if (params?.endDate) {
httpParams = httpParams.set('endDate', params.endDate);
}
if (params?.merchantPartnerId) {
httpParams = httpParams.set('merchantPartnerId', params.merchantPartnerId.toString());
}
/**
* Récupère les abonnements journaliers
*/
getDailySubscriptions(
startDate?: string,
endDate?: string,
merchantPartnerId?: number
): Observable<SubscriptionItem[]> {
let params = new HttpParams();
if (startDate) params = params.set('startDate', startDate);
if (endDate) params = params.set('endDate', endDate);
if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString());
const url = `${this.reportingApiUrl}/subscriptions/daily`;
console.log('Fetching daily subscriptions from:', url);
return this.http.get<SubscriptionResponse>(url, { params }).pipe(
map(response => {
console.log('Daily subscriptions raw response:', response);
return response?.items || [];
}),
catchError((error) => {
console.error('Error fetching daily subscriptions:', error);
return of([]);
})
);
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/daily`, { params: httpParams });
}
/**
* Récupère les abonnements hebdomadaires
*/
getWeeklySubscriptions(
startDate?: string,
endDate?: string,
merchantPartnerId?: number
): Observable<SubscriptionItem[]> {
let params = new HttpParams();
if (startDate) params = params.set('startDate', startDate);
if (endDate) params = params.set('endDate', endDate);
if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString());
const url = `${this.reportingApiUrl}/subscriptions/weekly`;
console.log('Fetching weekly subscriptions from:', url);
return this.http.get<SubscriptionResponse>(url, { params }).pipe(
map(response => {
console.log('Weekly subscriptions raw response:', response);
return response?.items || [];
}),
catchError((error) => {
console.error('Error fetching weekly subscriptions:', error);
return of([]);
})
);
getWeeklyTransactions(): Observable<TransactionReport> {
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/weekly`);
}
/**
* Récupère les abonnements mensuels
*/
getMonthlySubscriptions(
startDate?: string,
endDate?: string,
merchantPartnerId?: number
): Observable<SubscriptionItem[]> {
let params = new HttpParams();
if (startDate) params = params.set('startDate', startDate);
if (endDate) params = params.set('endDate', endDate);
if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString());
const url = `${this.reportingApiUrl}/subscriptions/monthly`;
console.log('Fetching monthly subscriptions from:', url);
return this.http.get<SubscriptionResponse>(url, { params }).pipe(
map(response => {
console.log('Monthly subscriptions raw response:', response);
return response?.items || [];
}),
catchError((error) => {
console.error('Error fetching monthly subscriptions:', error);
return of([]);
})
);
getMonthlyTransactions(): Observable<TransactionReport> {
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/monthly`);
}
/**
* Format une date au format YYYY-MM-DD
*/
getTransactionsWithDates(startDate: string, endDate: string): Observable<TransactionReport> {
const params = new HttpParams()
.set('startDate', startDate)
.set('endDate', endDate);
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/daily`, { params });
}
// === SUBSCRIPTIONS ===
getDailySubscriptions(): Observable<SubscriptionReport> {
return this.http.get<SubscriptionReport>(`${this.baseUrl}/subscriptions/daily`);
}
getMonthlySubscriptions(merchantPartnerId?: number): Observable<SubscriptionReport> {
let params = new HttpParams();
if (merchantPartnerId) {
params = params.set('merchantPartnerId', merchantPartnerId.toString());
}
return this.http.get<SubscriptionReport>(`${this.baseUrl}/subscriptions/monthly`, { params });
}
// === SYNC ===
triggerManualSync(): Observable<SyncResponse> {
return this.http.post<SyncResponse>(`${this.baseUrl}/sync/full`, {});
}
// === UTILS ===
formatDate(date: Date): string {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* Vide le cache
*/
clearCache(): void {
// Pas de cache dans cette version simplifiée
return date.toISOString().split('T')[0];
}
}

81
test_endpoints.bat Normal file
View File

@ -0,0 +1,81 @@
@echo off
set OUTPUT=api_results.txt
echo =========================================================================== > %OUTPUT%
echo RESULTATS DES TESTS API >> %OUTPUT%
echo =========================================================================== >> %OUTPUT%
echo Date: %date% >> %OUTPUT%
echo Heure: %time% >> %OUTPUT%
echo =========================================================================== >> %OUTPUT%
echo. >> %OUTPUT%
set i=1
:test1
echo [!i!] Test transactions/daily...
echo *** ENDPOINT !i! : Transactions journalieres (global) *** >> %OUTPUT%
curl -s https://api-reporting-service.dcb.pixpay.sn/api/v1/reporting/transactions/daily >> %OUTPUT%
echo. >> %OUTPUT% & echo. >> %OUTPUT%
set /a i+=1
:test2
echo [!i!] Test transactions/daily?merchantPartnerId=4...
echo *** ENDPOINT !i! : Transactions journalieres (merchant 4) *** >> %OUTPUT%
curl -s "https://api-reporting-service.dcb.pixpay.sn/api/v1/reporting/transactions/daily?merchantPartnerId=4" >> %OUTPUT%
echo. >> %OUTPUT% & echo. >> %OUTPUT%
set /a i+=1
:test3
echo [!i!] Test transactions/weekly...
echo *** ENDPOINT !i! : Transactions hebdomadaires *** >> %OUTPUT%
curl -s https://api-reporting-service.dcb.pixpay.sn/api/v1/reporting/transactions/weekly >> %OUTPUT%
echo. >> %OUTPUT% & echo. >> %OUTPUT%
set /a i+=1
:test4
echo [!i!] Test transactions/monthly...
echo *** ENDPOINT !i! : Transactions mensuelles *** >> %OUTPUT%
curl -s https://api-reporting-service.dcb.pixpay.sn/api/v1/reporting/transactions/monthly >> %OUTPUT%
echo. >> %OUTPUT% & echo. >> %OUTPUT%
set /a i+=1
:test5
echo [!i!] Test transactions avec dates...
echo *** ENDPOINT !i! : Transactions avec dates (01-30 nov 2024) *** >> %OUTPUT%
curl -s "https://api-reporting-service.dcb.pixpay.sn/api/v1/reporting/transactions/daily?startDate=2024-11-01&endDate=2024-11-30" >> %OUTPUT%
echo. >> %OUTPUT% & echo. >> %OUTPUT%
set /a i+=1
:test6
echo [!i!] Test subscriptions/daily...
echo *** ENDPOINT !i! : Subscriptions journalieres *** >> %OUTPUT%
curl -s https://api-reporting-service.dcb.pixpay.sn/api/v1/reporting/subscriptions/daily >> %OUTPUT%
echo. >> %OUTPUT% & echo. >> %OUTPUT%
set /a i+=1
:test7
echo [!i!] Test subscriptions/monthly?merchantPartnerId=4...
echo *** ENDPOINT !i! : Subscriptions mensuelles (merchant 4) *** >> %OUTPUT%
curl -s "https://api-reporting-service.dcb.pixpay.sn/api/v1/reporting/subscriptions/monthly?merchantPartnerId=4" >> %OUTPUT%
echo. >> %OUTPUT% & echo. >> %OUTPUT%
set /a i+=1
:test8
echo [!i!] Test sync/full (POST)...
echo *** ENDPOINT !i! : Synchronisation manuelle (POST) *** >> %OUTPUT%
curl -s -X POST https://api-reporting-service.dcb.pixpay.sn/api/v1/reporting/sync/full >> %OUTPUT%
echo. >> %OUTPUT% & echo. >> %OUTPUT%
echo =========================================================================== >> %OUTPUT%
echo TESTS TERMINES - !i! endpoints >> %OUTPUT%
echo =========================================================================== >> %OUTPUT%
echo.
echo ========================================
echo TOUS LES TESTS SONT TERMINES!
echo Resultats dans: %OUTPUT%
echo ========================================
echo.
echo Pour voir les resultats:
echo type %OUTPUT%
echo.
pause