import { Component, OnInit, OnDestroy, inject, ViewChild, ElementRef, AfterViewInit, ChangeDetectorRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { catchError, finalize, tap } from 'rxjs/operators'; import { of, Subscription, forkJoin, Observable } from 'rxjs'; import { Chart, ChartConfiguration as ChartJsConfiguration, registerables } from 'chart.js'; import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { NgIconComponent, provideIcons } from '@ng-icons/core'; import { lucideActivity, lucideAlertCircle, lucideCheckCircle2, lucideRefreshCw, lucideServer, lucideXCircle, lucideClock, lucideWifi, lucideWifiOff, lucideTrendingUp, lucideBarChart3, lucidePieChart, lucideCalendar, lucideDollarSign, lucideUsers, lucideCreditCard, lucideSettings, lucideLayoutDashboard, lucideStore, lucideFilter, lucideHeartPulse, lucideCpu, lucidePhone, lucideTrophy, lucidePlus, lucideListChecks, lucideDatabase, lucideInfo, lucideBell, lucideCode, lucideBanknoteArrowUp, lucideShield, lucideGlobe } from '@ng-icons/lucide'; import { TransactionReport, SubscriptionReport, SyncResponse, HealthCheckStatus, ChartDataNormalized, ReportPeriod } 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 ============ export type ChartType = 'line' | 'bar' | 'pie' | 'doughnut'; type ChartConfiguration = ChartJsConfiguration; export interface ChartConfig { type: ChartType; data: ChartDataNormalized; title?: string; colors?: { background?: string | string[]; border?: string; hover?: string; }; options?: { responsive?: boolean; maintainAspectRatio?: boolean; plugins?: any; scales?: any; }; } interface ServiceHealth { service: string; status: 'UP' | 'DOWN'; color: string; url: string; statusCode: number; responseTime?: string; checkedAt: string; error?: string; } interface Alert { type: 'warning' | 'info' | 'success' | 'danger'; title: string; description: string; time: string; timestamp: Date; } interface DataSelection { metric: 'revenue' | 'transactions' | 'successRate' | 'activeSubscriptions'; period: ReportPeriod; chartType: ChartType; merchantPartnerId?: number; } interface MetricOption { id: 'revenue' | 'transactions' | 'successRate' | 'activeSubscriptions'; label: string; icon: string; } interface PaymentStats { daily: { transactions: number; revenue: number; successRate: number; }; weekly: { transactions: number; revenue: number; successRate: number; }; monthly: { transactions: number; revenue: number; successRate: number; }; yearly: { transactions: number; revenue: number; successRate: number; }; overallSuccessRate: number; } interface SubscriptionStats { total: number; active: number; newToday: number; cancelled: number; activePercentage: number; } // ============ COMPOSANT ============ @Component({ selector: 'app-dcb-reporting-dashboard', templateUrl: './dcb-reporting-dashboard.html', styleUrls: ['./dcb-reporting-dashboard.css'], standalone: true, imports: [ CommonModule, FormsModule, NgIconComponent, NgbDropdownModule, PageTitle], providers: [ provideIcons({ lucideActivity, lucideAlertCircle, lucideCheckCircle2, lucideRefreshCw, lucideServer, lucideXCircle, lucideClock, lucideWifi, lucideWifiOff, lucideTrendingUp, lucideBarChart3, lucidePieChart, lucideCalendar, lucideDollarSign, lucideUsers, lucideCreditCard, lucideSettings, lucideLayoutDashboard, lucideStore, lucideFilter, lucideHeartPulse, lucideCpu, lucidePhone, lucideTrophy, lucidePlus, lucideListChecks, lucideDatabase, lucideInfo, lucideBell, lucideCode, lucideBanknoteArrowUp, lucideShield, lucideGlobe }) ] }) export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit { private subscriptions: Subscription[] = []; private reportService = inject(ReportService); private authService = inject(AuthService); @ViewChild('mainChartCanvas') mainChartCanvas!: ElementRef; @ViewChild('comparisonChartCanvas') comparisonChartCanvas!: ElementRef; @ViewChild('successRateChartCanvas') successRateChartCanvas!: ElementRef; @ViewChild('metricDropdown') metricDropdown?: NgbDropdown; @ViewChild('optionsDropdown') optionsDropdown?: NgbDropdown; // ============ DONNÉES ============ dailyTransactions: TransactionReport | null = null; merchantTransactions: TransactionReport | null = null; weeklyTransactions: TransactionReport | null = null; monthlyTransactions: TransactionReport | null = null; yearlyTransactions: TransactionReport | null = null; dailySubscriptions: SubscriptionReport | null = null; merchantSubscriptions: SubscriptionReport | null = null; syncResponse: SyncResponse | null = null; healthStatus: HealthCheckStatus[] = []; // ============ CONFIGURATION ============ chartConfigs: ChartConfig[] = []; currentChartType: ChartType = 'line'; availableChartTypes: ChartType[] = ['line', 'bar', 'pie', 'doughnut']; dataSelection: DataSelection = { metric: 'revenue', period: 'daily', chartType: 'line', merchantPartnerId: undefined }; availableMetrics: MetricOption[] = [ { id: 'revenue', label: 'Revenue', icon: 'lucideBanknoteArrowUp' }, { id: 'transactions', label: 'Transactions', icon: 'lucideCreditCard' }, { id: 'successRate', label: 'Taux de succès', icon: 'lucideActivity' }, { id: 'activeSubscriptions', label: 'Abonnements actifs', icon: 'lucideUsers' } ]; availablePeriods = [ { id: 'daily', label: 'Jour' }, { id: 'weekly', label: 'Semaine' }, { id: 'monthly', label: 'Mois' }, { id: 'yearly', label: 'Année' } ]; // ============ PARAMÈTRES ============ merchantId: number | undefined = undefined; startDate: string = new Date().toISOString().split('T')[0]; endDate: string = new Date().toISOString().split('T')[0]; currentYear = new Date().getFullYear(); // ============ ÉTATS ============ loading = { all: false, dailyTransactions: false, merchantTransactions: false, weeklyTransactions: false, monthlyTransactions: false, yearlyTransactions: false, dailySubscriptions: false, merchantSubscriptions: false, sync: false, healthCheck: false, chart: false, globalData: false, merchantData: false }; errors: { [key: string]: string } = {}; lastUpdated = new Date(); // ============ SANTÉ DU SYSTÈME ============ systemHealth: ServiceHealth[] = []; alerts: Alert[] = []; stats = { totalRevenue: 0, totalTransactions: 0, totalSubscriptions: 0, successRate: 0, activeSubscriptions: 0, avgTransaction: 0, maxRevenueDay: 0, avgSuccessRate: 98.5, yearlyRevenue: 0, yearlyTransactions: 0, totalServices: 0, onlineServices: 0, offlineServices: 0 }; overallHealth: { status: 'healthy' | 'warning' | 'critical'; message: string; icon: string; color: string; } = { status: 'healthy', message: 'Tous les services sont opérationnels', icon: 'lucideCheckCircle2', color: 'success' }; // ============ COULEURS POUR LES GRAPHIQUES ============ private readonly chartColors = { primary: { background: 'rgba(13, 110, 253, 0.1)', border: '#0d6efd', hover: 'rgba(13, 110, 253, 0.3)' }, success: { background: 'rgba(40, 167, 69, 0.1)', border: '#28a745', hover: 'rgba(40, 167, 69, 0.3)' }, warning: { background: 'rgba(255, 193, 7, 0.1)', border: '#ffc107', hover: 'rgba(255, 193, 7, 0.3)' }, danger: { background: 'rgba(220, 53, 69, 0.1)', border: '#dc3545', hover: 'rgba(220, 53, 69, 0.3)' }, info: { background: 'rgba(23, 162, 184, 0.1)', border: '#17a2b8', hover: 'rgba(23, 162, 184, 0.3)' }, palette: [ '#0d6efd', '#6610f2', '#6f42c1', '#d63384', '#fd7e14', '#20c997', '#198754', '#0dcaf0', '#6c757d', '#ffc107' ] }; // ============ PROPRIÉTÉS RÔLES ============ access!: DashboardAccess; currentRoleLabel: string = ''; currentRoleIcon: string = ''; allowedMerchants: AllowedMerchant[] = []; // ============ ÉTAT D'AFFICHAGE ============ isViewingGlobalData: boolean = true; constructor( private accessService: DashboardAccessService, private cdr: ChangeDetectorRef ) { Chart.register(...registerables); } ngOnInit(): void { // 1. Initialiser l'accès this.initializeAccess(); // 2. Initialiser le dashboard (avec délai pour laisser le temps à l'accès) setTimeout(() => { this.initializeDashboard(); }, 100); // 3. Charger les merchants (avec délai) setTimeout(() => { this.loadAllowedMerchants(); }, 150); if (this.accessService.shouldShowSystemHealth()) { setInterval(() => { this.checkSystemHealth(); }, 5 * 60 * 1000); } } // ============ INITIALISATION ============ private initializeAccess(): void { // Attendre que l'accès soit prêt this.subscriptions.push( this.accessService.waitForAccess().subscribe(() => { this.access = this.accessService.getDashboardAccess(); this.currentRoleLabel = this.access.roleLabel; this.currentRoleIcon = this.access.roleIcon; console.log('✅ Dashboard initialisé avec:', { access: this.access, merchantId: this.access.merchantId, isHubUser: this.access.isHubUser, }); // Pour les merchant users if (this.access.isMerchantUser) { const merchantId = this.access.merchantId; if (merchantId && merchantId > 0) { this.merchantId = merchantId; this.accessService.setSelectedMerchantId(merchantId); this.isViewingGlobalData = false; this.dataSelection.merchantPartnerId = merchantId; console.log(`✅ Merchant User: ID = ${this.merchantId}`); } else { console.error('❌ Merchant ID invalide pour Merchant User:', merchantId); // Essayer de récupérer directement depuis le profil const merchantPartnerId = this.authService.getCurrentMerchantPartnerId(); if (merchantPartnerId) { const id = Number(merchantPartnerId); if (!isNaN(id) && id > 0) { this.merchantId = id; this.accessService.setSelectedMerchantId(id); this.isViewingGlobalData = false; this.dataSelection.merchantPartnerId = id; console.log(`✅ Merchant ID récupéré depuis profil: ${id}`); } } else { this.addAlert('danger', 'Erreur de configuration', 'Impossible de déterminer le merchant ID', 'Maintenant'); this.isViewingGlobalData = false; } } } // Pour les hub users else if (this.access.isHubUser) { const selectedMerchantId = this.accessService.getSelectedMerchantId(); if (selectedMerchantId && selectedMerchantId > 0) { this.merchantId = selectedMerchantId; this.isViewingGlobalData = false; this.dataSelection.merchantPartnerId = selectedMerchantId; console.log(`✅ Hub User: Merchant sélectionné = ${this.merchantId}`); } else { this.isViewingGlobalData = true; this.merchantId = undefined; this.dataSelection.merchantPartnerId = undefined; console.log('✅ Hub User: Mode global (aucun merchant sélectionné)'); } } }) ); } 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( this.accessService.getAvailableMerchants().subscribe({ next: (merchants) => { this.allowedMerchants = merchants; this.cdr.detectChanges(); }, error: (err) => { console.error('Erreur lors du chargement des merchants:', err); this.allowedMerchants = []; } }) ); } private initializeDashboard(): void { // Vérifier d'abord si le profil est chargé const profile = this.authService.getProfile(); if (!profile) { console.log('⏳ Profil non chargé, attente...'); return; } if (this.access.isHubUser) { if (this.isViewingGlobalData) { this.loadGlobalData(); } else { this.loadMerchantData(this.merchantId); } } else { this.loadMerchantData(this.merchantId); } if (this.accessService.shouldShowSystemHealth()) { this.checkSystemHealth(); } } // ============ CHARGEMENT DES DONNÉES ============ private loadGlobalData(): void { if (!this.access.isHubUser) return; console.log('Chargement des données globales'); this.loading.globalData = true; this.lastUpdated = new Date(); const requests = [ this.loadDailyTransactions(), this.loadWeeklyTransactions(), this.loadMonthlyTransactions(), this.loadYearlyTransactions(), this.loadDailySubscriptions() ]; this.subscriptions.push( forkJoin(requests).subscribe({ next: () => { console.log('Données globales chargées avec succès'); this.loading.globalData = false; this.calculateStats(); this.cdr.detectChanges(); setTimeout(() => this.updateAllCharts(), 100); }, error: (err) => { console.error('Erreur lors du chargement des données globales:', err); this.loading.globalData = false; this.addAlert('danger', 'Erreur de chargement', 'Impossible de charger les données globales', 'Maintenant'); this.cdr.detectChanges(); } }) ); } private loadMerchantData(merchantId: number | undefined): void { console.log('Chargement des données pour merchant:', merchantId); // 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(); const requests = [ this.loadDailyTransactions(merchantId), this.loadWeeklyTransactions(merchantId), this.loadMonthlyTransactions(merchantId), this.loadYearlyTransactions(merchantId), this.loadDailySubscriptions(merchantId) ]; this.subscriptions.push( forkJoin(requests).subscribe({ next: () => { console.log(`Données du merchant ${merchantId} chargées avec succès`); this.loading.merchantData = false; this.calculateStats(); this.cdr.detectChanges(); setTimeout(() => this.updateAllCharts(), 100); }, 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.cdr.detectChanges(); } }) ); } // ============ MÉTHODES DE CHARGEMENT INDIVIDUELLES ============ private loadDailyTransactions(merchantId?: number): Observable { this.loading.dailyTransactions = true; this.errors['dailyTransactions'] = ''; return this.reportService.getDailyTransactions(merchantId).pipe( catchError(err => { this.errors['dailyTransactions'] = err.message || 'Erreur de chargement'; console.error('Error loading daily transactions:', err); return of(null); }), tap(data => { if (this.isValidTransactionReport(data)) { if (merchantId) { this.merchantTransactions = data; } else { this.dailyTransactions = data; } } else { if (merchantId) { this.merchantTransactions = null; } else { this.dailyTransactions = null; } } }), finalize(() => { this.loading.dailyTransactions = false; }) ); } private loadWeeklyTransactions(merchantId?: number): Observable { this.loading.weeklyTransactions = true; this.errors['weeklyTransactions'] = ''; return this.reportService.getWeeklyTransactions(merchantId).pipe( catchError(err => { this.errors['weeklyTransactions'] = err.message || 'Erreur de chargement'; console.error('Error loading weekly transactions:', err); return of(null); }), tap(data => { if (this.isValidTransactionReport(data)) { this.weeklyTransactions = data; } else { this.weeklyTransactions = null; } }), finalize(() => { this.loading.weeklyTransactions = false; }) ); } private loadMonthlyTransactions(merchantId?: number): Observable { this.loading.monthlyTransactions = true; this.errors['monthlyTransactions'] = ''; return this.reportService.getMonthlyTransactions(merchantId).pipe( catchError(err => { this.errors['monthlyTransactions'] = err.message || 'Erreur de chargement'; console.error('Error loading monthly transactions:', err); return of(null); }), tap(data => { if (this.isValidTransactionReport(data)) { this.monthlyTransactions = data; } else { this.monthlyTransactions = null; } }), finalize(() => { this.loading.monthlyTransactions = false; }) ); } private loadYearlyTransactions(merchantId?: number): Observable { this.loading.yearlyTransactions = true; this.errors['yearlyTransactions'] = ''; return this.reportService.getYearlyTransactions(this.currentYear, merchantId).pipe( catchError(err => { this.errors['yearlyTransactions'] = err.message || 'Erreur de chargement'; console.error('Error loading yearly transactions:', err); return of(null); }), tap(data => { if (this.isValidTransactionReport(data)) { this.yearlyTransactions = data; } else { this.yearlyTransactions = null; } }), finalize(() => { this.loading.yearlyTransactions = false; }) ); } private loadDailySubscriptions(merchantId?: number): Observable { this.loading.dailySubscriptions = true; this.errors['dailySubscriptions'] = ''; return this.reportService.getDailySubscriptions(merchantId).pipe( catchError(err => { this.errors['dailySubscriptions'] = err.message || 'Erreur de chargement'; console.error('Error loading daily subscriptions:', err); return of(null); }), tap(data => { if (this.isValidSubscriptionReport(data)) { if (merchantId) { this.merchantSubscriptions = data; } else { this.dailySubscriptions = data; } } else { if (merchantId) { this.merchantSubscriptions = null; } else { this.dailySubscriptions = null; } } }), finalize(() => { this.loading.dailySubscriptions = false; }) ); } // ============ GESTION DES MERCHANTS ============ selectMerchant(merchantId: number): void { if (!this.access.isHubUser) { console.warn('Sélection de merchant réservée aux Hub Users'); return; } if (merchantId === 0) { this.isViewingGlobalData = true; this.merchantId = 0; this.accessService.setSelectedMerchantId(0); this.loadGlobalData(); this.addAlert('info', 'Mode global activé', 'Affichage des données globales', 'Maintenant'); } else { this.isViewingGlobalData = false; this.merchantId = merchantId; this.accessService.setSelectedMerchantId(merchantId); this.loadMerchantData(merchantId); this.addAlert('info', 'Merchant changé', `Affichage des données du merchant ${merchantId}`, 'Maintenant'); } this.dataSelection.merchantPartnerId = merchantId === 0 ? undefined : merchantId; } // ============ GESTION DES GRAPHIQUES ============ ngAfterViewInit(): void { setTimeout(() => { this.updateAllCharts(); }, 500); } updateAllCharts(): void { const transactionData = this.getCurrentTransactionData(); if (!transactionData) { return; } try { if (this.mainChartCanvas?.nativeElement) { this.updateMainChart(); } if (this.comparisonChartCanvas?.nativeElement && this.weeklyTransactions && this.monthlyTransactions && this.isViewingGlobalData) { this.updateComparisonChart(); } if (this.successRateChartCanvas?.nativeElement) { this.updateSuccessRateChart(); } } catch (error) { console.error('Erreur lors de la mise à jour des graphiques:', error); } } private updateMainChart(): void { if (!this.mainChartCanvas?.nativeElement) return; const ctx = this.mainChartCanvas.nativeElement.getContext('2d'); if (!ctx) return; try { const existingChart = (ctx as any).chart; if (existingChart) { existingChart.destroy(); } this.loading.chart = true; const transactionData = this.getCurrentTransactionData(); const subscriptionData = this.getCurrentSubscriptionData(); if (!transactionData && !subscriptionData) { this.loading.chart = false; return; } const normalizedData = this.getNormalizedDataForMetric( this.dataSelection.metric, transactionData, subscriptionData ); if (!normalizedData || normalizedData.labels.length === 0) { this.loading.chart = false; return; } const config = this.createChartConfig( normalizedData, this.dataSelection.chartType, this.getChartTitle(this.dataSelection.metric) ); const newChart = new Chart(ctx, config); (ctx as any).chart = newChart; this.loading.chart = false; this.cdr.detectChanges(); } catch (error) { console.error('Erreur lors de la création du graphique principal:', error); this.loading.chart = false; this.cdr.detectChanges(); } } private updateComparisonChart(): void { if (!this.comparisonChartCanvas?.nativeElement || !this.weeklyTransactions || !this.monthlyTransactions) { return; } const ctx = this.comparisonChartCanvas.nativeElement.getContext('2d'); if (!ctx) return; try { const existingChart = (ctx as any).chart; if (existingChart) { existingChart.destroy(); } const weeklyData = this.reportService.normalizeForChart(this.weeklyTransactions, 'totalAmount'); const monthlyData = this.reportService.normalizeForChart(this.monthlyTransactions, 'totalAmount'); if (!weeklyData || !monthlyData) return; const labels = weeklyData.labels.slice(-8); const weeklyValues = weeklyData.dataset.slice(-8); const monthlyValues = monthlyData.dataset.slice(-8); const config: ChartJsConfiguration<'bar'> = { type: 'bar', data: { labels: labels, datasets: [ { label: 'Hebdomadaire', data: weeklyValues, backgroundColor: this.chartColors.primary.background, borderColor: this.chartColors.primary.border, borderWidth: 1 }, { label: 'Mensuel', data: monthlyValues, backgroundColor: this.chartColors.success.background, borderColor: this.chartColors.success.border, borderWidth: 1 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top' }, tooltip: { callbacks: { label: (context: any) => { const value = context.raw; return this.dataSelection.metric === 'revenue' ? `${this.formatCurrency(value)}` : `${this.formatNumber(value)}`; } } } }, scales: { y: { beginAtZero: true, ticks: { callback: (value: any) => { if (typeof value === 'number') { if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`; if (value >= 1000) return `${(value / 1000).toFixed(0)}K`; return value; } return value; } } } } } }; const newChart = new Chart(ctx, config); (ctx as any).chart = newChart; } catch (error) { console.error('Erreur lors de la création du graphique de comparaison:', error); } } private updateSuccessRateChart(): void { if (!this.successRateChartCanvas?.nativeElement) return; const ctx = this.successRateChartCanvas.nativeElement.getContext('2d'); if (!ctx) return; try { const existingChart = (ctx as any).chart; if (existingChart) { existingChart.destroy(); } const transactionData = this.getCurrentTransactionData(); if (!transactionData) return; const successRate = this.getSuccessRate(transactionData); const remaining = 100 - successRate; const config: ChartJsConfiguration<'doughnut'> = { type: 'doughnut', data: { datasets: [{ data: [successRate, remaining], backgroundColor: [this.getChartColor(successRate), 'rgba(0, 0, 0, 0.1)'], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, cutout: '70%', plugins: { legend: { display: false }, tooltip: { callbacks: { label: (context: any) => `${context.raw.toFixed(1)}%` } } } } }; const newChart = new Chart(ctx, config); (ctx as any).chart = newChart; } catch (error) { console.error('Erreur lors de la création du graphique de taux de succès:', error); } } // ============ UTILITAIRES DE DONNÉES ============ getCurrentTransactionData(): TransactionReport | null { return this.isViewingGlobalData ? this.dailyTransactions : (this.merchantTransactions || this.dailyTransactions); } getCurrentSubscriptionData(): SubscriptionReport | null { return this.isViewingGlobalData ? this.dailySubscriptions : (this.merchantSubscriptions || this.dailySubscriptions); } getNormalizedDataForMetric( metric: DataSelection['metric'], transactionData: TransactionReport | null, subscriptionData: SubscriptionReport | null = null ): ChartDataNormalized | null { if (!transactionData && !subscriptionData) return null; switch (metric) { case 'revenue': if (transactionData) { const normalized = this.reportService.normalizeForChart(transactionData, 'totalAmount'); normalized.datasetLabel = 'Revenue (XOF)'; normalized.backgroundColor = this.chartColors.primary.background; normalized.borderColor = this.chartColors.primary.border; return normalized; } break; case 'transactions': if (transactionData) { const normalized = this.reportService.normalizeForChart(transactionData, 'count'); normalized.datasetLabel = 'Nombre de transactions'; normalized.backgroundColor = this.chartColors.info.background; normalized.borderColor = this.chartColors.info.border; return normalized; } break; case 'successRate': if (transactionData) { const labels = transactionData.items.map(item => item.period); const dataset = transactionData.items.map(item => { const total = item.count || 0; const success = item.successCount || 0; return total > 0 ? (success / total) * 100 : 0; }); return { labels, dataset, datasetLabel: 'Taux de succès (%)', backgroundColor: this.chartColors.success.background, borderColor: this.chartColors.success.border }; } break; case 'activeSubscriptions': const subData = subscriptionData || this.dailySubscriptions; if (subData) { const normalized = this.reportService.normalizeForChart(subData, 'activeCount'); normalized.datasetLabel = 'Abonnements actifs'; normalized.backgroundColor = this.chartColors.warning.background; normalized.borderColor = this.chartColors.warning.border; return normalized; } break; } return null; } private createChartConfig( normalizedData: ChartDataNormalized, chartType: ChartType, title?: string ): ChartConfiguration { const isCircular = chartType === 'pie' || chartType === 'doughnut'; return { type: chartType, data: { labels: normalizedData.labels, datasets: [{ label: normalizedData.datasetLabel || 'Données', data: normalizedData.dataset, backgroundColor: normalizedData.backgroundColor || this.chartColors.palette, borderColor: normalizedData.borderColor || this.chartColors.primary.border, borderWidth: isCircular ? 0 : 2, fill: !isCircular, tension: chartType === 'line' ? 0.4 : 0, pointRadius: chartType === 'line' ? 3 : 0, pointHoverRadius: chartType === 'line' ? 5 : 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', display: !isCircular }, title: { display: !!title, text: title, font: { size: 14, weight: 'bold' } }, tooltip: { callbacks: { label: (context: any) => { const value = context.raw; if (normalizedData.datasetLabel?.includes('XOF')) { return `${normalizedData.datasetLabel}: ${this.formatCurrency(value)}`; } else if (normalizedData.datasetLabel?.includes('%')) { return `${normalizedData.datasetLabel}: ${value.toFixed(1)}%`; } return `${normalizedData.datasetLabel}: ${this.formatNumber(value)}`; } } } }, scales: isCircular ? {} : { y: { beginAtZero: true, grid: { color: 'rgba(0, 0, 0, 0.05)' }, ticks: { callback: (value: any) => { if (typeof value === 'number') { if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`; if (value >= 1000) return `${(value / 1000).toFixed(0)}K`; return value; } return value; } } }, x: { grid: { color: 'rgba(0, 0, 0, 0.05)' }, ticks: { maxRotation: 45 } } } } }; } // ============ GESTION DES ACTIONS ============ refreshData(): void { if (this.access.isHubUser) { if (this.isViewingGlobalData) { this.loadGlobalData(); } else { this.loadMerchantData(this.merchantId); } } else { this.loadMerchantData(this.merchantId); } } triggerSync(): void { if (!this.accessService.canTriggerSync()) { this.addAlert('warning', 'Permission refusée', 'Vous n\'avez pas la permission de déclencher une synchronisation', 'Maintenant'); return; } const syncService = this.systemHealth.find(s => s.service === 'SYNC'); if (syncService?.status === 'DOWN') { this.addAlert('danger', 'Synchronisation impossible', 'Le service de synchronisation est actuellement hors ligne', 'Maintenant'); return; } this.loading.sync = true; this.syncResponse = null; const sub = this.reportService.triggerManualSync().pipe( catchError(err => { this.errors['sync'] = err.message || 'Erreur de synchronisation'; if (this.accessService.shouldShowAlerts()) { this.addAlert('danger', 'Échec de synchronisation', `La synchronisation a échoué: ${err.message}`, 'Maintenant'); } return of(null); }), tap(data => { if (data && typeof data.message === 'string' && typeof data.timestamp === 'string') { this.syncResponse = data; if (this.accessService.shouldShowAlerts()) { this.addAlert('success', 'Synchronisation réussie', data.message, 'Maintenant'); } } }), finalize(() => { this.loading.sync = false; setTimeout(() => this.refreshData(), 2000); }) ).subscribe(); this.subscriptions.push(sub); } // ============ HEALTH CHECK ============ checkSystemHealth(): void { if (!this.accessService.shouldShowSystemHealth()) return; this.loading.healthCheck = true; this.systemHealth = []; this.alerts = []; this.subscriptions.push( this.reportService.detailedHealthCheck().pipe( tap(response => { this.healthStatus = response.details; this.systemHealth = response.details.map(api => ({ service: api.service, status: api.status, color: api.status === 'UP' ? 'success' : 'danger', url: api.url, statusCode: api.statusCode, checkedAt: api.checkedAt, })); this.stats.totalServices = response.summary.total; this.stats.onlineServices = response.summary.up; this.stats.offlineServices = response.summary.down; this.updateOverallHealth(); this.generateHealthAlerts(); this.loading.healthCheck = false; this.cdr.detectChanges(); }), catchError(err => { console.error('Erreur lors du health check:', err); this.addAlert('danger', 'Erreur de vérification', 'Impossible de vérifier la santé des services', 'Maintenant'); this.loading.healthCheck = false; this.cdr.detectChanges(); return of(null); }) ).subscribe() ); } private updateOverallHealth(): void { const offlineCount = this.stats.offlineServices; const totalCount = this.stats.totalServices; if (offlineCount === 0) { this.overallHealth = { status: 'healthy', message: `Tous les ${totalCount} services sont opérationnels`, icon: 'lucideCheckCircle2', color: 'success' }; } else if (offlineCount === 1) { const offlineService = this.systemHealth.find(s => s.status === 'DOWN')?.service || 'un service'; this.overallHealth = { status: 'warning', message: `${offlineService} est hors ligne`, icon: 'lucideAlertCircle', color: 'warning' }; } else { this.overallHealth = { status: 'critical', message: `${offlineCount} services sur ${totalCount} sont hors ligne`, icon: 'lucideXCircle', color: 'danger' }; } } private generateHealthAlerts(): void { this.alerts = this.alerts.filter(alert => !alert.title.includes('hors ligne') && !alert.title.includes('opérationnels') ); const offlineServices = this.systemHealth.filter(s => s.status === 'DOWN'); if (offlineServices.length > 0) { offlineServices.forEach(service => { this.addAlert('danger', `${service.service} hors ligne`, `Le service ${service.service} ne répond pas (${service.statusCode})`, this.formatTimeAgo(service.checkedAt)); }); } else { this.addAlert('success', 'Tous les services opérationnels', 'Toutes les APIs répondent correctement', 'Maintenant'); } } // ============ CALCUL DES STATISTIQUES ============ private calculateStats(): void { const transactionData = this.getCurrentTransactionData(); const subscriptionData = this.getCurrentSubscriptionData(); if (transactionData) { this.stats.totalRevenue = transactionData.totalAmount || 0; this.stats.totalTransactions = transactionData.totalCount || 0; this.stats.successRate = this.getSuccessRate(transactionData); if (transactionData.items?.length) { this.stats.maxRevenueDay = Math.max(...transactionData.items.map(item => item.totalAmount || 0)); } } this.stats.yearlyRevenue = this.yearlyTransactions?.totalAmount || 0; this.stats.yearlyTransactions = this.yearlyTransactions?.totalCount || 0; if (subscriptionData) { this.stats.totalSubscriptions = subscriptionData.totalCount || 0; this.stats.activeSubscriptions = subscriptionData.items?.reduce( (sum, item) => sum + (item.activeCount || 0), 0 ) || 0; } this.stats.avgTransaction = this.stats.totalTransactions > 0 ? this.stats.totalRevenue / this.stats.totalTransactions : 0; } getSuccessRate(transaction: TransactionReport | null): number { if (!transaction || !transaction.items || transaction.items.length === 0) return 0; const item = transaction.items[0]; const total = item.count || 0; const success = item.successCount || 0; return total > 0 ? (success / total) * 100 : 0; } // ============ MÉTHODES UTILITAIRES POUR LE TEMPLATE ============ getPaymentStats(): PaymentStats { const transactionData = this.getCurrentTransactionData(); return { daily: { transactions: transactionData?.totalCount || 0, revenue: transactionData?.totalAmount || 0, successRate: this.getSuccessRate(transactionData) }, weekly: { transactions: this.weeklyTransactions?.totalCount || 0, revenue: this.weeklyTransactions?.totalAmount || 0, successRate: this.getSuccessRate(this.weeklyTransactions) }, monthly: { transactions: this.monthlyTransactions?.totalCount || 0, revenue: this.monthlyTransactions?.totalAmount || 0, successRate: this.getSuccessRate(this.monthlyTransactions) }, yearly: { transactions: this.stats.yearlyTransactions, revenue: this.stats.yearlyRevenue, successRate: this.stats.avgSuccessRate }, overallSuccessRate: this.stats.successRate }; } getSubscriptionStats(): SubscriptionStats { const subscriptionData = this.getCurrentSubscriptionData(); const dailyItem = subscriptionData?.items?.[0]; return { total: subscriptionData?.totalCount || 0, active: this.stats.activeSubscriptions, newToday: dailyItem?.count || 0, cancelled: dailyItem?.cancelledCount || 0, activePercentage: subscriptionData?.totalCount ? (this.stats.activeSubscriptions / subscriptionData.totalCount) * 100 : 0 }; } getPerformanceLabel(successRate: number): string { if (successRate >= 95) return 'Excellent'; if (successRate >= 90) return 'Bon'; if (successRate >= 80) return 'Moyen'; return 'À améliorer'; } getAlertBadgeClass(): string { if (this.alerts.length === 0) return 'bg-success'; if (this.alerts.filter(a => a.type === 'danger').length > 0) return 'bg-danger'; if (this.alerts.filter(a => a.type === 'warning').length > 0) return 'bg-warning'; return 'bg-info'; } // ============ GESTION DES GRAPHIQUES (PUBLIC) ============ changeMetric(metric: 'revenue' | 'transactions' | 'successRate' | 'activeSubscriptions'): void { this.dataSelection.metric = metric; if (this.metricDropdown) { this.metricDropdown.close(); } this.cdr.detectChanges(); setTimeout(() => { this.updateMainChart(); }, 50); } changePeriod(period: ReportPeriod): void { this.dataSelection.period = period; this.cdr.detectChanges(); setTimeout(() => { this.updateMainChart(); }, 50); } changeChartType(type: ChartType): void { this.dataSelection.chartType = type; this.cdr.detectChanges(); setTimeout(() => { this.updateMainChart(); }, 50); } refreshChartData(): void { this.cdr.detectChanges(); setTimeout(() => { this.updateAllCharts(); }, 50); } // ============ FORMATAGE ============ formatCurrency(amount: number): string { if (amount >= 1000000) { return `${(amount / 1000000).toFixed(1)}M XOF`; } else if (amount >= 1000) { return `${(amount / 1000).toFixed(0)}K XOF`; } return `${Math.round(amount)} XOF`; } formatNumber(num: number): string { return num.toLocaleString('fr-FR'); } formatDate(dateString: string): string { try { return new Date(dateString).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } catch { return dateString; } } formatTimeAgo(date: Date | string | undefined): string { if (!date) return 'Jamais'; const checkDate = typeof date === 'string' ? new Date(date) : date; const now = new Date(); const diffMs = now.getTime() - checkDate.getTime(); const diffMins = Math.floor(diffMs / 60000); if (diffMins < 1) return 'À l\'instant'; if (diffMins < 60) return `Il y a ${diffMins} min`; if (diffMins < 1440) return `Il y a ${Math.floor(diffMins / 60)} h`; return `Il y a ${Math.floor(diffMins / 1440)} j`; } // ============ MÉTHODES POUR LE TEMPLATE ============ getSuccessRateClass(rate: number): string { if (rate >= 95) return 'text-success'; if (rate >= 90) return 'text-warning'; return 'text-danger'; } getHealthIcon(service: ServiceHealth): string { return service.status === 'UP' ? 'lucideCheckCircle2' : 'lucideXCircle'; } getStatusCodeClass(statusCode: number): string { if (statusCode >= 200 && statusCode < 300) return 'text-success'; if (statusCode >= 300 && statusCode < 400) return 'text-warning'; if (statusCode >= 400 && statusCode < 500) return 'text-warning'; if (statusCode >= 500) return 'text-danger'; return 'text-muted'; } getResponseTimeClass(responseTime?: string): string { if (!responseTime) return 'text-muted'; const time = parseInt(responseTime.replace('ms', '')); if (time < 500) return 'text-success'; if (time < 2000) return 'text-warning'; return 'text-danger'; } getMetricIcon(metricId: string): string { const metric = this.availableMetrics.find(m => m.id === metricId); return metric?.icon || 'lucideActivity'; } getCurrentMetricLabel(): string { const metric = this.availableMetrics.find(m => m.id === this.dataSelection.metric); return metric?.label || 'Revenue'; } getChartTitle(metric: DataSelection['metric']): string { const titles: Record = { revenue: 'Évolution du Revenue', transactions: 'Nombre de Transactions', successRate: 'Taux de Succès', activeSubscriptions: 'Abonnements Actifs' }; return titles[metric]; } private getChartColor(rate: number): string { if (rate >= 95) return '#28a745'; if (rate >= 90) return '#ffc107'; return '#dc3545'; } // ============ MÉTHODES UTILITAIRES POUR LES RÔLES ============ getRoleStatusIcon(): string { if (this.access.isHubUser) return 'lucideShield'; return 'lucideStore'; } getRoleStatusColor(): string { if (this.access.isHubUser) return 'text-primary'; return 'text-success'; } getRoleBadgeClass(): string { if (this.access.isHubUser) return 'bg-primary'; return 'bg-success'; } // ============ MÉTHODES UTILITAIRES POUR LES PERMISSIONS ============ shouldShowSystemHealth(): boolean { return this.accessService.shouldShowSystemHealth(); } shouldShowAllTransactions(): boolean { return this.access.isHubUser; } canTriggerSync(): boolean { return this.accessService.canTriggerSync(); } canManageMerchants(): boolean { return this.accessService.canManageMerchants(); } shouldShowTransactions(): boolean { return true; } shouldShowCharts(): boolean { return true; } shouldShowKPIs(): boolean { return true; } shouldShowAlerts(): boolean { return this.accessService.shouldShowAlerts(); } canRefreshData(): boolean { return true; } canSelectMerchant(): boolean { return this.access.isHubUser && this.allowedMerchants.length > 0; } shouldShowMerchantId(): boolean { return this.accessService.shouldShowMerchantId(); } canEditMerchantFilter(): boolean { return this.accessService.canEditMerchantFilter(); } // ============ MÉTHODES SPÉCIFIQUES AU CONTEXTE ============ isViewingGlobal(): boolean { return this.isViewingGlobalData; } isViewingMerchant(): boolean { return !this.isViewingGlobalData && !this.merchantId ; } getCurrentMerchantName(): string { if (this.isViewingGlobalData) { return 'Données globales'; } const merchant = this.allowedMerchants.find(m => m.id === this.merchantId); return merchant ? merchant.name : `Merchant ${this.merchantId}`; } private getCurrentMerchantPartnerId(): string | null { // Utiliser une valeur par défaut sécurisée return this.access?.merchantId?.toString() || null; } shouldShowMerchantSelector(): boolean { return this.access.isHubUser && this.allowedMerchants.length > 1; } // ============ MÉTHODES POUR LES KPIs ============ getDisplayedKPIs(): any[] { const kpis = []; const paymentStats = this.getPaymentStats(); const subscriptionStats = this.getSubscriptionStats(); kpis.push({ title: 'Transactions', value: this.formatNumber(paymentStats.daily.transactions), subtext: 'Journalier', metric: 'transactions', color: 'primary', icon: 'lucideStore', period: 'daily' }); if (this.access.isHubUser && this.isViewingGlobalData) { kpis.push({ title: 'Transactions', value: this.formatNumber(paymentStats.weekly.transactions), subtext: 'Hebdomadaire', metric: 'transactions', color: 'info', icon: 'lucideCalendar', period: 'weekly' }); } kpis.push({ title: `Revenue ${this.currentYear}`, value: this.formatCurrency(this.stats.yearlyRevenue), subtext: 'Annuel', metric: 'revenue', color: 'purple', icon: 'lucideTrophy', period: 'yearly' }); kpis.push({ title: 'Abonnements', value: this.formatNumber(subscriptionStats.active), subtext: 'Actifs', metric: 'subscriptions', color: 'warning', icon: 'lucideUsers', period: 'current' }); return kpis; } shouldShowChart(chartType: 'main' | 'comparison' | 'successRate'): boolean { switch (chartType) { case 'main': return true; case 'comparison': return this.access.isHubUser && this.isViewingGlobalData; case 'successRate': return this.getCurrentTransactionData() !== null; default: return false; } } shouldShowSection(section: 'health' | 'alerts' | 'tables' | 'footerInfo'): boolean { switch (section) { case 'health': return this.shouldShowSystemHealth(); case 'alerts': return this.shouldShowAlerts(); case 'tables': return true; case 'footerInfo': return true; default: return false; } } // ============ GESTION DES ALERTES ============ private addAlert(type: 'warning' | 'info' | 'success' | 'danger', title: string, description: string, time: string): void { if (this.alerts.length >= 5) { this.alerts.shift(); } this.alerts.push({ type, title, description, time, timestamp: new Date() }); } // ============ VALIDATION ============ private isValidTransactionReport(data: any): data is TransactionReport { return data !== null && data !== undefined && typeof data === 'object' && Array.isArray(data.items); } private isValidSubscriptionReport(data: any): data is SubscriptionReport { return data !== null && data !== undefined && typeof data === 'object' && Array.isArray(data.items); } // ============ DESTRUCTION ============ ngOnDestroy(): void { this.subscriptions.forEach(sub => sub.unsubscribe()); } }