feat: add DCB Reporting Dashboard - Comprehensive analytics system with real-time charts and API health monitoring
This commit is contained in:
parent
d46dbd7a0b
commit
cd9746a404
@ -5,7 +5,7 @@
|
||||
<div>
|
||||
<h1 class="h3 mb-0 text-primary">
|
||||
<ng-icon name="lucideLayoutDashboard" class="me-2"></ng-icon>
|
||||
Dashboard Reporting DCB
|
||||
Dashboard FinTech Reporting
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Surveillance en temps réel des transactions et abonnements</p>
|
||||
</div>
|
||||
@ -57,29 +57,24 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<div class="dropdown" ngbDropdown>
|
||||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
ngbDropdownToggle>
|
||||
<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()">
|
||||
<div class="dropdown-menu dropdown-menu-end" ngbDropdownMenu>
|
||||
<a class="dropdown-item" href="javascript:void(0)" (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()">
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="javascript:void(0)" (click)="loadAllData()">
|
||||
<ng-icon name="lucideRefreshCw" class="me-2"></ng-icon>
|
||||
Rafraîchir tout
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -278,75 +273,132 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== SECTION DES GRAPHIQUES ==================== -->
|
||||
<div class="charts-section mb-4">
|
||||
<!-- ==================== SECTION DES GRAPHIQUES FLEXIBLES ==================== -->
|
||||
<div class="charts-section mb-4" *ngIf="!loading.all">
|
||||
<div class="row g-4">
|
||||
<!-- Revenue Chart -->
|
||||
<!-- Graphique principal dynamique -->
|
||||
<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
|
||||
<ng-icon [name]="getMetricIcon(dataSelection.metric)" class="text-primary me-2"></ng-icon>
|
||||
{{ getChartTitle(dataSelection.metric) }}
|
||||
</h5>
|
||||
<p class="text-muted small mb-0">Courbe des revenus par période</p>
|
||||
<p class="text-muted small mb-0">Visualisation en temps réel</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
|
||||
<!-- Sélection de métrique -->
|
||||
<div class="dropdown" ngbDropdown>
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle"
|
||||
type="button"
|
||||
ngbDropdownToggle>
|
||||
<ng-icon [name]="getMetricIcon(dataSelection.metric)" class="me-1"></ng-icon>
|
||||
{{ getCurrentMetricLabel() }}
|
||||
</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 class="dropdown-menu dropdown-menu-end" ngbDropdownMenu>
|
||||
<a *ngFor="let metric of availableMetrics"
|
||||
class="dropdown-item d-flex align-items-center"
|
||||
href="javascript:void(0)"
|
||||
(click)="changeMetric(metric.id)">
|
||||
<ng-icon [name]="metric.icon" class="me-2"></ng-icon>
|
||||
{{ metric.label }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sélection de type de graphique -->
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button *ngFor="let type of availableChartTypes"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
[class.active]="dataSelection.chartType === type"
|
||||
(click)="changeChartType(type)"
|
||||
title="{{ type === 'line' ? 'Courbe' : type === 'bar' ? 'Barres' : type === 'pie' ? 'Camembert' : 'Anneau' }}">
|
||||
<ng-icon [name]="type === 'line' ? 'lucideTrendingUp' : type === 'bar' ? 'lucideBarChart3' : 'lucidePieChart'"></ng-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
(click)="refreshChartData()"
|
||||
[disabled]="loading.chart">
|
||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading.chart" class="me-1"></ng-icon>
|
||||
Rafraîchir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pt-0">
|
||||
<div *ngIf="loading.dailyTransactions" class="text-center py-5">
|
||||
<div *ngIf="loading.chart" class="text-center py-5">
|
||||
<div class="spinner-border text-primary"></div>
|
||||
<p class="mt-2 text-muted">Chargement des données...</p>
|
||||
<p class="mt-2 text-muted">Chargement du graphique...</p>
|
||||
</div>
|
||||
<div *ngIf="!loading.dailyTransactions && revenueChartData.length === 0"
|
||||
<div *ngIf="!loading.chart && !dailyTransactions"
|
||||
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>
|
||||
<button class="btn btn-sm btn-outline-primary mt-2" (click)="loadDailyTransactions()">
|
||||
Charger les données
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="!loading.dailyTransactions && revenueChartData.length > 0"
|
||||
<div *ngIf="!loading.chart && dailyTransactions"
|
||||
class="position-relative" style="height: 300px;">
|
||||
<canvas #revenueChart></canvas>
|
||||
<canvas #mainChartCanvas></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance & Santé -->
|
||||
<!-- Panneau droit avec 2 mini-graphiques -->
|
||||
<div class="col-xl-4">
|
||||
<div class="row h-100 g-4">
|
||||
<!-- Taux de succès -->
|
||||
<!-- Graphique de comparaison -->
|
||||
<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="lucideBarChart3" class="text-info me-2"></ng-icon>
|
||||
Comparaison Hebdo/Mens
|
||||
</h5>
|
||||
<small class="text-muted">Dernières 8 périodes</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pt-0">
|
||||
<div *ngIf="!weeklyTransactions || !monthlyTransactions"
|
||||
class="text-center py-4 text-muted">
|
||||
<ng-icon name="lucideBarChart3" class="fs-1 opacity-25 mb-2 d-block"></ng-icon>
|
||||
<small>Données de comparaison indisponibles</small>
|
||||
</div>
|
||||
<div *ngIf="weeklyTransactions && monthlyTransactions"
|
||||
class="position-relative" style="height: 180px;">
|
||||
<canvas #comparisonChartCanvas></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Taux de succès circulaire -->
|
||||
<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="lucideActivity" class="text-success me-2"></ng-icon>
|
||||
Performance
|
||||
Performance Globale
|
||||
</h5>
|
||||
<button class="btn btn-sm btn-outline-success btn-sm"
|
||||
(click)="refreshDailyTransactions()"
|
||||
[disabled]="loading.dailyTransactions">
|
||||
<ng-icon name="lucideRefreshCw" [class.spin]="loading.dailyTransactions"></ng-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-4">
|
||||
<div class="text-center mb-3">
|
||||
<div class="position-relative d-inline-block">
|
||||
<canvas #successRateChart width="120" height="120"></canvas>
|
||||
<canvas #successRateChartCanvas width="140" height="140"></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' }}%
|
||||
@ -356,37 +408,59 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row text-center">
|
||||
<div class="col-6">
|
||||
<div class="border-end">
|
||||
<div class="row text-center g-2">
|
||||
<div class="col-4">
|
||||
<div class="p-2 border rounded bg-success bg-opacity-10">
|
||||
<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="col-4">
|
||||
<div class="p-2 border rounded bg-danger bg-opacity-10">
|
||||
<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 class="col-4">
|
||||
<div class="p-2 border rounded bg-info bg-opacity-10">
|
||||
<div class="text-info fw-bold">
|
||||
{{ formatNumber(dailyTransactions?.items?.[0]?.pendingCount || 0) }}
|
||||
</div>
|
||||
<small class="text-muted">En attente</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Santé du système -->
|
||||
<div class="col-12">
|
||||
<div class="card h-100">
|
||||
<!-- ==================== SECTION SANTÉ DES APIS ==================== -->
|
||||
<div class="health-section mb-4">
|
||||
<div class="card">
|
||||
<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="lucideHeartPulse" class="text-danger me-2"></ng-icon>
|
||||
Santé du système
|
||||
<ng-icon name="lucideServer" class="text-dark me-2"></ng-icon>
|
||||
Santé des APIs DCB
|
||||
</h5>
|
||||
<p class="text-muted small mb-0">Statut en temps réel des services backend</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<div class="alert" [ngClass]="'alert-' + overallHealth.color">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon [name]="overallHealth.icon" class="me-2"></ng-icon>
|
||||
<small class="fw-medium">{{ overallHealth.message }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
(click)="refreshHealthCheck()"
|
||||
[disabled]="loading.healthCheck">
|
||||
@ -394,38 +468,58 @@
|
||||
</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 class="card-body">
|
||||
<div class="row g-3">
|
||||
<div *ngFor="let service of systemHealth" class="col-xl-3 col-md-6">
|
||||
<div class="card h-100" [ngClass]="'border-' + service.color">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<small class="text-muted me-2" *ngIf="service.responseTime">
|
||||
{{ service.responseTime }}ms
|
||||
</small>
|
||||
<span class="badge" [ngClass]="'bg-' + service.color">
|
||||
<h6 class="mb-0">{{ service.service }}</h6>
|
||||
<small class="text-muted">{{ service.url }}</small>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-items-end">
|
||||
<span class="badge mb-1" [ngClass]="'bg-' + service.color">
|
||||
{{ service.status }}
|
||||
</span>
|
||||
<small class="text-muted" [ngClass]="getStatusCodeClass(service.statusCode)">
|
||||
{{ service.statusCode }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<div>
|
||||
<small class="text-muted">
|
||||
<ng-icon name="lucideClock" class="me-1"></ng-icon>
|
||||
{{ formatTimeAgo(service.checkedAt) }}
|
||||
</small>
|
||||
</div>
|
||||
<div *ngIf="service.responseTime">
|
||||
<small class="text-muted" [ngClass]="getResponseTimeClass(service.responseTime)">
|
||||
{{ service.responseTime }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="service.error" class="mt-2">
|
||||
<small class="text-danger">
|
||||
<ng-icon name="lucideAlertTriangle" class="me-1"></ng-icon>
|
||||
{{ service.error }}
|
||||
</small>
|
||||
</div>
|
||||
</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) }}
|
||||
<ng-icon name="lucideInfo" class="me-1"></ng-icon>
|
||||
Vérification automatique toutes les 5 minutes
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== SECTION DES TABLEAUX ==================== -->
|
||||
<div class="tables-section">
|
||||
@ -456,28 +550,23 @@
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="ps-3">Date/Heure</th>
|
||||
<th class="ps-3">Période</th>
|
||||
<th class="text-end">Montant</th>
|
||||
<th class="text-end">Statut</th>
|
||||
<th class="text-end pe-3">Taux</th>
|
||||
<th class="text-end">Transactions</th>
|
||||
<th class="text-end pe-3">Succès</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>
|
||||
<small class="text-muted">{{ item.merchantPartnerId ? 'Merchant ' + item.merchantPartnerId : 'Tous' }}</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>
|
||||
<div>{{ formatNumber(item.count) }}</div>
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<span [ngClass]="getSuccessRateClass((item.count > 0 ? (item.successCount / item.count) * 100 : 0))">
|
||||
@ -499,7 +588,7 @@
|
||||
<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
|
||||
{{ (dailyTransactions?.items?.length || 0) }} périodes au total
|
||||
</small>
|
||||
<a href="#" class="btn btn-sm btn-outline-primary">Voir tout</a>
|
||||
</div>
|
||||
@ -564,7 +653,7 @@
|
||||
<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' }}
|
||||
Dernière vérification: {{ formatTimeAgo(alerts[0]?.timestamp) || 'Jamais' }}
|
||||
</small>
|
||||
<button class="btn btn-sm btn-outline-warning" (click)="checkSystemHealth()">
|
||||
<ng-icon name="lucideRefreshCw" class="me-1"></ng-icon>
|
||||
@ -584,11 +673,11 @@
|
||||
<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
|
||||
Dashboard FinTech v2.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
|
||||
APIs: IAM, Config, Core, Reporting
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<ng-icon name="lucideClock" class="me-1"></ng-icon>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,8 @@
|
||||
export type ReportPeriod = 'daily' | 'weekly' | 'monthly' | 'yearly';
|
||||
|
||||
export interface TransactionReport {
|
||||
type: 'transaction';
|
||||
period: 'daily' | 'weekly' | 'monthly';
|
||||
period: ReportPeriod;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
merchantPartnerId?: number;
|
||||
@ -12,7 +14,7 @@ export interface TransactionReport {
|
||||
}
|
||||
|
||||
export interface TransactionItem {
|
||||
period: string;
|
||||
period: string; // day, week, month or year label
|
||||
totalAmount: number;
|
||||
totalTax: number;
|
||||
count: number;
|
||||
@ -30,7 +32,7 @@ export interface TransactionSummary {
|
||||
|
||||
export interface SubscriptionReport {
|
||||
type: 'subscription';
|
||||
period: 'daily' | 'weekly' | 'monthly';
|
||||
period: ReportPeriod;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
merchantPartnerId?: number;
|
||||
@ -66,3 +68,21 @@ export interface ReportParams {
|
||||
endDate?: string;
|
||||
merchantPartnerId?: number;
|
||||
}
|
||||
|
||||
export interface HealthCheckStatus {
|
||||
service: string;
|
||||
url: string;
|
||||
status: 'UP' | 'DOWN';
|
||||
statusCode: number;
|
||||
checkedAt: string;
|
||||
}
|
||||
|
||||
// ChartDataNormalized : normalisation des données pour tous types de chart
|
||||
export interface ChartDataNormalized {
|
||||
labels: string[]; // Périodes : YYYY-MM-DD / YYYY-W05 / YYYY-MM / YYYY
|
||||
dataset: number[]; // Valeurs correspondantes à chaque période
|
||||
datasetLabel?: string; // Optionnel : nom de la métrique (ex: "Montant total", "Nombre transactions")
|
||||
backgroundColor?: string | string[]; // Optionnel : pour Bar/Pie/Heatmap
|
||||
borderColor?: string; // Optionnel : pour Line chart
|
||||
}
|
||||
|
||||
|
||||
@ -1,168 +1,296 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, timeout } from 'rxjs/operators';
|
||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
||||
import { Observable, throwError, forkJoin, of } from 'rxjs';
|
||||
import { map, timeout, catchError, retry } from 'rxjs/operators';
|
||||
import {
|
||||
ReportParams,
|
||||
TransactionReport,
|
||||
SubscriptionReport,
|
||||
SyncResponse,
|
||||
HealthCheckStatus,
|
||||
ChartDataNormalized
|
||||
} from '../models/dcb-reporting.models';
|
||||
import { environment } from '@environments/environment';
|
||||
|
||||
// ============ 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;
|
||||
}
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ReportService {
|
||||
private baseUrl = `${environment.reportingApiUrl}/reporting`;
|
||||
private readonly DEFAULT_TIMEOUT = 5000; // Timeout réduit pour les health checks
|
||||
private readonly DEFAULT_RETRY = 1;
|
||||
private readonly REPORTING_TIMEOUT = 10000; // Timeout normal pour les autres requêtes
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DcbReportingService {
|
||||
private baseUrl = environment.reportingApiUrl + '/reporting';
|
||||
// Configuration des APIs à scanner
|
||||
private apiEndpoints = {
|
||||
IAM: environment.iamApiUrl,
|
||||
CONFIG: environment.configApiUrl,
|
||||
CORE: environment.apiCoreUrl,
|
||||
REPORTING: environment.reportingApiUrl
|
||||
};
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
healthCheck(endpoint: string): Observable<any> {
|
||||
return this.http.get(`${this.baseUrl}/${endpoint}`, {
|
||||
observe: 'response',
|
||||
responseType: 'text'
|
||||
}).pipe(
|
||||
timeout(5000),
|
||||
map(response => ({
|
||||
status: 'success',
|
||||
statusCode: response.status
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
syncHealthCheck(): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/sync/full`, {}, {
|
||||
observe: 'response',
|
||||
responseType: 'text'
|
||||
}).pipe(
|
||||
timeout(5000),
|
||||
map(response => ({
|
||||
status: 'success',
|
||||
statusCode: response.status,
|
||||
service: 'Synchronisation'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// === TRANSACTIONS ===
|
||||
getDailyTransactions(params?: ReportParams): Observable<TransactionReport> {
|
||||
// ---------------------
|
||||
// Helpers
|
||||
// ---------------------
|
||||
private buildParams(params?: ReportParams): HttpParams {
|
||||
let httpParams = new HttpParams();
|
||||
|
||||
if (params?.startDate) {
|
||||
httpParams = httpParams.set('startDate', params.startDate);
|
||||
}
|
||||
if (params?.endDate) {
|
||||
httpParams = httpParams.set('endDate', params.endDate);
|
||||
}
|
||||
if (params?.merchantPartnerId) {
|
||||
httpParams = httpParams.set('merchantPartnerId', params.merchantPartnerId.toString());
|
||||
if (!params) return httpParams;
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) httpParams = httpParams.set(key, value.toString());
|
||||
});
|
||||
return httpParams;
|
||||
}
|
||||
|
||||
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/daily`, { params: httpParams });
|
||||
private handleError(err: any) {
|
||||
return throwError(() => ({
|
||||
message: err?.message || 'Unknown error',
|
||||
status: err?.status || 0,
|
||||
error: err?.error || null
|
||||
}));
|
||||
}
|
||||
|
||||
getWeeklyTransactions(): Observable<TransactionReport> {
|
||||
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/weekly`);
|
||||
}
|
||||
|
||||
getMonthlyTransactions(): Observable<TransactionReport> {
|
||||
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/monthly`);
|
||||
}
|
||||
|
||||
getTransactionsWithDates(startDate: string, endDate: string): Observable<TransactionReport> {
|
||||
const params = new HttpParams()
|
||||
.set('startDate', startDate)
|
||||
.set('endDate', endDate);
|
||||
|
||||
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/daily`, { params });
|
||||
}
|
||||
|
||||
// === SUBSCRIPTIONS ===
|
||||
getDailySubscriptions(): Observable<SubscriptionReport> {
|
||||
return this.http.get<SubscriptionReport>(`${this.baseUrl}/subscriptions/daily`);
|
||||
}
|
||||
|
||||
getMonthlySubscriptions(merchantPartnerId?: number): Observable<SubscriptionReport> {
|
||||
let params = new HttpParams();
|
||||
if (merchantPartnerId) {
|
||||
params = params.set('merchantPartnerId', merchantPartnerId.toString());
|
||||
}
|
||||
|
||||
return this.http.get<SubscriptionReport>(`${this.baseUrl}/subscriptions/monthly`, { params });
|
||||
}
|
||||
|
||||
// === SYNC ===
|
||||
triggerManualSync(): Observable<SyncResponse> {
|
||||
return this.http.post<SyncResponse>(`${this.baseUrl}/sync/full`, {});
|
||||
}
|
||||
|
||||
// === UTILS ===
|
||||
formatDate(date: Date): string {
|
||||
return date.toISOString().split('T')[0];
|
||||
const y = date.getFullYear();
|
||||
const m = ('0' + (date.getMonth() + 1)).slice(-2);
|
||||
const d = ('0' + date.getDate()).slice(-2);
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// Health checks
|
||||
// ---------------------
|
||||
|
||||
private checkApiAvailability(
|
||||
service: string,
|
||||
url: string
|
||||
): Observable<HealthCheckStatus> {
|
||||
const startTime = Date.now();
|
||||
|
||||
return this.http.head(url, {
|
||||
observe: 'response'
|
||||
}).pipe(
|
||||
timeout(this.DEFAULT_TIMEOUT),
|
||||
map((resp: HttpResponse<any>) => {
|
||||
const responseTime = Date.now() - startTime;
|
||||
return {
|
||||
service,
|
||||
url,
|
||||
status: 'UP' as const,
|
||||
statusCode: resp.status,
|
||||
checkedAt: new Date().toISOString(),
|
||||
responseTime: `${responseTime}ms`
|
||||
};
|
||||
}),
|
||||
catchError((error) => {
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
// Si HEAD échoue, essayez GET sur la racine
|
||||
return this.http.get(url, {
|
||||
observe: 'response'
|
||||
}).pipe(
|
||||
timeout(this.DEFAULT_TIMEOUT),
|
||||
map((resp: HttpResponse<any>) => {
|
||||
const finalResponseTime = Date.now() - startTime;
|
||||
return {
|
||||
service,
|
||||
url,
|
||||
status: 'UP' as const,
|
||||
statusCode: resp.status,
|
||||
checkedAt: new Date().toISOString(),
|
||||
responseTime: `${finalResponseTime}ms`,
|
||||
note: 'Used GET fallback'
|
||||
};
|
||||
}),
|
||||
catchError((finalError) => {
|
||||
const finalResponseTime = Date.now() - startTime;
|
||||
return of({
|
||||
service,
|
||||
url,
|
||||
status: 'DOWN' as const,
|
||||
statusCode: finalError.status || 0,
|
||||
checkedAt: new Date().toISOString(),
|
||||
responseTime: `${finalResponseTime}ms`,
|
||||
error: this.getErrorMessage(finalError)
|
||||
});
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private getErrorMessage(error: any): string {
|
||||
if (error.status === 0) {
|
||||
return 'Network error - Service unreachable';
|
||||
} else if (error.status === 404) {
|
||||
return 'Endpoint not found but service responds';
|
||||
} else if (error.status === 401 || error.status === 403) {
|
||||
return 'Service requires authentication';
|
||||
} else {
|
||||
return error.message || `HTTP ${error.status}`;
|
||||
}
|
||||
}
|
||||
|
||||
private checkApiHealth(
|
||||
service: string,
|
||||
url: string,
|
||||
method: 'GET' | 'POST' | 'HEAD'
|
||||
): Observable<HealthCheckStatus> {
|
||||
const startTime = Date.now();
|
||||
|
||||
return this.http.request(method, url, {
|
||||
observe: 'response',
|
||||
responseType: 'text'
|
||||
}).pipe(
|
||||
timeout(this.DEFAULT_TIMEOUT),
|
||||
retry(this.DEFAULT_RETRY),
|
||||
map((resp: HttpResponse<any>) => {
|
||||
const responseTime = Date.now() - startTime;
|
||||
return {
|
||||
service,
|
||||
url,
|
||||
status: resp.status >= 200 && resp.status < 500 ? 'UP' as const : 'DOWN' as const,
|
||||
statusCode: resp.status,
|
||||
checkedAt: new Date().toISOString(),
|
||||
responseTime: `${responseTime}ms`
|
||||
};
|
||||
}),
|
||||
catchError((error) => {
|
||||
const responseTime = Date.now() - startTime;
|
||||
return of({
|
||||
service,
|
||||
url,
|
||||
status: 'DOWN' as const,
|
||||
statusCode: error.status || 0,
|
||||
checkedAt: new Date().toISOString(),
|
||||
responseTime: `${responseTime}ms`,
|
||||
error: error.message || 'Connection failed'
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check global de toutes les APIs
|
||||
* Scanne chaque URL d'API directement
|
||||
*/
|
||||
globalHealthCheck(): Observable<HealthCheckStatus[]> {
|
||||
const healthChecks: Observable<HealthCheckStatus>[] = [];
|
||||
|
||||
// Vérifiez chaque service avec sa racine
|
||||
Object.entries(this.apiEndpoints).forEach(([service, url]) => {
|
||||
healthChecks.push(this.checkApiAvailability(service, url));
|
||||
});
|
||||
|
||||
return forkJoin(healthChecks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check spécifique pour l'API de synchronisation
|
||||
*/
|
||||
syncHealthCheck(): Observable<HealthCheckStatus> {
|
||||
return this.checkApiHealth('SYNC', `${this.baseUrl}/sync/full`, 'POST');
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check rapide (vérifie seulement si les APIs répondent)
|
||||
*/
|
||||
quickHealthCheck(): Observable<{ [key: string]: boolean }> {
|
||||
const services = Object.entries(this.apiEndpoints);
|
||||
const checks = services.map(([service, url]) =>
|
||||
this.checkApiHealth(service, url, 'GET').pipe(
|
||||
map(result => ({ [service]: result.status === 'UP' })),
|
||||
catchError(() => of({ [service]: false }))
|
||||
)
|
||||
);
|
||||
|
||||
return forkJoin(checks).pipe(
|
||||
map(results => {
|
||||
return results.reduce((acc, curr) => ({ ...acc, ...curr }), {});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check détaillé avec métriques
|
||||
*/
|
||||
detailedHealthCheck(): Observable<{
|
||||
summary: { total: number; up: number; down: number; timestamp: string };
|
||||
details: HealthCheckStatus[];
|
||||
}> {
|
||||
return this.globalHealthCheck().pipe(
|
||||
map(results => {
|
||||
const adjustedResults = results.map(result => ({
|
||||
...result,
|
||||
status: result.statusCode > 0 ? 'UP' as const : 'DOWN' as const
|
||||
}));
|
||||
|
||||
const upCount = adjustedResults.filter(r => r.status === 'UP').length;
|
||||
const downCount = adjustedResults.filter(r => r.status === 'DOWN').length;
|
||||
|
||||
return {
|
||||
summary: {
|
||||
total: adjustedResults.length,
|
||||
up: upCount,
|
||||
down: downCount,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
details: adjustedResults
|
||||
};
|
||||
}),
|
||||
catchError(err => this.handleError(err))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
triggerManualSync(): Observable<SyncResponse> {
|
||||
return this.http.post<SyncResponse>(`${this.baseUrl}/sync/full`, {}).pipe(
|
||||
timeout(this.REPORTING_TIMEOUT),
|
||||
retry(this.DEFAULT_RETRY),
|
||||
catchError(err => this.handleError(err))
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// Transactions & Subscriptions Reports
|
||||
// ---------------------
|
||||
getTransactionReport(params?: ReportParams, period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily'): Observable<TransactionReport> {
|
||||
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/${period}`, { params: this.buildParams(params) }).pipe(
|
||||
timeout(this.REPORTING_TIMEOUT),
|
||||
retry(this.DEFAULT_RETRY),
|
||||
catchError(err => this.handleError(err))
|
||||
);
|
||||
}
|
||||
|
||||
getSubscriptionReport(params?: ReportParams, period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily'): Observable<SubscriptionReport> {
|
||||
return this.http.get<SubscriptionReport>(`${this.baseUrl}/subscriptions/${period}`, { params: this.buildParams(params) }).pipe(
|
||||
timeout(this.REPORTING_TIMEOUT),
|
||||
retry(this.DEFAULT_RETRY),
|
||||
catchError(err => this.handleError(err))
|
||||
);
|
||||
}
|
||||
|
||||
getTransactionsWithDates(startDate: string, endDate: string, merchantPartnerId?: number) {
|
||||
return this.getTransactionReport({ startDate, endDate, merchantPartnerId }, 'daily');
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// Multi-partner normalization for charts
|
||||
// ---------------------
|
||||
normalizeForChart(data: TransactionReport | SubscriptionReport, key: string): ChartDataNormalized {
|
||||
const labels = data.items.map(i => i.period);
|
||||
const dataset = data.items.map(i => (i as any)[key] ?? 0);
|
||||
return { labels, dataset };
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// Multi-period & Multi-partner reporting
|
||||
// ---------------------
|
||||
getMultiPartnerReports(partners: number[], period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily', type: 'transaction' | 'subscription' = 'transaction') {
|
||||
const requests = partners.map(id => {
|
||||
return type === 'transaction'
|
||||
? this.getTransactionReport({ merchantPartnerId: id }, period)
|
||||
: this.getSubscriptionReport({ merchantPartnerId: id }, period);
|
||||
});
|
||||
return forkJoin(requests);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user