+
+
-
-
-
-
- Direct Carrier Billing
-
-
-
-
-
-
- @if (lastUpdated) {
-
- MAJ: {{ lastUpdated | date:'HH:mm:ss' }}
-
- }
-
-
-
-
-
- Auto-refresh
-
-
-
-
-
-
-
-
-
-
-
- {{ range }}
-
-
-
-
+
-
- @if (error) {
-
- }
+
+
-
- @if (loading && !analytics) {
-
-
- Chargement...
-
-
Chargement des données DCB...
-
- }
-
- @if (!loading && analytics) {
-
-
-
-
-
-
-
-
-
Chiffre d'Affaires
-
{{ formatCurrency(analytics.totalRevenue) }}
-
-
-
- {{ analytics.monthlyGrowth > 0 ? '+' : '' }}{{ analytics.monthlyGrowth }}%
-
- vs mois dernier
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Transactions
-
{{ formatNumber(analytics.totalTransactions) }}
-
-
- {{ formatPercentage(analytics.successRate) }}
-
- taux de succès
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Montant Moyen
-
{{ formatCurrency(analytics.averageAmount) }}
-
par transaction
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Aujourd'hui
-
{{ formatCurrency(analytics.todayStats.revenue) }}
-
- {{ analytics.todayStats.transactions }} transactions
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
- Opérateur
- Pays
- Statut
- Taux de Succès
- Progression
-
-
-
- @for (operator of operators; track operator.id) {
-
- {{ operator.name }}
-
- {{ operator.country }}
-
-
-
- {{ operator.status === 'ACTIVE' ? 'Actif' : 'Inactif' }}
-
-
-
-
- {{ formatPercentage(operator.successRate) }}
-
-
-
-
-
-
-
-
{{ operator.successRate }}%
-
-
-
- }
-
-
-
-
-
-
-
-
-
-
-
-
- @for (operator of analytics.topOperators; track operator.operator; let i = $index) {
-
-
-
-
{{ operator.operator }}
-
- {{ operator.count }} trans.
-
- {{ formatCurrency(operator.revenue) }}
-
-
-
-
- }
-
-
-
+
-
-
-
-
-
-
-
-
-
-
- MSISDN
- Opérateur
- Produit
- Montant
- Statut
- Date
-
-
-
- @for (transaction of recentTransactions; track transaction.id) {
-
- {{ transaction.msisdn }}
-
- {{ transaction.operator }}
-
-
- {{ transaction.productName }}
-
-
- {{ formatCurrency(transaction.amount, transaction.currency) }}
-
-
-
-
- {{ transaction.status }}
-
-
-
- {{ transaction.transactionDate | date:'dd/MM/yy HH:mm' }}
-
-
- }
- @empty {
-
-
-
- Aucune transaction récente
-
-
- }
-
-
-
-
-
-
+
+
+
- }
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/modules/dcb-dashboard/dcb-dashboard.ts b/src/app/modules/dcb-dashboard/dcb-dashboard.ts
index 6e861b2..957385b 100644
--- a/src/app/modules/dcb-dashboard/dcb-dashboard.ts
+++ b/src/app/modules/dcb-dashboard/dcb-dashboard.ts
@@ -1,198 +1,25 @@
-import { Component, inject, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
-import { NgIcon, provideNgIconsConfig } from '@ng-icons/core';
-
+import { Component } from '@angular/core';
import { PageTitle } from '@app/components/page-title/page-title';
-
-import {
- lucideEuro,
- lucideCreditCard,
- lucideBarChart3,
- lucideSmartphone,
- lucideTrendingUp,
- lucideTrendingDown,
- lucideAlertCircle,
- lucideCheckCircle,
- lucideClock,
- lucideXCircle,
- lucideRefreshCw
-} from '@ng-icons/lucide';
-import { NgbAlertModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
-
-import { DcbService } from './services/dcb.service';
-import { DcbAnalytics, TransactionStats, DcbTransaction, DcbOperator } from './models/dcb';
-
-// Type pour les plages de temps
-type TimeRange = '24h' | '7d' | '30d' | '90d';
+import { PaymentStats } from './components/payment-stats';
+import { ActiveSubscriptions } from './components/active-subscriptions';
+import { RevenueChart } from './components/revenue-chart';
+import { OperatorPerformance } from './components/operator-performance';
+import { RecentTransactions } from './components/recent-transactions';
+import { SystemHealth } from './components/system-health';
+import { AlertWidget } from './components/alert-widget';
@Component({
selector: 'app-dcb-dashboard',
- standalone: true,
imports: [
- CommonModule,
- FormsModule,
- NgIcon,
- NgbAlertModule,
- NgbProgressbarModule,
- NgbTooltipModule,
- PageTitle
+ PageTitle,
+ PaymentStats,
+ ActiveSubscriptions,
+ RevenueChart,
+ OperatorPerformance,
+ RecentTransactions,
+ SystemHealth,
+ AlertWidget,
],
- providers: [
- provideNgIconsConfig({
- size: '1.25em'
- })
- ],
- templateUrl: './dcb-dashboard.html'
+ templateUrl: './dcb-dashboard.html',
})
-export class DcbDashboard implements OnInit, OnDestroy {
- private dcbService = inject(DcbService);
- private cdRef = inject(ChangeDetectorRef);
-
- // États
- loading = false;
- error = '';
- lastUpdated: Date | null = null;
-
- // Données
- analytics: DcbAnalytics | null = null;
- stats: TransactionStats | null = null;
- recentTransactions: DcbTransaction[] = [];
- operators: DcbOperator[] = [];
-
- // Filtres
- timeRange: TimeRange = '7d';
- autoRefresh = true;
- private refreshInterval: any;
-
- // Plages de temps disponibles (pour le template)
- timeRanges: TimeRange[] = ['24h', '7d', '30d', '90d'];
-
- ngOnInit() {
- this.loadDashboardData();
- this.startAutoRefresh();
- }
-
- ngOnDestroy() {
- this.stopAutoRefresh();
- }
-
- startAutoRefresh() {
- if (this.autoRefresh) {
- this.refreshInterval = setInterval(() => {
- this.loadDashboardData();
- }, 30000); // Refresh toutes les 30 secondes
- }
- }
-
- stopAutoRefresh() {
- if (this.refreshInterval) {
- clearInterval(this.refreshInterval);
- }
- }
-
- loadDashboardData() {
- this.loading = true;
- this.error = '';
-
- // Charger toutes les données en parallèle
- Promise.all([
- this.dcbService.getAnalytics(this.timeRange).toPromise(),
- this.dcbService.getTransactionStats().toPromise(),
- this.dcbService.getRecentTransactions(8).toPromise(),
- this.dcbService.getOperators().toPromise()
- ]).then(([analytics, stats, transactions, operators]) => {
- this.analytics = analytics || null;
- this.stats = stats || null;
- this.recentTransactions = transactions || [];
- this.operators = operators || [];
- this.loading = false;
- this.lastUpdated = new Date();
- this.cdRef.detectChanges();
- }).catch(error => {
- this.error = 'Erreur lors du chargement des données du dashboard';
- this.loading = false;
- this.lastUpdated = new Date();
- this.cdRef.detectChanges();
- console.error('Dashboard loading error:', error);
- });
- }
-
- onTimeRangeChange(range: TimeRange) {
- this.timeRange = range;
- this.loadDashboardData();
- }
-
- onRefresh() {
- this.loadDashboardData();
- }
-
- onAutoRefreshToggle() {
- this.autoRefresh = !this.autoRefresh;
- if (this.autoRefresh) {
- this.startAutoRefresh();
- } else {
- this.stopAutoRefresh();
- }
- }
-
- // Utilitaires d'affichage
- formatCurrency(amount: number, currency: string = 'EUR'): string {
- return new Intl.NumberFormat('fr-FR', {
- style: 'currency',
- currency: currency
- }).format(amount);
- }
-
- formatNumber(num: number): string {
- return new Intl.NumberFormat('fr-FR').format(num);
- }
-
- formatPercentage(value: number): string {
- return `${value.toFixed(1)}%`;
- }
-
- getStatusBadgeClass(status: string): string {
- switch (status) {
- case 'SUCCESS': return 'badge bg-success';
- case 'PENDING': return 'badge bg-warning';
- case 'FAILED': return 'badge bg-danger';
- case 'REFUNDED': return 'badge bg-info';
- default: return 'badge bg-secondary';
- }
- }
-
- getStatusIcon(status: string): string {
- switch (status) {
- case 'SUCCESS': return 'lucideCheckCircle';
- case 'PENDING': return 'lucideClock';
- case 'FAILED': return 'lucideXCircle';
- case 'REFUNDED': return 'lucideRefreshCw';
- default: return 'lucideAlertCircle';
- }
- }
-
- getGrowthClass(growth: number): string {
- return growth >= 0 ? 'text-success' : 'text-danger';
- }
-
- getGrowthIcon(growth: number): string {
- return growth >= 0 ? 'lucideTrendingUp' : 'lucideTrendingDown';
- }
-
- getOperatorStatusClass(status: string): string {
- return status === 'ACTIVE' ? 'badge bg-success' : 'badge bg-secondary';
- }
-
- getSuccessRateClass(rate: number): string {
- if (rate >= 90) return 'text-success';
- if (rate >= 80) return 'text-warning';
- return 'text-danger';
- }
-
- getProgressBarClass(rate: number): string {
- if (rate >= 90) return 'bg-success';
- if (rate >= 80) return 'bg-warning';
- return 'bg-danger';
- }
-}
\ No newline at end of file
+export class DcbDashboard {}
\ No newline at end of file
diff --git a/src/app/modules/dcb-dashboard/models/dcb-dashboard.models.ts b/src/app/modules/dcb-dashboard/models/dcb-dashboard.models.ts
new file mode 100644
index 0000000..82fe287
--- /dev/null
+++ b/src/app/modules/dcb-dashboard/models/dcb-dashboard.models.ts
@@ -0,0 +1,50 @@
+export interface KpiCardModel {
+ title: string;
+ value: number | string;
+ change?: number;
+ changeType?: 'positive' | 'negative' | 'neutral';
+ icon: string;
+ color: string;
+ format?: 'currency' | 'number' | 'percentage';
+ currency?: string;
+}
+
+export interface PaymentChartData {
+ period: string;
+ successful: number;
+ failed: number;
+ pending: number;
+ revenue: number;
+}
+
+export interface SubscriptionStatsModel {
+ total: number;
+ active: number;
+ trial: number;
+ cancelled: number;
+ expired: number;
+ growthRate: number;
+}
+
+export interface Alert {
+ id: string;
+ type: 'error' | 'warning' | 'info' | 'success';
+ title: string;
+ message: string;
+ timestamp: Date;
+ acknowledged: boolean;
+ priority: 'low' | 'medium' | 'high';
+ action?: {
+ label: string;
+ route?: string;
+ action?: () => void;
+ };
+}
+
+export interface DcbDashboardData {
+ kpis: KpiCardModel[];
+ paymentChart: PaymentChartData[];
+ subscriptionStats: SubscriptionStatsModel;
+ alerts: Alert[];
+ lastUpdated: Date;
+}
\ No newline at end of file
diff --git a/src/app/modules/dcb-dashboard/models/dcb.ts b/src/app/modules/dcb-dashboard/models/dcb.ts
deleted file mode 100644
index 543af11..0000000
--- a/src/app/modules/dcb-dashboard/models/dcb.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-export interface DcbTransaction {
- id: string;
- msisdn: string;
- operator: string;
- country: string;
- amount: number;
- currency: string;
- status: TransactionStatus;
- productId?: string;
- productName: string;
- transactionDate: Date;
- createdAt: Date;
- errorCode?: string;
- errorMessage?: string;
-}
-
-export interface DcbAnalytics {
- totalRevenue: number;
- totalTransactions: number;
- successRate: number;
- averageAmount: number;
- topOperators: { operator: string; revenue: number; count: number }[];
- dailyStats: { date: string; revenue: number; transactions: number }[];
- monthlyGrowth: number;
- todayStats: {
- revenue: number;
- transactions: number;
- successCount: number;
- failedCount: number;
- };
-}
-
-export interface TransactionStats {
- pending: number;
- successful: number;
- failed: number;
- refunded: number;
-}
-
-export type TransactionStatus =
- | 'PENDING'
- | 'SUCCESS'
- | 'FAILED'
- | 'REFUNDED';
-
-export interface DcbOperator {
- id: string;
- name: string;
- code: string;
- country: string;
- status: 'ACTIVE' | 'INACTIVE';
- successRate: number;
-}
\ No newline at end of file
diff --git a/src/app/modules/dcb-dashboard/services/dcb-dashboard.service.ts b/src/app/modules/dcb-dashboard/services/dcb-dashboard.service.ts
new file mode 100644
index 0000000..728d97b
--- /dev/null
+++ b/src/app/modules/dcb-dashboard/services/dcb-dashboard.service.ts
@@ -0,0 +1,133 @@
+import { Injectable, inject } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable, of } from 'rxjs';
+import { map, catchError } from 'rxjs/operators';
+import { DcbDashboardData, KpiCardModel, PaymentChartData, SubscriptionStatsModel, Alert } from '../models/dcb-dashboard.models';
+import { environment } from '@environments/environment';
+
+@Injectable({ providedIn: 'root' })
+export class DcbDashboardService {
+ private http = inject(HttpClient);
+ private apiUrl = `${environment.apiUrl}/dcb-dashboard`;
+
+ // Données mockées pour le développement
+ private mockData: DcbDashboardData = {
+ kpis: [
+ {
+ title: 'Revenue Total',
+ value: 125430,
+ change: 12.5,
+ changeType: 'positive',
+ icon: 'lucideTrendingUp',
+ color: 'success',
+ format: 'currency',
+ currency: 'XOF'
+ },
+ {
+ title: 'Paiements Today',
+ value: 342,
+ change: -2.3,
+ changeType: 'negative',
+ icon: 'lucideCreditCard',
+ color: 'primary',
+ format: 'number'
+ },
+ {
+ title: 'Taux de Succès',
+ value: 98.2,
+ change: 1.2,
+ changeType: 'positive',
+ icon: 'lucideCheckCircle',
+ color: 'info',
+ format: 'percentage'
+ },
+ {
+ title: 'Abonnements Actifs',
+ value: 12543,
+ change: 8.7,
+ changeType: 'positive',
+ icon: 'lucideUsers',
+ color: 'warning',
+ format: 'number'
+ }
+ ],
+ paymentChart: [
+ { period: 'Jan', successful: 12000, failed: 300, pending: 500, revenue: 4500000 },
+ { period: 'Fév', successful: 15000, failed: 250, pending: 400, revenue: 5200000 },
+ { period: 'Mar', successful: 18000, failed: 200, pending: 300, revenue: 6100000 },
+ { period: 'Avr', successful: 22000, failed: 150, pending: 250, revenue: 7500000 },
+ { period: 'Mai', successful: 25000, failed: 100, pending: 200, revenue: 8900000 },
+ { period: 'Jun', successful: 30000, failed: 80, pending: 150, revenue: 10500000 }
+ ],
+ subscriptionStats: {
+ total: 15600,
+ active: 12543,
+ trial: 1850,
+ cancelled: 857,
+ expired: 350,
+ growthRate: 8.7
+ },
+ alerts: [
+ {
+ id: '1',
+ type: 'warning',
+ title: 'Taux d\'échec élevé Orange CI',
+ message: 'Le taux d\'échec des paiements Orange Côte d\'Ivoire a augmenté de 15%',
+ timestamp: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago
+ acknowledged: false,
+ priority: 'medium',
+ action: {
+ label: 'Voir les détails',
+ route: '/payments'
+ }
+ },
+ {
+ id: '2',
+ type: 'info',
+ title: 'Maintenance planifiée',
+ message: 'Maintenance système prévue ce soir de 22h à 00h',
+ timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
+ acknowledged: false,
+ priority: 'low'
+ },
+ {
+ id: '3',
+ type: 'success',
+ title: 'Nouveau partenaire activé',
+ message: 'Le partenaire "MTN Senegal" a été configuré avec succès',
+ timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000), // 4 hours ago
+ acknowledged: true,
+ priority: 'low'
+ }
+ ],
+ lastUpdated: new Date()
+ };
+
+ getDcbDashboardData(): Observable
{
+ // En production, utiliser l'API réelle
+ if (environment.production) {
+ return this.http.get(this.apiUrl).pipe(
+ catchError(() => of(this.mockData)) // Fallback sur les données mockées en cas d'erreur
+ );
+ }
+
+ // En développement, retourner les données mockées
+ return of(this.mockData);
+ }
+
+ acknowledgeAlert(alertId: string): Observable<{ success: boolean }> {
+ // Simuler l'acknowledgement
+ const alert = this.mockData.alerts.find(a => a.id === alertId);
+ if (alert) {
+ alert.acknowledged = true;
+ }
+
+ return of({ success: true });
+ }
+
+ refreshData(): Observable {
+ // Simuler un rafraîchissement des données
+ this.mockData.lastUpdated = new Date();
+ return of(this.mockData);
+ }
+}
\ No newline at end of file
diff --git a/src/app/modules/dcb-dashboard/services/dcb.service.ts b/src/app/modules/dcb-dashboard/services/dcb.service.ts
deleted file mode 100644
index c218eda..0000000
--- a/src/app/modules/dcb-dashboard/services/dcb.service.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import { Injectable, inject } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
-import { environment } from '@environments/environment';
-import { Observable, map, catchError, of } from 'rxjs';
-
-import {
- DcbTransaction,
- DcbAnalytics,
- TransactionStats,
- DcbOperator
-} from '../models/dcb';
-
-@Injectable({ providedIn: 'root' })
-export class DcbService {
- private http = inject(HttpClient);
- private apiUrl = `${environment.apiUrl}/dcb`;
-
- // === ANALYTICS & DASHBOARD ===
- getAnalytics(timeRange: string = '7d'): Observable {
- return this.http.get(`${this.apiUrl}/analytics?range=${timeRange}`).pipe(
- catchError(error => {
- console.error('Error loading analytics:', error);
- // Retourner des données mockées en cas d'erreur
- return of(this.getMockAnalytics());
- })
- );
- }
-
- getTransactionStats(): Observable {
- return this.http.get(`${this.apiUrl}/analytics/stats`).pipe(
- catchError(error => {
- console.error('Error loading transaction stats:', error);
- return of({
- pending: 0,
- successful: 0,
- failed: 0,
- refunded: 0
- });
- })
- );
- }
-
- getRecentTransactions(limit: number = 10): Observable {
- return this.http.get(`${this.apiUrl}/transactions/recent?limit=${limit}`).pipe(
- catchError(error => {
- console.error('Error loading recent transactions:', error);
- return of(this.getMockTransactions());
- })
- );
- }
-
- getOperators(): Observable {
- return this.http.get(`${this.apiUrl}/operators`).pipe(
- catchError(error => {
- console.error('Error loading operators:', error);
- return of(this.getMockOperators());
- })
- );
- }
-
- // Données mockées pour le développement
- private getMockAnalytics(): DcbAnalytics {
- return {
- totalRevenue: 125430.50,
- totalTransactions: 2847,
- successRate: 87.5,
- averageAmount: 44.07,
- monthlyGrowth: 12.3,
- todayStats: {
- revenue: 3420.75,
- transactions: 78,
- successCount: 68,
- failedCount: 10
- },
- topOperators: [
- { operator: 'Orange', revenue: 45210.25, count: 1024 },
- { operator: 'Free', revenue: 38150.75, count: 865 },
- { operator: 'SFR', revenue: 22470.50, count: 512 },
- { operator: 'Bouygues', revenue: 19598.00, count: 446 }
- ],
- dailyStats: [
- { date: '2024-01-01', revenue: 3420.75, transactions: 78 },
- { date: '2024-01-02', revenue: 3985.25, transactions: 91 },
- { date: '2024-01-03', revenue: 3125.50, transactions: 71 },
- { date: '2024-01-04', revenue: 4250.00, transactions: 96 },
- { date: '2024-01-05', revenue: 3875.25, transactions: 88 },
- { date: '2024-01-06', revenue: 2980.75, transactions: 68 },
- { date: '2024-01-07', revenue: 4125.50, transactions: 94 }
- ]
- };
- }
-
- private getMockTransactions(): DcbTransaction[] {
- return [
- {
- id: '1',
- msisdn: '+33612345678',
- operator: 'Orange',
- country: 'FR',
- amount: 4.99,
- currency: 'EUR',
- status: 'SUCCESS',
- productName: 'Premium Content',
- transactionDate: new Date('2024-01-07T14:30:00'),
- createdAt: new Date('2024-01-07T14:30:00')
- },
- {
- id: '2',
- msisdn: '+33798765432',
- operator: 'Free',
- country: 'FR',
- amount: 2.99,
- currency: 'EUR',
- status: 'PENDING',
- productName: 'Basic Subscription',
- transactionDate: new Date('2024-01-07T14:25:00'),
- createdAt: new Date('2024-01-07T14:25:00')
- },
- {
- id: '3',
- msisdn: '+33687654321',
- operator: 'SFR',
- country: 'FR',
- amount: 9.99,
- currency: 'EUR',
- status: 'FAILED',
- productName: 'Pro Package',
- transactionDate: new Date('2024-01-07T14:20:00'),
- createdAt: new Date('2024-01-07T14:20:00'),
- errorCode: 'INSUFFICIENT_FUNDS',
- errorMessage: 'Solde insuffisant'
- }
- ];
- }
-
- private getMockOperators(): DcbOperator[] {
- return [
- { id: '1', name: 'Orange', code: 'ORANGE', country: 'FR', status: 'ACTIVE', successRate: 92.5 },
- { id: '2', name: 'Free', code: 'FREE', country: 'FR', status: 'ACTIVE', successRate: 88.2 },
- { id: '3', name: 'SFR', code: 'SFR', country: 'FR', status: 'ACTIVE', successRate: 85.7 },
- { id: '4', name: 'Bouygues', code: 'BOUYGTEL', country: 'FR', status: 'ACTIVE', successRate: 83.9 }
- ];
- }
-}
\ No newline at end of file
diff --git a/src/app/modules/merchants/config/config.html b/src/app/modules/merchants/config/config.html
deleted file mode 100644
index e5bfcf9..0000000
--- a/src/app/modules/merchants/config/config.html
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/src/app/modules/merchants/config/config.spec.ts b/src/app/modules/merchants/config/config.spec.ts
deleted file mode 100644
index 3324e88..0000000
--- a/src/app/modules/merchants/config/config.spec.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-import { MerchantsConfig } from './config';
-describe('MerchantsConfig', () => {});
\ No newline at end of file
diff --git a/src/app/modules/merchants/config/config.ts b/src/app/modules/merchants/config/config.ts
deleted file mode 100644
index d838016..0000000
--- a/src/app/modules/merchants/config/config.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Component, Input } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-import { UiCard } from '@app/components/ui-card';
-import { InputFields } from '@/app/modules/components/input-fields';
-import { InputGroups } from '@/app/modules/components/input-groups';
-import { CheckboxesAndRadios } from '@/app/modules/components/checkboxes-and-radios';
-import { FloatingLabels } from '@/app/modules/components/floating-labels';
-
-@Component({
- selector: 'app-config',
- standalone: true,
- imports: [FormsModule, UiCard, InputFields, InputGroups, CheckboxesAndRadios, FloatingLabels],
- templateUrl: './config.html',
-})
-export class MerchantsConfig {
- @Input() merchantId: string = '';
-
- config = {
- apiKey: '',
- secretKey: '',
- webhookUrl: '',
- callbackUrls: {
- paymentSuccess: '',
- paymentFailed: '',
- subscriptionCreated: '',
- subscriptionCancelled: ''
- },
- allowedIPs: [''],
- rateLimit: 100,
- isActive: true,
- enableSMS: true,
- enableEmail: false
- };
-
- addIP() {
- this.config.allowedIPs.push('');
- }
-
- removeIP(index: number) {
- this.config.allowedIPs.splice(index, 1);
- }
-
- saveConfig() {
- console.log('Saving config:', this.config);
- // Logique de sauvegarde
- }
-
- testWebhook() {
- console.log('Testing webhook:', this.config.webhookUrl);
- // Logique de test
- }
-
- regenerateKeys() {
- console.log('Regenerating API keys');
- // Logique de régénération
- }
-}
\ No newline at end of file
diff --git a/src/app/modules/merchants/history/history.html b/src/app/modules/merchants/history/history.html
deleted file mode 100644
index dba98b7..0000000
--- a/src/app/modules/merchants/history/history.html
+++ /dev/null
@@ -1 +0,0 @@
-Merchants - History
\ No newline at end of file
diff --git a/src/app/modules/merchants/history/history.spec.ts b/src/app/modules/merchants/history/history.spec.ts
deleted file mode 100644
index 3615357..0000000
--- a/src/app/modules/merchants/history/history.spec.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-import { MerchantsHistory } from './history';
-describe('MerchantsHistory', () => {});
\ No newline at end of file
diff --git a/src/app/modules/merchants/history/history.ts b/src/app/modules/merchants/history/history.ts
deleted file mode 100644
index e5230b2..0000000
--- a/src/app/modules/merchants/history/history.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Component } from '@angular/core';
-
-@Component({
- selector: 'app-history',
- templateUrl: './history.html',
-})
-export class MerchantsHistory {}
\ No newline at end of file
diff --git a/src/app/modules/merchants/list/list.html b/src/app/modules/merchants/list/list.html
deleted file mode 100644
index f9f0cfd..0000000
--- a/src/app/modules/merchants/list/list.html
+++ /dev/null
@@ -1 +0,0 @@
-Merchants - List
\ No newline at end of file
diff --git a/src/app/modules/merchants/list/list.spec.ts b/src/app/modules/merchants/list/list.spec.ts
deleted file mode 100644
index d7803eb..0000000
--- a/src/app/modules/merchants/list/list.spec.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-import { MerchantsList } from './list';
-describe('MerchantsList', () => {});
\ No newline at end of file
diff --git a/src/app/modules/merchants/list/list.ts b/src/app/modules/merchants/list/list.ts
deleted file mode 100644
index 24a114a..0000000
--- a/src/app/modules/merchants/list/list.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Component } from '@angular/core';
-
-@Component({
- selector: 'app-list',
- templateUrl: './list.html',
-})
-export class MerchantsList {}
\ No newline at end of file
diff --git a/src/app/modules/merchants/merchants.html b/src/app/modules/merchants/merchants.html
index 55b161e..8a4b5fa 100644
--- a/src/app/modules/merchants/merchants.html
+++ b/src/app/modules/merchants/merchants.html
@@ -1,13 +1,588 @@
-
+
+
-
+
+
+
+
+ Liste des Merchants
+
+
+
+
+
+
+
+
+ Nouveau Merchant
+
+
+
+ Actualiser
+
+
+
+
+
+
+
+
+
+
+ Tous les statuts
+ Actifs
+ En attente
+ Suspendus
+
+
+
+
+ @for (country of countries; track country.code) {
+ {{ country.name }}
+ }
+
+
+
+
+
+
+
+ @if (loading) {
+
+
+ Chargement...
+
+
Chargement des merchants...
+
+ }
+
+
+ @if (error && !loading) {
+
+
+ {{ error }}
+
+ }
+
+
+ @if (!loading && !error) {
+
+
+
+
+
+
+ Merchant
+ Contact
+ Pays
+ Statut
+ Date création
+ Actions
+
+
+
+ @for (merchant of displayedMerchants; track merchant.partnerId) {
+
+
+
+
+
+
+
+
{{ merchant.name }}
+ @if (merchant.companyInfo && merchant.companyInfo.legalName) {
+
{{ merchant.companyInfo.legalName }}
+ }
+
+
+
+
+ {{ merchant.email }}
+ {{ merchant.companyInfo?.address }}
+
+
+ {{ getCountryName(merchant.country) }}
+
+
+
+ {{ getStatusText(merchant.status) }}
+
+
+
+
+ {{ merchant.createdAt | date:'dd/MM/yyyy' }}
+
+
+
+
+
+
+
+
+ @if (merchant.status === 'ACTIVE') {
+
+
+
+ } @else if (merchant.status === 'SUSPENDED') {
+
+
+
+ }
+
+
+
+
+
+
+
+ }
+ @empty {
+
+
+
+ Aucun merchant trouvé
+
+ Créer le premier merchant
+
+
+
+ }
+
+
+
+
+
+
+ @if (filteredMerchants.length > 0) {
+
+ }
+
+ }
+
+
+
+
+
+
+ Configuration
+
+
+
+
+
+ Payment Hub DCB
+
+
+
+
+
+
+
+
+
+
+ @if (configError) {
+
+
+ {{ configError }}
+
+ }
+
+ @if (configSuccess) {
+
+
+ {{ configSuccess }}
+
+ }
+
+
+
+ @for (step of wizardSteps; track $index; let i = $index) {
+
+
+
+
+
+ @if (i > 0) {
+
+ ← Précédent
+
+ } @else {
+
+ }
+
+ @if (i < wizardSteps.length - 1) {
+
+ Suivant →
+
+ } @else {
+
+ @if (configLoading) {
+
+ Chargement...
+
+ }
+ Créer le Merchant
+
+ }
+
+
+ }
+
+
+
+
+
+
+
+
+
+ Statistiques
+
+
+
+
+ @if (statsLoading) {
+
+
+ Chargement...
+
+
Chargement des statistiques...
+
+ }
+
+
+ @if (stats && !statsLoading) {
+
+
+
+
+
+
+
{{ formatNumber(stats.totalTransactions) }}
+
Transactions totales
+
+
+
+
+
+
+
+
+
{{ stats.successRate }}%
+
Taux de succès
+
+
+
+
+
+
+
+
+
{{ formatNumber(stats.activeSubscriptions) }}
+
Abonnements actifs
+
+
+
+
+
+
+
+
+
{{ formatCurrency(stats.totalRevenue) }}
+
Revenue total
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ formatNumber(stats.successfulTransactions) }}
+
Transactions réussies
+
+
+
+
+
{{ formatNumber(stats.failedTransactions) }}
+
Transactions échouées
+
+
+
+
+
+{{ stats.monthlyGrowth }}%
+
Croissance mensuelle
+
+
+
+
+
+
+
+ }
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/app/modules/merchants/merchants.routes.ts b/src/app/modules/merchants/merchants.routes.ts
deleted file mode 100644
index 4ff4bdd..0000000
--- a/src/app/modules/merchants/merchants.routes.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Routes } from '@angular/router';
-import { Merchants } from './merchants';
-import { authGuard } from '../../core/guards/auth.guard';
-import { roleGuard } from '../../core/guards/role.guard';
-
-export const MERCHANTS_ROUTES: Routes = [
- {
- path: 'merchants',
- canActivate: [authGuard, roleGuard],
- component: Merchants,
- data: {
- title: 'Gestion partenaires',
- }
- }
-];
\ No newline at end of file
diff --git a/src/app/modules/merchants/merchants.ts b/src/app/modules/merchants/merchants.ts
index 4ca022a..f423005 100644
--- a/src/app/modules/merchants/merchants.ts
+++ b/src/app/modules/merchants/merchants.ts
@@ -1,24 +1,339 @@
-import { Component } from '@angular/core';
+import { Component, inject, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { RouterModule } from '@angular/router';
+import { NgIconComponent } from '@ng-icons/core';
+import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormControl } from '@angular/forms';
+import { NgbNavModule, NgbProgressbarModule, NgbPaginationModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
+import { firstValueFrom } from 'rxjs'; // ← AJOUT IMPORT
import { PageTitle } from '@app/components/page-title/page-title';
-import { MerchantsList } from './list/list';
-import { MerchantsConfig } from './config/config';
-import { MerchantsHistory } from './history/history';
+import { UiCard } from '@app/components/ui-card';
+import { MerchantsService } from './services/merchants.service';
+import { MerchantResponse, MerchantStats, MerchantFormData } from './models/merchant.models';
@Component({
- selector: 'app-merchants',
- imports: [PageTitle,
- //MerchantsList,
- //MerchantsConfig,
- //MerchantsHistory
- ],
+ selector: 'app-merchant',
+ standalone: true,
+ imports: [
+ CommonModule,
+ RouterModule,
+ NgIconComponent,
+ FormsModule,
+ ReactiveFormsModule,
+ NgbNavModule,
+ NgbProgressbarModule,
+ NgbPaginationModule,
+ NgbDropdownModule,
+ PageTitle,
+ UiCard
+ ],
templateUrl: './merchants.html',
})
-export class Merchants {
- activeView: string = 'list';
- selectedMerchantId: string = '';
+export class Merchants implements OnInit {
+ private fb = inject(FormBuilder);
+ private merchantsService = inject(MerchantsService);
- setActiveView(view: string, merchantId?: string) {
- this.activeView = view;
- this.selectedMerchantId = merchantId || '';
+ // Navigation par onglets
+ activeTab: 'list' | 'config' | 'stats' = 'list';
+
+ // === DONNÉES LISTE ===
+ merchants: MerchantResponse[] = [];
+ loading = false;
+ error = '';
+
+ // Pagination et filtres
+ currentPage = 1;
+ itemsPerPage = 10;
+ searchTerm = '';
+ statusFilter: 'all' | 'ACTIVE' | 'PENDING' | 'SUSPENDED' = 'all';
+ countryFilter = 'all';
+
+ // === DONNÉES CONFIG (WIZARD) ===
+ currentStep = 0;
+ wizardSteps = [
+ { id: 'company-info', icon: 'lucideBuilding', title: 'Informations Société', subtitle: 'Détails entreprise' },
+ { id: 'contact-info', icon: 'lucideUser', title: 'Contact Principal', subtitle: 'Personne de contact' },
+ { id: 'payment-config', icon: 'lucideCreditCard', title: 'Configuration Paiements', subtitle: 'Paramètres DCB' },
+ { id: 'webhooks', icon: 'lucideWebhook', title: 'Webhooks', subtitle: 'Notifications et retours' },
+ { id: 'review', icon: 'lucideCheckCircle', title: 'Validation', subtitle: 'Vérification finale' }
+ ];
+ configLoading = false;
+ configError = '';
+ configSuccess = '';
+
+ // === DONNÉES STATS ===
+ stats: MerchantStats | null = null;
+ statsLoading = false;
+
+ supportedOperatorsArray: FormControl[] = [
+ this.fb.control(false), // Orange
+ this.fb.control(false), // MTN
+ this.fb.control(false), // Airtel
+ this.fb.control(false) // Moov
+ ];
+
+
+ // === FORMULAIRE ===
+ merchantForm = this.fb.group({
+ companyInfo: this.fb.group({
+ name: ['', [Validators.required, Validators.minLength(2)]],
+ legalName: ['', [Validators.required]],
+ taxId: [''],
+ address: ['', [Validators.required]],
+ country: ['CIV', [Validators.required]]
+ }),
+ contactInfo: this.fb.group({
+ email: ['', [Validators.required, Validators.email]],
+ phone: ['', [Validators.required]],
+ firstName: ['', [Validators.required]],
+ lastName: ['', [Validators.required]]
+ }),
+ paymentConfig: this.fb.group({
+ // CORRECTION : Utiliser l'array déclaré séparément
+ supportedOperators: this.fb.array(this.supportedOperatorsArray),
+ defaultCurrency: ['XOF', [Validators.required]],
+ maxTransactionAmount: [50000, [Validators.min(1000), Validators.max(1000000)]]
+ }),
+ webhookConfig: this.fb.group({
+ subscription: this.fb.group({
+ onCreate: ['', [Validators.pattern('https?://.+')]],
+ onRenew: ['', [Validators.pattern('https?://.+')]],
+ onCancel: ['', [Validators.pattern('https?://.+')]],
+ onExpire: ['', [Validators.pattern('https?://.+')]]
+ }),
+ payment: this.fb.group({
+ onSuccess: ['', [Validators.pattern('https?://.+')]],
+ onFailure: ['', [Validators.pattern('https?://.+')]],
+ onRefund: ['', [Validators.pattern('https?://.+')]]
+ })
+ })
+ });
+
+ // Données partagées
+ countries = [
+ { code: 'all', name: 'Tous les pays' },
+ { code: 'CIV', name: 'Côte d\'Ivoire' },
+ { code: 'SEN', name: 'Sénégal' },
+ { code: 'CMR', name: 'Cameroun' },
+ { code: 'COD', name: 'RDC' },
+ { code: 'TUN', name: 'Tunisie' },
+ { code: 'BFA', name: 'Burkina Faso' },
+ { code: 'MLI', name: 'Mali' },
+ { code: 'GIN', name: 'Guinée' }
+ ];
+
+ operators = ['Orange', 'MTN', 'Airtel', 'Moov'];
+
+ ngOnInit() {
+ this.loadMerchants();
+ this.loadStats();
+ }
+
+ // === MÉTHODES LISTE ===
+ loadMerchants() {
+ this.loading = true;
+ this.merchantsService.getAllMerchants().subscribe({
+ next: (merchants) => {
+ this.merchants = merchants;
+ this.loading = false;
+ },
+ error: (error) => {
+ this.error = 'Erreur lors du chargement des merchants';
+ this.loading = false;
+ console.error('Error loading merchants:', error);
+ }
+ });
+ }
+
+ get filteredMerchants(): MerchantResponse[] {
+ return this.merchants.filter(merchant => {
+ const matchesSearch = !this.searchTerm ||
+ merchant.name.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
+ merchant.email.toLowerCase().includes(this.searchTerm.toLowerCase());
+
+ const matchesStatus = this.statusFilter === 'all' || merchant.status === this.statusFilter;
+ const matchesCountry = this.countryFilter === 'all' || merchant.country === this.countryFilter;
+
+ return matchesSearch && matchesStatus && matchesCountry;
+ });
+ }
+
+ get displayedMerchants(): MerchantResponse[] {
+ const startIndex = (this.currentPage - 1) * this.itemsPerPage;
+ return this.filteredMerchants.slice(startIndex, startIndex + this.itemsPerPage);
+ }
+
+ getStatusBadgeClass(status: string): string {
+ switch (status) {
+ case 'ACTIVE': return 'bg-success';
+ case 'PENDING': return 'bg-warning';
+ case 'SUSPENDED': return 'bg-danger';
+ default: return 'bg-secondary';
+ }
+ }
+
+ getStatusText(status: string): string {
+ switch (status) {
+ case 'ACTIVE': return 'Actif';
+ case 'PENDING': return 'En attente';
+ case 'SUSPENDED': return 'Suspendu';
+ default: return 'Inconnu';
+ }
+ }
+
+ getCountryName(code: string): string {
+ const country = this.countries.find(c => c.code === code);
+ return country ? country.name : code;
+ }
+
+ suspendMerchant(merchant: MerchantResponse) {
+ if (confirm(`Êtes-vous sûr de vouloir suspendre ${merchant.name} ?`)) {
+ this.merchantsService.updateMerchantStatus(merchant.partnerId, 'SUSPENDED').subscribe({
+ next: () => {
+ merchant.status = 'SUSPENDED';
+ },
+ error: (error) => {
+ console.error('Error suspending merchant:', error);
+ alert('Erreur lors de la suspension du merchant');
+ }
+ });
+ }
+ }
+
+ activateMerchant(merchant: MerchantResponse) {
+ this.merchantsService.updateMerchantStatus(merchant.partnerId, 'ACTIVE').subscribe({
+ next: () => {
+ merchant.status = 'ACTIVE';
+ },
+ error: (error) => {
+ console.error('Error activating merchant:', error);
+ alert('Erreur lors de l\'activation du merchant');
+ }
+ });
+ }
+
+ onPageChange(page: number) {
+ this.currentPage = page;
+ }
+
+ clearFilters() {
+ this.searchTerm = '';
+ this.statusFilter = 'all';
+ this.countryFilter = 'all';
+ this.currentPage = 1;
+ }
+
+ // === MÉTHODES CONFIG (WIZARD) ===
+ get progressValue(): number {
+ return ((this.currentStep + 1) / this.wizardSteps.length) * 100;
+ }
+
+ nextStep() {
+ if (this.currentStep < this.wizardSteps.length - 1) {
+ this.currentStep++;
+ }
+ }
+
+ previousStep() {
+ if (this.currentStep > 0) {
+ this.currentStep--;
+ }
+ }
+
+ goToStep(index: number) {
+ this.currentStep = index;
+ }
+
+ getSelectedOperators(): string[] {
+ return this.supportedOperatorsArray
+ .map((control, index) => control.value ? this.operators[index] : null)
+ .filter(op => op !== null) as string[];
+ }
+
+ async submitForm() {
+ if (this.merchantForm.valid) {
+ this.configLoading = true;
+ this.configError = '';
+
+ try {
+ const formData = this.merchantForm.value as unknown as MerchantFormData;
+
+ const registrationData = {
+ name: formData.companyInfo.name,
+ email: formData.contactInfo.email,
+ country: formData.companyInfo.country,
+ companyInfo: {
+ legalName: formData.companyInfo.legalName,
+ taxId: formData.companyInfo.taxId,
+ address: formData.companyInfo.address
+ }
+ };
+
+ // CORRECTION : Utilisation de firstValueFrom au lieu de toPromise()
+ const partnerResponse = await firstValueFrom(
+ this.merchantsService.registerMerchant(registrationData)
+ );
+
+ if (partnerResponse) {
+ // CORRECTION : Utilisation de firstValueFrom au lieu de toPromise()
+ await firstValueFrom(
+ this.merchantsService.updateCallbacks(partnerResponse.partnerId, formData.webhookConfig)
+ );
+
+ this.configSuccess = `Merchant créé avec succès! API Key: ${partnerResponse.apiKey}`;
+ this.merchantForm.reset();
+ this.currentStep = 0;
+ this.loadMerchants(); // Recharger la liste
+ this.activeTab = 'list'; // Retourner à la liste
+ }
+
+ } catch (error) {
+ this.configError = 'Erreur lors de la création du merchant';
+ console.error('Error creating merchant:', error);
+ } finally {
+ this.configLoading = false;
+ }
+ }
+ }
+
+ // === MÉTHODES STATS ===
+ loadStats() {
+ this.statsLoading = true;
+ // Données mockées pour l'exemple
+ this.stats = {
+ totalTransactions: 12543,
+ successfulTransactions: 12089,
+ failedTransactions: 454,
+ totalRevenue: 45875000,
+ activeSubscriptions: 8450,
+ successRate: 96.4,
+ monthlyGrowth: 12.3
+ };
+ this.statsLoading = false;
+ }
+
+ formatNumber(num: number): string {
+ return new Intl.NumberFormat('fr-FR').format(num);
+ }
+
+ formatCurrency(amount: number): string {
+ return new Intl.NumberFormat('fr-FR', {
+ style: 'currency',
+ currency: 'XOF'
+ }).format(amount);
+ }
+
+ // Méthode utilitaire pour Math.min dans le template
+ mathMin(a: number, b: number): number {
+ return Math.min(a, b);
+ }
+
+ // === MÉTHODES COMMUNES ===
+ onTabChange(tab: 'list' | 'config' | 'stats') {
+ this.activeTab = tab;
+ if (tab === 'list') {
+ this.loadMerchants();
+ } else if (tab === 'stats') {
+ this.loadStats();
+ }
}
}
\ No newline at end of file
diff --git a/src/app/modules/merchants/models/merchant.models.ts b/src/app/modules/merchants/models/merchant.models.ts
new file mode 100644
index 0000000..1dea78d
--- /dev/null
+++ b/src/app/modules/merchants/models/merchant.models.ts
@@ -0,0 +1,81 @@
+export interface MerchantRegistration {
+ name: string;
+ email: string;
+ country: string;
+ companyInfo?: {
+ legalName?: string;
+ taxId?: string;
+ address?: string;
+ };
+}
+
+export interface MerchantResponse {
+ partnerId: string;
+ name: string;
+ email: string;
+ country: string;
+ apiKey: string;
+ secretKey: string;
+ status: 'PENDING' | 'ACTIVE' | 'SUSPENDED';
+ createdAt: string;
+ companyInfo?: {
+ legalName?: string;
+ taxId?: string;
+ address?: string;
+ };
+}
+
+export interface CallbackConfiguration {
+ headerEnrichment?: {
+ url?: string;
+ method?: 'GET' | 'POST';
+ headers?: { [key: string]: string };
+ };
+ subscription?: {
+ onCreate?: string;
+ onRenew?: string;
+ onCancel?: string;
+ onExpire?: string;
+ };
+ payment?: {
+ onSuccess?: string;
+ onFailure?: string;
+ onRefund?: string;
+ };
+ authentication?: {
+ onSuccess?: string;
+ onFailure?: string;
+ };
+}
+
+export interface MerchantStats {
+ totalTransactions: number;
+ successfulTransactions: number;
+ failedTransactions: number;
+ totalRevenue: number;
+ activeSubscriptions: number;
+ successRate: number;
+ monthlyGrowth: number;
+}
+
+export interface MerchantFormData {
+ companyInfo: {
+ name: string;
+ legalName: string;
+ taxId: string;
+ address: string;
+ country: string;
+ };
+ contactInfo: {
+ email: string;
+ phone: string;
+ firstName: string;
+ lastName: string;
+ };
+ paymentConfig: {
+ supportedOperators: string[];
+ defaultCurrency: string;
+ maxTransactionAmount: number;
+ };
+ webhookConfig: CallbackConfiguration;
+}
\ No newline at end of file
diff --git a/src/app/modules/merchants/services/config.service.ts b/src/app/modules/merchants/services/config.service.ts
deleted file mode 100644
index 21b5f67..0000000
--- a/src/app/modules/merchants/services/config.service.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Injectable, inject } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
-import { environment } from '@environments/environment';
-import { Observable } from 'rxjs';
-
-export interface MerchantsConfig {
- apiKey: string;
- secretKey: string;
- webhookUrl: string;
- callbackUrls: {
- paymentSuccess: string;
- paymentFailed: string;
- subscriptionCreated: string;
- subscriptionCancelled: string;
- };
- allowedIPs: string[];
- rateLimit: number;
- isActive: boolean;
-}
-
-@Injectable({ providedIn: 'root' })
-export class MerchantsConfigService {
- private http = inject(HttpClient);
-
- getConfig(merchantId: string): Observable {
- return this.http.get(
- `${environment.apiUrl}/merchants/${merchantId}/config`
- );
- }
-
- updateConfig(merchantId: string, config: MerchantsConfig): Observable {
- return this.http.put(
- `${environment.apiUrl}/merchants/${merchantId}/config`,
- config
- );
- }
-
- regenerateApiKey(merchantId: string): Observable<{ apiKey: string }> {
- return this.http.post<{ apiKey: string }>(
- `${environment.apiUrl}/merchants/${merchantId}/regenerate-key`,
- {}
- );
- }
-
- testWebhook(merchantId: string): Observable<{ success: boolean; message: string }> {
- return this.http.post<{ success: boolean; message: string }>(
- `${environment.apiUrl}/merchants/${merchantId}/test-webhook`,
- {}
- );
- }
-}
\ No newline at end of file
diff --git a/src/app/modules/merchants/services/history.service.ts b/src/app/modules/merchants/services/history.service.ts
deleted file mode 100644
index 83b6df0..0000000
--- a/src/app/modules/merchants/services/history.service.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Injectable } from '@angular/core';
-
-@Injectable({
- providedIn: 'root'
-})
-export class MerchantsHistoryService {
- constructor() {}
-}
\ No newline at end of file
diff --git a/src/app/modules/merchants/services/list.service.ts b/src/app/modules/merchants/services/list.service.ts
deleted file mode 100644
index 6d7cd86..0000000
--- a/src/app/modules/merchants/services/list.service.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Injectable } from '@angular/core';
-
-@Injectable({
- providedIn: 'root'
-})
-export class MerchantsListService {
- constructor() {}
-}
\ No newline at end of file
diff --git a/src/app/modules/merchants/services/merchants.service.ts b/src/app/modules/merchants/services/merchants.service.ts
index 1844459..3b41a52 100644
--- a/src/app/modules/merchants/services/merchants.service.ts
+++ b/src/app/modules/merchants/services/merchants.service.ts
@@ -1,104 +1,54 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
-import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms';
import { environment } from '@environments/environment';
import { Observable } from 'rxjs';
-
-export interface Merchant {
- id?: string;
- name: string;
- logo?: string;
- description: string;
- address: string;
- phone: string;
- technicalContacts: TechnicalContact[];
- services: Service[];
- status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
-}
-
-export interface TechnicalContact {
- firstName: string;
- lastName: string;
- phone: string;
- email: string;
-}
-
-export interface Service {
- name: string;
- description: string;
- pricing: Pricing[];
-}
-
-export interface Pricing {
- name: string;
- type: 'Daily' | 'Weekly' | 'Monthly' | 'OneTime';
- amount: number;
- tax: number;
- currency: string;
- periodicity: 'Daily' | 'Weekly' | 'Monthly' | 'OneTime';
-}
+import {
+ MerchantRegistration,
+ MerchantResponse,
+ CallbackConfiguration,
+ MerchantStats
+} from '../models/merchant.models';
@Injectable({ providedIn: 'root' })
-export class MerchantService {
+export class MerchantsService {
private http = inject(HttpClient);
- private fb = inject(FormBuilder);
+ private apiUrl = `${environment.apiUrl}/partners`;
- // Formulaires réactifs pour création/édition
- createMerchantForm(): FormGroup {
- return this.fb.group({
- name: ['', [Validators.required, Validators.minLength(2)]],
- logo: [''],
- description: ['', [Validators.required]],
- address: ['', [Validators.required]],
- phone: ['', [Validators.required, Validators.pattern(/^\+?[\d\s-]+$/)]],
- technicalContacts: this.fb.array([]),
- services: this.fb.array([]),
- status: ['ACTIVE']
- });
+ // Enregistrement d'un nouveau merchant
+ registerMerchant(registration: MerchantRegistration): Observable {
+ return this.http.post(`${this.apiUrl}/register`, registration);
}
- createTechnicalContactForm(): FormGroup {
- return this.fb.group({
- firstName: ['', Validators.required],
- lastName: ['', Validators.required],
- phone: ['', Validators.required],
- email: ['', [Validators.required, Validators.email]]
- });
+ // Configuration des webhooks
+ updateCallbacks(partnerId: string, callbacks: CallbackConfiguration): Observable {
+ return this.http.put(`${this.apiUrl}/${partnerId}/callbacks`, callbacks);
}
- createServiceForm(): FormGroup {
- return this.fb.group({
- name: ['', Validators.required],
- description: ['', Validators.required],
- pricing: this.fb.array([])
- });
+ // Récupération de tous les merchants
+ getAllMerchants(): Observable {
+ return this.http.get(`${this.apiUrl}`);
}
- createPricingForm(): FormGroup {
- return this.fb.group({
- name: ['', Validators.required],
- type: ['Daily', Validators.required],
- amount: [0, [Validators.required, Validators.min(0)]],
- tax: [0, [Validators.required, Validators.min(0)]],
- currency: ['XOF', Validators.required],
- periodicity: ['Daily', Validators.required]
- });
+ // Récupération d'un merchant par ID
+ getMerchantById(partnerId: string): Observable {
+ return this.http.get(`${this.apiUrl}/${partnerId}`);
}
- // Méthodes API
- getMerchants(): Observable {
- return this.http.get(`${environment.apiUrl}/merchants`);
+ // Statistiques d'un merchant
+ getMerchantStats(partnerId: string): Observable {
+ return this.http.get(`${this.apiUrl}/${partnerId}/stats`);
}
- createMerchant(merchant: Merchant): Observable {
- return this.http.post(`${environment.apiUrl}/merchants`, merchant);
+ // Test des webhooks
+ testWebhook(url: string, event: string): Observable<{ success: boolean; response: any; responseTime: number }> {
+ return this.http.post<{ success: boolean; response: any; responseTime: number }>(
+ `${environment.apiUrl}/webhooks/test`,
+ { url, event }
+ );
}
- updateMerchant(id: string, merchant: Merchant): Observable {
- return this.http.put(`${environment.apiUrl}/merchants/${id}`, merchant);
- }
-
- deleteMerchant(id: string): Observable {
- return this.http.delete(`${environment.apiUrl}/merchants/${id}`);
+ // Suspension/Activation d'un merchant
+ updateMerchantStatus(partnerId: string, status: 'ACTIVE' | 'SUSPENDED'): Observable {
+ return this.http.patch(`${this.apiUrl}/${partnerId}`, { status });
}
}
\ No newline at end of file
diff --git a/src/app/modules/merchants/wizard-with-progress.ts b/src/app/modules/merchants/wizard-with-progress.ts
new file mode 100644
index 0000000..bba8641
--- /dev/null
+++ b/src/app/modules/merchants/wizard-with-progress.ts
@@ -0,0 +1,312 @@
+import { Component } from '@angular/core'
+import { NgIcon } from '@ng-icons/core'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { UiCard } from '@app/components/ui-card'
+import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap'
+import { wizardSteps } from '@/app/modules/merchants/data'
+
+@Component({
+ selector: 'app-wizard-with-progress',
+ imports: [
+ NgIcon,
+ ReactiveFormsModule,
+ FormsModule,
+ UiCard,
+ NgbProgressbarModule,
+ ],
+ template: `
+
+ Exclusive
+
+
+
+
+
+
+ @for (step of wizardSteps; track $index; let i = $index) {
+
+ @switch (i) {
+ @case (0) {
+
+ }
+ @case (1) {
+
+ }
+ @case (2) {
+
+
+ Choose Course
+
+ Select
+ Engineering
+ Medical
+ Business
+
+
+
+ Enrollment Type
+
+ Select
+ Full Time
+ Part Time
+
+
+
+ Preferred Batch Time
+
+ Select Time
+ Morning (8am – 12pm)
+ Afternoon (1pm – 5pm)
+ Evening (6pm – 9pm)
+
+
+
+ Mode of Study
+
+ Select Mode
+ Offline
+ Online
+ Hybrid
+
+
+
+ }
+ @case (3) {
+
+ }
+ @case (4) {
+
+ Upload ID Proof
+
+
+
+ Upload Previous Marksheet
+
+
+ }
+ }
+
+
+ @if (i > 0) {
+
+ ← Back:
+ {{ step.title }}
+
+ }
+ @if (i < wizardSteps.length - 1) {
+
+ Next: {{ step.title }} →
+
+ }
+ @if (i === wizardSteps.length - 1) {
+
+ Submit Application
+
+ }
+
+
+ }
+
+
+
+ `,
+ styles: ``,
+})
+export class WizardWithProgress {
+ currentStep = 0
+
+ nextStep() {
+ if (this.currentStep < wizardSteps.length - 1) this.currentStep++
+ }
+
+ previousStep() {
+ if (this.currentStep > 0) this.currentStep--
+ }
+
+ goToStep(index: number) {
+ this.currentStep = index
+ }
+ get progressValue(): number {
+ const totalSteps = this.wizardSteps.length
+ return ((this.currentStep + 1) / totalSteps) * 100
+ }
+
+ protected readonly wizardSteps = wizardSteps
+}
diff --git a/src/app/modules/merchants/wizard.html b/src/app/modules/merchants/wizard.html
index 125fe93..e310fc9 100644
--- a/src/app/modules/merchants/wizard.html
+++ b/src/app/modules/merchants/wizard.html
@@ -7,11 +7,7 @@
diff --git a/src/app/modules/merchants/wizard.ts b/src/app/modules/merchants/wizard.ts
index 2140ede..1990d11 100644
--- a/src/app/modules/merchants/wizard.ts
+++ b/src/app/modules/merchants/wizard.ts
@@ -1,12 +1,10 @@
import { Component } from '@angular/core'
import { PageTitle } from '@app/components/page-title/page-title'
-import { BasicWizard } from '@/app/modules/components/basic-wizard'
import { WizardWithProgress } from '@/app/modules/components/wizard-with-progress'
-import { VerticalWizard } from '@/app/modules/components/vertical-wizard'
@Component({
- selector: 'app-wizard',
- imports: [PageTitle, BasicWizard, WizardWithProgress, VerticalWizard],
+ selector: 'app-merchant-wizard',
+ imports: [PageTitle, WizardWithProgress],
templateUrl: './wizard.html',
styles: ``,
})
diff --git a/src/app/modules/modules-routing.module.ts b/src/app/modules/modules.routes.ts
similarity index 62%
rename from src/app/modules/modules-routing.module.ts
rename to src/app/modules/modules.routes.ts
index 2297b33..f76ae14 100644
--- a/src/app/modules/modules-routing.module.ts
+++ b/src/app/modules/modules.routes.ts
@@ -6,26 +6,14 @@ import { Users } from '@modules/users/users';
// Composants principaux
import { DcbDashboard } from './dcb-dashboard/dcb-dashboard';
-
import { Team } from './team/team';
import { Transactions } from './transactions/transactions';
-import { TransactionsList } from './transactions/list/list';
-import { TransactionDetails } from './transactions/details/details';
-
import { Merchants } from './merchants/merchants';
-import { MerchantsList } from './merchants/list/list';
-import { MerchantsConfig } from './merchants/config/config';
-import { MerchantsHistory } from './merchants/history/history';
-
-import { Operators } from './operators/operators';
import { OperatorsConfig } from './operators/config/config';
import { OperatorsStats } from './operators/stats/stats';
-
-import { Webhooks } from './webhooks/webhooks';
import { WebhooksHistory } from './webhooks/history/history';
import { WebhooksStatus } from './webhooks/status/status';
import { WebhooksRetry } from './webhooks/retry/retry';
-
import { Settings } from './settings/settings';
import { Integrations } from './integrations/integrations';
import { Support } from './support/support';
@@ -34,25 +22,9 @@ import { Documentation } from './documentation/documentation';
import { Help } from './help/help';
import { About } from './about/about';
-
const routes: Routes = [
-
-
// ---------------------------
- // Users
- // ---------------------------
- {
- path: 'users',
- canActivate: [authGuard, roleGuard],
- component: Users,
- data: {
- title: 'Gestion des Utilisateurs',
- requiredRoles: ['admin'] // pour information
- }
- },
-
- // ---------------------------
- // Dashboard & Team
+ // Dashboard
// ---------------------------
{
path: 'dcb-dashboard',
@@ -60,15 +32,21 @@ const routes: Routes = [
component: DcbDashboard,
data: {
title: 'Dashboard DCB',
- requiredRoles: ['admin', 'merchant', 'support']
+ module: 'dcb-dashboard'
}
},
+ // ---------------------------
+ // Team
+ // ---------------------------
{
path: 'team',
component: Team,
canActivate: [authGuard, roleGuard],
- data: { title: 'Team' }
+ data: {
+ title: 'Team',
+ module: 'team'
+ }
},
// ---------------------------
@@ -80,7 +58,7 @@ const routes: Routes = [
canActivate: [authGuard, roleGuard],
data: {
title: 'Transactions DCB',
- requiredRoles: ['admin', 'merchant', 'support']
+ module: 'transactions'
}
},
{
@@ -89,7 +67,20 @@ const routes: Routes = [
canActivate: [authGuard, roleGuard],
data: {
title: 'Détails Transaction',
- requiredRoles: ['admin', 'merchant', 'support']
+ module: 'transactions'
+ }
+ },
+
+ // ---------------------------
+ // Users (Admin seulement)
+ // ---------------------------
+ {
+ path: 'users',
+ canActivate: [authGuard, roleGuard],
+ component: Users,
+ data: {
+ title: 'Gestion des Utilisateurs',
+ module: 'users'
}
},
@@ -98,23 +89,39 @@ const routes: Routes = [
// ---------------------------
{
path: 'merchants',
+ component: Merchants,
canActivate: [authGuard, roleGuard],
- children: [
- { path: 'list', component: MerchantsList, data: { title: 'Liste des Marchands' } },
- { path: 'config', component: MerchantsConfig, data: { title: 'Configuration API / Webhooks' } },
- { path: 'history', component: MerchantsHistory, data: { title: 'Statistiques & Historique' } },
- ]
+ data: {
+ title: 'Gestion des Merchants',
+ module: 'merchants',
+ requiredRoles: ['admin', 'support']
+ }
},
-
+
// ---------------------------
- // Operators
+ // Operators (Admin seulement)
// ---------------------------
{
path: 'operators',
canActivate: [authGuard, roleGuard],
+ data: { module: 'operators' },
children: [
- { path: 'config', component: OperatorsConfig, data: { title: 'Paramètres d\'Intégration' } },
- { path: 'stats', component: OperatorsStats, data: { title: 'Performance & Monitoring' } },
+ {
+ path: 'config',
+ component: OperatorsConfig,
+ data: {
+ title: 'Paramètres d\'Intégration',
+ module: 'operators/config'
+ }
+ },
+ {
+ path: 'stats',
+ component: OperatorsStats,
+ data: {
+ title: 'Performance & Monitoring',
+ module: 'operators/stats'
+ }
+ },
]
},
@@ -124,27 +131,59 @@ const routes: Routes = [
{
path: 'webhooks',
canActivate: [authGuard, roleGuard],
+ data: { module: 'webhooks' },
children: [
- { path: 'history', component: WebhooksHistory, data: { title: 'Historique' } },
- { path: 'status', component: WebhooksStatus, data: { title: 'Statut des Requêtes' } },
- { path: 'retry', component: WebhooksRetry, data: { title: 'Relancer Webhook' } },
+ {
+ path: 'history',
+ component: WebhooksHistory,
+ data: {
+ title: 'Historique',
+ module: 'webhooks/history'
+ }
+ },
+ {
+ path: 'status',
+ component: WebhooksStatus,
+ data: {
+ title: 'Statut des Requêtes',
+ module: 'webhooks/status'
+ }
+ },
+ {
+ path: 'retry',
+ component: WebhooksRetry,
+ data: {
+ title: 'Relancer Webhook',
+ module: 'webhooks/retry'
+ }
+ },
]
},
// ---------------------------
- // Settings & Integrations (Admin seulement)
+ // Settings
// ---------------------------
{
path: 'settings',
component: Settings,
canActivate: [authGuard, roleGuard],
- data: { title: 'Paramètres Système' }
+ data: {
+ title: 'Paramètres Système',
+ module: 'settings'
+ }
},
+
+ // ---------------------------
+ // Integrations (Admin seulement)
+ // ---------------------------
{
path: 'integrations',
component: Integrations,
canActivate: [authGuard, roleGuard],
- data: { title: 'Intégrations Externes' }
+ data: {
+ title: 'Intégrations Externes',
+ module: 'integrations'
+ }
},
// ---------------------------
@@ -154,13 +193,19 @@ const routes: Routes = [
path: 'support',
component: Support,
canActivate: [authGuard, roleGuard],
- data: { title: 'Support' }
+ data: {
+ title: 'Support',
+ module: 'support'
+ }
},
{
path: 'profile',
component: MyProfile,
canActivate: [authGuard, roleGuard],
- data: { title: 'Mon Profil' }
+ data: {
+ title: 'Mon Profil',
+ module: 'profile'
+ }
},
// ---------------------------
@@ -170,19 +215,28 @@ const routes: Routes = [
path: 'documentation',
component: Documentation,
canActivate: [authGuard, roleGuard],
- data: { title: 'Documentation' }
+ data: {
+ title: 'Documentation',
+ module: 'documentation'
+ }
},
{
path: 'help',
component: Help,
canActivate: [authGuard, roleGuard],
- data: { title: 'Aide' }
+ data: {
+ title: 'Aide',
+ module: 'help'
+ }
},
{
path: 'about',
component: About,
canActivate: [authGuard, roleGuard],
- data: { title: 'À propos' }
+ data: {
+ title: 'À propos',
+ module: 'about'
+ }
},
];
@@ -190,4 +244,8 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
-export class ModulesRoutingModule {}
+export class ModulesRoutes {
+ constructor() {
+ console.log('Modules routes loaded:', routes);
+ }
+}
\ No newline at end of file
diff --git a/src/app/modules/operators/config/config.ts b/src/app/modules/operators/config/config.ts
index a750efd..2bf2c7a 100644
--- a/src/app/modules/operators/config/config.ts
+++ b/src/app/modules/operators/config/config.ts
@@ -6,7 +6,7 @@ import { CheckboxesAndRadios } from '@/app/modules/components/checkboxes-and-rad
import { InputTouchspin } from '@/app/modules/components/input-touchspin';
@Component({
- selector: 'app-config',
+ selector: 'app-operator-config',
//imports: [FormsModule, UiCard, InputFields, CheckboxesAndRadios, InputTouchspin],
templateUrl: './config.html',
})
diff --git a/src/app/modules/transactions/plugins.html b/src/app/modules/transactions/plugins.html
deleted file mode 100644
index 900feca..0000000
--- a/src/app/modules/transactions/plugins.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
diff --git a/src/app/modules/transactions/plugins.spec.ts b/src/app/modules/transactions/plugins.spec.ts
deleted file mode 100644
index 33f0a2f..0000000
--- a/src/app/modules/transactions/plugins.spec.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing'
-
-import { Plugins } from './plugins'
-
-describe('Plugins', () => {
- let component: Plugins
- let fixture: ComponentFixture
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [Plugins],
- }).compileComponents()
-
- fixture = TestBed.createComponent(Plugins)
- component = fixture.componentInstance
- fixture.detectChanges()
- })
-
- it('should create', () => {
- expect(component).toBeTruthy()
- })
-})
diff --git a/src/app/modules/transactions/plugins.ts b/src/app/modules/transactions/plugins.ts
deleted file mode 100644
index c91c316..0000000
--- a/src/app/modules/transactions/plugins.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Component } from '@angular/core'
-import { PageTitle } from '@app/components/page-title/page-title'
-import { Flatpickr } from '@/app/modules/components/flatpickr'
-import { Choicesjs } from '@/app/modules/components/choicesjs'
-import { Typeaheds } from '@/app/modules/components/typeaheds'
-import { InputTouchspin } from '@/app/modules/components/input-touchspin'
-
-@Component({
- selector: 'app-plugins',
- imports: [PageTitle, Flatpickr, Choicesjs, Typeaheds, InputTouchspin],
- templateUrl: './plugins.html',
- styles: ``,
-})
-export class Plugins {}