feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
e5b0368e8c
commit
ef0ba8237d
34
api_results.txt
Normal file
34
api_results.txt
Normal 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
8
package-lock.json
generated
@ -23,7 +23,7 @@
|
|||||||
"@fullcalendar/list": "^6.1.19",
|
"@fullcalendar/list": "^6.1.19",
|
||||||
"@fullcalendar/timegrid": "^6.1.19",
|
"@fullcalendar/timegrid": "^6.1.19",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
|
"@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/lucide": "^32.2.0",
|
||||||
"@ng-icons/tabler-icons": "^32.2.0",
|
"@ng-icons/tabler-icons": "^32.2.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
@ -3108,9 +3108,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ng-icons/core": {
|
"node_modules/@ng-icons/core": {
|
||||||
"version": "32.2.0",
|
"version": "32.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ng-icons/core/-/core-32.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ng-icons/core/-/core-32.5.0.tgz",
|
||||||
"integrity": "sha512-42S9QFH+FaigjXQp0QtWLHyJz8G8EaqJqcrK3qfZH4OyH86o32s3pkkf+lWnEtp2tR+07qNPSLYKCIgDE64Tug==",
|
"integrity": "sha512-6zAXQ5vryaclOWEVzprFJjJAW6NSOl0eBm+I6BwmcMk+vR+1vHU82DNpNTbUE9Wn4CGXEP1yd+S+pTKIaRTXjg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
|
|||||||
@ -26,7 +26,7 @@
|
|||||||
"@fullcalendar/list": "^6.1.19",
|
"@fullcalendar/list": "^6.1.19",
|
||||||
"@fullcalendar/timegrid": "^6.1.19",
|
"@fullcalendar/timegrid": "^6.1.19",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
|
"@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/lucide": "^32.2.0",
|
||||||
"@ng-icons/tabler-icons": "^32.2.0",
|
"@ng-icons/tabler-icons": "^32.2.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
1069
src/app/modules/dcb-dashboard/components/dcb-reporting-dashboard.ts
Normal file
1069
src/app/modules/dcb-dashboard/components/dcb-reporting-dashboard.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div class="row g-4 mb-4">
|
<div class="row g-4 mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<app-dashboard-report />
|
<app-dcb-reporting-dashboard></app-dcb-reporting-dashboard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1,17 +1,17 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
import { Dashboard } from './dcb-dashboard'
|
import { DcbDashboard } from './dcb-dashboard'
|
||||||
|
|
||||||
describe('Dashboard', () => {
|
describe('Dashboard', () => {
|
||||||
let component: Dashboard
|
let component: DcbDashboard
|
||||||
let fixture: ComponentFixture<Dashboard>
|
let fixture: ComponentFixture<DcbDashboard>
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [Dashboard],
|
imports: [DcbDashboard],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
fixture = TestBed.createComponent(Dashboard)
|
fixture = TestBed.createComponent(DcbDashboard)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
import { DashboardReport } from './components/dcb-dashboard-report';
|
import { DcbReportingDashboard } from './components/dcb-reporting-dashboard';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dcb-dashboard',
|
selector: 'app-dcb-dashboard',
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
DashboardReport,
|
DcbReportingDashboard,
|
||||||
],
|
],
|
||||||
templateUrl: './dcb-dashboard.html',
|
templateUrl: './dcb-dashboard.html',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
68
src/app/modules/dcb-dashboard/models/dcb-reporting.models.ts
Normal file
68
src/app/modules/dcb-dashboard/models/dcb-reporting.models.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +1,23 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map, catchError } from 'rxjs/operators';
|
import { map, timeout } from 'rxjs/operators';
|
||||||
import { environment } from '@environments/environment';
|
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 {
|
export interface TransactionItem {
|
||||||
period: string;
|
period: string;
|
||||||
totalAmount: number;
|
totalAmount: number;
|
||||||
@ -16,20 +29,22 @@ export interface TransactionItem {
|
|||||||
merchantPartnerId?: number;
|
merchantPartnerId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionResponse {
|
export interface TransactionSummary {
|
||||||
type: string;
|
avgAmount: number;
|
||||||
period: string;
|
minAmount: number;
|
||||||
|
maxAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionReport {
|
||||||
|
type: 'subscription';
|
||||||
|
period: 'daily' | 'weekly' | 'monthly';
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
merchantPartnerId?: number;
|
merchantPartnerId?: number;
|
||||||
totalAmount: number;
|
totalAmount: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
items: TransactionItem[];
|
items: SubscriptionItem[];
|
||||||
summary: {
|
summary: SubscriptionSummary;
|
||||||
avgAmount: number;
|
|
||||||
minAmount: number;
|
|
||||||
maxAmount: number;
|
|
||||||
};
|
|
||||||
generatedAt: string;
|
generatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,218 +54,115 @@ export interface SubscriptionItem {
|
|||||||
count: number;
|
count: number;
|
||||||
activeCount: number;
|
activeCount: number;
|
||||||
cancelledCount: number;
|
cancelledCount: number;
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionResponse {
|
|
||||||
type: string;
|
|
||||||
period: string;
|
|
||||||
startDate: string;
|
|
||||||
endDate: string;
|
|
||||||
merchantPartnerId?: number;
|
merchantPartnerId?: number;
|
||||||
totalAmount: number;
|
|
||||||
totalCount: number;
|
|
||||||
items: SubscriptionItem[];
|
|
||||||
summary: {
|
|
||||||
avgAmount: number;
|
|
||||||
minAmount: number;
|
|
||||||
maxAmount: number;
|
|
||||||
};
|
|
||||||
generatedAt: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
export class DcbReportingService {
|
export class DcbReportingService {
|
||||||
private http = inject(HttpClient);
|
private baseUrl = environment.reportingApiUrl + '/reporting';
|
||||||
private reportingApiUrl = `${environment.reportingApiUrl}/reporting`;
|
|
||||||
|
|
||||||
/**
|
constructor(private http: HttpClient) {}
|
||||||
* Récupère les transactions journalières
|
|
||||||
*/
|
|
||||||
getDailyTransactions(
|
|
||||||
startDate?: string,
|
|
||||||
endDate?: string,
|
|
||||||
merchantPartnerId?: number
|
|
||||||
): Observable<TransactionItem[]> {
|
|
||||||
let params = new HttpParams();
|
|
||||||
|
|
||||||
if (startDate) params = params.set('startDate', startDate);
|
healthCheck(endpoint: string): Observable<any> {
|
||||||
if (endDate) params = params.set('endDate', endDate);
|
return this.http.get(`${this.baseUrl}/${endpoint}`, {
|
||||||
if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString());
|
observe: 'response',
|
||||||
|
responseType: 'text'
|
||||||
const url = `${this.reportingApiUrl}/transactions/daily`;
|
}).pipe(
|
||||||
console.log('Fetching daily transactions from:', url, 'with params:', params.toString());
|
timeout(5000),
|
||||||
|
map(response => ({
|
||||||
return this.http.get<TransactionResponse>(url, { params }).pipe(
|
status: 'success',
|
||||||
map(response => {
|
statusCode: response.status
|
||||||
console.log('Daily transactions raw response:', response);
|
}))
|
||||||
return response?.items || [];
|
|
||||||
}),
|
|
||||||
catchError((error) => {
|
|
||||||
console.error('Error fetching daily transactions:', error);
|
|
||||||
return of([]);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
syncHealthCheck(): Observable<any> {
|
||||||
* Récupère les transactions hebdomadaires
|
return this.http.post(`${this.baseUrl}/sync/full`, {}, {
|
||||||
*/
|
observe: 'response',
|
||||||
getWeeklyTransactions(
|
responseType: 'text'
|
||||||
startDate?: string,
|
}).pipe(
|
||||||
endDate?: string,
|
timeout(5000),
|
||||||
merchantPartnerId?: number
|
map(response => ({
|
||||||
): Observable<TransactionItem[]> {
|
status: 'success',
|
||||||
let params = new HttpParams();
|
statusCode: response.status,
|
||||||
|
service: 'Synchronisation'
|
||||||
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([]);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// === TRANSACTIONS ===
|
||||||
* Récupère les transactions mensuelles
|
getDailyTransactions(params?: ReportParams): Observable<TransactionReport> {
|
||||||
*/
|
let httpParams = new HttpParams();
|
||||||
getMonthlyTransactions(
|
|
||||||
startDate?: string,
|
|
||||||
endDate?: string,
|
|
||||||
merchantPartnerId?: number
|
|
||||||
): Observable<TransactionItem[]> {
|
|
||||||
let params = new HttpParams();
|
|
||||||
|
|
||||||
if (startDate) params = params.set('startDate', startDate);
|
if (params?.startDate) {
|
||||||
if (endDate) params = params.set('endDate', endDate);
|
httpParams = httpParams.set('startDate', params.startDate);
|
||||||
if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString());
|
}
|
||||||
|
if (params?.endDate) {
|
||||||
|
httpParams = httpParams.set('endDate', params.endDate);
|
||||||
|
}
|
||||||
|
if (params?.merchantPartnerId) {
|
||||||
|
httpParams = httpParams.set('merchantPartnerId', params.merchantPartnerId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
const url = `${this.reportingApiUrl}/transactions/monthly`;
|
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/daily`, { params: httpParams });
|
||||||
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([]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getWeeklyTransactions(): Observable<TransactionReport> {
|
||||||
* Récupère les abonnements journaliers
|
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/weekly`);
|
||||||
*/
|
|
||||||
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([]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getMonthlyTransactions(): Observable<TransactionReport> {
|
||||||
* Récupère les abonnements hebdomadaires
|
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/monthly`);
|
||||||
*/
|
|
||||||
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([]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getTransactionsWithDates(startDate: string, endDate: string): Observable<TransactionReport> {
|
||||||
* Récupère les abonnements mensuels
|
const params = new HttpParams()
|
||||||
*/
|
.set('startDate', startDate)
|
||||||
getMonthlySubscriptions(
|
.set('endDate', endDate);
|
||||||
startDate?: string,
|
|
||||||
endDate?: string,
|
|
||||||
merchantPartnerId?: number
|
|
||||||
): Observable<SubscriptionItem[]> {
|
|
||||||
let params = new HttpParams();
|
|
||||||
|
|
||||||
if (startDate) params = params.set('startDate', startDate);
|
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/daily`, { params });
|
||||||
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([]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// === SUBSCRIPTIONS ===
|
||||||
* Format une date au format YYYY-MM-DD
|
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 {
|
formatDate(date: Date): string {
|
||||||
const year = date.getFullYear();
|
return date.toISOString().split('T')[0];
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
81
test_endpoints.bat
Normal file
81
test_endpoints.bat
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user