feat: add DCB Reporting Dashboard - Comprehensive analytics system with real-time charts and API health monitoring

This commit is contained in:
diallolatoile 2025-12-02 13:31:50 +00:00
parent d46dbd7a0b
commit cd9746a404
4 changed files with 1420 additions and 756 deletions

View File

@ -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()">
<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 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>
<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>
</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
</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>
<!-- 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-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">
<h5 class="card-title mb-0">
<ng-icon name="lucideActivity" class="text-success me-2"></ng-icon>
Performance
</h5>
<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 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,73 +408,115 @@
</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="text-danger fw-bold">
<ng-icon name="lucideXCircle" class="me-1"></ng-icon>
{{ formatNumber(dailyTransactions?.items?.[0]?.failedCount || 0) }}
<div class="col-4">
<div class="p-2 border rounded bg-danger bg-opacity-10">
<div class="text-danger fw-bold">
{{ 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>
<small class="text-muted">Échouées</small>
</div>
</div>
</div>
</div>
</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>
<!-- ==================== 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="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">
<ng-icon name="lucideRefreshCw" [class.spin]="loading.healthCheck"></ng-icon>
</button>
</div>
</div>
</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="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 class="d-flex justify-content-between align-items-start mb-2">
<div>
<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="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) }}
<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="lucideInfo" class="me-1"></ng-icon>
Vérification automatique toutes les 5 minutes
</small>
</div>
</div>
</div>
</div>
@ -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>

View File

@ -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
}

View File

@ -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());
}
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/daily`, { params: httpParams });
if (!params) return httpParams;
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) httpParams = httpParams.set(key, value.toString());
});
return httpParams;
}
getWeeklyTransactions(): Observable<TransactionReport> {
return this.http.get<TransactionReport>(`${this.baseUrl}/transactions/weekly`);
private handleError(err: any) {
return throwError(() => ({
message: err?.message || 'Unknown error',
status: err?.status || 0,
error: err?.error || null
}));
}
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);
}
}