feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
02d58ba4fa
commit
296fe413a9
@ -62,13 +62,13 @@ export class AuthService {
|
||||
private readonly tokenKey = 'access_token';
|
||||
private readonly refreshTokenKey = 'refresh_token';
|
||||
|
||||
private readonly dashboardAccessService = inject(DashboardAccessService);
|
||||
private readonly transactionAccessService = inject(TransactionAccessService);
|
||||
|
||||
private authState$ = new BehaviorSubject<boolean>(this.isAuthenticated());
|
||||
private userProfile$ = new BehaviorSubject<User | null>(null);
|
||||
private initialized$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
private readonly dashboardAccessService = inject(DashboardAccessService);
|
||||
private readonly transactionAccessService = inject(TransactionAccessService);
|
||||
|
||||
// === INITIALISATION DE L'APPLICATION ===
|
||||
|
||||
/**
|
||||
|
||||
@ -128,7 +128,6 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
* Charge le profil utilisateur explicitement
|
||||
*/
|
||||
loadUserProfile(): void {
|
||||
console.log('🚀 Loading user profile...');
|
||||
this.isLoading = true;
|
||||
this.hasError = false;
|
||||
this.cdr.detectChanges();
|
||||
@ -137,7 +136,6 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (profile) => {
|
||||
console.log('✅ Profile loaded successfully:', profile.username);
|
||||
// Note: le profil sera automatiquement mis à jour via la subscription getUserProfile()
|
||||
this.isLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
@ -240,13 +238,7 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
* Obtient l'URL de l'avatar de l'utilisateur
|
||||
*/
|
||||
getUserAvatar(): string {
|
||||
if (!this.user) {
|
||||
return 'assets/images/users/user-default.jpg';
|
||||
}
|
||||
|
||||
// Vous pouvez implémenter une logique pour générer un avatar personnalisé
|
||||
// ou utiliser une image par défaut basée sur l'email/nom
|
||||
return `assets/images/users/user-${(this.user.id?.charCodeAt(0) % 5) + 1}.jpg`;
|
||||
return `assets/images/users/user-2.jpg`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -254,7 +246,7 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
onAvatarError(event: Event): void {
|
||||
const img = event.target as HTMLImageElement;
|
||||
img.src = 'assets/images/users/user-default.jpg';
|
||||
img.onerror = null; // Éviter les boucles infinies
|
||||
img.src = 'assets/images/users/user-2.jpg';
|
||||
img.onerror = null;
|
||||
}
|
||||
}
|
||||
@ -1,768 +0,0 @@
|
||||
<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 FinTech Reporting
|
||||
<span *ngIf="access.isMerchantUser" class="badge bg-success ms-2">
|
||||
<ng-icon name="lucideStore" class="me-1"></ng-icon>
|
||||
Merchant {{ merchantId }}
|
||||
</span>
|
||||
<span *ngIf="access.isHubUser" class="badge bg-primary ms-2">
|
||||
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||
Hub Admin
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
<ng-icon [name]="currentRoleIcon" class="me-1"></ng-icon>
|
||||
{{ currentRoleLabel }} - {{ getCurrentMerchantName() }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<!-- Contrôles rapides -->
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<!-- Bouton Actualiser selon le type -->
|
||||
<button class="btn btn-outline-primary btn-sm"
|
||||
(click)="refreshData()"
|
||||
[disabled]="loading.globalData || loading.merchantData">
|
||||
<ng-icon name="lucideRefreshCw"
|
||||
[class.spin]="loading.globalData || loading.merchantData"
|
||||
class="me-1"></ng-icon>
|
||||
{{ (loading.globalData || loading.merchantData) ? 'Chargement...' : 'Actualiser' }}
|
||||
</button>
|
||||
|
||||
<!-- Bouton Sync seulement si autorisé -->
|
||||
<button *ngIf="canTriggerSync()"
|
||||
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 selon le type -->
|
||||
<div *ngIf="canSelectMerchant() && shouldShowMerchantSelector()" class="filters-card">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<!-- Filtre Merchant pour hub users -->
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-light">
|
||||
<ng-icon name="lucideStore"></ng-icon>
|
||||
</span>
|
||||
|
||||
<select class="form-control form-control-sm"
|
||||
[ngModel]="merchantId"
|
||||
(ngModelChange)="selectMerchant($event)"
|
||||
style="width: 180px;">
|
||||
<option [value]="undefined">
|
||||
<ng-icon name="lucideGlobe" class="me-1"></ng-icon>
|
||||
Données globales
|
||||
</option>
|
||||
<option *ngFor="let merchant of allowedMerchants" [value]="merchant.id">
|
||||
{{ merchant.name }} (ID: {{ merchant.id }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Badge de contexte -->
|
||||
<div class="badge" [ngClass]="isViewingGlobal() ? 'bg-info' : 'bg-success'">
|
||||
<ng-icon [name]="isViewingGlobal() ? 'lucideGlobe' : 'lucideStore'" class="me-1"></ng-icon>
|
||||
{{ getCurrentMerchantName() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options dropdown seulement pour hub users admin -->
|
||||
<div *ngIf="access.isHubUser && canManageMerchants()" ngbDropdown class="dropdown">
|
||||
<!-- Bouton déclencheur -->
|
||||
<button class="btn btn-outline-secondary dropdown-toggle"
|
||||
ngbDropdownToggle
|
||||
type="button">
|
||||
<ng-icon name="lucideSettings" class="me-2"></ng-icon>
|
||||
Options
|
||||
</button>
|
||||
|
||||
<!-- Menu déroulant -->
|
||||
<div class="dropdown-menu dropdown-menu-end" ngbDropdownMenu>
|
||||
<button ngbDropdownItem (click)="checkSystemHealth()">
|
||||
<ng-icon name="lucideHeartPulse" class="me-2"></ng-icon>
|
||||
Vérifier la santé
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button ngbDropdownItem (click)="refreshData()">
|
||||
<ng-icon name="lucideRefreshCw" class="me-2"></ng-icon>
|
||||
Rafraîchir tout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Barre d'état rapide -->
|
||||
<div class="status-bar mb-4">
|
||||
<div class="row g-2">
|
||||
<!-- Info rôle -->
|
||||
<div class="col-auto">
|
||||
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||
<ng-icon [name]="getRoleStatusIcon()" [class]="getRoleStatusColor()"></ng-icon>
|
||||
<small>{{ currentRoleLabel }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode d'affichage -->
|
||||
<div class="col-auto">
|
||||
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||
<ng-icon [name]="isViewingGlobal() ? 'lucideGlobe' : 'lucideStore'"
|
||||
[class]="isViewingGlobal() ? 'text-info' : 'text-success'"></ng-icon>
|
||||
<small>{{ getCurrentMerchantName() }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Services en ligne seulement pour hub users -->
|
||||
<div *ngIf="shouldShowSystemHealth()" 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>
|
||||
|
||||
<!-- Info merchant pour merchant users -->
|
||||
<div *ngIf="access.isMerchantUser" class="col-auto">
|
||||
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||
<ng-icon name="lucideStore" class="text-success"></ng-icon>
|
||||
<small>Merchant ID: {{ merchantId }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Merchant sélectionné pour hub users -->
|
||||
<div *ngIf="access.isHubUser && isViewingMerchant()" class="col-auto">
|
||||
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||
<ng-icon name="lucideStore" class="text-info"></ng-icon>
|
||||
<small>Merchant ID: {{ merchantId }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message d'erreur si pas de permissions -->
|
||||
<div *ngIf="!shouldShowTransactions()"
|
||||
class="alert alert-warning mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||
<div class="flex-grow-1">
|
||||
<strong>Permissions insuffisantes</strong>
|
||||
<div class="text-muted small">Vous n'avez pas les permissions nécessaires pour voir les données.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message de sync seulement si autorisé à voir les alertes -->
|
||||
<div *ngIf="syncResponse && shouldShowAlerts()" class="alert alert-success alert-dismissible fade show mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideCheckCircle2" 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 *ngIf="shouldShowKPIs()" 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 Mensuel -->
|
||||
<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().monthly.transactions) }}</h4>
|
||||
<small class="text-muted">Mensuel</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().monthly.revenue) }}</span>
|
||||
<span class="badge bg-info bg-opacity-25 text-info">
|
||||
<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="lucideCheckCircle2" 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 FLEXIBLES ==================== -->
|
||||
<div *ngIf="shouldShowCharts() && !loading.globalData && !loading.merchantData" class="charts-section mb-4">
|
||||
<div class="row g-4">
|
||||
<!-- 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]="getMetricIcon(dataSelection.metric)" class="text-primary me-2"></ng-icon>
|
||||
{{ getChartTitle(dataSelection.metric) }}
|
||||
<span *ngIf="isViewingMerchant()" class="badge bg-success ms-2">
|
||||
<ng-icon name="lucideStore" class="me-1"></ng-icon>
|
||||
Merchant {{ merchantId }}
|
||||
</span>
|
||||
<span *ngIf="isViewingGlobal()" class="badge bg-info ms-2">
|
||||
<ng-icon name="lucideGlobe" class="me-1"></ng-icon>
|
||||
Données globales
|
||||
</span>
|
||||
</h5>
|
||||
<p class="text-muted small mb-0">Visualisation en temps réel</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<!-- 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.chart" class="text-center py-5">
|
||||
<div class="spinner-border text-primary"></div>
|
||||
<p class="mt-2 text-muted">Chargement du graphique...</p>
|
||||
</div>
|
||||
<div *ngIf="!loading.chart && !getCurrentTransactionData()"
|
||||
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)="refreshData()">
|
||||
Charger les données
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="!loading.chart && getCurrentTransactionData()"
|
||||
class="position-relative" style="height: 300px;">
|
||||
<canvas #mainChartCanvas></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panneau droit avec 2 mini-graphiques -->
|
||||
<div class="col-xl-4">
|
||||
<div class="row h-100 g-4">
|
||||
<!-- Graphique de comparaison seulement pour hub users en mode global -->
|
||||
<div *ngIf="shouldShowChart('comparison')" 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 *ngIf="shouldShowChart('successRate')" 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
|
||||
</h5>
|
||||
<button class="btn btn-sm btn-outline-success btn-sm"
|
||||
(click)="refreshData()"
|
||||
[disabled]="loading.globalData || loading.merchantData">
|
||||
<ng-icon name="lucideRefreshCw"
|
||||
[class.spin]="loading.globalData || loading.merchantData"></ng-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-3">
|
||||
<div class="position-relative d-inline-block">
|
||||
<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' }}%
|
||||
</h3>
|
||||
<small class="text-muted">Taux de succès</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{{ formatNumber(getCurrentTransactionData()?.items?.[0]?.successCount || 0) }}
|
||||
</div>
|
||||
<small class="text-muted">Réussies</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="p-2 border rounded bg-danger bg-opacity-10">
|
||||
<div class="text-danger fw-bold">
|
||||
{{ formatNumber(getCurrentTransactionData()?.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(getCurrentTransactionData()?.items?.[0]?.pendingCount || 0) }}
|
||||
</div>
|
||||
<small class="text-muted">En attente</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== SECTION SANTÉ DES APIS ==================== -->
|
||||
<div *ngIf="shouldShowSystemHealth()" 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)="checkSystemHealth()"
|
||||
[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="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="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>
|
||||
|
||||
<!-- ==================== SECTION DES TABLEAUX ==================== -->
|
||||
<div *ngIf="shouldShowTransactions()" 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
|
||||
<span class="badge ms-2"
|
||||
[ngClass]="isViewingGlobal() ? 'bg-info' : 'bg-success'">
|
||||
{{ getCurrentMerchantName() }}
|
||||
</span>
|
||||
</h5>
|
||||
<p class="text-muted small mb-0">Dernières 24 heures</p>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
(click)="refreshData()"
|
||||
[disabled]="loading.globalData || loading.merchantData">
|
||||
<ng-icon name="lucideRefreshCw"
|
||||
[class.spin]="loading.globalData || loading.merchantData"></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">Période</th>
|
||||
<th class="text-end">Montant</th>
|
||||
<th class="text-end">Transactions</th>
|
||||
<th class="text-end pe-3">Succès</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of (getCurrentTransactionData()?.items?.slice(0, 5) || [])">
|
||||
<td class="ps-3">
|
||||
<div>{{ item.period }}</div>
|
||||
<small class="text-muted">{{ isViewingGlobal() ? 'Tous merchants' : 'Merchant ' + merchantId }}</small>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="fw-medium">{{ formatCurrency(item.totalAmount) }}</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div>{{ formatNumber(item.count) }}</div>
|
||||
</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="!getCurrentTransactionData()?.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>
|
||||
{{ getCurrentTransactionData()?.items?.length || 0 }} périodes au total
|
||||
</small>
|
||||
<a href="#" class="btn btn-sm btn-outline-primary">Voir tout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alertes système - Masqué si pas autorisé -->
|
||||
<div *ngIf="shouldShowAlerts()" 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="lucideCheckCircle2"
|
||||
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="lucideCheckCircle2" 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: {{ 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>
|
||||
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 FinTech v2.0 •
|
||||
<ng-icon [name]="currentRoleIcon" class="me-1 ms-2"></ng-icon>
|
||||
{{ currentRoleLabel }}
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<ng-icon [name]="isViewingGlobal() ? 'lucideGlobe' : 'lucideStore'" class="me-1"></ng-icon>
|
||||
{{ getCurrentMerchantName() }}
|
||||
</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>
|
||||
@ -1,13 +0,0 @@
|
||||
<div class="container-fluid">
|
||||
<app-page-title
|
||||
title="Payment Aggregation Hub"
|
||||
subTitle="Monitoring en temps réel des paiements mobiles DCB"
|
||||
[badge]="{icon: 'lucideSmartphone', text: 'Direct Carrier Billing'}"
|
||||
/>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12">
|
||||
<app-dcb-reporting-dashboard></app-dcb-reporting-dashboard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,22 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { DcbDashboard } from './dcb-dashboard'
|
||||
|
||||
describe('Dashboard', () => {
|
||||
let component: DcbDashboard
|
||||
let fixture: ComponentFixture<DcbDashboard>
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DcbDashboard],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(DcbDashboard)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@ -1,15 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { PageTitle } from '@app/components/page-title/page-title';
|
||||
import { DcbReportingDashboard } from './components/dcb-reporting-dashboard';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dcb-dashboard',
|
||||
imports: [
|
||||
CommonModule,
|
||||
PageTitle,
|
||||
DcbReportingDashboard,
|
||||
],
|
||||
templateUrl: './dcb-dashboard.html',
|
||||
})
|
||||
export class DcbDashboard {}
|
||||
785
src/app/modules/dcb-dashboard/dcb-reporting-dashboard.html
Normal file
785
src/app/modules/dcb-dashboard/dcb-reporting-dashboard.html
Normal file
@ -0,0 +1,785 @@
|
||||
<div class="container-fluid">
|
||||
<app-page-title
|
||||
title="Payment Aggregation Hub"
|
||||
subTitle="Monitoring en temps réel des paiements mobiles DCB"
|
||||
[badge]="{icon: 'lucideSmartphone', text: 'Direct Carrier Billing'}"
|
||||
/>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12">
|
||||
<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 FinTech Reporting
|
||||
<span *ngIf="access.isMerchantUser" class="badge bg-success ms-2">
|
||||
<ng-icon name="lucideStore" class="me-1"></ng-icon>
|
||||
<div *ngIf="isValidMerchantId(merchantId)">
|
||||
Merchant ID: {{ merchantId }}
|
||||
</div>
|
||||
<div *ngIf="!isValidMerchantId(merchantId)">
|
||||
Aucun merchant sélectionné
|
||||
</div>
|
||||
</span>
|
||||
<span *ngIf="access.isHubUser" class="badge bg-primary ms-2">
|
||||
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||
Hub Admin
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
<ng-icon [name]="currentRoleIcon" class="me-1"></ng-icon>
|
||||
{{ currentRoleLabel }} - {{ getCurrentMerchantName() }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<!-- Contrôles rapides -->
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<!-- Bouton Actualiser selon le type -->
|
||||
<button class="btn btn-outline-primary btn-sm"
|
||||
(click)="refreshData()"
|
||||
[disabled]="loading.globalData || loading.merchantData">
|
||||
<ng-icon name="lucideRefreshCw"
|
||||
[class.spin]="loading.globalData || loading.merchantData"
|
||||
class="me-1"></ng-icon>
|
||||
{{ (loading.globalData || loading.merchantData) ? 'Chargement...' : 'Actualiser' }}
|
||||
</button>
|
||||
|
||||
<!-- Bouton Sync seulement si autorisé -->
|
||||
<button *ngIf="canTriggerSync()"
|
||||
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 selon le type -->
|
||||
<div *ngIf="canSelectMerchant() && shouldShowMerchantSelector()" class="filters-card">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<!-- Filtre Merchant pour hub users -->
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-light">
|
||||
<ng-icon name="lucideStore"></ng-icon>
|
||||
</span>
|
||||
|
||||
<select class="form-control form-control-sm"
|
||||
[ngModel]="merchantId"
|
||||
(ngModelChange)="selectMerchant($event)"
|
||||
style="width: 180px;">
|
||||
<option [value]="undefined">
|
||||
<ng-icon name="lucideGlobe" class="me-1"></ng-icon>
|
||||
Données globales
|
||||
</option>
|
||||
<option *ngFor="let merchant of allowedMerchants" [value]="merchant.id">
|
||||
{{ merchant.name }} (ID: {{ merchant.id }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Badge de contexte -->
|
||||
<div class="badge" [ngClass]="isViewingGlobal() ? 'bg-info' : 'bg-success'">
|
||||
<ng-icon [name]="isViewingGlobal() ? 'lucideGlobe' : 'lucideStore'" class="me-1"></ng-icon>
|
||||
{{ getCurrentMerchantName() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options dropdown seulement pour hub users admin -->
|
||||
<div *ngIf="access.isHubUser && canManageMerchants()" ngbDropdown class="dropdown">
|
||||
<!-- Bouton déclencheur -->
|
||||
<button class="btn btn-outline-secondary dropdown-toggle"
|
||||
ngbDropdownToggle
|
||||
type="button">
|
||||
<ng-icon name="lucideSettings" class="me-2"></ng-icon>
|
||||
Options
|
||||
</button>
|
||||
|
||||
<!-- Menu déroulant -->
|
||||
<div class="dropdown-menu dropdown-menu-end" ngbDropdownMenu>
|
||||
<button ngbDropdownItem (click)="checkSystemHealth()">
|
||||
<ng-icon name="lucideHeartPulse" class="me-2"></ng-icon>
|
||||
Vérifier la santé
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button ngbDropdownItem (click)="refreshData()">
|
||||
<ng-icon name="lucideRefreshCw" class="me-2"></ng-icon>
|
||||
Rafraîchir tout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Barre d'état rapide -->
|
||||
<div class="status-bar mb-4">
|
||||
<div class="row g-2">
|
||||
<!-- Info rôle -->
|
||||
<div class="col-auto">
|
||||
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||
<ng-icon [name]="getRoleStatusIcon()" [class]="getRoleStatusColor()"></ng-icon>
|
||||
<small>{{ currentRoleLabel }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode d'affichage -->
|
||||
<div class="col-auto">
|
||||
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||
<ng-icon [name]="isViewingGlobal() ? 'lucideGlobe' : 'lucideStore'"
|
||||
[class]="isViewingGlobal() ? 'text-info' : 'text-success'"></ng-icon>
|
||||
<small>{{ getCurrentMerchantName() }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Services en ligne seulement pour hub users -->
|
||||
<div *ngIf="shouldShowSystemHealth()" 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>
|
||||
|
||||
<!-- Info merchant pour merchant users -->
|
||||
<div *ngIf="access.isMerchantUser" class="col-auto">
|
||||
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||
<ng-icon name="lucideStore" class="text-success"></ng-icon>
|
||||
<small>Merchant ID: {{ merchantId }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Merchant sélectionné pour hub users -->
|
||||
<div *ngIf="access.isHubUser && isViewingMerchant()" class="col-auto">
|
||||
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||
<ng-icon name="lucideStore" class="text-info"></ng-icon>
|
||||
<small>Merchant ID: {{ merchantId }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message d'erreur si pas de permissions -->
|
||||
<div *ngIf="!shouldShowTransactions()"
|
||||
class="alert alert-warning mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||
<div class="flex-grow-1">
|
||||
<strong>Permissions insuffisantes</strong>
|
||||
<div class="text-muted small">Vous n'avez pas les permissions nécessaires pour voir les données.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message de sync seulement si autorisé à voir les alertes -->
|
||||
<div *ngIf="syncResponse && shouldShowAlerts()" class="alert alert-success alert-dismissible fade show mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideCheckCircle2" 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 *ngIf="shouldShowKPIs()" 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 Mensuel -->
|
||||
<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().monthly.transactions) }}</h4>
|
||||
<small class="text-muted">Mensuel</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().monthly.revenue) }}</span>
|
||||
<span class="badge bg-info bg-opacity-25 text-info">
|
||||
<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="lucideCheckCircle2" 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 FLEXIBLES ==================== -->
|
||||
<div *ngIf="shouldShowCharts() && !loading.globalData && !loading.merchantData" class="charts-section mb-4">
|
||||
<div class="row g-4">
|
||||
<!-- 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]="getMetricIcon(dataSelection.metric)" class="text-primary me-2"></ng-icon>
|
||||
{{ getChartTitle(dataSelection.metric) }}
|
||||
<span *ngIf="isViewingMerchant()" class="badge bg-success ms-2">
|
||||
<ng-icon name="lucideStore" class="me-1"></ng-icon>
|
||||
Merchant {{ merchantId }}
|
||||
</span>
|
||||
<span *ngIf="isViewingGlobal()" class="badge bg-info ms-2">
|
||||
<ng-icon name="lucideGlobe" class="me-1"></ng-icon>
|
||||
Données globales
|
||||
</span>
|
||||
</h5>
|
||||
<p class="text-muted small mb-0">Visualisation en temps réel</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<!-- 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.chart" class="text-center py-5">
|
||||
<div class="spinner-border text-primary"></div>
|
||||
<p class="mt-2 text-muted">Chargement du graphique...</p>
|
||||
</div>
|
||||
<div *ngIf="!loading.chart && !getCurrentTransactionData()"
|
||||
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)="refreshData()">
|
||||
Charger les données
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="!loading.chart && getCurrentTransactionData()"
|
||||
class="position-relative" style="height: 300px;">
|
||||
<canvas #mainChartCanvas></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panneau droit avec 2 mini-graphiques -->
|
||||
<div class="col-xl-4">
|
||||
<div class="row h-100 g-4">
|
||||
<!-- Graphique de comparaison seulement pour hub users en mode global -->
|
||||
<div *ngIf="shouldShowChart('comparison')" 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 *ngIf="shouldShowChart('successRate')" 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
|
||||
</h5>
|
||||
<button class="btn btn-sm btn-outline-success btn-sm"
|
||||
(click)="refreshData()"
|
||||
[disabled]="loading.globalData || loading.merchantData">
|
||||
<ng-icon name="lucideRefreshCw"
|
||||
[class.spin]="loading.globalData || loading.merchantData"></ng-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-3">
|
||||
<div class="position-relative d-inline-block">
|
||||
<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' }}%
|
||||
</h3>
|
||||
<small class="text-muted">Taux de succès</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{{ formatNumber(getCurrentTransactionData()?.items?.[0]?.successCount || 0) }}
|
||||
</div>
|
||||
<small class="text-muted">Réussies</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="p-2 border rounded bg-danger bg-opacity-10">
|
||||
<div class="text-danger fw-bold">
|
||||
{{ formatNumber(getCurrentTransactionData()?.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(getCurrentTransactionData()?.items?.[0]?.pendingCount || 0) }}
|
||||
</div>
|
||||
<small class="text-muted">En attente</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== SECTION SANTÉ DES APIS ==================== -->
|
||||
<div *ngIf="shouldShowSystemHealth()" 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)="checkSystemHealth()"
|
||||
[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="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="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>
|
||||
|
||||
<!-- ==================== SECTION DES TABLEAUX ==================== -->
|
||||
<div *ngIf="shouldShowTransactions()" 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
|
||||
<span class="badge ms-2"
|
||||
[ngClass]="isViewingGlobal() ? 'bg-info' : 'bg-success'">
|
||||
{{ getCurrentMerchantName() }}
|
||||
</span>
|
||||
</h5>
|
||||
<p class="text-muted small mb-0">Dernières 24 heures</p>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
(click)="refreshData()"
|
||||
[disabled]="loading.globalData || loading.merchantData">
|
||||
<ng-icon name="lucideRefreshCw"
|
||||
[class.spin]="loading.globalData || loading.merchantData"></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">Période</th>
|
||||
<th class="text-end">Montant</th>
|
||||
<th class="text-end">Transactions</th>
|
||||
<th class="text-end pe-3">Succès</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of (getCurrentTransactionData()?.items?.slice(0, 5) || [])">
|
||||
<td class="ps-3">
|
||||
<div>{{ item.period }}</div>
|
||||
<small class="text-muted">{{ isViewingGlobal() ? 'Tous merchants' : 'Merchant ' + merchantId }}</small>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="fw-medium">{{ formatCurrency(item.totalAmount) }}</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div>{{ formatNumber(item.count) }}</div>
|
||||
</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="!getCurrentTransactionData()?.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>
|
||||
{{ getCurrentTransactionData()?.items?.length || 0 }} périodes au total
|
||||
</small>
|
||||
<a href="#" class="btn btn-sm btn-outline-primary">Voir tout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alertes système - Masqué si pas autorisé -->
|
||||
<div *ngIf="shouldShowAlerts()" 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="lucideCheckCircle2"
|
||||
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="lucideCheckCircle2" 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: {{ 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>
|
||||
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 FinTech v2.0 •
|
||||
<ng-icon [name]="currentRoleIcon" class="me-1 ms-2"></ng-icon>
|
||||
{{ currentRoleLabel }}
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<ng-icon [name]="isViewingGlobal() ? 'lucideGlobe' : 'lucideStore'" class="me-1"></ng-icon>
|
||||
{{ getCurrentMerchantName() }}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -34,10 +34,11 @@ import {
|
||||
HealthCheckStatus,
|
||||
ChartDataNormalized,
|
||||
ReportPeriod
|
||||
} from '../models/dcb-reporting.models';
|
||||
import { ReportService } from '../services/dcb-reporting.service';
|
||||
import { DashboardAccess, AllowedMerchant, DashboardAccessService } from '../services/dashboard-access.service';
|
||||
} from './models/dcb-reporting.models';
|
||||
import { ReportService } from './services/dcb-reporting.service';
|
||||
import { DashboardAccess, AllowedMerchant, DashboardAccessService } from './services/dashboard-access.service';
|
||||
import { AuthService } from '@core/services/auth.service';
|
||||
import { PageTitle } from '@app/components/page-title/page-title';
|
||||
|
||||
// ============ TYPES ET INTERFACES ============
|
||||
|
||||
@ -132,7 +133,12 @@ interface SubscriptionStats {
|
||||
templateUrl: './dcb-reporting-dashboard.html',
|
||||
styleUrls: ['./dcb-reporting-dashboard.css'],
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, NgIconComponent, NgbDropdownModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
NgIconComponent,
|
||||
NgbDropdownModule,
|
||||
PageTitle],
|
||||
providers: [
|
||||
provideIcons({
|
||||
lucideActivity, lucideAlertCircle, lucideCheckCircle2, lucideRefreshCw,
|
||||
@ -197,7 +203,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
];
|
||||
|
||||
// ============ PARAMÈTRES ============
|
||||
merchantId: number | null = null;
|
||||
merchantId: number | undefined = undefined;
|
||||
startDate: string = new Date().toISOString().split('T')[0];
|
||||
endDate: string = new Date().toISOString().split('T')[0];
|
||||
currentYear = new Date().getFullYear();
|
||||
@ -315,42 +321,58 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
// ============ INITIALISATION ============
|
||||
|
||||
private initializeAccess(): void {
|
||||
this.access = this.accessService.getDashboardAccess();
|
||||
this.currentRoleLabel = this.access.roleLabel;
|
||||
this.currentRoleIcon = this.access.roleIcon;
|
||||
|
||||
const merchantPartnerId = this.access.merchantId;
|
||||
// Récupérer le merchant ID du service d'accès
|
||||
const merchantPartnerId = this.getCurrentMerchantPartnerId();
|
||||
|
||||
if (this.access.isMerchantUser) {
|
||||
|
||||
// Pour les merchant users, vérifier que l'ID est valide
|
||||
if (merchantPartnerId) {
|
||||
// Utiliser le merchantId du cache directement s'il existe
|
||||
if (this.access.merchantId) {
|
||||
this.merchantId = this.access.merchantId;
|
||||
} else {
|
||||
// Sinon, essayer de le récupérer
|
||||
const idNum = Number(merchantPartnerId);
|
||||
this.merchantId = isNaN(idNum) ? null : idNum;
|
||||
}
|
||||
|
||||
if (this.merchantId) {
|
||||
this.merchantId = Number(merchantPartnerId);
|
||||
this.accessService.setSelectedMerchantId(this.merchantId);
|
||||
this.isViewingGlobalData = false;
|
||||
}
|
||||
|
||||
console.log(`Merchant User: ID = ${this.merchantId}`);
|
||||
} else {
|
||||
console.error('Merchant ID invalide pour Merchant User:', merchantPartnerId);
|
||||
this.addAlert('danger', 'Erreur de configuration',
|
||||
'Impossible de déterminer le merchant ID', 'Maintenant');
|
||||
this.isViewingGlobalData = false;
|
||||
}
|
||||
} else if (this.access.isHubUser) {
|
||||
// Pour les hub users, vérifier si un merchant est sélectionné
|
||||
const selectedMerchantId = this.accessService.getSelectedMerchantId();
|
||||
if (selectedMerchantId) {
|
||||
|
||||
if (selectedMerchantId && selectedMerchantId > 0) {
|
||||
this.merchantId = selectedMerchantId;
|
||||
this.isViewingGlobalData = false;
|
||||
console.log(`Hub User: Merchant sélectionné = ${this.merchantId}`);
|
||||
} else {
|
||||
this.isViewingGlobalData = true;
|
||||
}
|
||||
this.merchantId = undefined;
|
||||
console.log('Hub User: Mode global (aucun merchant sélectionné)');
|
||||
}
|
||||
}
|
||||
|
||||
// ============ INITIALISATION ============
|
||||
// Mettre à jour la sélection de données
|
||||
this.dataSelection.merchantPartnerId = this.isViewingGlobalData ?
|
||||
undefined : this.merchantId;
|
||||
}
|
||||
|
||||
isValidMerchantId(id: any): boolean {
|
||||
if (id === null || id === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const numId = Number(id);
|
||||
return !isNaN(numId) && Number.isInteger(numId) && numId > 0;
|
||||
}
|
||||
|
||||
private loadAllowedMerchants(): void {
|
||||
this.subscriptions.push(
|
||||
@ -368,7 +390,6 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
private initializeDashboard(): void {
|
||||
console.log(`Dashboard initialisé pour: ${this.currentRoleLabel} (${this.access.isHubUser ? 'Hub User' : 'Merchant User'})`);
|
||||
|
||||
if (this.access.isHubUser) {
|
||||
if (this.isViewingGlobalData) {
|
||||
@ -420,14 +441,29 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
);
|
||||
}
|
||||
|
||||
private loadMerchantData(merchantId: number | null): void {
|
||||
private loadMerchantData(merchantId: number | undefined): void {
|
||||
console.log('Chargement des données pour merchant:', merchantId);
|
||||
|
||||
if (!merchantId) {
|
||||
console.error('Merchant ID invalide:', merchantId);
|
||||
this.addAlert('danger', 'Erreur', 'Merchant ID invalide', 'Maintenant');
|
||||
// Vérification plus robuste
|
||||
if (!merchantId || merchantId <= 0 || isNaN(merchantId)) {
|
||||
console.error('Merchant ID invalide ou manquant:', merchantId);
|
||||
this.addAlert('warning', 'Merchant non spécifié',
|
||||
'Veuillez sélectionner un merchant valide', 'Maintenant');
|
||||
|
||||
// Pour les merchant users, essayer de récupérer l'ID depuis le cache
|
||||
if (this.access.isMerchantUser) {
|
||||
const cachedId = this.accessService.getSelectedMerchantId();
|
||||
if (cachedId && cachedId > 0) {
|
||||
merchantId = cachedId;
|
||||
this.merchantId = cachedId;
|
||||
console.log(`Utilisation du merchant ID du cache: ${merchantId}`);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.loading.merchantData = true;
|
||||
this.lastUpdated = new Date();
|
||||
@ -452,7 +488,8 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
error: (err) => {
|
||||
console.error(`Erreur lors du chargement des données du merchant ${merchantId}:`, err);
|
||||
this.loading.merchantData = false;
|
||||
this.addAlert('danger', 'Erreur de chargement', `Impossible de charger les données du merchant ${merchantId}`, 'Maintenant');
|
||||
this.addAlert('danger', 'Erreur de chargement',
|
||||
`Impossible de charger les données du merchant ${merchantId}`, 'Maintenant');
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
})
|
||||
@ -75,11 +75,21 @@ export class DashboardAccessService {
|
||||
|
||||
const merchantPartnerId = authService.getCurrentMerchantPartnerId();
|
||||
|
||||
if (!merchantPartnerId) return undefined;
|
||||
// Vérifier si la valeur existe et est numérique
|
||||
if (!merchantPartnerId) {
|
||||
console.warn('Aucun merchant ID trouvé pour l\'utilisateur');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const merchantId = parseInt(merchantPartnerId, 10);
|
||||
// Convertir en nombre en gérant les erreurs
|
||||
const merchantId = Number(merchantPartnerId);
|
||||
|
||||
return isNaN(merchantId) ? undefined : merchantId;
|
||||
if (isNaN(merchantId) || !Number.isInteger(merchantId)) {
|
||||
console.error(`Merchant ID invalide: ${merchantPartnerId}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return merchantId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -6,7 +6,7 @@ import { HubUsersManagement } from '@modules/hub-users-management/hub-users';
|
||||
import { MerchantUsersManagement } from '@modules/hub-users-management/merchant-users';
|
||||
|
||||
// Composants principaux
|
||||
import { DcbDashboard } from '@modules/dcb-dashboard/dcb-dashboard';
|
||||
import { DcbReportingDashboard } from '@modules/dcb-dashboard/dcb-reporting-dashboard';
|
||||
import { Team } from '@modules/team/team';
|
||||
import { Transactions } from '@modules/transactions/transactions';
|
||||
import { OperatorsConfig } from '@modules/operators/config/config';
|
||||
@ -32,7 +32,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'dcb-dashboard',
|
||||
canActivate: [authGuard, roleGuard],
|
||||
component: DcbDashboard,
|
||||
component: DcbReportingDashboard,
|
||||
data: {
|
||||
title: 'Dashboard DCB',
|
||||
module: 'dcb-dashboard'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user