From 79ec4ac00d6c04e05c4e4d62f3ddbd0e1fa5dcbb Mon Sep 17 00:00:00 2001 From: diallolatoile Date: Tue, 28 Oct 2025 16:58:11 +0000 Subject: [PATCH] feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature --- src/app/app.routes.ts | 13 +- src/app/app.scss | 653 +++++++++++++++--- src/app/core/guards/role.guard.ts | 27 +- src/app/core/services/menu.service.ts | 4 +- src/app/core/services/permissions.service.ts | 5 +- .../components/active-subscriptions.ts | 44 ++ .../dcb-dashboard/components/alert-widget.ts | 59 ++ .../components/operator-performance.ts | 67 ++ .../dcb-dashboard/components/payment-stats.ts | 127 ++++ .../components/recent-transactions.ts | 190 +++++ .../dcb-dashboard/components/revenue-chart.ts | 83 +++ .../dcb-dashboard/components/system-health.ts | 46 ++ .../modules/dcb-dashboard/dcb-dashboard.html | 359 +--------- .../modules/dcb-dashboard/dcb-dashboard.ts | 209 +----- .../models/dcb-dashboard.models.ts | 50 ++ src/app/modules/dcb-dashboard/models/dcb.ts | 53 -- .../services/dcb-dashboard.service.ts | 133 ++++ .../dcb-dashboard/services/dcb.service.ts | 144 ---- src/app/modules/merchants/config/config.html | 70 -- .../modules/merchants/config/config.spec.ts | 2 - src/app/modules/merchants/config/config.ts | 57 -- .../modules/merchants/history/history.html | 1 - .../modules/merchants/history/history.spec.ts | 2 - src/app/modules/merchants/history/history.ts | 7 - src/app/modules/merchants/list/list.html | 1 - src/app/modules/merchants/list/list.spec.ts | 2 - src/app/modules/merchants/list/list.ts | 7 - src/app/modules/merchants/merchants.html | 587 +++++++++++++++- src/app/modules/merchants/merchants.routes.ts | 15 - src/app/modules/merchants/merchants.ts | 347 +++++++++- .../merchants/models/merchant.models.ts | 81 +++ .../merchants/services/config.service.ts | 51 -- .../merchants/services/history.service.ts | 8 - .../merchants/services/list.service.ts | 8 - .../merchants/services/merchants.service.ts | 114 +-- .../modules/merchants/wizard-with-progress.ts | 312 +++++++++ src/app/modules/merchants/wizard.html | 4 - src/app/modules/merchants/wizard.ts | 6 +- ...es-routing.module.ts => modules.routes.ts} | 166 +++-- src/app/modules/operators/config/config.ts | 2 +- src/app/modules/transactions/plugins.html | 19 - src/app/modules/transactions/plugins.spec.ts | 22 - src/app/modules/transactions/plugins.ts | 14 - 43 files changed, 2891 insertions(+), 1280 deletions(-) create mode 100644 src/app/modules/dcb-dashboard/components/active-subscriptions.ts create mode 100644 src/app/modules/dcb-dashboard/components/alert-widget.ts create mode 100644 src/app/modules/dcb-dashboard/components/operator-performance.ts create mode 100644 src/app/modules/dcb-dashboard/components/payment-stats.ts create mode 100644 src/app/modules/dcb-dashboard/components/recent-transactions.ts create mode 100644 src/app/modules/dcb-dashboard/components/revenue-chart.ts create mode 100644 src/app/modules/dcb-dashboard/components/system-health.ts create mode 100644 src/app/modules/dcb-dashboard/models/dcb-dashboard.models.ts delete mode 100644 src/app/modules/dcb-dashboard/models/dcb.ts create mode 100644 src/app/modules/dcb-dashboard/services/dcb-dashboard.service.ts delete mode 100644 src/app/modules/dcb-dashboard/services/dcb.service.ts delete mode 100644 src/app/modules/merchants/config/config.html delete mode 100644 src/app/modules/merchants/config/config.spec.ts delete mode 100644 src/app/modules/merchants/config/config.ts delete mode 100644 src/app/modules/merchants/history/history.html delete mode 100644 src/app/modules/merchants/history/history.spec.ts delete mode 100644 src/app/modules/merchants/history/history.ts delete mode 100644 src/app/modules/merchants/list/list.html delete mode 100644 src/app/modules/merchants/list/list.spec.ts delete mode 100644 src/app/modules/merchants/list/list.ts delete mode 100644 src/app/modules/merchants/merchants.routes.ts create mode 100644 src/app/modules/merchants/models/merchant.models.ts delete mode 100644 src/app/modules/merchants/services/config.service.ts delete mode 100644 src/app/modules/merchants/services/history.service.ts delete mode 100644 src/app/modules/merchants/services/list.service.ts create mode 100644 src/app/modules/merchants/wizard-with-progress.ts rename src/app/modules/{modules-routing.module.ts => modules.routes.ts} (62%) delete mode 100644 src/app/modules/transactions/plugins.html delete mode 100644 src/app/modules/transactions/plugins.spec.ts delete mode 100644 src/app/modules/transactions/plugins.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 10fdea5..61e73fd 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,7 +1,5 @@ import { Routes } from '@angular/router' import { VerticalLayout } from '@layouts/vertical-layout/vertical-layout' -import { authGuard } from '@core/guards/auth.guard' -import { roleGuard } from '@core/guards/role.guard' export const routes: Routes = [ { path: '', redirectTo: '/dcb-dashboard', pathMatch: 'full' }, @@ -20,14 +18,13 @@ export const routes: Routes = [ import('./modules/auth/error/error.route').then(mod => mod.ERROR_PAGES_ROUTES), }, - // Routes protégées + // Routes protégées - SANS guards au niveau parent { path: '', component: VerticalLayout, - canActivate: [authGuard, roleGuard], loadChildren: () => - import('./modules/modules-routing.module').then( - m => m.ModulesRoutingModule + import('./modules/modules.routes').then( + m => m.ModulesRoutes ), }, @@ -35,6 +32,6 @@ export const routes: Routes = [ { path: '404', redirectTo: '/error/404' }, { path: '403', redirectTo: '/error/403' }, - // Catch-all - Rediriger vers 404 au lieu de login + // Catch-all { path: '**', redirectTo: '/error/404' }, -] \ No newline at end of file +]; \ No newline at end of file diff --git a/src/app/app.scss b/src/app/app.scss index 24f949b..d1fd304 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -11,109 +11,6 @@ font-size: 12px; } - -.dcb-dashboard { - .kpi-card { - border: none; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - transition: all 0.3s ease; - - &:hover { - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); - } - - .card-body { - padding: 1.5rem; - } - } - - .kpi-icon { - width: 60px; - height: 60px; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - color: white; - - ng-icon { - font-size: 24px; - } - } - - .rank-badge { - width: 32px; - height: 32px; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 0.875rem; - } - - .progress-sm { - height: 6px; - } - - // Animation de spin pour l'icône de refresh - .spin { - animation: spin 1s linear infinite; - } - - @keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } - } - - // Badges personnalisés - .badge { - font-size: 0.75em; - font-weight: 500; - } - - // Table styles - .table { - th { - border-top: none; - font-weight: 600; - color: #6c757d; - font-size: 0.875rem; - padding: 1rem 0.75rem; - } - - td { - padding: 1rem 0.75rem; - vertical-align: middle; - } - - tbody tr { - transition: background-color 0.2s ease; - - &:hover { - background-color: rgba(0, 0, 0, 0.02); - } - } - } - - // Responsive adjustments - @media (max-width: 768px) { - .kpi-icon { - width: 50px; - height: 50px; - - ng-icon { - font-size: 20px; - } - } - - .card-body { - padding: 1rem; - } - } -} - .transactions-container { .cursor-pointer { cursor: pointer; @@ -198,4 +95,554 @@ box-shadow: none !important; } } +} + +.merchants-container { + .cursor-pointer { + cursor: pointer; + + &:hover { + background-color: rgba(0, 0, 0, 0.02); + } + } + + .fs-12 { + font-size: 12px; + } + + .spin { + animation: spin 1s linear infinite; + } + + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + + .avatar-sm { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 14px; + } + + .table { + th { + border-top: none; + font-weight: 600; + color: #6c757d; + font-size: 0.875rem; + padding: 1rem 0.75rem; + background-color: #f8f9fa; + + &.cursor-pointer { + &:hover { + background-color: #e9ecef; + } + } + } + + td { + padding: 1rem 0.75rem; + vertical-align: middle; + border-top: 1px solid #e9ecef; + } + + tbody tr { + transition: all 0.2s ease; + + &:hover { + background-color: rgba(0, 123, 255, 0.04); + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + } + } + + .badge { + font-size: 0.75em; + font-weight: 500; + padding: 0.35em 0.65em; + + // Couleurs personnalisées pour les catégories + &.bg-purple { + background-color: #6f42c1 !important; + } + + &.bg-teal { + background-color: #20c997 !important; + } + + // Statuts + &.bg-success { + background: linear-gradient(135deg, #28a745, #20c997); + } + + &.bg-warning { + background: linear-gradient(135deg, #ffc107, #fd7e14); + } + + &.bg-danger { + background: linear-gradient(135deg, #dc3545, #e83e8c); + } + + &.bg-info { + background: linear-gradient(135deg, #17a2b8, #6f42c1); + } + + &.bg-secondary { + background: linear-gradient(135deg, #6c757d, #868e96); + } + } + + .font-monospace { + font-family: 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', monospace; + font-size: 0.875em; + } + + .btn-group-sm { + .btn { + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + + &:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + } + + // Cartes de statistiques + .stats-card { + border: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + } + + .card-body { + padding: 1.5rem; + } + } + + // Pagination personnalisée + .pagination { + .page-item { + margin: 0 2px; + + .page-link { + border: none; + border-radius: 6px; + color: #6c757d; + font-weight: 500; + padding: 0.5rem 0.75rem; + + &:hover { + background-color: #e9ecef; + color: #495057; + } + } + + &.active .page-link { + background: linear-gradient(135deg, #007bff, #0056b3); + border-color: #007bff; + } + + &.disabled .page-link { + color: #adb5bd; + background-color: #f8f9fa; + } + } + } + + // Dropdown personnalisé + .dropdown-menu { + border: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border-radius: 8px; + padding: 0.5rem; + + .dropdown-item { + padding: 0.5rem 1rem; + border-radius: 4px; + font-size: 0.875rem; + display: flex; + align-items: center; + + &:hover { + background-color: #f8f9fa; + } + + ng-icon { + margin-right: 0.5rem; + font-size: 1em; + } + + &.text-danger { + color: #dc3545 !important; + + &:hover { + background-color: #f8d7da; + } + } + } + + .dropdown-divider { + margin: 0.5rem 0; + border-color: #e9ecef; + } + } + + // Alertes personnalisées + .alert { + border: none; + border-radius: 8px; + padding: 1rem 1.5rem; + + &.alert-danger { + background: linear-gradient(135deg, #f8d7da, #f1aeb5); + color: #721c24; + } + } + + // Responsive design + @media (max-width: 768px) { + .table-responsive { + border: 1px solid #e9ecef; + border-radius: 8px; + + .table { + margin-bottom: 0; + + th, td { + padding: 0.75rem 0.5rem; + font-size: 0.875rem; + } + } + } + + .btn-group-sm { + flex-direction: column; + + .btn { + margin-bottom: 2px; + border-radius: 4px !important; + + &:not(:last-child) { + margin-right: 0; + } + } + } + + .d-flex.gap-2 { + gap: 0.5rem !important; + } + + .stats-card .card-body { + padding: 1rem; + } + } + + @media (max-width: 576px) { + .table { + font-size: 0.875rem; + + th, td { + padding: 0.5rem 0.25rem; + } + + .badge { + font-size: 0.7em; + padding: 0.25em 0.5em; + } + } + + .pagination { + .page-link { + padding: 0.375rem 0.5rem; + font-size: 0.875rem; + } + } + } + + // Animation pour le chargement + .loading-skeleton { + animation: pulse 1.5s ease-in-out infinite; + } + + @keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } + } + + // Styles pour les états vides + .empty-state { + padding: 3rem 1rem; + text-align: center; + + ng-icon { + font-size: 4rem; + margin-bottom: 1rem; + opacity: 0.5; + } + + p { + color: #6c757d; + margin-bottom: 1.5rem; + } + } +} + +.merchant-details-container { + .merchant-avatar { + width: 60px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + } + + .font-monospace { + font-family: 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', monospace; + } + + .badge { + font-size: 0.75em; + font-weight: 500; + } + + .spin { + animation: spin 1s linear infinite; + } + + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + + // Styles pour l'impression + @media print { + .btn, .card-header .d-flex:last-child { + display: none !important; + } + + .card { + border: 1px solid #dee2e6 !important; + box-shadow: none !important; + } + } +} + +.merchant-config-container { + .form-label { + font-weight: 500; + margin-bottom: 0.5rem; + } + + .card { + border: 1px solid #e9ecef; + + .card-header { + background-color: #f8f9fa; + border-bottom: 1px solid #e9ecef; + } + } + + .input-group-text { + background-color: #f8f9fa; + border-color: #ced4da; + } + + // Responsive adjustments + @media (max-width: 768px) { + .row.g-3 { + margin-bottom: -0.5rem; + + > [class*="col-"] { + margin-bottom: 0.5rem; + } + } + } +} + +.merchant-stats-container { + .stats-card { + border: none; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; + border-radius: 12px; + background: white; + + &:hover { + transform: translateY(-4px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + } + + .card-body { + padding: 1.5rem; + } + } + + .stats-icon { + width: 60px; + height: 60px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + + ng-icon { + font-size: 24px; + } + } + + .stats-title { + color: #2c3e50; + font-weight: 600; + } + + .stats-controls { + .btn-sm { + padding: 0.375rem 0.5rem; + border-radius: 8px; + } + } + + // Progress bars personnalisées + .progress { + border-radius: 10px; + background-color: #f8f9fa; + overflow: hidden; + + .progress-bar { + border-radius: 10px; + transition: width 0.6s ease; + + &.bg-success { + background: linear-gradient(135deg, #28a745, #20c997) !important; + } + + &.bg-warning { + background: linear-gradient(135deg, #ffc107, #fd7e14) !important; + } + } + } + + // Animations + .spin { + animation: spin 1s linear infinite; + } + + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + + // Couleurs pour les variations + .text-success { + color: #28a745 !important; + } + + .text-danger { + color: #dc3545 !important; + } + + .text-primary { + color: #007bff !important; + } + + .text-warning { + color: #ffc107 !important; + } + + .text-info { + color: #17a2b8 !important; + } + + // État vide + .empty-state { + padding: 3rem 2rem; + + ng-icon { + opacity: 0.3; + } + } + + // Responsive design + @media (max-width: 768px) { + .stats-card .card-body { + padding: 1.25rem; + } + + .stats-icon { + width: 50px; + height: 50px; + + ng-icon { + font-size: 20px; + } + } + + .h4 { + font-size: 1.5rem; + } + } + + @media (max-width: 576px) { + .stats-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .stats-controls { + align-self: flex-end; + } + + .stats-card .card-body { + padding: 1rem; + } + + .stats-icon { + width: 45px; + height: 45px; + + ng-icon { + font-size: 18px; + } + } + } + + // Typographie améliorée + .h4 { + font-weight: 700; + margin-bottom: 0.5rem; + } + + .text-muted { + color: #6c757d !important; + font-size: 0.875rem; + } + + .small { + font-size: 0.8125rem; + } } \ No newline at end of file diff --git a/src/app/core/guards/role.guard.ts b/src/app/core/guards/role.guard.ts index 7bcee6b..010d2db 100644 --- a/src/app/core/guards/role.guard.ts +++ b/src/app/core/guards/role.guard.ts @@ -19,6 +19,13 @@ export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state) = // Récupérer les rôles depuis le token const userRoles = authService.getCurrentUserRoles(); + + if (!userRoles || userRoles.length === 0) { + console.warn('RoleGuard: User has no roles'); + router.navigate(['/unauthorized']); + return false; + } + const modulePath = getModulePath(route); console.log('RoleGuard check:', { @@ -42,14 +49,32 @@ export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state) = // Fonction utilitaire pour extraire le chemin du module function getModulePath(route: ActivatedRouteSnapshot): string { + // Chercher le module dans les data de la route actuelle ou parente + let currentRoute: ActivatedRouteSnapshot | null = route; + + while (currentRoute) { + // Si le module est défini dans les data, l'utiliser + if (currentRoute.data['module']) { + return currentRoute.data['module']; + } + + currentRoute = currentRoute.parent; + } + + // Fallback: construire le chemin à partir des segments d'URL + return buildPathFromUrl(route); +} + +function buildPathFromUrl(route: ActivatedRouteSnapshot): string { const segments: string[] = []; let currentRoute: ActivatedRouteSnapshot | null = route; + // Parcourir jusqu'à la racine while (currentRoute) { if (currentRoute.url.length > 0) { segments.unshift(...currentRoute.url.map(segment => segment.path)); } - currentRoute = currentRoute.firstChild; + currentRoute = currentRoute.parent; } return segments.join('/'); diff --git a/src/app/core/services/menu.service.ts b/src/app/core/services/menu.service.ts index ec0d51f..16c558d 100644 --- a/src/app/core/services/menu.service.ts +++ b/src/app/core/services/menu.service.ts @@ -82,9 +82,9 @@ export class MenuService { icon: 'lucideStore', isCollapsed: true, children: [ - { label: 'Liste des Marchands', url: '/merchants/list' }, + { label: 'Gestion des Marchands', url: '/merchants/list' }, { label: 'Configuration API / Webhooks', url: '/merchants/config' }, - { label: 'Statistiques & Historique', url: '/merchants/history' }, + { label: 'Statistiques & Historique', url: '/merchants/stats' }, ], }, { diff --git a/src/app/core/services/permissions.service.ts b/src/app/core/services/permissions.service.ts index 360fc6c..0f00780 100644 --- a/src/app/core/services/permissions.service.ts +++ b/src/app/core/services/permissions.service.ts @@ -24,11 +24,12 @@ export class PermissionsService { // Merchants { module: 'merchants', - roles: ['admin', 'merchant'], + roles: ['admin', 'merchant', 'support'], children: { 'list': ['admin'], + 'details': ['admin', 'merchant', 'support'], 'config': ['admin', 'merchant'], - 'history': ['admin', 'merchant'] + 'stats': ['admin', 'merchant'] } }, diff --git a/src/app/modules/dcb-dashboard/components/active-subscriptions.ts b/src/app/modules/dcb-dashboard/components/active-subscriptions.ts new file mode 100644 index 0000000..34311c0 --- /dev/null +++ b/src/app/modules/dcb-dashboard/components/active-subscriptions.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; +import { NgIconComponent } from '@ng-icons/core'; +import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap'; +import { CountUpModule } from 'ngx-countup'; + +@Component({ + selector: 'app-active-subscriptions', + imports: [NgIconComponent, NgbProgressbarModule, CountUpModule], + template: ` +
+
+
+
+
Abonnements Actifs
+

+ 12,543 +

+

Total abonnements

+
+
+ +
+
+ + + +
+
+ Nouveaux +
156
+
+
+ Renouvellements +
89%
+
+
+
+ +
+ `, +}) +export class ActiveSubscriptions {} \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/components/alert-widget.ts b/src/app/modules/dcb-dashboard/components/alert-widget.ts new file mode 100644 index 0000000..01fb6cd --- /dev/null +++ b/src/app/modules/dcb-dashboard/components/alert-widget.ts @@ -0,0 +1,59 @@ +import { Component } from '@angular/core'; +import { NgIconComponent } from '@ng-icons/core'; + +@Component({ + selector: 'app-alert-widget', + imports: [NgIconComponent], + template: ` +
+
+
Alertes Récentes
+ 3 +
+ +
+
+
+
+ +
+
Taux d'échec élevé Airtel
+

Le taux d'échec a augmenté de 15%

+ Il y a 30 min +
+
+
+ +
+
+ +
+
Maintenance planifiée
+

Maintenance ce soir de 22h à 00h

+ Il y a 2h +
+
+
+ +
+
+ +
+
Nouveau partenaire
+

MTN Sénégal configuré avec succès

+ Il y a 4h +
+
+
+
+
+ + +
+ `, +}) +export class AlertWidget {} \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/components/operator-performance.ts b/src/app/modules/dcb-dashboard/components/operator-performance.ts new file mode 100644 index 0000000..c6cc170 --- /dev/null +++ b/src/app/modules/dcb-dashboard/components/operator-performance.ts @@ -0,0 +1,67 @@ +import { Component } from '@angular/core'; +import { NgIconComponent } from '@ng-icons/core'; + +@Component({ + selector: 'app-operator-performance', + imports: [NgIconComponent], + template: ` +
+
+
+
+
Performance Opérateurs
+
+
+ +
+
+ +
+
+ Orange +
+ 98.5% +
+
+
+
+
+ +
+ MTN +
+ 96.2% +
+
+
+
+
+ +
+ Airtel +
+ 87.4% +
+
+
+
+
+ +
+ Moov +
+ 92.1% +
+
+
+
+
+
+
+ +
+ `, +}) +export class OperatorPerformance {} \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/components/payment-stats.ts b/src/app/modules/dcb-dashboard/components/payment-stats.ts new file mode 100644 index 0000000..604ba39 --- /dev/null +++ b/src/app/modules/dcb-dashboard/components/payment-stats.ts @@ -0,0 +1,127 @@ +import { Component } from '@angular/core'; +import { NgIconComponent } from '@ng-icons/core'; +import { CountUpModule } from 'ngx-countup'; + +@Component({ + selector: 'app-payment-stats', + imports: [NgIconComponent, CountUpModule], + template: ` +
+
+
Statistiques des Paiements
+
+
+
+ +
+
+
+ +
Journalier
+

+ 342 +

+

Transactions

+
+ 245K XOF + + + 98.2% + +
+
+
+
+ + +
+
+
+ +
Hebdomadaire
+

+ 2,150 +

+

Transactions

+
+ 1.58M XOF + + + 97.8% + +
+
+
+
+ + +
+
+
+ +
Mensuel
+

+ 8,450 +

+

Transactions

+
+ 6.25M XOF + + + 96.5% + +
+
+
+
+ + +
+
+
+ +
Annuel
+

+ 12,500 +

+

Transactions

+
+ 9.85M XOF + + + 95.2% + +
+
+
+
+
+ + +
+
+
+
+
+ Performance globale: + 97.4% de taux de succès +
+
+ Dernière mise à jour: {{ getCurrentTime() }} +
+
+
+
+
+
+
+ `, +}) +export class PaymentStats { + getCurrentTime(): string { + return new Date().toLocaleTimeString('fr-FR', { + hour: '2-digit', + minute: '2-digit' + }); + } +} \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/components/recent-transactions.ts b/src/app/modules/dcb-dashboard/components/recent-transactions.ts new file mode 100644 index 0000000..fbcf67c --- /dev/null +++ b/src/app/modules/dcb-dashboard/components/recent-transactions.ts @@ -0,0 +1,190 @@ +import { Component } from '@angular/core'; +import { NgIconComponent } from '@ng-icons/core'; +import { DecimalPipe } from '@angular/common'; +import { NgbDropdownModule, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; + +interface Transaction { + id: string; + user: string; + operator: string; + amount: number; + status: 'success' | 'pending' | 'failed'; + date: string; +} + +@Component({ + selector: 'app-recent-transactions', + imports: [ + NgIconComponent, + DecimalPipe, + NgbPaginationModule, + NgbDropdownModule, + ], + template: ` +
+
+

Transactions Récentes

+ + + Exporter + +
+ +
+
+ + + + + + + + + + + + + @for (transaction of transactions; track transaction.id) { + + + + + + + + + } + +
TransactionUtilisateurOpérateurMontantStatut
+ #{{ transaction.id }} + + {{ transaction.user }} + + {{ transaction.operator }} + + {{ transaction.amount | number }} XOF + + + {{ getStatusText(transaction.status) }} + + + +
+
+
+ + +
+ `, +}) +export class RecentTransactions { + transactions: Transaction[] = [ + { + id: 'TX-5001', + user: 'Alice Koné', + operator: 'Orange', + amount: 2500, + status: 'success', + date: '2024-10-28 14:30' + }, + { + id: 'TX-5002', + user: 'David Sarr', + operator: 'MTN', + amount: 1500, + status: 'success', + date: '2024-10-28 14:25' + }, + { + id: 'TX-5003', + user: 'Sophia Diop', + operator: 'Airtel', + amount: 3000, + status: 'pending', + date: '2024-10-28 14:20' + }, + { + id: 'TX-5004', + user: 'James Traoré', + operator: 'Moov', + amount: 2000, + status: 'success', + date: '2024-10-28 14:15' + }, + { + id: 'TX-5005', + user: 'Ava Camara', + operator: 'Orange', + amount: 1000, + status: 'failed', + date: '2024-10-28 14:10' + }, + { + id: 'TX-5006', + user: 'Ethan Diallo', + operator: 'MTN', + amount: 3500, + status: 'success', + date: '2024-10-28 14:05' + }, + { + id: 'TX-5007', + user: 'Mia Bah', + operator: 'Orange', + amount: 1800, + status: 'success', + date: '2024-10-28 14:00' + }, + { + id: 'TX-5008', + user: 'Lucas Keita', + operator: 'Airtel', + amount: 2200, + status: 'pending', + date: '2024-10-28 13:55' + } + ]; + + getStatusClass(status: string): string { + switch (status) { + case 'success': return 'bg-success'; + case 'pending': return 'bg-warning'; + case 'failed': return 'bg-danger'; + default: return 'bg-secondary'; + } + } + + getStatusText(status: string): string { + switch (status) { + case 'success': return 'Succès'; + case 'pending': return 'En cours'; + case 'failed': return 'Échec'; + default: return 'Inconnu'; + } + } +} \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/components/revenue-chart.ts b/src/app/modules/dcb-dashboard/components/revenue-chart.ts new file mode 100644 index 0000000..7e51e03 --- /dev/null +++ b/src/app/modules/dcb-dashboard/components/revenue-chart.ts @@ -0,0 +1,83 @@ +import { Component } from '@angular/core'; +import { Chartjs } from '@app/components/chartjs'; +import { ChartConfiguration } from 'chart.js'; +import { getColor } from '@/app/utils/color-utils'; + +@Component({ + selector: 'app-revenue-chart', + imports: [Chartjs], + template: ` +
+
+

Revenue Mensuel

+ +
+
+ + +
+
+ Semaine 1 +
175K XOF
+
+
+ Semaine 2 +
125K XOF
+
+
+ Semaine 3 +
105K XOF
+
+
+ Semaine 4 +
85K XOF
+
+
+
+
+ `, +}) +export class RevenueChart { + public revenueChart = (): ChartConfiguration => ({ + type: 'bar', + data: { + labels: ['Sem 1', 'Sem 2', 'Sem 3', 'Sem 4'], + datasets: [ + { + label: 'Revenue (K XOF)', + data: [175, 125, 105, 85], + backgroundColor: getColor('chart-primary'), + borderRadius: 6, + borderSkipped: false, + }, + ], + }, + options: { + plugins: { + legend: { display: false }, + }, + scales: { + x: { + grid: { display: false }, + }, + y: { + beginAtZero: true, + ticks: { + callback: function(value) { + return value + 'K'; + } + } + }, + }, + }, + }); +} \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/components/system-health.ts b/src/app/modules/dcb-dashboard/components/system-health.ts new file mode 100644 index 0000000..dd90a70 --- /dev/null +++ b/src/app/modules/dcb-dashboard/components/system-health.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; +import { NgIconComponent } from '@ng-icons/core'; + +@Component({ + selector: 'app-system-health', + imports: [NgIconComponent], + template: ` +
+
+
+
+
Santé du Système
+

100%

+

Tous les services opérationnels

+
+
+ +
+
+ +
+
+ API Gateway + Online +
+
+ Base de données + Online +
+
+ SMS Gateway + Online +
+
+ Webhooks + Online +
+
+
+ +
+ `, +}) +export class SystemHealth {} \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/dcb-dashboard.html b/src/app/modules/dcb-dashboard/dcb-dashboard.html index 807ed64..7ea93ef 100644 --- a/src/app/modules/dcb-dashboard/dcb-dashboard.html +++ b/src/app/modules/dcb-dashboard/dcb-dashboard.html @@ -1,338 +1,51 @@
- -
- -
+ +
-
-
- -
- -
- - @if (lastUpdated) { - - MAJ: {{ lastUpdated | date:'HH:mm:ss' }} - - } - - -
- - -
- - - - - -
- -
-
-
+
- - @if (error) { -
- -
{{ 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 -
-
-
-
- -
-
-
-
-
-
+
+
- -
- -
-
-
-
Performances par Opérateur
- {{ operators.length }} opérateurs -
-
-
- - - - - - - - - - - - @for (operator of operators; track operator.id) { - - - - - - - - } - -
OpérateurPaysStatutTaux de SuccèsProgression
{{ operator.name }} - {{ operator.country }} - - - {{ operator.status === 'ACTIVE' ? 'Actif' : 'Inactif' }} - - - - {{ formatPercentage(operator.successRate) }} - - -
-
- -
- {{ operator.successRate }}% -
-
-
-
-
-
- - -
-
-
-
Top Opérateurs - Revenus
-
-
- @for (operator of analytics.topOperators; track operator.operator; let i = $index) { -
-
-
{{ i + 1 }}
-
-
-
{{ operator.operator }}
-
- {{ operator.count }} trans. - - {{ formatCurrency(operator.revenue) }} - -
-
-
- } -
-
-
+
+
- -
-
-
-
-
Transactions Récentes
- - Voir toutes les transactions - -
-
-
- - - - - - - - - - - - - @for (transaction of recentTransactions; track transaction.id) { - - - - - - - - - } - @empty { - - - - } - -
MSISDNOpérateurProduitMontantStatutDate
{{ transaction.msisdn }} - {{ transaction.operator }} - - {{ transaction.productName }} - - {{ formatCurrency(transaction.amount, transaction.currency) }} - - - - {{ transaction.status }} - - - {{ transaction.transactionDate | date:'dd/MM/yy HH:mm' }} -
- - 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 @@ - - -
- - -
-
Clés d'API
-
- -
- -
- -
- -
- - -
-
URLs de Callback
-
- -
- -
- - -
-
Configuration Avancée
-
- -
- - -
- -
- - -
- - -
- - @for (ip of config.allowedIPs; track $index; let i = $index) { -
- -
- } - -
- - -
-
- - - -
-
-
-
\ 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 @@
-
+ +
- + + +
-
+
\ 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) { +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ } + @case (3) { +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ } + @case (4) { +
+ + +
+
+ + +
+ } + } + +
+ @if (i > 0) { + + } + @if (i < wizardSteps.length - 1) { + + } + @if (i === wizardSteps.length - 1) { + + } +
+
+ } +
+
+
+ `, + 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 {}