From 3bb7d21a7fdcea62f72c15c6500d3e680c74f6f3 Mon Sep 17 00:00:00 2001 From: diallolatoile Date: Mon, 27 Oct 2025 18:12:52 +0000 Subject: [PATCH] feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature --- package-lock.json | 19 +- package.json | 1 + src/app/app.routes.ts | 28 +- src/app/app.scss | 201 ++++++++ src/app/app.ts | 15 +- src/app/core/directive/has-role.directive.ts | 31 ++ src/app/core/guards/role.guard.ts | 56 +++ src/app/core/interceptors/auth.interceptor.ts | 68 ++- src/app/core/services/auth.service.ts | 122 ++++- src/app/core/services/menu.service.ts | 148 ++++++ .../core/services/notifications.service.ts | 23 + src/app/core/services/permissions.service.ts | 148 ++++++ src/app/layouts/components/data.ts | 34 +- .../app-menu/app-menu.component.html | 29 +- .../components/app-menu/app-menu.component.ts | 52 +- .../user-profile/user-profile.component.html | 2 +- .../components/user-profile/user-profile.html | 80 +-- .../components/user-profile/user-profile.ts | 44 +- src/app/modules/auth/error/error-404.ts | 89 ++++ src/app/modules/auth/error/error.route.ts | 21 + src/app/modules/auth/reset-password.ts | 141 +++++- src/app/modules/auth/sign-in.ts | 2 +- src/app/modules/auth/unauthorized.ts | 96 ++++ src/app/modules/components/data.ts | 7 + .../dashboard/components/active-users.ts | 42 -- .../components/api-performance-metrics.ts | 110 ---- .../components/model-usage-summary.ts | 103 ---- .../dashboard/components/prompts-usage.ts | 87 ---- .../dashboard/components/recent-sessions.ts | 259 ---------- .../components/request-statistics.ts | 129 ----- .../dashboard/components/response-accuracy.ts | 81 --- .../dashboard/components/token-usage.ts | 90 ---- src/app/modules/dashboard/dashboard.html | 43 -- src/app/modules/dashboard/dashboard.ts | 28 -- .../modules/dcb-dashboard/dcb-dashboard.html | 338 +++++++++++++ .../dcb-dashboard.spec.ts} | 2 +- .../modules/dcb-dashboard/dcb-dashboard.ts | 198 ++++++++ src/app/modules/dcb-dashboard/models/dcb.ts | 53 ++ .../dcb-dashboard/services/dcb.service.ts | 144 ++++++ src/app/modules/merchants/merchants.routes.ts | 15 + src/app/modules/modules-routing.module.ts | 170 ++++--- src/app/modules/profile/profile.html | 406 ++++++++++++++- src/app/modules/profile/profile.ts | 226 ++++++++- .../profile/services/profile.service.ts | 8 - src/app/modules/settings/settings.routes.ts | 15 + .../modules/transactions/details/details.html | 352 ++++++++++++- .../modules/transactions/details/details.ts | 259 +++++++++- .../modules/transactions/export/export.html | 1 - .../transactions/export/export.spec.ts | 2 - src/app/modules/transactions/export/export.ts | 40 -- .../modules/transactions/filters/filters.html | 62 --- .../transactions/filters/filters.spec.ts | 2 - .../modules/transactions/filters/filters.ts | 46 -- src/app/modules/transactions/list/list.html | 299 ++++++++++- src/app/modules/transactions/list/list.ts | 356 ++++++++++++- .../transactions/models/transaction.ts | 80 +++ .../transactions/services/details.service.ts | 8 - .../transactions/services/export.service.ts | 27 - .../transactions/services/filters.service.ts | 59 --- .../transactions/services/list.service.ts | 8 - .../services/transactions.service.ts | 195 ++++++-- .../modules/transactions/transactions.html | 85 ++-- src/app/modules/transactions/transactions.ts | 48 +- src/app/modules/users/audits/audits.html | 1 - src/app/modules/users/audits/audits.spec.ts | 2 - src/app/modules/users/audits/audits.ts | 7 - src/app/modules/users/list/list.html | 244 ++++++++- src/app/modules/users/list/list.ts | 255 +++++++++- src/app/modules/users/models/user.ts | 180 +++++++ src/app/modules/users/profile/profile.html | 355 +++++++++++++ src/app/modules/users/profile/profile.spec.ts | 2 + src/app/modules/users/profile/profile.ts | 240 +++++++++ src/app/modules/users/roles/roles.html | 1 - src/app/modules/users/roles/roles.spec.ts | 2 - src/app/modules/users/roles/roles.ts | 46 -- .../modules/users/services/list.service.ts | 8 - .../modules/users/services/roles.service.ts | 42 -- .../modules/users/services/users.service.ts | 182 +++++-- src/app/modules/users/structure.txt | 15 + src/app/modules/users/users.html | 473 +++++++++++++++++- src/app/modules/users/users.routes.ts | 16 + src/app/modules/users/users.ts | 337 ++++++++++++- src/app/types/layout.ts | 12 + src/environments/environment.ts | 88 +++- tsconfig.json | 1 + 85 files changed, 6712 insertions(+), 1730 deletions(-) create mode 100644 src/app/core/directive/has-role.directive.ts create mode 100644 src/app/core/guards/role.guard.ts create mode 100644 src/app/core/services/menu.service.ts create mode 100644 src/app/core/services/notifications.service.ts create mode 100644 src/app/core/services/permissions.service.ts create mode 100644 src/app/modules/auth/error/error-404.ts create mode 100644 src/app/modules/auth/error/error.route.ts create mode 100644 src/app/modules/auth/unauthorized.ts delete mode 100644 src/app/modules/dashboard/components/active-users.ts delete mode 100644 src/app/modules/dashboard/components/api-performance-metrics.ts delete mode 100644 src/app/modules/dashboard/components/model-usage-summary.ts delete mode 100644 src/app/modules/dashboard/components/prompts-usage.ts delete mode 100644 src/app/modules/dashboard/components/recent-sessions.ts delete mode 100644 src/app/modules/dashboard/components/request-statistics.ts delete mode 100644 src/app/modules/dashboard/components/response-accuracy.ts delete mode 100644 src/app/modules/dashboard/components/token-usage.ts delete mode 100644 src/app/modules/dashboard/dashboard.html delete mode 100644 src/app/modules/dashboard/dashboard.ts create mode 100644 src/app/modules/dcb-dashboard/dcb-dashboard.html rename src/app/modules/{dashboard/dashboard.spec.ts => dcb-dashboard/dcb-dashboard.spec.ts} (91%) create mode 100644 src/app/modules/dcb-dashboard/dcb-dashboard.ts create mode 100644 src/app/modules/dcb-dashboard/models/dcb.ts create mode 100644 src/app/modules/dcb-dashboard/services/dcb.service.ts create mode 100644 src/app/modules/merchants/merchants.routes.ts delete mode 100644 src/app/modules/profile/services/profile.service.ts create mode 100644 src/app/modules/settings/settings.routes.ts delete mode 100644 src/app/modules/transactions/export/export.html delete mode 100644 src/app/modules/transactions/export/export.spec.ts delete mode 100644 src/app/modules/transactions/export/export.ts delete mode 100644 src/app/modules/transactions/filters/filters.html delete mode 100644 src/app/modules/transactions/filters/filters.spec.ts delete mode 100644 src/app/modules/transactions/filters/filters.ts create mode 100644 src/app/modules/transactions/models/transaction.ts delete mode 100644 src/app/modules/transactions/services/details.service.ts delete mode 100644 src/app/modules/transactions/services/export.service.ts delete mode 100644 src/app/modules/transactions/services/filters.service.ts delete mode 100644 src/app/modules/transactions/services/list.service.ts delete mode 100644 src/app/modules/users/audits/audits.html delete mode 100644 src/app/modules/users/audits/audits.spec.ts delete mode 100644 src/app/modules/users/audits/audits.ts create mode 100644 src/app/modules/users/models/user.ts create mode 100644 src/app/modules/users/profile/profile.html create mode 100644 src/app/modules/users/profile/profile.spec.ts create mode 100644 src/app/modules/users/profile/profile.ts delete mode 100644 src/app/modules/users/roles/roles.html delete mode 100644 src/app/modules/users/roles/roles.spec.ts delete mode 100644 src/app/modules/users/roles/roles.ts delete mode 100644 src/app/modules/users/services/list.service.ts delete mode 100644 src/app/modules/users/services/roles.service.ts create mode 100644 src/app/modules/users/structure.txt create mode 100644 src/app/modules/users/users.routes.ts diff --git a/package-lock.json b/package-lock.json index 3b91700..c316ae2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "simple", + "name": "dcb-bo-admin", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "simple", + "name": "dcb-bo-admin", "version": "0.0.0", "dependencies": { "@angular/common": "^20.3.6", @@ -57,6 +57,7 @@ "ngx-countup": "^13.2.0", "ngx-dropzone-wrapper": "^17.0.0", "ngx-quill": "^28.0.1", + "ngx-toastr": "^19.1.0", "prettier": "^3.6.2", "quill": "^2.0.3", "rxjs": "~7.8.2", @@ -8334,6 +8335,20 @@ "rxjs": "^7.0.0" } }, + "node_modules/ngx-toastr": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-19.1.0.tgz", + "integrity": "sha512-Qa7Kg7QzGKNtp1v04hu3poPKKx8BGBD/Onkhm6CdH5F0vSMdq+BdR/f8DTpZnGFksW891tAFufpiWb9UZX+3vg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0-0", + "@angular/core": ">=16.0.0-0", + "@angular/platform-browser": ">=16.0.0-0" + } + }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", diff --git a/package.json b/package.json index 8ff0dab..31bc37c 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "ngx-countup": "^13.2.0", "ngx-dropzone-wrapper": "^17.0.0", "ngx-quill": "^28.0.1", + "ngx-toastr": "^19.1.0", "prettier": "^3.6.2", "quill": "^2.0.3", "rxjs": "~7.8.2", diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index e29ac24..10fdea5 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,9 +1,10 @@ -import { Routes } from '@angular/router'; -import { VerticalLayout } from '@layouts/vertical-layout/vertical-layout'; -import { authGuard } from './core/guards/auth.guard'; +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: '/dashboard', pathMatch: 'full' }, + { path: '', redirectTo: '/dcb-dashboard', pathMatch: 'full' }, // Routes publiques (auth) { @@ -12,17 +13,28 @@ export const routes: Routes = [ import('./modules/auth/auth.route').then(mod => mod.Auth_ROUTES), }, + // Routes d'erreur (publiques) + { + path: '', + loadChildren: () => + import('./modules/auth/error/error.route').then(mod => mod.ERROR_PAGES_ROUTES), + }, + // Routes protégées { path: '', component: VerticalLayout, - canActivate: [authGuard], + canActivate: [authGuard, roleGuard], loadChildren: () => import('./modules/modules-routing.module').then( m => m.ModulesRoutingModule ), }, - // Catch-all - { path: '**', redirectTo: '/auth/sign-in' }, -]; + // Redirections pour les erreurs courantes + { path: '404', redirectTo: '/error/404' }, + { path: '403', redirectTo: '/error/403' }, + + // Catch-all - Rediriger vers 404 au lieu de login + { path: '**', redirectTo: '/error/404' }, +] \ No newline at end of file diff --git a/src/app/app.scss b/src/app/app.scss index e69de29..24f949b 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -0,0 +1,201 @@ +/* Ajoutez ce CSS dans votre composant ou global */ +.cursor-pointer { + cursor: pointer; +} + +.cursor-pointer:hover { + background-color: #f8f9fa; +} + +.fs-12 { + 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; + + &: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); } + } + + .table { + th { + border-top: none; + font-weight: 600; + color: #6c757d; + font-size: 0.875rem; + } + + td { + vertical-align: middle; + } + } + + .badge { + font-size: 0.75em; + font-weight: 500; + } + + .font-monospace { + font-family: 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', monospace; + } +} + +.transaction-details { + .transaction-amount-icon, + .transaction-date-icon { + 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; + } + } +} \ No newline at end of file diff --git a/src/app/app.ts b/src/app/app.ts index be2199c..b81456c 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -24,8 +24,19 @@ export class App implements OnInit { private authService = inject(AuthService); async ngOnInit(): Promise { - // Initialiser l'authentification - await this.authService.initialize(); + try { + // Initialiser l'authentification avec gestion d'erreur + const isAuthenticated = await this.authService.initialize(); + + console.log('Authentication initialized:', isAuthenticated); + + if (!isAuthenticated) { + console.log('👤 User not authenticated, may redirect to login'); + // Note: Votre AuthService gère déjà la redirection dans logout() + } + } catch (error) { + console.error('Error during authentication initialization:', error); + } // Configurer le titre de la page this.setupTitleListener(); diff --git a/src/app/core/directive/has-role.directive.ts b/src/app/core/directive/has-role.directive.ts new file mode 100644 index 0000000..0beca08 --- /dev/null +++ b/src/app/core/directive/has-role.directive.ts @@ -0,0 +1,31 @@ +import { Directive, Input, TemplateRef, ViewContainerRef, inject, OnDestroy } from '@angular/core'; +import { AuthService } from '../services/auth.service'; +import { Subscription } from 'rxjs'; + +@Directive({ + selector: '[hasRole]', + standalone: true +}) +export class HasRoleDirective implements OnDestroy { + private authService = inject(AuthService); + private templateRef = inject(TemplateRef); + private viewContainer = inject(ViewContainerRef); + private subscription?: Subscription; + + @Input() set hasRole(roles: string | string[]) { + const requiredRoles = Array.isArray(roles) ? roles : [roles]; + const userRoles = this.authService.getCurrentUserRoles(); + + const hasAccess = requiredRoles.some(role => userRoles.includes(role)); + + if (hasAccess) { + this.viewContainer.createEmbeddedView(this.templateRef); + } else { + this.viewContainer.clear(); + } + } + + ngOnDestroy() { + this.subscription?.unsubscribe(); + } +} \ No newline at end of file diff --git a/src/app/core/guards/role.guard.ts b/src/app/core/guards/role.guard.ts new file mode 100644 index 0000000..7bcee6b --- /dev/null +++ b/src/app/core/guards/role.guard.ts @@ -0,0 +1,56 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router, ActivatedRouteSnapshot } from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { PermissionsService } from '../services/permissions.service'; + +export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state) => { + const authService = inject(AuthService); + const permissionsService = inject(PermissionsService); + const router = inject(Router); + + // Vérifier d'abord l'authentification + if (!authService.isAuthenticated()) { + console.log('RoleGuard: User not authenticated, redirecting to login'); + router.navigate(['/auth/sign-in'], { + queryParams: { returnUrl: state.url } + }); + return false; + } + + // Récupérer les rôles depuis le token + const userRoles = authService.getCurrentUserRoles(); + const modulePath = getModulePath(route); + + console.log('RoleGuard check:', { + module: modulePath, + userRoles: userRoles, + url: state.url + }); + + // Vérifier les permissions + const hasAccess = permissionsService.canAccessModule(modulePath, userRoles); + + if (!hasAccess) { + console.warn('RoleGuard: Access denied for', modulePath, 'User roles:', userRoles); + router.navigate(['/unauthorized']); + return false; + } + + console.log('RoleGuard: Access granted for', modulePath); + return true; +}; + +// Fonction utilitaire pour extraire le chemin du module +function getModulePath(route: ActivatedRouteSnapshot): string { + const segments: string[] = []; + let currentRoute: ActivatedRouteSnapshot | null = route; + + while (currentRoute) { + if (currentRoute.url.length > 0) { + segments.unshift(...currentRoute.url.map(segment => segment.path)); + } + currentRoute = currentRoute.firstChild; + } + + return segments.join('/'); +} \ No newline at end of file diff --git a/src/app/core/interceptors/auth.interceptor.ts b/src/app/core/interceptors/auth.interceptor.ts index 7cf1125..9ffc62d 100644 --- a/src/app/core/interceptors/auth.interceptor.ts +++ b/src/app/core/interceptors/auth.interceptor.ts @@ -1,43 +1,81 @@ -import { HttpInterceptorFn, HttpRequest } from '@angular/common/http'; +import { HttpInterceptorFn, HttpRequest, HttpHandlerFn, HttpErrorResponse } from '@angular/common/http'; import { inject } from '@angular/core'; import { AuthService } from '../services/auth.service'; +import { Router } from '@angular/router'; +import { catchError, switchMap, throwError } from 'rxjs'; export const authInterceptor: HttpInterceptorFn = (req, next) => { const authService = inject(AuthService); + const router = inject(Router); - // On ignore les requêtes de login, refresh, logout + // On ignore les requêtes d'authentification if (isAuthRequest(req)) { return next(req); } const token = authService.getToken(); - // On ajoute le token uniquement si c’est une requête API + // On ajoute le token uniquement si c'est une requête API et que le token existe if (token && isApiRequest(req)) { - const cloned = req.clone({ - setHeaders: { - Authorization: `Bearer ${token}`, - } - }); - return next(cloned); + const cloned = addToken(req, token); + + return next(cloned).pipe( + catchError((error: HttpErrorResponse) => { + if (error.status === 401) { + // Token expiré, on tente de le rafraîchir + return handle401Error(authService, router, req, next); + } + return throwError(() => error); + }) + ); } return next(req); }; -// Détermine si c’est une requête vers ton backend API -function isApiRequest(req: HttpRequest): boolean { - return req.url.includes('/api/') || ( - req.url.includes('/auth/') && !isAuthRequest(req) +// Ajoute le token à la requête +function addToken(req: HttpRequest, token: string): HttpRequest { + return req.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + } + }); +} + +// Gère les erreurs 401 (token expiré) +function handle401Error( + authService: AuthService, + router: Router, + req: HttpRequest, + next: HttpHandlerFn +) { + return authService.refreshToken().pipe( + switchMap((response: any) => { + // Nouveau token obtenu, on relance la requête originale + const newToken = response.access_token; + const newRequest = addToken(req, newToken); + return next(newRequest); + }), + catchError((refreshError) => { + // Échec du rafraîchissement, on déconnecte + authService.logout(); + router.navigate(['/auth/sign-in']); + return throwError(() => refreshError); + }) ); } +// Détermine si c'est une requête vers le backend API +function isApiRequest(req: HttpRequest): boolean { + return req.url.includes('/api/') || req.url.includes('/auth/'); +} + // Liste des endpoints où le token ne doit pas être ajouté function isAuthRequest(req: HttpRequest): boolean { const url = req.url; return ( url.includes('/auth/login') || - url.includes('/auth/refresh') || - url.includes('/auth/logout') + url.endsWith('/auth/refresh') || + url.endsWith('/auth/logout') ); } diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts index a760db1..d0bbd54 100644 --- a/src/app/core/services/auth.service.ts +++ b/src/app/core/services/auth.service.ts @@ -1,8 +1,8 @@ import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Router } from '@angular/router'; import { environment } from '@environments/environment'; -import { BehaviorSubject, tap, catchError, Observable, throwError } from 'rxjs'; +import { BehaviorSubject, tap, catchError, Observable, throwError, map } from 'rxjs'; import { jwtDecode } from "jwt-decode"; interface DecodedToken { @@ -13,8 +13,10 @@ interface DecodedToken { email?: string; given_name?: string; family_name?: string; - realm_access?: { - roles: string[]; + resource_access?: { + [key: string]: { + roles: string[]; + }; }; } @@ -33,8 +35,88 @@ export class AuthService { private readonly router = inject(Router); private authState$ = new BehaviorSubject(this.isAuthenticated()); + private userRoles$ = new BehaviorSubject(this.getRolesFromToken()); + /** - * Initialisation simple - à appeler dans app.component.ts + * Récupère les rôles depuis le token JWT + */ + private getRolesFromToken(): string[] { + const token = this.getToken(); + if (!token) return []; + + try { + const decoded: DecodedToken = jwtDecode(token); + + // Priorité 2: Rôles du client (resource_access) + // Prendre tous les rôles de tous les clients + if (decoded.resource_access) { + const allClientRoles: string[] = []; + + Object.values(decoded.resource_access).forEach(client => { + if (client?.roles) { + allClientRoles.push(...client.roles); + } + }); + + // Retourner les rôles uniques + return [...new Set(allClientRoles)]; + } + + return []; + } catch { + return []; + } + } + + /** + * Récupère les rôles de l'utilisateur courant + */ + getCurrentUserRoles(): string[] { + return this.userRoles$.value; + } + + /** + * Observable des rôles + */ + onRolesChange(): Observable { + return this.userRoles$.asObservable(); + } + + /** + * Vérifications rapides par rôle + */ + isAdmin(): boolean { + return this.getCurrentUserRoles().includes('admin'); + } + + isMerchant(): boolean { + return this.getCurrentUserRoles().includes('merchant'); + } + + isSupport(): boolean { + return this.getCurrentUserRoles().includes('support'); + } + + hasAnyRole(roles: string[]): boolean { + const userRoles = this.getCurrentUserRoles(); + return roles.some(role => userRoles.includes(role)); + } + + hasAllRoles(roles: string[]): boolean { + const userRoles = this.getCurrentUserRoles(); + return roles.every(role => userRoles.includes(role)); + } + + /** + * Rafraîchir les rôles (utile après modification des rôles) + */ + refreshRoles(): void { + const roles = this.getRolesFromToken(); + this.userRoles$.next(roles); + } + + /** + * Initialisation simple - à appeler dans app.ts */ initialize(): Promise { return new Promise((resolve) => { @@ -70,15 +152,15 @@ export class AuthService { tap(response => { this.handleLoginResponse(response); }), - catchError(error => { - console.error('Login failed', error); + catchError((error: HttpErrorResponse) => { + console.error('Login failed:', error); return throwError(() => this.getErrorMessage(error)); }) ); } /** - * Rafraîchit le token d'accès (retourne un Observable maintenant) + * Rafraîchit le token d'accès */ refreshToken(): Observable { const refreshToken = localStorage.getItem(this.refreshTokenKey); @@ -94,8 +176,8 @@ export class AuthService { tap(response => { this.handleLoginResponse(response); }), - catchError(error => { - console.error('Token refresh failed', error); + catchError((error: HttpErrorResponse) => { + console.error('Token refresh failed:', error); this.clearSession(); return throwError(() => error); }) @@ -110,7 +192,7 @@ export class AuthService { // Appel API optionnel (ne pas bloquer dessus) this.http.post(`${environment.apiUrl}/auth/logout`, {}).subscribe({ - error: (err) => console.warn('Logout API call failed', err) + error: () => {} // Ignorer silencieusement les erreurs de logout }); } @@ -118,12 +200,14 @@ export class AuthService { localStorage.removeItem(this.tokenKey); localStorage.removeItem(this.refreshTokenKey); this.authState$.next(false); + this.userRoles$.next([]); if (redirect) { this.router.navigate(['/auth/sign-in']); } } + private handleLoginResponse(response: AuthResponse): void { if (response?.access_token) { localStorage.setItem(this.tokenKey, response.access_token); @@ -132,11 +216,15 @@ export class AuthService { localStorage.setItem(this.refreshTokenKey, response.refresh_token); } + // Mettre à jour les rôles après login + const roles = this.getRolesFromToken(); + this.userRoles$.next(roles); + this.authState$.next(true); } } - private getErrorMessage(error: any): string { + private getErrorMessage(error: HttpErrorResponse): string { if (error?.error?.message) { return error.error.message; } @@ -162,8 +250,7 @@ export class AuthService { try { const decoded: DecodedToken = jwtDecode(token); const now = Math.floor(Date.now() / 1000); - // Marge de sécurité de 60 secondes - return decoded.exp < (now + 60); + return decoded.exp < (now + 60); // Marge de sécurité de 60 secondes } catch { return true; } @@ -175,7 +262,6 @@ export class AuthService { isAuthenticated(): boolean { const token = this.getToken(); if (!token) return false; - return !this.isTokenExpired(token); } @@ -187,6 +273,10 @@ export class AuthService { * Récupère les infos utilisateur depuis le backend */ getProfile(): Observable { - return this.http.get(`${environment.apiUrl}/auth/me`); + return this.http.get(`${environment.apiUrl}/users/profile/me`).pipe( + catchError(error => { + return throwError(() => error); + }) + ); } } \ No newline at end of file diff --git a/src/app/core/services/menu.service.ts b/src/app/core/services/menu.service.ts new file mode 100644 index 0000000..ec0d51f --- /dev/null +++ b/src/app/core/services/menu.service.ts @@ -0,0 +1,148 @@ +import { Injectable, inject } from '@angular/core'; +import { AuthService } from './auth.service'; +import { PermissionsService } from './permissions.service'; +import { MenuItemType, UserDropdownItemType } from '@/app/types/layout'; + +@Injectable({ providedIn: 'root' }) +export class MenuService { + private authService = inject(AuthService); + private permissionsService = inject(PermissionsService); + + getMenuItems(): MenuItemType[] { + const userRoles = this.authService.getCurrentUserRoles(); + return this.filterMenuItems(this.getFullMenu(), userRoles); + } + + getUserDropdownItems(): UserDropdownItemType[] { + const userRoles = this.authService.getCurrentUserRoles(); + return this.filterUserDropdownItems(this.getFullUserDropdown(), userRoles); + } + + canAccess(modulePath: string): boolean { + const userRoles = this.authService.getCurrentUserRoles(); + return this.permissionsService.canAccessModule(modulePath, userRoles); + } + + private filterMenuItems(items: MenuItemType[], userRoles: string[]): MenuItemType[] { + return items + .filter(item => this.shouldDisplayMenuItem(item, userRoles)) + .map(item => ({ + ...item, + children: item.children ? this.filterMenuItems(item.children, userRoles) : undefined + })); + } + + private shouldDisplayMenuItem(item: MenuItemType, userRoles: string[]): boolean { + if (item.isTitle) return true; + + if (item.url && item.url !== '#') { + const modulePath = this.normalizePath(item.url); + return this.permissionsService.canAccessModule(modulePath, userRoles); + } + + if (item.children) { + return this.filterMenuItems(item.children, userRoles).length > 0; + } + + return true; + } + + private filterUserDropdownItems(items: UserDropdownItemType[], userRoles: string[]): UserDropdownItemType[] { + return items.filter(item => { + if (item.isDivider || item.isHeader || !item.url || item.url === '#') { + return true; + } + const modulePath = this.normalizePath(item.url); + return this.permissionsService.canAccessModule(modulePath, userRoles); + }); + } + + private normalizePath(url: string): string { + return url.startsWith('/') ? url.substring(1) : url; + } + + private getFullMenu(): MenuItemType[] { + return [ + { label: 'Pilotage', isTitle: true }, + { + label: 'Tableau de Bord', + icon: 'lucideBarChart2', + url: '/dcb-dashboard', + }, + + { label: 'Business & Transactions', isTitle: true }, + { + label: 'Transactions DCB', + icon: 'lucideCreditCard', + url: '/transactions', + }, + + { + label: 'Marchands', + icon: 'lucideStore', + isCollapsed: true, + children: [ + { label: 'Liste des Marchands', url: '/merchants/list' }, + { label: 'Configuration API / Webhooks', url: '/merchants/config' }, + { label: 'Statistiques & Historique', url: '/merchants/history' }, + ], + }, + { + label: 'Opérateurs', + icon: 'lucideServer', + isCollapsed: true, + children: [ + { label: 'Paramètres d\'Intégration', url: '/operators/config' }, + { label: 'Performance & Monitoring', url: '/operators/stats' }, + ], + }, + + { + label: 'Webhooks', + icon: 'lucideShare', + isCollapsed: true, + children: [ + { label: 'Historique', url: '/webhooks/history' }, + { label: 'Statut des Requêtes', url: '/webhooks/status' }, + { label: 'Relancer Webhook', url: '/webhooks/retry' }, + ], + }, + + { label: 'Utilisateurs & Sécurité', isTitle: true }, + { + label: 'Gestion des Utilisateurs', + icon: 'lucideUsers', + url: '/users', + }, + + { label: 'Configuration', isTitle: true }, + { label: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' }, + { label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' }, + + { label: 'Support & Profil', isTitle: true }, + { label: 'Support', icon: 'lucideLifeBuoy', url: '/support' }, + { label: 'Mon Profil', icon: 'lucideUser', url: '/profile' }, + + { label: 'Informations', isTitle: true }, + { label: 'Documentation', icon: 'lucideBookOpen', url: '/documentation' }, + { label: 'Aide', icon: 'lucideHelpCircle', url: '/help' }, + { label: 'À propos', icon: 'lucideInfo', url: '/about' }, + ]; + } + + private getFullUserDropdown(): UserDropdownItemType[] { + return [ + { label: 'Welcome back!', isHeader: true }, + { label: 'Profile', icon: 'tablerUserCircle', url: '/profile' }, + { label: 'Account Settings', icon: 'tablerSettings2', url: '/settings' }, + { label: 'Support Center', icon: 'tablerHeadset', url: '/support' }, + { isDivider: true }, + { + label: 'Log Out', + icon: 'tablerLogout2', + url: '#', + class: 'fw-semibold text-danger' + }, + ]; +} +} \ No newline at end of file diff --git a/src/app/core/services/notifications.service.ts b/src/app/core/services/notifications.service.ts new file mode 100644 index 0000000..fb92488 --- /dev/null +++ b/src/app/core/services/notifications.service.ts @@ -0,0 +1,23 @@ +import { Injectable, inject } from '@angular/core'; +import { ToastrService } from 'ngx-toastr'; + +@Injectable({ providedIn: 'root' }) +export class NotificationService { + private toastr = inject(ToastrService); + + success(message: string, title?: string): void { + this.toastr.success(message, title); + } + + error(message: string, title?: string): void { + this.toastr.error(message, title); + } + + warning(message: string, title?: string): void { + this.toastr.warning(message, title); + } + + info(message: string, title?: string): void { + this.toastr.info(message, title); + } +} \ No newline at end of file diff --git a/src/app/core/services/permissions.service.ts b/src/app/core/services/permissions.service.ts new file mode 100644 index 0000000..360fc6c --- /dev/null +++ b/src/app/core/services/permissions.service.ts @@ -0,0 +1,148 @@ +import { Injectable } from '@angular/core'; + +export interface ModulePermission { + module: string; + roles: string[]; + children?: { [key: string]: string[] }; +} + +@Injectable({ providedIn: 'root' }) +export class PermissionsService { + private readonly permissions: ModulePermission[] = [ + // Dashboard + { + module: 'dcb-dashboard', + roles: ['admin', 'merchant', 'support'], + }, + + // Transactions + { + module: 'transactions', + roles: ['admin', 'merchant', 'support'], + }, + + // Merchants + { + module: 'merchants', + roles: ['admin', 'merchant'], + children: { + 'list': ['admin'], + 'config': ['admin', 'merchant'], + 'history': ['admin', 'merchant'] + } + }, + + // Operators (Admin only) + { + module: 'operators', + roles: ['admin'], + children: { + 'config': ['admin'], + 'stats': ['admin'] + } + }, + + // Webhooks + { + module: 'webhooks', + roles: ['admin', 'merchant'], + children: { + 'history': ['admin', 'merchant'], + 'status': ['admin', 'merchant'], + 'retry': ['admin'] + } + }, + + // Users (Admin only) + { + module: 'users', + roles: ['admin'] + }, + + // Support (All authenticated users) + { + module: 'settings', + roles: ['admin', 'merchant', 'support'] + }, + + // Integrations (Admin only) + { + module: 'integrations', + roles: ['admin'] + }, + + // Support (All authenticated users) + { + module: 'support', + roles: ['admin', 'merchant', 'support'] + }, + + // Profile (All authenticated users) + { + module: 'profile', + roles: ['admin', 'merchant', 'support'] + }, + + // Documentation (All authenticated users) + { + module: 'documentation', + roles: ['admin', 'merchant', 'support'] + }, + + // Help (All authenticated users) + { + module: 'help', + roles: ['admin', 'merchant', 'support'] + }, + + // About (All authenticated users) + { + module: 'about', + roles: ['admin', 'merchant', 'support'] + } + ]; + + canAccessModule(modulePath: string, userRoles: string[]): boolean { + if (!userRoles || userRoles.length === 0) { + return false; + } + + const [mainModule, subModule] = modulePath.split('/'); + const permission = this.findPermission(mainModule); + + if (!permission) { + console.warn(`No permission configuration for module: ${mainModule}`); + return false; + } + + // Check main module access + const hasModuleAccess = this.hasAnyRole(permission.roles, userRoles); + if (!hasModuleAccess) return false; + + // Check sub-module access if specified + if (subModule && permission.children) { + const subModuleRoles = permission.children[subModule]; + if (!subModuleRoles) { + console.warn(`No permission configuration for submodule: ${mainModule}/${subModule}`); + return false; + } + return this.hasAnyRole(subModuleRoles, userRoles); + } + + return true; + } + + private findPermission(module: string): ModulePermission | undefined { + return this.permissions.find(p => p.module === module); + } + + private hasAnyRole(requiredRoles: string[], userRoles: string[]): boolean { + return requiredRoles.some(role => userRoles.includes(role)); + } + + getAccessibleModules(userRoles: string[]): string[] { + return this.permissions + .filter(permission => this.hasAnyRole(permission.roles, userRoles)) + .map(permission => permission.module); + } +} \ No newline at end of file diff --git a/src/app/layouts/components/data.ts b/src/app/layouts/components/data.ts index eea65fa..f148621 100644 --- a/src/app/layouts/components/data.ts +++ b/src/app/layouts/components/data.ts @@ -19,11 +19,6 @@ export const userDropdownItems: UserDropdownItemType[] = [ icon: 'tablerUserCircle', url: '#', }, - { - label: 'Notifications', - icon: 'tablerBellRinging', - url: '#', - }, { label: 'Account Settings', icon: 'tablerSettings2', @@ -58,22 +53,7 @@ export const menuItems: MenuItemType[] = [ { label: 'Tableau de Bord', icon: 'lucideBarChart2', - isCollapsed: true, - children: [ - { label: 'Vue Globale', url: '/dashboard/overview' }, - { label: 'KPIs & Graphiques', url: '/dashboard/kpis' }, - { label: 'Rapports', url: '/dashboard/reports' }, - ], - }, - { - label: 'Rapports Avancés', - icon: 'lucideFile', - isCollapsed: true, - children: [ - { label: 'Financiers', url: '/reports/financial' }, - { label: 'Opérationnels', url: '/reports/operations' }, - { label: 'Export CSV / PDF', url: '/reports/export' }, - ], + url: '/dcb-dashboard', }, // --------------------------- @@ -83,13 +63,7 @@ export const menuItems: MenuItemType[] = [ { label: 'Transactions DCB', icon: 'lucideCreditCard', - isCollapsed: true, - children: [ - { label: 'Liste & Recherche', url: '/transactions/list' }, - { label: 'Filtres Avancés', url: '/transactions/filters' }, - { label: 'Détails & Logs', url: '/transactions/details' }, - { label: 'Export', url: '/transactions/export' }, - ], + url: '/transactions', }, { label: 'Marchands', @@ -145,9 +119,7 @@ export const menuItems: MenuItemType[] = [ icon: 'lucideUsers', isCollapsed: true, children: [ - { label: 'Liste des Utilisateurs', url: '/users/list' }, - { label: 'Rôles & Permissions', url: '/users/roles' }, - { label: 'Audit & Historique', url: '/users/audits' }, + { label: 'Liste des Utilisateurs', url: '/users' }, ], }, { diff --git a/src/app/layouts/components/sidenav/components/app-menu/app-menu.component.html b/src/app/layouts/components/sidenav/components/app-menu/app-menu.component.html index 7d06d2c..0234164 100644 --- a/src/app/layouts/components/sidenav/components/app-menu/app-menu.component.html +++ b/src/app/layouts/components/sidenav/components/app-menu/app-menu.component.html @@ -4,7 +4,7 @@
  • {{ item.label }}
  • } - @if (!item.isTitle) { + @if (!item.isTitle && shouldDisplayItem(item)) { @if (!hasSubMenu(item)) { @@ -49,18 +49,21 @@ > @@ -93,4 +96,4 @@ } - + \ No newline at end of file diff --git a/src/app/layouts/components/sidenav/components/app-menu/app-menu.component.ts b/src/app/layouts/components/sidenav/components/app-menu/app-menu.component.ts index d1ff3e3..a4bbae4 100644 --- a/src/app/layouts/components/sidenav/components/app-menu/app-menu.component.ts +++ b/src/app/layouts/components/sidenav/components/app-menu/app-menu.component.ts @@ -4,25 +4,27 @@ import { OnInit, TemplateRef, ViewChild, + OnDestroy, } from '@angular/core' import { MenuItemType } from '@/app/types/layout' import { CommonModule } from '@angular/common' import { NgIcon } from '@ng-icons/core' import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap' import { NavigationEnd, Router, RouterLink } from '@angular/router' -import { filter } from 'rxjs' +import { filter, Subscription } from 'rxjs' import { scrollToElement } from '@/app/utils/layout-utils' -import { menuItems } from '@layouts/components/data' import { LayoutStoreService } from '@core/services/layout-store.service' +import { MenuService } from '@core/services/menu.service' @Component({ selector: 'app-menu', imports: [NgIcon, NgbCollapse, RouterLink, CommonModule], templateUrl: './app-menu.component.html', }) -export class AppMenuComponent implements OnInit { - router = inject(Router) - layout = inject(LayoutStoreService) +export class AppMenuComponent implements OnInit, OnDestroy { + private router = inject(Router) + private layout = inject(LayoutStoreService) + private menuService = inject(MenuService) @ViewChild('MenuItemWithChildren', { static: true }) menuItemWithChildren!: TemplateRef<{ item: MenuItemType }> @@ -30,9 +32,12 @@ export class AppMenuComponent implements OnInit { @ViewChild('MenuItem', { static: true }) menuItem!: TemplateRef<{ item: MenuItemType }> - menuItems = menuItems + menuItems: MenuItemType[] = [] + private subscription?: Subscription ngOnInit(): void { + this.loadFilteredMenu() + this.router.events .pipe(filter((event) => event instanceof NavigationEnd)) .subscribe(() => { @@ -40,15 +45,25 @@ export class AppMenuComponent implements OnInit { setTimeout(() => this.scrollToActiveLink(), 50) }) - this.expandActivePaths(this.menuItems) - setTimeout(() => this.scrollToActiveLink(), 100) + setTimeout(() => { + this.expandActivePaths(this.menuItems) + this.scrollToActiveLink() + }, 100) + } + + ngOnDestroy(): void { + this.subscription?.unsubscribe() + } + + private loadFilteredMenu(): void { + this.menuItems = this.menuService.getMenuItems() } hasSubMenu(item: MenuItemType): boolean { - return !!item.children + return !!item.children && item.children.length > 0 } - expandActivePaths(items: MenuItemType[]) { + expandActivePaths(items: MenuItemType[]): void { for (const item of items) { if (this.hasSubMenu(item)) { item.isCollapsed = !this.isChildActive(item) @@ -86,4 +101,19 @@ export class AppMenuComponent implements OnInit { scrollToElement(scrollContainer, scrollContainer.scrollTop + offset, 500) } } -} + + /** + * Vérifie si un élément de menu doit être affiché selon les permissions + */ + shouldDisplayItem(item: MenuItemType): boolean { + // Les titres sont toujours affichés + if (item.isTitle) return true + + // Les éléments sans URL sont affichés (comme les conteneurs) + if (!item.url || item.url === '#') return true + + // Pour les éléments avec URL, vérifier les permissions + const modulePath = item.url.startsWith('/') ? item.url.substring(1) : item.url + return this.menuService.canAccess(modulePath) + } +} \ No newline at end of file diff --git a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.html b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.html index abd87e5..109656c 100644 --- a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.html +++ b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.html @@ -6,7 +6,7 @@ alt="user-image" />
    -
    {{ user?.given_name }} - {{ user?.family_name }}
    +
    {{ user?.firstName }} - {{ user?.lastName }}
    Administrateur
    diff --git a/src/app/layouts/components/topbar/components/user-profile/user-profile.html b/src/app/layouts/components/topbar/components/user-profile/user-profile.html index 960b921..1b7f80d 100644 --- a/src/app/layouts/components/topbar/components/user-profile/user-profile.html +++ b/src/app/layouts/components/topbar/components/user-profile/user-profile.html @@ -12,42 +12,52 @@ /> diff --git a/src/app/layouts/components/topbar/components/user-profile/user-profile.ts b/src/app/layouts/components/topbar/components/user-profile/user-profile.ts index 833ddbb..2c7b6d9 100644 --- a/src/app/layouts/components/topbar/components/user-profile/user-profile.ts +++ b/src/app/layouts/components/topbar/components/user-profile/user-profile.ts @@ -1,13 +1,15 @@ -import { Component, inject } from '@angular/core' -import { AuthService } from '@core/services/auth.service'; +import { Component, inject, OnInit, OnDestroy } from '@angular/core' +import { AuthService } from '@core/services/auth.service' +import { MenuService } from '@core/services/menu.service' import { NgbDropdown, NgbDropdownMenu, NgbDropdownToggle, } from '@ng-bootstrap/ng-bootstrap' -import { userDropdownItems } from '@layouts/components/data' import { RouterLink } from '@angular/router' import { NgIcon } from '@ng-icons/core' +import { UserDropdownItemType } from '@/app/types/layout' +import { Subscription } from 'rxjs' @Component({ selector: 'app-user-profile-topbar', @@ -20,21 +22,37 @@ import { NgIcon } from '@ng-icons/core' ], templateUrl: './user-profile.html', }) -export class UserProfile { - private authService = inject(AuthService); +export class UserProfile implements OnInit, OnDestroy { + private authService = inject(AuthService) + private menuService = inject(MenuService) + private subscription?: Subscription - menuItems = userDropdownItems; + menuItems: UserDropdownItemType[] = [] - // Méthode pour gérer le logout - logout() { - this.authService.logout(); + ngOnInit() { + this.loadDropdownItems() + + // Optionnel : réagir aux changements d'authentification + this.subscription = this.authService.onAuthState().subscribe(() => { + this.loadDropdownItems() + }) } - // Méthode pour gérer les clics sur les items du menu - handleItemClick(item: any) { + ngOnDestroy() { + this.subscription?.unsubscribe() + } + + private loadDropdownItems() { + this.menuItems = this.menuService.getUserDropdownItems() + } + + logout() { + this.authService.logout() + } + + handleItemClick(item: UserDropdownItemType) { if (item.label === 'Log Out') { - this.logout(); + this.logout() } - // Pour les autres items, la navigation se fait via routerLink } } \ No newline at end of file diff --git a/src/app/modules/auth/error/error-404.ts b/src/app/modules/auth/error/error-404.ts new file mode 100644 index 0000000..cede1a0 --- /dev/null +++ b/src/app/modules/auth/error/error-404.ts @@ -0,0 +1,89 @@ +// error-404.component.ts +import { Component } from '@angular/core' +import { Router } from '@angular/router' +import { credits, currentYear } from '@/app/constants' + +@Component({ + selector: 'app-error-404', + imports: [], + template: ` +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    404
    +
    +
    + + +

    Page Non Trouvée

    +

    404 - Introuvable

    +

    + La page que vous recherchez n'existe pas ou a été déplacée. + Vérifiez l'URL ou utilisez la navigation principale. +

    + + +
    + + +
    +
    +
    +
    + +

    + © {{ currentYear }} Simple — by + {{ credits.name }} +

    +
    +
    +
    +
    + `, + styles: [` + .auth-box { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + } + .card { + border: none; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + } + `] +}) +export class Error404 { + protected readonly currentYear = currentYear + protected readonly credits = credits + + constructor(private router: Router) {} + + goHome() { + this.router.navigate(['/dcb-dashboard']) + } + + goBack() { + window.history.back() + } +} + + diff --git a/src/app/modules/auth/error/error.route.ts b/src/app/modules/auth/error/error.route.ts new file mode 100644 index 0000000..6a9239b --- /dev/null +++ b/src/app/modules/auth/error/error.route.ts @@ -0,0 +1,21 @@ +import { Routes } from '@angular/router' +import { Error404 } from './error-404' +import { Unauthorized } from '../unauthorized' + +export const ERROR_PAGES_ROUTES: Routes = [ + { + path: 'error/404', + component: Error404, + data: { title: 'Page Non Trouvée' }, + }, + { + path: 'error/403', + component: Unauthorized, + data: { title: 'Accès Refusé' }, + }, + { + path: 'unauthorized', + component: Unauthorized, + data: { title: 'Accès Refusé' }, + } +] \ No newline at end of file diff --git a/src/app/modules/auth/reset-password.ts b/src/app/modules/auth/reset-password.ts index 290766c..100ed04 100644 --- a/src/app/modules/auth/reset-password.ts +++ b/src/app/modules/auth/reset-password.ts @@ -1,11 +1,13 @@ -import { Component } from '@angular/core' -import { appName, credits, currentYear } from '@/app/constants' +import { Component, inject } from '@angular/core' import { RouterLink } from '@angular/router' +import { FormsModule } from '@angular/forms' import { AppLogo } from '@app/components/app-logo' +import { UsersService } from '../users/services/users.service' +import { credits, currentYear } from '@/app/constants' @Component({ selector: 'app-reset-password', - imports: [RouterLink, AppLogo], + imports: [RouterLink, AppLogo, FormsModule], template: `
    @@ -16,23 +18,36 @@ import { AppLogo } from '@app/components/app-logo'

    - Enter your email address and we'll send you a link to reset - your password. + Entrez votre adresse email et nous vous enverrons un lien pour réinitialiser votre mot de passe.

    -
    + +
    + {{ successMessage }} +
    + + +
    + {{ errorMessage }} +
    + +
    - +
    @@ -43,10 +58,14 @@ import { AppLogo } from '@app/components/app-logo' class="form-check-input form-check-input-light fs-14" type="checkbox" id="termAndPolicy" + [(ngModel)]="termsAccepted" + name="termsAccepted" + required + [disabled]="loading" /> - +
    @@ -54,25 +73,28 @@ import { AppLogo } from '@app/components/app-logo'

    - Return to + Retour à Sign in + Connexion +

    - © {{ currentYear }} {{ appName }}. Tous droits réservés. — Développé par + © {{ currentYear }} Simple — par {{ credits.name }}

    @@ -80,10 +102,89 @@ import { AppLogo } from '@app/components/app-logo' `, - styles: ``, + styles: [` + .auth-box { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + } + .card { + border: none; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + } + `] }) export class ResetPassword { - protected readonly appName = appName + private usersService = inject(UsersService) + + email = '' + termsAccepted = false + loading = false + successMessage = '' + errorMessage = '' + protected readonly currentYear = currentYear protected readonly credits = credits -} + + onSubmit() { + if (!this.email || !this.termsAccepted) { + this.errorMessage = 'Veuillez remplir tous les champs obligatoires et accepter les conditions.'; + return; + } + + this.loading = true; + this.successMessage = ''; + this.errorMessage = ''; + + // Simulation d'envoi d'email de réinitialisation + // Note: Cette fonctionnalité nécessite un backend configuré pour envoyer des emails + this.usersService.findUserByEmail(this.email).subscribe({ + next: (users) => { + this.loading = false; + + if (users && users.length > 0) { + // Si l'utilisateur existe, afficher un message de succès + this.successMessage = 'Un lien de réinitialisation a été envoyé à votre adresse email.'; + this.errorMessage = ''; + + // Ici, vous devriez normalement appeler un service backend + // qui envoie un email de réinitialisation + console.log('Email de réinitialisation envoyé à:', this.email); + } else { + this.errorMessage = 'Aucun utilisateur trouvé avec cette adresse email.'; + this.successMessage = ''; + } + }, + error: (error) => { + this.loading = false; + this.errorMessage = 'Une erreur est survenue lors de la recherche de l\'utilisateur.'; + this.successMessage = ''; + console.error('Error finding user:', error); + } + }); + } + + // Alternative: Réinitialisation directe du mot de passe + resetPasswordDirectly(userId: string) { + const newPassword = prompt('Nouveau mot de passe:'); + if (newPassword && newPassword.length >= 8) { + const resetDto = { + userId: userId, + newPassword: newPassword, + temporary: false + }; + + this.usersService.resetPassword(resetDto).subscribe({ + next: () => { + alert('Mot de passe réinitialisé avec succès'); + }, + error: (error) => { + console.error('Error resetting password:', error); + alert('Erreur lors de la réinitialisation du mot de passe'); + } + }); + } else if (newPassword) { + alert('Le mot de passe doit contenir au moins 8 caractères'); + } + } +} \ No newline at end of file diff --git a/src/app/modules/auth/sign-in.ts b/src/app/modules/auth/sign-in.ts index 51c7d99..fd90b87 100644 --- a/src/app/modules/auth/sign-in.ts +++ b/src/app/modules/auth/sign-in.ts @@ -160,7 +160,7 @@ export class SignIn { this.authService.login(this.username, this.password).subscribe({ next: (res) => { - this.router.navigate(['/dashboard']); + this.router.navigate(['/dcb-dashboard']); this.loading = false; }, error: (err) => { diff --git a/src/app/modules/auth/unauthorized.ts b/src/app/modules/auth/unauthorized.ts new file mode 100644 index 0000000..957bd5b --- /dev/null +++ b/src/app/modules/auth/unauthorized.ts @@ -0,0 +1,96 @@ +// unauthorized.component.ts +import { Component } from '@angular/core' +import { Router } from '@angular/router' +import { credits, currentYear } from '@/app/constants' + +@Component({ + selector: 'app-unauthorized', + imports: [], + template: ` +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    403
    +
    +
    + + +

    Accès Refusé

    +

    403 - Non Autorisé

    +

    + Vous n'avez pas les permissions nécessaires pour accéder à cette page. + Contactez votre administrateur si vous pensez qu'il s'agit d'une erreur. +

    + + +
    + + +
    + + +
    + + + Rôles requis : Administrateur, Gestionnaire ou Support + +
    +
    +
    +
    + + +

    + © {{ currentYear }} Simple — by + {{ credits.name }} +

    +
    +
    +
    +
    + `, + styles: [` + .auth-box { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + } + .card { + border: none; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + } + `] +}) +export class Unauthorized { + protected readonly currentYear = currentYear + protected readonly credits = credits + + constructor(private router: Router) {} + + goHome() { + this.router.navigate(['/dcb-dashboard']) + } + + goBack() { + window.history.back() + } +} \ No newline at end of file diff --git a/src/app/modules/components/data.ts b/src/app/modules/components/data.ts index 7fc4925..3a65516 100644 --- a/src/app/modules/components/data.ts +++ b/src/app/modules/components/data.ts @@ -1,3 +1,10 @@ +export const paginationIcons = { + first: ``, + previous: ``, + next: ``, + last: ``, +} + export const states = [ 'Alabama', 'Alaska', diff --git a/src/app/modules/dashboard/components/active-users.ts b/src/app/modules/dashboard/components/active-users.ts deleted file mode 100644 index d1ac51e..0000000 --- a/src/app/modules/dashboard/components/active-users.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Component } from '@angular/core' -import { NgIcon } from '@ng-icons/core' -import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap' - -@Component({ - selector: 'app-active-users', - imports: [NgIcon, NgbProgressbarModule], - template: ` -
    -
    -
    -
    -
    Active Users
    -

    342

    -

    In the last hour

    -
    -
    - -
    -
    - - - -
    -
    - Avg. Session Time -
    4m 12s
    -
    -
    - Returning Users -
    54.9%
    -
    -
    -
    - -
    - `, - styles: ``, -}) -export class ActiveUsers {} diff --git a/src/app/modules/dashboard/components/api-performance-metrics.ts b/src/app/modules/dashboard/components/api-performance-metrics.ts deleted file mode 100644 index 70052e0..0000000 --- a/src/app/modules/dashboard/components/api-performance-metrics.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Component } from '@angular/core' -import { TableType } from '@/app/types' -import { currency } from '@/app/constants' -import { DecimalPipe } from '@angular/common' - -type APIPerformanceMetricsType = { - endpoint: string - latency: string - requests: string - errorRate: number - cost: number -} - -@Component({ - selector: 'app-api-performance-metrics', - imports: [DecimalPipe], - template: ` -
    -
    -

    AI API Performance Metrics

    -
    - -
    -
    - - - - @for ( - header of apiPerformanceMetricsTable.headers; - track header - ) { - - } - - - - @for ( - item of apiPerformanceMetricsTable.body; - track item.endpoint - ) { - - - - - - - - } - -
    {{ header }}
    {{ item.endpoint }}{{ item.latency }}{{ item.requests }}{{ item.errorRate | number: '1.2-2' }}%{{ item.cost | number: '1.2-2' }}
    -
    -
    - - -
    - `, - styles: ``, -}) -export class ApiPerformanceMetrics { - apiPerformanceMetricsTable: TableType = { - headers: [ - 'Endpoint', - 'Latency', - 'Requests', - 'Error Rate', - `Cost (${currency})`, - ], - body: [ - { - endpoint: '/v1/chat/completions', - latency: '720ms', - requests: '8,204', - errorRate: 0.18, - cost: 128.34, - }, - { - endpoint: '/v1/images/generations', - latency: '930ms', - requests: '1,029', - errorRate: 0.03, - cost: 43.89, - }, - { - endpoint: '/v1/audio/transcriptions', - latency: '1.2s', - requests: '489', - errorRate: 0.0, - cost: 16.45, - }, - { - endpoint: '/v1/embeddings', - latency: '610ms', - requests: '2,170', - errorRate: 0.1, - cost: 24.98, - }, - { - endpoint: '/v1/chat/moderation', - latency: '450ms', - requests: '5,025', - errorRate: 0.01, - cost: 7.52, - }, - ], - } -} diff --git a/src/app/modules/dashboard/components/model-usage-summary.ts b/src/app/modules/dashboard/components/model-usage-summary.ts deleted file mode 100644 index fda5b37..0000000 --- a/src/app/modules/dashboard/components/model-usage-summary.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Component } from '@angular/core' -import { TableType } from '@/app/types' -import { DecimalPipe } from '@angular/common' - -type ModelUsageType = { - model: string - requests: number - totalTokens: number - averageTokens: number - lastUsed: string -} - -@Component({ - selector: 'app-model-usage-summary', - imports: [DecimalPipe], - template: ` -
    -
    -

    AI Model Usage Summary

    -
    - -
    -
    - - - - @for (header of modelUsageTable.headers; track header) { - - } - - - - @for (item of modelUsageTable.body; track item.model) { - - - - - - - - } - -
    {{ header }}
    {{ item.model }}{{ item.requests | number }}{{ item.totalTokens | number }}{{ item.averageTokens | number }}{{ item.lastUsed }}
    -
    -
    - - -
    - `, - styles: ``, -}) -export class ModelUsageSummary { - modelUsageTable: TableType = { - headers: [ - 'Model', - 'Requests', - 'Total Tokens', - 'Average Tokens', - 'Last Used', - ], - body: [ - { - model: 'GPT-4', - requests: 1248, - totalTokens: 2483920, - averageTokens: 1989, - lastUsed: '2025-06-15', - }, - { - model: 'DALL·E', - requests: 328, - totalTokens: 194320, - averageTokens: 592, - lastUsed: '2025-06-14', - }, - { - model: 'Claude 2', - requests: 814, - totalTokens: 1102390, - averageTokens: 1354, - lastUsed: '2025-06-13', - }, - { - model: 'Whisper', - requests: 512, - totalTokens: 653210, - averageTokens: 1275, - lastUsed: '2025-06-12', - }, - { - model: 'Stable Diffusion', - requests: 102, - totalTokens: 61400, - averageTokens: 602, - lastUsed: '2025-06-10', - }, - ], - } -} diff --git a/src/app/modules/dashboard/components/prompts-usage.ts b/src/app/modules/dashboard/components/prompts-usage.ts deleted file mode 100644 index d83835b..0000000 --- a/src/app/modules/dashboard/components/prompts-usage.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Component } from '@angular/core' -import { BaseChartDirective } from 'ng2-charts' -import { NgIcon } from '@ng-icons/core' -import { Chartjs } from '@app/components/chartjs' -import { ChartConfiguration } from 'chart.js' -import { getColor } from '@/app/utils/color-utils' -import { CountUpModule } from 'ngx-countup' - -@Component({ - selector: 'app-prompts-usage', - imports: [NgIcon, Chartjs, CountUpModule], - template: ` -
    -
    -
    -
    -
    Today's Prompts
    -
    -
    - -
    -
    - -
    - -
    - -
    -
    - Today -
    - 1,245 prompts -
    -
    -
    - Yesterday -
    - 1,110 - -
    -
    -
    -
    - -
    - `, - styles: ``, -}) -export class PromptsUsage { - public promptChart = (): ChartConfiguration => ({ - type: 'bar', - data: { - labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], - datasets: [ - { - data: [120, 150, 180, 220, 200, 245, 145], - backgroundColor: getColor('chart-primary'), - borderRadius: 4, - borderSkipped: false, - }, - ], - }, - options: { - plugins: { - legend: { display: false }, - tooltip: { enabled: false }, - }, - scales: { - x: { - display: false, - grid: { display: false }, - }, - y: { - display: false, - grid: { display: false }, - }, - }, - }, - }) -} diff --git a/src/app/modules/dashboard/components/recent-sessions.ts b/src/app/modules/dashboard/components/recent-sessions.ts deleted file mode 100644 index ad3ff8f..0000000 --- a/src/app/modules/dashboard/components/recent-sessions.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { Component } from '@angular/core' -import { NgIcon } from '@ng-icons/core' -import { RouterLink } from '@angular/router' -import { DecimalPipe } from '@angular/common' -import { toTitleCase } from '@/app/utils/string-utils' -import { - NgbDropdownModule, - NgbPaginationModule, -} from '@ng-bootstrap/ng-bootstrap' - -type SessionType = { - id: string - user: { - name: string - avatar: string - } - aiModel: string - date: string - tokens: number - status: 'completed' | 'pending' | 'failed' -} - -@Component({ - selector: 'app-recent-sessions', - imports: [ - NgIcon, - RouterLink, - DecimalPipe, - NgbPaginationModule, - NgbDropdownModule, - ], - template: ` -
    -
    -

    Recent AI Sessions

    - -
    - -
    -
    - - - @for (session of sessions; track session.id) { - - - - - - - - - } - -
    -
    - -
    - {{ - session.user.name - }} -
    - {{ - session.id - }} -
    -
    -
    -
    - Model -
    - {{ session.aiModel }} -
    -
    - Date -
    {{ session.date }}
    -
    - Tokens -
    - {{ session.tokens | number }} -
    -
    - Status -
    - - {{ toTitleCase(session.status) }} -
    -
    -
    - - - - -
    -
    -
    -
    - - -
    - `, - styles: ``, -}) -export class RecentSessions { - sessions: SessionType[] = [ - { - id: '#AI-5001', - user: { name: 'Alice Cooper', avatar: 'assets/images/users/user-1.jpg' }, - aiModel: 'GPT-4', - date: '2025-05-01', - tokens: 2304, - status: 'completed', - }, - { - id: '#AI-5002', - user: { name: 'David Lee', avatar: 'assets/images/users/user-2.jpg' }, - aiModel: 'DALL·E', - date: '2025-04-30', - tokens: 580, - status: 'pending', - }, - { - id: '#AI-5003', - user: { name: 'Sophia Turner', avatar: 'assets/images/users/user-3.jpg' }, - aiModel: 'Whisper', - date: '2025-04-29', - tokens: 1102, - status: 'completed', - }, - { - id: '#AI-5004', - user: { name: 'James Wilson', avatar: 'assets/images/users/user-4.jpg' }, - aiModel: 'GPT-3.5', - date: '2025-04-28', - tokens: 760, - status: 'failed', - }, - { - id: '#AI-5005', - user: { name: 'Ava Carter', avatar: 'assets/images/users/user-5.jpg' }, - aiModel: 'Claude 2', - date: '2025-04-27', - tokens: 1678, - status: 'completed', - }, - { - id: '#AI-5006', - user: { name: 'Ethan Brooks', avatar: 'assets/images/users/user-6.jpg' }, - aiModel: 'Gemini Pro', - date: '2025-04-26', - tokens: 945, - status: 'pending', - }, - { - id: '#AI-5007', - user: { name: 'Mia Clarke', avatar: 'assets/images/users/user-7.jpg' }, - aiModel: 'GPT-4 Turbo', - date: '2025-04-25', - tokens: 2189, - status: 'completed', - }, - { - id: '#AI-5008', - user: { name: 'Lucas Perry', avatar: 'assets/images/users/user-8.jpg' }, - aiModel: 'Stable Diffusion', - date: '2025-04-24', - tokens: 312, - status: 'failed', - }, - { - id: '#AI-5009', - user: { name: 'Chloe Adams', avatar: 'assets/images/users/user-9.jpg' }, - aiModel: 'GPT-4', - date: '2025-04-23', - tokens: 1784, - status: 'completed', - }, - { - id: '#AI-5010', - user: { - name: 'Benjamin Gray', - avatar: 'assets/images/users/user-10.jpg', - }, - aiModel: 'Whisper', - date: '2025-04-22', - tokens: 890, - status: 'pending', - }, - ] - protected readonly toTitleCase = toTitleCase -} diff --git a/src/app/modules/dashboard/components/request-statistics.ts b/src/app/modules/dashboard/components/request-statistics.ts deleted file mode 100644 index 3f79dca..0000000 --- a/src/app/modules/dashboard/components/request-statistics.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Component } from '@angular/core' -import { CountUpModule } from 'ngx-countup' -import { NgIcon } from '@ng-icons/core' -import { ChartConfiguration } from 'chart.js' -import { getColor } from '@/app/utils/color-utils' -import { Chartjs } from '@app/components/chartjs' - -@Component({ - selector: 'app-request-statistics', - imports: [CountUpModule, NgIcon, Chartjs], - template: ` -
    -
    -
    -
    -
    -

    - - AI Requests -

    -

    0

    -

    Total AI requests in last 30 days

    -

    - Data from May -

    -
    -
    - -
    -
    -

    - - Usage Duration -

    -

    9 Months

    -

    Including 4 weeks this quarter

    -

    - - Last accessed: 12.06.2025 -

    -
    -
    - -
    -
    - -
    -
    -
    -
    - - -
    - `, - styles: ``, -}) -export class RequestStatistics { - generateSmoothData(count: number, start: number = 40, variation: number = 5) { - const data = [start] - for (let i = 1; i < count; i++) { - const prev = data[i - 1] - const next = prev + (Math.random() * variation * 2 - variation) - data.push(Math.round(next)) - } - return data - } - - generateHigherData(baseData: number[], diffRange: [number, number] = [3, 6]) { - return baseData.map( - (val) => - val + - Math.floor(Math.random() * (diffRange[1] - diffRange[0] + 1)) + - diffRange[0] - ) - } - - labels = ['0h', '3h', '6h', '9h', '12h', '15h', '18h', '21h'] - - currentAiUsers = this.generateSmoothData(8, 45, 4) - previousAiUsers = this.generateHigherData(this.currentAiUsers) - - activeUsersChartOptions = (): ChartConfiguration => ({ - type: 'line', - data: { - labels: this.labels, - datasets: [ - { - label: 'AI Users (Today)', - data: this.currentAiUsers, - fill: true, - borderColor: getColor('chart-primary'), - backgroundColor: getColor('chart-primary-rgb', 0.2), - tension: 0.4, - pointRadius: 0, - borderWidth: 1, - }, - { - label: 'AI Users (Yesterday)', - data: this.previousAiUsers, - fill: true, - borderColor: getColor('chart-gray'), - backgroundColor: getColor('chart-gray-rgb', 0.2), - tension: 0.4, - pointRadius: 0, - borderWidth: 1, - }, - ], - }, - }) -} diff --git a/src/app/modules/dashboard/components/response-accuracy.ts b/src/app/modules/dashboard/components/response-accuracy.ts deleted file mode 100644 index d5cad16..0000000 --- a/src/app/modules/dashboard/components/response-accuracy.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Component } from '@angular/core' -import { NgIcon } from '@ng-icons/core' -import { getColor } from '@/app/utils/color-utils' -import { Chartjs } from '@app/components/chartjs' -import { ChartConfiguration } from 'chart.js' - -@Component({ - selector: 'app-response-accuracy', - imports: [NgIcon, Chartjs], - template: ` -
    -
    -
    -
    -
    Response Accuracy
    -
    -
    - -
    -
    - -
    - -
    -
    - -
    - `, - styles: ``, -}) -export class ResponseAccuracy { - accuracyChartOptions = (): ChartConfiguration => ({ - type: 'pie', - data: { - labels: ['Correct', 'Partially Correct', 'Incorrect', 'Unclear'], - datasets: [ - { - data: [65, 20, 10, 5], - backgroundColor: [ - getColor('chart-primary'), - getColor('chart-secondary'), - getColor('chart-gray'), - getColor('chart-dark'), - ], - borderColor: '#fff', - borderWidth: 0, - }, - ], - }, - options: { - plugins: { - legend: { display: false }, - tooltip: { - enabled: true, - callbacks: { - label: function (ctx: any) { - return `${ctx.label}: ${ctx.parsed}%` - }, - }, - }, - }, - scales: { - x: { - display: false, - grid: { display: false }, - ticks: { display: false }, - }, - y: { - display: false, - grid: { display: false }, - ticks: { display: false }, - }, - }, - }, - }) -} diff --git a/src/app/modules/dashboard/components/token-usage.ts b/src/app/modules/dashboard/components/token-usage.ts deleted file mode 100644 index 6aa6192..0000000 --- a/src/app/modules/dashboard/components/token-usage.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Component } from '@angular/core' -import { ChartConfiguration } from 'chart.js' -import { getColor } from '@/app/utils/color-utils' -import { NgIcon } from '@ng-icons/core' -import { Chartjs } from '@app/components/chartjs' -import { CountUpModule } from 'ngx-countup' - -@Component({ - selector: 'app-token-usage', - imports: [NgIcon, Chartjs, CountUpModule], - template: ` -
    -
    -
    -
    -
    Token Usage
    -
    -
    - -
    -
    - -
    - -
    - -
    -
    - Today -
    - 920,400 tokens -
    -
    -
    - Yesterday -
    - 865,100 - -
    -
    -
    -
    - -
    - `, - styles: ``, -}) -export class TokenUsage { - tokenChartOptions = (): ChartConfiguration => ({ - type: 'line', - data: { - labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], - datasets: [ - { - data: [82000, 95000, 103000, 112000, 121500, 135200, 148000], - backgroundColor: getColor('chart-primary-rgb', 0.1), - borderColor: getColor('chart-primary'), - tension: 0.4, - fill: true, - pointRadius: 0, - borderWidth: 2, - }, - ], - }, - options: { - plugins: { - legend: { display: false }, - tooltip: { enabled: false }, - }, - scales: { - x: { - display: false, - grid: { display: false }, - }, - y: { - display: false, - grid: { display: false }, - }, - }, - }, - }) -} diff --git a/src/app/modules/dashboard/dashboard.html b/src/app/modules/dashboard/dashboard.html deleted file mode 100644 index cc6dad1..0000000 --- a/src/app/modules/dashboard/dashboard.html +++ /dev/null @@ -1,43 +0,0 @@ -
    - - -
    -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    - -
    - - - -
    -
    -
    diff --git a/src/app/modules/dashboard/dashboard.ts b/src/app/modules/dashboard/dashboard.ts deleted file mode 100644 index d85f7d4..0000000 --- a/src/app/modules/dashboard/dashboard.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Component } from '@angular/core' -import { PageTitle } from '@app/components/page-title/page-title' -import { PromptsUsage } from './components/prompts-usage' -import { ActiveUsers } from './components/active-users' -import { ResponseAccuracy } from './components/response-accuracy' -import { TokenUsage } from './components/token-usage' -import { RequestStatistics } from './components/request-statistics' -import { RecentSessions } from './components/recent-sessions' -import { ModelUsageSummary } from './components/model-usage-summary' -import { ApiPerformanceMetrics } from './components/api-performance-metrics' - -@Component({ - selector: 'app-dashboard', - imports: [ - PageTitle, - PromptsUsage, - ActiveUsers, - ResponseAccuracy, - TokenUsage, - RequestStatistics, - RecentSessions, - ModelUsageSummary, - ApiPerformanceMetrics, - ], - templateUrl: './dashboard.html', - styles: ``, -}) -export class Dashboard {} diff --git a/src/app/modules/dcb-dashboard/dcb-dashboard.html b/src/app/modules/dcb-dashboard/dcb-dashboard.html new file mode 100644 index 0000000..807ed64 --- /dev/null +++ b/src/app/modules/dcb-dashboard/dcb-dashboard.html @@ -0,0 +1,338 @@ +
    + + +
    + +
    +
    +
    +
    + +
    + +
    + + @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 +
    +
    +
    +
    +
    +
    + } +
    + +
    diff --git a/src/app/modules/dashboard/dashboard.spec.ts b/src/app/modules/dcb-dashboard/dcb-dashboard.spec.ts similarity index 91% rename from src/app/modules/dashboard/dashboard.spec.ts rename to src/app/modules/dcb-dashboard/dcb-dashboard.spec.ts index efdbb41..8bd3987 100644 --- a/src/app/modules/dashboard/dashboard.spec.ts +++ b/src/app/modules/dcb-dashboard/dcb-dashboard.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { Dashboard } from './dashboard' +import { Dashboard } from './dcb-dashboard' describe('Dashboard', () => { let component: Dashboard diff --git a/src/app/modules/dcb-dashboard/dcb-dashboard.ts b/src/app/modules/dcb-dashboard/dcb-dashboard.ts new file mode 100644 index 0000000..6e861b2 --- /dev/null +++ b/src/app/modules/dcb-dashboard/dcb-dashboard.ts @@ -0,0 +1,198 @@ +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 { 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'; + +@Component({ + selector: 'app-dcb-dashboard', + standalone: true, + imports: [ + CommonModule, + FormsModule, + NgIcon, + NgbAlertModule, + NgbProgressbarModule, + NgbTooltipModule, + PageTitle + ], + providers: [ + provideNgIconsConfig({ + size: '1.25em' + }) + ], + 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 diff --git a/src/app/modules/dcb-dashboard/models/dcb.ts b/src/app/modules/dcb-dashboard/models/dcb.ts new file mode 100644 index 0000000..543af11 --- /dev/null +++ b/src/app/modules/dcb-dashboard/models/dcb.ts @@ -0,0 +1,53 @@ +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.service.ts b/src/app/modules/dcb-dashboard/services/dcb.service.ts new file mode 100644 index 0000000..c218eda --- /dev/null +++ b/src/app/modules/dcb-dashboard/services/dcb.service.ts @@ -0,0 +1,144 @@ +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/merchants.routes.ts b/src/app/modules/merchants/merchants.routes.ts new file mode 100644 index 0000000..4ff4bdd --- /dev/null +++ b/src/app/modules/merchants/merchants.routes.ts @@ -0,0 +1,15 @@ +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/modules-routing.module.ts b/src/app/modules/modules-routing.module.ts index cb2906e..2297b33 100644 --- a/src/app/modules/modules-routing.module.ts +++ b/src/app/modules/modules-routing.module.ts @@ -1,15 +1,16 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { authGuard } from '../core/guards/auth.guard'; +import { roleGuard } from '../core/guards/role.guard'; +import { Users } from '@modules/users/users'; // Composants principaux -import { Dashboard } from './dashboard/dashboard'; +import { DcbDashboard } from './dcb-dashboard/dcb-dashboard'; + import { Team } from './team/team'; import { Transactions } from './transactions/transactions'; import { TransactionsList } from './transactions/list/list'; -import { TransactionsFilters } from './transactions/filters/filters'; -import { TransactionsDetails } from './transactions/details/details'; -import { TransactionsExport } from './transactions/export/export'; +import { TransactionDetails } from './transactions/details/details'; import { Merchants } from './merchants/merchants'; import { MerchantsList } from './merchants/list/list'; @@ -20,47 +21,53 @@ import { Operators } from './operators/operators'; import { OperatorsConfig } from './operators/config/config'; import { OperatorsStats } from './operators/stats/stats'; -import { Notifications } from './notifications/notifications'; -import { NotificationsList } from './notifications/list/list'; -import { NotificationsFilters } from './notifications/filters/filters'; -import { NotificationsActions } from './notifications/actions/actions'; - import { Webhooks } from './webhooks/webhooks'; import { WebhooksHistory } from './webhooks/history/history'; import { WebhooksStatus } from './webhooks/status/status'; import { WebhooksRetry } from './webhooks/retry/retry'; -import { Users } from './users/users'; -import { UsersList } from './users/list/list'; -import { UsersRoles } from './users/roles/roles'; -import { UsersAudits } from './users/audits/audits'; - import { Settings } from './settings/settings'; import { Integrations } from './integrations/integrations'; import { Support } from './support/support'; -import { Profile } from './profile/profile'; +import { MyProfile } from './profile/profile'; 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 // --------------------------- { - path: 'dashboard', - canActivate: [authGuard], - children: [ - { path: '', component: Dashboard, data: { title: 'Dashboard' } }, - { path: 'overview', component: Dashboard, data: { title: 'Vue Globale' } }, - { path: 'kpis', component: Dashboard, data: { title: 'KPIs & Graphiques' } }, - { path: 'reports', component: Dashboard, data: { title: 'Rapports' } }, - ] + path: 'dcb-dashboard', + canActivate: [authGuard, roleGuard], + component: DcbDashboard, + data: { + title: 'Dashboard DCB', + requiredRoles: ['admin', 'merchant', 'support'] + } }, + { path: 'team', component: Team, - canActivate: [authGuard], + canActivate: [authGuard, roleGuard], data: { title: 'Team' } }, @@ -69,14 +76,21 @@ const routes: Routes = [ // --------------------------- { path: 'transactions', - //component: Transactions, - canActivate: [authGuard], - children: [ - { path: 'list', component: TransactionsList, data: { title: 'Liste & Recherche' } }, - { path: 'filters', component: TransactionsFilters, data: { title: 'Filtres Avancés' } }, - { path: 'details', component: TransactionsDetails, data: { title: 'Détails & Logs' } }, - { path: 'export', component: TransactionsExport, data: { title: 'Export' } }, - ] + component: Transactions, + canActivate: [authGuard, roleGuard], + data: { + title: 'Transactions DCB', + requiredRoles: ['admin', 'merchant', 'support'] + } + }, + { + path: 'transactions/:id', + component: Transactions, + canActivate: [authGuard, roleGuard], + data: { + title: 'Détails Transaction', + requiredRoles: ['admin', 'merchant', 'support'] + } }, // --------------------------- @@ -84,8 +98,7 @@ const routes: Routes = [ // --------------------------- { path: 'merchants', - //component: Merchants, - canActivate: [authGuard], + canActivate: [authGuard, roleGuard], children: [ { path: 'list', component: MerchantsList, data: { title: 'Liste des Marchands' } }, { path: 'config', component: MerchantsConfig, data: { title: 'Configuration API / Webhooks' } }, @@ -98,35 +111,19 @@ const routes: Routes = [ // --------------------------- { path: 'operators', - //component: Operators, - canActivate: [authGuard], + canActivate: [authGuard, roleGuard], children: [ - { path: 'config', component: OperatorsConfig, data: { title: 'Paramètres d’Intégration' } }, + { path: 'config', component: OperatorsConfig, data: { title: 'Paramètres d\'Intégration' } }, { path: 'stats', component: OperatorsStats, data: { title: 'Performance & Monitoring' } }, ] }, - // --------------------------- - // Notifications - // --------------------------- - { - path: 'notifications', - //component: Notifications, - canActivate: [authGuard], - children: [ - { path: 'list', component: NotificationsList, data: { title: 'Liste des Notifications' } }, - { path: 'filters', component: NotificationsFilters, data: { title: 'Filtrage par Type' } }, - { path: 'actions', component: NotificationsActions, data: { title: 'Actions Automatiques' } }, - ] - }, - // --------------------------- // Webhooks // --------------------------- { path: 'webhooks', - //component: Webhooks, - canActivate: [authGuard], + canActivate: [authGuard, roleGuard], children: [ { path: 'history', component: WebhooksHistory, data: { title: 'Historique' } }, { path: 'status', component: WebhooksStatus, data: { title: 'Statut des Requêtes' } }, @@ -135,37 +132,58 @@ const routes: Routes = [ }, // --------------------------- - // Users + // Settings & Integrations (Admin seulement) // --------------------------- - { - path: 'users', - //component: Users, - canActivate: [authGuard], - children: [ - { path: 'list', component: UsersList, data: { title: 'Liste des Utilisateurs' } }, - { path: 'roles', component: UsersRoles, data: { title: 'Rôles & Permissions' } }, - { path: 'audits', component: UsersAudits, data: { title: 'Audit & Historique' } }, - ] + { + path: 'settings', + component: Settings, + canActivate: [authGuard, roleGuard], + data: { title: 'Paramètres Système' } + }, + { + path: 'integrations', + component: Integrations, + canActivate: [authGuard, roleGuard], + data: { title: 'Intégrations Externes' } }, // --------------------------- - // Settings & Integrations + // Support & Profile (Tous les utilisateurs authentifiés) // --------------------------- - { path: 'settings', component: Settings, canActivate: [authGuard], data: { title: 'Paramètres Système' } }, - { path: 'integrations', component: Integrations, canActivate: [authGuard], data: { title: 'Intégrations Externes' } }, + { + path: 'support', + component: Support, + canActivate: [authGuard, roleGuard], + data: { title: 'Support' } + }, + { + path: 'profile', + component: MyProfile, + canActivate: [authGuard, roleGuard], + data: { title: 'Mon Profil' } + }, // --------------------------- - // Support & Profile + // Documentation & Help (Tous les utilisateurs authentifiés) // --------------------------- - { path: 'support', component: Support, canActivate: [authGuard], data: { title: 'Support' } }, - { path: 'profile', component: Profile, canActivate: [authGuard], data: { title: 'Mon Profil' } }, - - // --------------------------- - // Documentation & Help - // --------------------------- - { path: 'documentation', component: Documentation, canActivate: [authGuard], data: { title: 'Documentation' } }, - { path: 'help', component: Help, canActivate: [authGuard], data: { title: 'Aide' } }, - { path: 'about', component: About, canActivate: [authGuard], data: { title: 'À propos' } }, + { + path: 'documentation', + component: Documentation, + canActivate: [authGuard, roleGuard], + data: { title: 'Documentation' } + }, + { + path: 'help', + component: Help, + canActivate: [authGuard, roleGuard], + data: { title: 'Aide' } + }, + { + path: 'about', + component: About, + canActivate: [authGuard, roleGuard], + data: { title: 'À propos' } + }, ]; @NgModule({ diff --git a/src/app/modules/profile/profile.html b/src/app/modules/profile/profile.html index 8bf9f73..6a8a697 100644 --- a/src/app/modules/profile/profile.html +++ b/src/app/modules/profile/profile.html @@ -1 +1,405 @@ -

    Profile

    \ No newline at end of file + +
    + +
    +
    +
    +
    +

    + @if (user) { + Mon Profil - {{ getUserDisplayName() }} + } @else { + Mon Profil + } +

    + +
    + +
    + @if (user && !isEditing) { + + } +
    +
    +
    +
    + + + @if (error) { +
    + + {{ error }} +
    + } + + @if (success) { +
    + + {{ success }} +
    + } + +
    + + @if (loading) { +
    +
    + Chargement... +
    +

    Chargement de votre profil...

    +
    + } + + + @if (user && !loading) { + +
    + +
    +
    +
    Mon Profil
    +
    +
    + +
    +
    + {{ getUserInitials() }} +
    +
    + +
    {{ getUserDisplayName() }}
    +

    @{{ user.username }}

    + + + + {{ getStatusText() }} + + + +
    +
    + + {{ user.email }} +
    +
    + + Membre depuis {{ formatTimestamp(user.createdTimestamp) }} +
    +
    + + Vous pouvez modifier votre mot de passe ici +
    +
    + + +
    + +
    +
    +
    + + +
    +
    +
    Mes Rôles
    +
    +
    +
    + @for (role of user.clientRoles; track role) { + + {{ role }} + + } +
    +
    +
    +
    + + +
    +
    +
    +
    + @if (isEditing) { + Modification du Profil + } @else { + Mes Informations + } +
    +
    + +
    +
    + +
    + + @if (isEditing) { + + } @else { +
    + {{ user.firstName || 'Non renseigné' }} +
    + } +
    + + +
    + + @if (isEditing) { + + } @else { +
    + {{ user.lastName || 'Non renseigné' }} +
    + } +
    + + +
    + +
    + {{ user.username }} +
    + Le nom d'utilisateur ne peut pas être modifié +
    + + +
    + + @if (isEditing) { + + } @else { +
    + {{ user.email }} +
    + } +
    + + +
    +
    +
    + + Sécurité du Compte +
    +
    +
    +
    + Mot de passe +
    + Vous pouvez changer votre mot de passe à tout moment +
    +
    +
    +
    + + + @if (isEditing) { +
    +
    + + +
    +
    + } + + + @if (!isEditing) { +
    +
    +
    Informations Système
    +
    +
    + +
    + {{ user.id }} +
    +
    +
    + +
    + {{ formatTimestamp(user.createdTimestamp) }} +
    +
    +
    +
    + } +
    +
    +
    +
    + } +
    +
    + + + + + + + + + + \ No newline at end of file diff --git a/src/app/modules/profile/profile.ts b/src/app/modules/profile/profile.ts index 6edbf86..d924c91 100644 --- a/src/app/modules/profile/profile.ts +++ b/src/app/modules/profile/profile.ts @@ -1,7 +1,227 @@ -import { Component } from '@angular/core'; +import { Component, inject, OnInit, TemplateRef, ViewChild, ChangeDetectorRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgIcon } from '@ng-icons/core'; +import { NgbAlertModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; +import { UsersService } from '@modules/users/services/users.service'; +import { AuthService } from '@core/services/auth.service'; +import { UserResponse, UpdateUserDto } from '@modules/users/models/user'; @Component({ - selector: 'app-profile', + selector: 'app-my-profile', + standalone: true, + imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule, NgbModalModule], templateUrl: './profile.html', + styles: [` + .avatar-lg { + width: 80px; + height: 80px; + } + .fs-24 { + font-size: 24px; + } + `] }) -export class Profile {} \ No newline at end of file +export class MyProfile implements OnInit { + private usersService = inject(UsersService); + private authService = inject(AuthService); + private modalService = inject(NgbModal); + private cdRef = inject(ChangeDetectorRef); + + @ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef; + + user: UserResponse | null = null; + loading = false; + saving = false; + error = ''; + success = ''; + + // Édition + isEditing = false; + editedUser: UpdateUserDto = {}; + + // Réinitialisation mot de passe + newPassword = ''; + temporaryPassword = false; + resettingPassword = false; + resetPasswordError = ''; + resetPasswordSuccess = ''; + + ngOnInit() { + this.loadMyProfile(); + } + + loadMyProfile() { + this.loading = true; + this.error = ''; + + this.usersService.getCurrentUserProfile().subscribe({ + next: (user) => { + this.user = user; + this.loading = false; + this.cdRef.detectChanges(); + }, + error: (error) => { + this.error = 'Erreur lors du chargement de votre profil'; + this.loading = false; + this.cdRef.detectChanges(); + console.error('Error loading my profile:', error); + } + }); + } + + // Ouvrir le modal de réinitialisation de mot de passe + openResetPasswordModal() { + if (!this.user) return; + + this.newPassword = ''; + this.temporaryPassword = false; + this.resetPasswordError = ''; + this.resetPasswordSuccess = ''; + + this.modalService.open(this.resetPasswordModal, { + centered: true, + size: 'md' + }); + } + + // Réinitialiser le mot de passe + confirmResetPassword() { + if (!this.user || !this.newPassword || this.newPassword.length < 8) { + this.resetPasswordError = 'Veuillez saisir un mot de passe valide (au moins 8 caractères).'; + return; + } + + this.resettingPassword = true; + this.resetPasswordError = ''; + this.resetPasswordSuccess = ''; + + const resetDto = { + userId: this.user.id, + newPassword: this.newPassword, + temporary: this.temporaryPassword + }; + + this.usersService.resetPassword(resetDto).subscribe({ + next: () => { + this.resettingPassword = false; + this.resetPasswordSuccess = 'Votre mot de passe a été réinitialisé avec succès !'; + this.cdRef.detectChanges(); + + // Déconnexion automatique si mot de passe temporaire + if (this.temporaryPassword) { + setTimeout(() => { + this.authService.logout(); + }, 2000); + } + }, + error: (error) => { + this.resettingPassword = false; + this.resetPasswordError = this.getResetPasswordErrorMessage(error); + this.cdRef.detectChanges(); + console.error('Error resetting password:', error); + } + }); + } + + // Gestion des erreurs pour la réinitialisation + private getResetPasswordErrorMessage(error: any): string { + if (error.error?.message) { + return error.error.message; + } + if (error.status === 404) { + return 'Utilisateur non trouvé.'; + } + if (error.status === 400) { + return 'Le mot de passe ne respecte pas les critères de sécurité.'; + } + return 'Erreur lors de la réinitialisation du mot de passe. Veuillez réessayer.'; + } + + startEditing() { + this.isEditing = true; + this.editedUser = { + firstName: this.user?.firstName, + lastName: this.user?.lastName, + email: this.user?.email + }; + } + + cancelEditing() { + this.isEditing = false; + this.editedUser = {}; + this.error = ''; + this.success = ''; + } + + saveProfile() { + if (!this.user) return; + + this.saving = true; + this.error = ''; + this.success = ''; + + this.usersService.updateCurrentUserProfile(this.editedUser).subscribe({ + next: (updatedUser) => { + this.user = updatedUser; + this.isEditing = false; + this.saving = false; + this.success = 'Profil mis à jour avec succès'; + this.editedUser = {}; + }, + error: (error) => { + this.error = 'Erreur lors de la mise à jour du profil'; + this.saving = false; + console.error('Error updating profile:', error); + } + }); + } + + // Utilitaires d'affichage + getStatusBadgeClass(): string { + if (!this.user) return 'badge bg-secondary'; + if (!this.user.enabled) return 'badge bg-danger'; + if (!this.user.emailVerified) return 'badge bg-warning'; + return 'badge bg-success'; + } + + getStatusText(): string { + if (!this.user) return 'Inconnu'; + if (!this.user.enabled) return 'Désactivé'; + if (!this.user.emailVerified) return 'Email non vérifié'; + return 'Actif'; + } + + formatTimestamp(timestamp: number): string { + return new Date(timestamp).toLocaleDateString('fr-FR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } + + getUserInitials(): string { + if (!this.user) return 'U'; + return (this.user.firstName?.charAt(0) || '') + (this.user.lastName?.charAt(0) || '') || 'U'; + } + + getUserDisplayName(): string { + if (!this.user) return 'Utilisateur'; + if (this.user.firstName && this.user.lastName) { + return `${this.user.firstName} ${this.user.lastName}`; + } + return this.user.username; + } + + getRoleBadgeClass(role: string): string { + switch (role) { + case 'admin': return 'bg-danger'; + case 'merchant': return 'bg-success'; + case 'support': return 'bg-info'; + case 'user': return 'bg-secondary'; + default: return 'bg-secondary'; + } + } +} \ No newline at end of file diff --git a/src/app/modules/profile/services/profile.service.ts b/src/app/modules/profile/services/profile.service.ts deleted file mode 100644 index bc16f60..0000000 --- a/src/app/modules/profile/services/profile.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class ProfileService { - constructor() {} -} \ No newline at end of file diff --git a/src/app/modules/settings/settings.routes.ts b/src/app/modules/settings/settings.routes.ts new file mode 100644 index 0000000..da99686 --- /dev/null +++ b/src/app/modules/settings/settings.routes.ts @@ -0,0 +1,15 @@ +import { Routes } from '@angular/router'; +import { Settings } from './settings'; +import { authGuard } from '../../core/guards/auth.guard'; +import { roleGuard } from '../../core/guards/role.guard'; + +export const SETTINGS_ROUTES: Routes = [ + { + path: 'settings', + canActivate: [authGuard, roleGuard], + component: Settings, + data: { + title: 'Configuration', + } + } +]; \ No newline at end of file diff --git a/src/app/modules/transactions/details/details.html b/src/app/modules/transactions/details/details.html index 2a7f6bd..a3a21a7 100644 --- a/src/app/modules/transactions/details/details.html +++ b/src/app/modules/transactions/details/details.html @@ -1 +1,351 @@ -

    Transactions - Details

    \ No newline at end of file +
    + + @if (loading && !transaction) { +
    +
    + Chargement... +
    +

    Chargement des détails de la transaction...

    +
    + } + + + @if (error) { +
    + +
    {{ error }}
    + +
    + } + + @if (success) { +
    + +
    {{ success }}
    + +
    + } + + @if (transaction && !loading) { +
    + +
    + +
    +
    +
    +
    Transaction #{{ transaction.id }}
    + + + {{ transaction.status }} + +
    +
    + + + +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    Montant
    +
    + {{ formatCurrency(transaction.amount, transaction.currency) }} +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    Date de transaction
    +
    {{ formatDate(transaction.transactionDate) }}
    + {{ formatRelativeTime(transaction.transactionDate) }} +
    +
    +
    +
    + + +
    +
    +
    Informations de la transaction
    +
    + +
    + +
    + + {{ transaction.msisdn }} + +
    +
    + +
    + +
    + + {{ transaction.operator }} + {{ transaction.country }} +
    +
    + +
    + +
    + +
    +
    {{ transaction.productName }}
    + ID: {{ transaction.productId }} +
    +
    +
    + +
    + +
    + {{ transaction.productCategory }} +
    +
    + + @if (transaction.merchantName) { +
    + +
    + + {{ transaction.merchantName }} +
    +
    + } + + @if (transaction.externalId) { +
    + +
    + {{ transaction.externalId }} + +
    +
    + } +
    + + +
    +
    +
    Informations techniques
    +
    + +
    + +
    {{ formatDate(transaction.createdAt) }}
    +
    + +
    + +
    {{ formatDate(transaction.updatedAt) }}
    +
    + + @if (transaction.userAgent) { +
    + +
    {{ transaction.userAgent }}
    +
    + } + + @if (transaction.ipAddress) { +
    + +
    {{ transaction.ipAddress }}
    +
    + } +
    + + + @if (showErrorDetails()) { +
    +
    +
    + + Détails de l'erreur +
    +
    + + @if (transaction.errorCode) { +
    + +
    {{ transaction.errorCode }}
    +
    + } + + @if (transaction.errorMessage) { +
    + +
    {{ transaction.errorMessage }}
    +
    + } +
    + } +
    +
    +
    + + +
    + +
    +
    +
    Actions
    +
    +
    +
    + + @if (canRefund()) { + + } + + + @if (canRetry()) { + + } + + + @if (canCancel()) { + + } + + + +
    +
    +
    + + +
    +
    +
    Métadonnées
    +
    +
    +
    +
    + ID Transaction: + {{ transaction.id }} +
    + +
    + Opérateur ID: + {{ transaction.operatorId }} +
    + + @if (transaction.merchantId) { +
    + Marchand ID: + {{ transaction.merchantId }} +
    + } + +
    + Devise: + {{ transaction.currency }} +
    + +
    + Statut: + + {{ transaction.status }} + +
    +
    +
    +
    + + + @if (getCustomDataKeys().length > 0) { +
    +
    +
    Données personnalisées
    +
    +
    +
    + @for (key of getCustomDataKeys(); track key) { +
    + {{ key }}: + {{ transaction.customData![key] }} +
    + } +
    +
    +
    + } +
    +
    + } + + + @if (!transaction && !loading) { +
    + +
    Transaction non trouvée
    +

    La transaction avec l'ID "{{ transactionId }}" n'existe pas ou a été supprimée.

    + +
    + } +
    \ No newline at end of file diff --git a/src/app/modules/transactions/details/details.ts b/src/app/modules/transactions/details/details.ts index 9a6cbcc..b50a54c 100644 --- a/src/app/modules/transactions/details/details.ts +++ b/src/app/modules/transactions/details/details.ts @@ -1,7 +1,258 @@ -import { Component } from '@angular/core'; +import { Component, inject, OnInit, Input, ChangeDetectorRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { NgIcon, provideNgIconsConfig } from '@ng-icons/core'; +import { + lucideArrowLeft, + lucideCopy, + lucideRefreshCw, + lucideDownload, + lucidePrinter, + lucideCheckCircle, + lucideClock, + lucideXCircle, + lucideUndo2, + lucideBan, + lucideCalendar, + lucideSmartphone, + lucideEuro, + lucidePackage, + lucideUser, + lucideGlobe, + lucideAlertCircle, + lucideInfo +} from '@ng-icons/lucide'; +import { NgbAlertModule, NgbTooltipModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { TransactionsService } from '../services/transactions.service'; +import { Transaction, TransactionStatus, RefundRequest } from '../models/transaction'; @Component({ - selector: 'app-details', - templateUrl: './details.html', + selector: 'app-transaction-details', + standalone: true, + imports: [ + CommonModule, + FormsModule, + RouterModule, + NgIcon, + NgbAlertModule, + NgbTooltipModule + ], + providers: [ + provideNgIconsConfig({ + size: '1.25em' + }) + ], + templateUrl: './details.html' }) -export class TransactionsDetails {} \ No newline at end of file +export class TransactionDetails implements OnInit { + private transactionsService = inject(TransactionsService); + private modalService = inject(NgbModal); + private cdRef = inject(ChangeDetectorRef); + + @Input() transactionId!: string; + + // Données + transaction: Transaction | null = null; + loading = false; + error = ''; + success = ''; + + // Actions + refunding = false; + retrying = false; + + ngOnInit() { + if (this.transactionId) { + this.loadTransactionDetails(); + } + } + + loadTransactionDetails() { + this.loading = true; + this.error = ''; + + this.transactionsService.getTransactionById(this.transactionId).subscribe({ + next: (transaction) => { + this.transaction = transaction; + this.loading = false; + this.cdRef.detectChanges(); + }, + error: (error) => { + this.error = 'Erreur lors du chargement des détails de la transaction'; + this.loading = false; + + // Données mockées pour le développement + const mockTransactions = this.transactionsService.getMockTransactions(); + this.transaction = mockTransactions.find(tx => tx.id === this.transactionId) || mockTransactions[0]; + this.loading = false; + + this.cdRef.detectChanges(); + console.error('Error loading transaction details:', error); + } + }); + } + + // Actions sur la transaction + refundTransaction() { + if (!this.transaction) return; + + this.refunding = true; + const refundRequest: RefundRequest = { + transactionId: this.transaction.id, + reason: 'Remboursement manuel par l\'administrateur' + }; + + this.transactionsService.refundTransaction(refundRequest).subscribe({ + next: (response) => { + this.transaction = response.transaction; + this.refunding = false; + this.success = 'Transaction remboursée avec succès'; + this.cdRef.detectChanges(); + }, + error: (error) => { + this.refunding = false; + this.error = 'Erreur lors du remboursement de la transaction'; + this.cdRef.detectChanges(); + console.error('Error refunding transaction:', error); + } + }); + } + + retryTransaction() { + if (!this.transaction) return; + + this.retrying = true; + this.transactionsService.retryTransaction(this.transaction.id).subscribe({ + next: (response) => { + this.transaction = response.transaction; + this.retrying = false; + this.success = 'Nouvelle tentative lancée avec succès'; + this.cdRef.detectChanges(); + }, + error: (error) => { + this.retrying = false; + this.error = 'Erreur lors de la nouvelle tentative'; + this.cdRef.detectChanges(); + console.error('Error retrying transaction:', error); + } + }); + } + + cancelTransaction() { + if (!this.transaction) return; + + this.transactionsService.cancelTransaction(this.transaction.id).subscribe({ + next: () => { + this.success = 'Transaction annulée avec succès'; + this.loadTransactionDetails(); // Recharger les données + this.cdRef.detectChanges(); + }, + error: (error) => { + this.error = 'Erreur lors de l\'annulation de la transaction'; + this.cdRef.detectChanges(); + console.error('Error cancelling transaction:', error); + } + }); + } + + // Utilitaires + copyToClipboard(text: string) { + navigator.clipboard.writeText(text).then(() => { + this.success = 'Copié dans le presse-papier'; + setTimeout(() => this.success = '', 3000); + this.cdRef.detectChanges(); + }); + } + + printDetails() { + window.print(); + } + + // Méthode pour obtenir les clés des données personnalisées + getCustomDataKeys(): string[] { + if (!this.transaction?.customData) { + return []; + } + return Object.keys(this.transaction.customData); + } + + // Getters pour l'affichage + getStatusBadgeClass(status: TransactionStatus): 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'; + case 'CANCELLED': return 'badge bg-secondary'; + case 'EXPIRED': return 'badge bg-dark'; + default: return 'badge bg-secondary'; + } + } + + getStatusIcon(status: TransactionStatus): string { + switch (status) { + case 'SUCCESS': return 'lucideCheckCircle'; + case 'PENDING': return 'lucideClock'; + case 'FAILED': return 'lucideXCircle'; + case 'REFUNDED': return 'lucideUndo2'; + case 'CANCELLED': return 'lucideBan'; + default: return 'lucideClock'; + } + } + + getAmountColor(amount: number): string { + if (amount >= 10) return 'text-danger'; + if (amount >= 5) return 'text-warning'; + return 'text-success'; + } + + formatCurrency(amount: number, currency: string = 'EUR'): string { + return new Intl.NumberFormat('fr-FR', { + style: 'currency', + currency: currency + }).format(amount); + } + + formatDate(date: Date): string { + return new Intl.DateTimeFormat('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }).format(new Date(date)); + } + + formatRelativeTime(date: Date): string { + const now = new Date(); + const diffMs = now.getTime() - new Date(date).getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMins < 1) return 'À l\'instant'; + if (diffMins < 60) return `Il y a ${diffMins} min`; + if (diffHours < 24) return `Il y a ${diffHours} h`; + if (diffDays < 7) return `Il y a ${diffDays} j`; + return this.formatDate(date); + } + + canRefund(): boolean { + return this.transaction?.status === 'SUCCESS'; + } + + canRetry(): boolean { + return this.transaction?.status === 'FAILED'; + } + + canCancel(): boolean { + return this.transaction?.status === 'PENDING'; + } + + showErrorDetails(): boolean { + return !!this.transaction?.errorCode || !!this.transaction?.errorMessage; + } +} \ No newline at end of file diff --git a/src/app/modules/transactions/export/export.html b/src/app/modules/transactions/export/export.html deleted file mode 100644 index 31de04b..0000000 --- a/src/app/modules/transactions/export/export.html +++ /dev/null @@ -1 +0,0 @@ -

    Transactions - Export

    \ No newline at end of file diff --git a/src/app/modules/transactions/export/export.spec.ts b/src/app/modules/transactions/export/export.spec.ts deleted file mode 100644 index 4b9ea2b..0000000 --- a/src/app/modules/transactions/export/export.spec.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { TransactionsExport } from './export'; -describe('TransactionsExport', () => {}); \ No newline at end of file diff --git a/src/app/modules/transactions/export/export.ts b/src/app/modules/transactions/export/export.ts deleted file mode 100644 index e167be1..0000000 --- a/src/app/modules/transactions/export/export.ts +++ /dev/null @@ -1,40 +0,0 @@ -// src/app/modules/transactions/components/export/transaction-export.ts -import { Component } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { UiCard } from '@app/components/ui-card'; -import { InputFields } from '@/app/modules/components/input-fields'; -import { CheckboxesAndRadios } from '@/app/modules/components/checkboxes-and-radios'; -import { Flatpickr } from '@/app/modules/components/flatpickr'; - -@Component({ - selector: 'app-export', - standalone: true, - imports: [FormsModule, UiCard, InputFields, CheckboxesAndRadios, Flatpickr], - templateUrl: './export.html', -}) -export class TransactionsExport { - exportConfig = { - format: 'CSV', - includeHeaders: true, - dateRange: true, - startDate: '', - endDate: '', - columns: ['date', 'amount', 'status', 'operator', 'merchant'], - compression: false - }; - - availableColumns = [ - { value: 'date', label: 'Date', selected: true }, - { value: 'amount', label: 'Montant', selected: true }, - { value: 'tax', label: 'Taxe', selected: false }, - { value: 'status', label: 'Statut', selected: true }, - { value: 'operator', label: 'Opérateur', selected: true }, - { value: 'merchant', label: 'Marchand', selected: true }, - { value: 'customer', label: 'Client', selected: false } - ]; - - exportData() { - console.log('Export configuration:', this.exportConfig); - // Logique d'export - } -} \ No newline at end of file diff --git a/src/app/modules/transactions/filters/filters.html b/src/app/modules/transactions/filters/filters.html deleted file mode 100644 index cf7545e..0000000 --- a/src/app/modules/transactions/filters/filters.html +++ /dev/null @@ -1,62 +0,0 @@ - -
    - - -
    - - -
    - -
    - - -
    - - -
    - - -
    - - -
    - - -
    - - -
    - - -
    - -
    - - -
    - - -
    -
    - - -
    -
    -
    -
    \ No newline at end of file diff --git a/src/app/modules/transactions/filters/filters.spec.ts b/src/app/modules/transactions/filters/filters.spec.ts deleted file mode 100644 index 460e11f..0000000 --- a/src/app/modules/transactions/filters/filters.spec.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { TransactionsFilters } from './filters'; -describe('TransactionsFilters', () => {}); \ No newline at end of file diff --git a/src/app/modules/transactions/filters/filters.ts b/src/app/modules/transactions/filters/filters.ts deleted file mode 100644 index c75cc4e..0000000 --- a/src/app/modules/transactions/filters/filters.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component, EventEmitter, Output } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { UiCard } from '@app/components/ui-card'; -//import { Flatpickr } from '@/app/modules/components/flatpickr'; -import { Choicesjs } from '@/app/modules/components/choicesjs'; -import { InputFields } from '@/app/modules/components/input-fields'; -import { CheckboxesAndRadios } from '@/app/modules/components/checkboxes-and-radios'; - -@Component({ - selector: 'app-filters', - imports: [FormsModule, UiCard, Choicesjs, InputFields, CheckboxesAndRadios], - templateUrl: './filters.html', -}) -export class TransactionsFilters { - @Output() filtersApplied = new EventEmitter(); - - statusOptions = ['SUCCESS', 'FAILED', 'PENDING']; - operatorOptions = ['ORANGE', 'MTN', 'AIRTEL', 'VODACOM', 'MOOV']; - - filters = { - startDate: '', - endDate: '', - status: [] as string[], - operators: [] as string[], - minAmount: null as number | null, - maxAmount: null as number | null, - includeTax: true - }; - - applyFilters() { - this.filtersApplied.emit(this.filters); - } - - resetFilters() { - this.filters = { - startDate: '', - endDate: '', - status: [], - operators: [], - minAmount: null, - maxAmount: null, - includeTax: true - }; - this.filtersApplied.emit(this.filters); - } -} \ No newline at end of file diff --git a/src/app/modules/transactions/list/list.html b/src/app/modules/transactions/list/list.html index 2cd75da..acb2b2e 100644 --- a/src/app/modules/transactions/list/list.html +++ b/src/app/modules/transactions/list/list.html @@ -1 +1,298 @@ -

    Transactions - List

    \ No newline at end of file +
    + +
    +
    +
    +
    +

    Gestion des Transactions

    + +
    + +
    + +
    + +
    + + + +
    +
    + + + +
    +
    +
    +
    + + + @if (paginatedData?.stats) { +
    +
    +
    +
    +
    +
    + Total +
    {{ getTotal() }}
    +
    +
    + Succès +
    {{ getSuccessCount() }}
    +
    +
    + Échecs +
    {{ getFailedCount() }}
    +
    +
    + En attente +
    {{ getPendingCount() }}
    +
    +
    + Taux de succès +
    {{ getSuccessRate() }}%
    +
    +
    + Montant total +
    {{ formatCurrency(getTotalAmount()) }}
    +
    +
    +
    +
    +
    +
    + } + + +
    +
    +
    + + + + +
    +
    + +
    +
    + + + + + + + + + +
    +
    +
    + + + @if (error) { +
    + + {{ error }} +
    + } + + + @if (loading) { +
    +
    + Chargement... +
    +

    Chargement des transactions...

    +
    + } + + + @if (!loading) { +
    +
    +
    + + + + + + + + + + + + + + + + @for (transaction of transactions; track transaction.id) { + + + + + + + + + + + + } + @empty { + + + + } + +
    + + +
    + ID + +
    +
    +
    + MSISDN + +
    +
    Opérateur +
    + Montant + +
    +
    ProduitStatut +
    + Date + +
    +
    Actions
    + + {{ transaction.id }}{{ transaction.msisdn }} + {{ transaction.operator }} + + + {{ formatCurrency(transaction.amount, transaction.currency) }} + + +
    + {{ transaction.productName }} +
    +
    + + + {{ transaction.status }} + + + {{ formatDate(transaction.transactionDate) }} + +
    + + + @if (transaction.status === 'SUCCESS') { + + } + + @if (transaction.status === 'FAILED') { + + } +
    +
    + +

    Aucune transaction trouvée

    + +
    +
    +
    +
    + + + @if (paginatedData && paginatedData.totalPages > 1) { +
    +
    + Affichage de {{ (filters.page! - 1) * filters.limit! + 1 }} à + {{ (filters.page! * filters.limit!) > (paginatedData?.total || 0) ? (paginatedData?.total || 0) : (filters.page! * filters.limit!) }} + sur {{ paginatedData?.total || 0 }} transactions +
    + +
    + } + } +
    \ No newline at end of file diff --git a/src/app/modules/transactions/list/list.ts b/src/app/modules/transactions/list/list.ts index da25db4..b51c977 100644 --- a/src/app/modules/transactions/list/list.ts +++ b/src/app/modules/transactions/list/list.ts @@ -1,7 +1,355 @@ -import { Component } from '@angular/core'; +import { Component, inject, OnInit, ChangeDetectorRef, Output, EventEmitter } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgIcon, provideNgIconsConfig } from '@ng-icons/core'; +import { + lucideSearch, + lucideFilter, + lucideX, + lucideDownload, + lucideEye, + lucideRefreshCw, + lucideArrowUpDown, + lucideArrowUp, + lucideArrowDown, + lucideCheckCircle, + lucideClock, + lucideXCircle, + lucideUndo2, + lucideBan +} from '@ng-icons/lucide'; +import { NgbPaginationModule, NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; + +import { TransactionsService } from '../services/transactions.service'; +import { Transaction, TransactionQuery, TransactionStatus, PaginatedTransactions } from '../models/transaction'; +import { environment } from '@environments/environment'; @Component({ - selector: 'app-list', - templateUrl: './list.html', + selector: 'app-transactions-list', + standalone: true, + imports: [ + CommonModule, + FormsModule, + NgIcon, + NgbPaginationModule, + NgbDropdownModule, + NgbTooltipModule + ], + providers: [ + provideNgIconsConfig({ + size: '1.25em' + }) + ], + templateUrl: './list.html' }) -export class TransactionsList {} \ No newline at end of file +export class TransactionsList implements OnInit { + private transactionsService = inject(TransactionsService); + private cdRef = inject(ChangeDetectorRef); + + @Output() transactionSelected = new EventEmitter(); + @Output() openRefundModal = new EventEmitter(); + + // Données + transactions: Transaction[] = []; + paginatedData: PaginatedTransactions | null = null; + + // États + loading = false; + error = ''; + + // Filtres et recherche + searchTerm = ''; + filters: TransactionQuery = { + page: 1, + limit: 20, + status: undefined, + operator: '', + country: '', + startDate: undefined, + endDate: undefined, + msisdn: '', + sortBy: 'transactionDate', + sortOrder: 'desc' + }; + + // Options de filtre + statusOptions: TransactionStatus[] = ['PENDING', 'SUCCESS', 'FAILED', 'REFUNDED', 'CANCELLED']; + operatorOptions: string[] = ['Orange', 'Free', 'SFR', 'Bouygues']; + countryOptions: string[] = ['FR', 'BE', 'CH', 'LU']; + + // Tri + sortField: string = 'transactionDate'; + sortDirection: 'asc' | 'desc' = 'desc'; + + // Sélection multiple + selectedTransactions: Set = new Set(); + selectAll = false; + + ngOnInit() { + this.loadTransactions(); + } + + loadTransactions() { + this.loading = true; + this.error = ''; + + // Mettre à jour les filtres avec la recherche + if (this.searchTerm) { + this.filters.search = this.searchTerm; + } else { + delete this.filters.search; + } + + // Appliquer le tri + this.filters.sortBy = this.sortField; + this.filters.sortOrder = this.sortDirection; + + this.transactionsService.getTransactions(this.filters).subscribe({ + next: (data) => { + this.paginatedData = data; + this.transactions = data.data; + this.loading = false; + this.cdRef.detectChanges(); + }, + error: (error) => { + this.error = 'Erreur lors du chargement des transactions'; + this.loading = false; + + // Fallback sur les données mockées en développement + if (environment.production === false) { + this.transactions = this.transactionsService.getMockTransactions(); + this.paginatedData = { + data: this.transactions, + total: this.transactions.length, + page: 1, + limit: 20, + totalPages: 1, + stats: { + total: this.transactions.length, + totalAmount: this.transactions.reduce((sum, tx) => sum + tx.amount, 0), + successCount: this.transactions.filter(tx => tx.status === 'SUCCESS').length, + failedCount: this.transactions.filter(tx => tx.status === 'FAILED').length, + pendingCount: this.transactions.filter(tx => tx.status === 'PENDING').length, + refundedCount: this.transactions.filter(tx => tx.status === 'REFUNDED').length, + successRate: 75, + averageAmount: 4.74 + } + }; + this.loading = false; + } + + this.cdRef.detectChanges(); + console.error('Error loading transactions:', error); + } + }); + } + + // Recherche et filtres + onSearch() { + this.filters.page = 1; + this.loadTransactions(); + } + + onClearFilters() { + this.searchTerm = ''; + this.filters = { + page: 1, + limit: 20, + status: undefined, + operator: '', + country: '', + startDate: undefined, + endDate: undefined, + msisdn: '', + sortBy: 'transactionDate', + sortOrder: 'desc' + }; + this.loadTransactions(); + } + + onStatusFilterChange(status: TransactionStatus | 'all') { + this.filters.status = status === 'all' ? undefined : status; + this.filters.page = 1; + this.loadTransactions(); + } + + onOperatorFilterChange(operator: string) { + this.filters.operator = operator; + this.filters.page = 1; + this.loadTransactions(); + } + + onDateRangeChange(start: Date | null, end: Date | null) { + this.filters.startDate = start || undefined; + this.filters.endDate = end || undefined; + this.filters.page = 1; + this.loadTransactions(); + } + + // Tri + sort(field: string) { + if (this.sortField === field) { + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + this.sortField = field; + this.sortDirection = 'desc'; + } + this.loadTransactions(); + } + + getSortIcon(field: string): string { + if (this.sortField !== field) return 'lucideArrowUpDown'; + return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown'; + } + + // Pagination + onPageChange(page: number) { + this.filters.page = page; + this.loadTransactions(); + } + + // Actions + viewTransactionDetails(transactionId: string) { + this.transactionSelected.emit(transactionId); + } + + refundTransaction(transactionId: string) { + this.openRefundModal.emit(transactionId); + } + + retryTransaction(transactionId: string) { + this.transactionsService.retryTransaction(transactionId).subscribe({ + next: () => { + this.loadTransactions(); + }, + error: (error) => { + console.error('Error retrying transaction:', error); + this.error = 'Erreur lors de la nouvelle tentative'; + this.cdRef.detectChanges(); + } + }); + } + + // Sélection multiple + toggleTransactionSelection(transactionId: string) { + if (this.selectedTransactions.has(transactionId)) { + this.selectedTransactions.delete(transactionId); + } else { + this.selectedTransactions.add(transactionId); + } + this.updateSelectAllState(); + } + + toggleSelectAll() { + if (this.selectAll) { + this.transactions.forEach(tx => this.selectedTransactions.add(tx.id)); + } else { + this.selectedTransactions.clear(); + } + } + + updateSelectAllState() { + this.selectAll = this.transactions.length > 0 && + this.selectedTransactions.size === this.transactions.length; + } + + // Export + exportTransactions(format: 'csv' | 'excel' | 'pdf') { + const exportRequest = { + format: format, + query: this.filters, + columns: ['id', 'msisdn', 'operator', 'amount', 'status', 'transactionDate', 'productName'] + }; + + this.transactionsService.exportTransactions(exportRequest).subscribe({ + next: (response) => { + // Télécharger le fichier + const link = document.createElement('a'); + link.href = response.url; + link.download = response.filename; + link.click(); + }, + error: (error) => { + console.error('Error exporting transactions:', error); + this.error = 'Erreur lors de l\'export'; + this.cdRef.detectChanges(); + } + }); + } + + // Utilitaires d'affichage + getStatusBadgeClass(status: TransactionStatus): 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'; + case 'CANCELLED': return 'badge bg-secondary'; + case 'EXPIRED': return 'badge bg-dark'; + default: return 'badge bg-secondary'; + } + } + + getStatusIcon(status: TransactionStatus): string { + switch (status) { + case 'SUCCESS': return 'lucideCheckCircle'; + case 'PENDING': return 'lucideClock'; + case 'FAILED': return 'lucideXCircle'; + case 'REFUNDED': return 'lucideUndo2'; + case 'CANCELLED': return 'lucideBan'; + default: return 'lucideClock'; + } + } + + formatCurrency(amount: number, currency: string = 'EUR'): string { + return new Intl.NumberFormat('fr-FR', { + style: 'currency', + currency: currency + }).format(amount); + } + + formatDate(date: Date): string { + return new Intl.DateTimeFormat('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }).format(new Date(date)); + } + + getAmountColor(amount: number): string { + if (amount >= 10) return 'text-danger fw-bold'; + if (amount >= 5) return 'text-warning fw-semibold'; + return 'text-success'; + } + + // Méthodes pour sécuriser l'accès aux stats + getTotal(): number { + return this.paginatedData?.stats?.total || 0; + } + + getSuccessCount(): number { + return this.paginatedData?.stats?.successCount || 0; + } + + getFailedCount(): number { + return this.paginatedData?.stats?.failedCount || 0; + } + + getPendingCount(): number { + return this.paginatedData?.stats?.pendingCount || 0; + } + + getSuccessRate(): number { + return this.paginatedData?.stats?.successRate || 0; + } + + getTotalAmount(): number { + return this.paginatedData?.stats?.totalAmount || 0; + } + + getMinValue(a: number, b: number): number { + return Math.min(a, b); + } +} \ No newline at end of file diff --git a/src/app/modules/transactions/models/transaction.ts b/src/app/modules/transactions/models/transaction.ts new file mode 100644 index 0000000..ae12b9a --- /dev/null +++ b/src/app/modules/transactions/models/transaction.ts @@ -0,0 +1,80 @@ +export interface Transaction { + id: string; + msisdn: string; + operator: string; + operatorId: string; + country: string; + amount: number; + currency: string; + status: TransactionStatus; + productId: string; + productName: string; + productCategory: string; + transactionDate: Date; + createdAt: Date; + updatedAt: Date; + externalId?: string; + merchantId?: string; + merchantName?: string; + errorCode?: string; + errorMessage?: string; + userAgent?: string; + ipAddress?: string; + customData?: { [key: string]: any }; +} + +export interface TransactionQuery { + page?: number; + limit?: number; + search?: string; + status?: TransactionStatus; + operator?: string; + country?: string; + startDate?: Date; + endDate?: Date; + msisdn?: string; + productId?: string; + merchantId?: string; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +} + +export interface TransactionStats { + total: number; + totalAmount: number; + successCount: number; + failedCount: number; + pendingCount: number; + refundedCount: number; + successRate: number; + averageAmount: number; +} + +export interface PaginatedTransactions { + data: Transaction[]; + total: number; + page: number; + limit: number; + totalPages: number; + stats: TransactionStats; +} + +export type TransactionStatus = + | 'PENDING' + | 'SUCCESS' + | 'FAILED' + | 'REFUNDED' + | 'CANCELLED' + | 'EXPIRED'; + +export interface RefundRequest { + transactionId: string; + reason?: string; + amount?: number; +} + +export interface TransactionExportRequest { + format: 'csv' | 'excel' | 'pdf'; + query: TransactionQuery; + columns?: string[]; +} \ No newline at end of file diff --git a/src/app/modules/transactions/services/details.service.ts b/src/app/modules/transactions/services/details.service.ts deleted file mode 100644 index 49b4e1d..0000000 --- a/src/app/modules/transactions/services/details.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class TransactionsDetailsService { - constructor() {} -} \ No newline at end of file diff --git a/src/app/modules/transactions/services/export.service.ts b/src/app/modules/transactions/services/export.service.ts deleted file mode 100644 index 261511b..0000000 --- a/src/app/modules/transactions/services/export.service.ts +++ /dev/null @@ -1,27 +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 ExportConfig { - format: 'CSV' | 'EXCEL' | 'PDF'; - includeTax: boolean; - dateRange: boolean; - columns: string[]; - filters: any; -} - -@Injectable({ providedIn: 'root' }) -export class TransactionsExportService { - private http = inject(HttpClient); - - exportTransactions(config: ExportConfig): Observable { - return this.http.post(`${environment.apiUrl}/transactions/export`, config, { - responseType: 'blob' - }); - } - - getExportTemplates(): Observable { - return this.http.get(`${environment.apiUrl}/transactions/export/templates`); - } -} \ No newline at end of file diff --git a/src/app/modules/transactions/services/filters.service.ts b/src/app/modules/transactions/services/filters.service.ts deleted file mode 100644 index e2c381f..0000000 --- a/src/app/modules/transactions/services/filters.service.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Component, EventEmitter, Output } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { Flatpickr } from '@/app/modules/components/flatpickr'; -import { Choicesjs } from '@/app/modules/components/choicesjs'; -import { InputFields } from '@/app/modules/components/input-fields'; - -@Component({ - selector: 'app-filters', - imports: [FormsModule, Flatpickr, Choicesjs, InputFields], - template: ` -
    -
    -
    Filtres Avancés des Transactions
    - -
    -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    -
    - -
    - - -
    -
    -
    - ` -}) -export class TransactionsFilters { - @Output() filtersChange = new EventEmitter(); - - applyFilters() { - // Logique d'application des filtres - } - - resetFilters() { - // Logique de réinitialisation - } -} \ No newline at end of file diff --git a/src/app/modules/transactions/services/list.service.ts b/src/app/modules/transactions/services/list.service.ts deleted file mode 100644 index 1856289..0000000 --- a/src/app/modules/transactions/services/list.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class TransactionsListService { - constructor() {} -} \ No newline at end of file diff --git a/src/app/modules/transactions/services/transactions.service.ts b/src/app/modules/transactions/services/transactions.service.ts index 0c61a1d..be6974c 100644 --- a/src/app/modules/transactions/services/transactions.service.ts +++ b/src/app/modules/transactions/services/transactions.service.ts @@ -1,46 +1,177 @@ import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { environment } from '@environments/environment'; -import { Observable } from 'rxjs'; +import { Observable, map, catchError, throwError } from 'rxjs'; -export interface Transaction { - id: string; - date: Date; - amount: number; - tax: number; - status: 'SUCCESS' | 'FAILED' | 'PENDING'; - merchantId: string; - operator: string; - customerAlias: string; - subscriptionId?: string; -} - -export interface TransactionFilter { - startDate?: Date; - endDate?: Date; - status?: string; - merchantId?: string; - operator?: string; - minAmount?: number; - maxAmount?: number; -} +import { + Transaction, + TransactionQuery, + PaginatedTransactions, + TransactionStats, + RefundRequest +} from '../models/transaction'; @Injectable({ providedIn: 'root' }) -export class TransactionService { +export class TransactionsService { private http = inject(HttpClient); + private apiUrl = `${environment.apiUrl}/transactions`; - getTransactions(filters?: TransactionFilter): Observable { - return this.http.post( - `${environment.apiUrl}/transactions/list`, - filters + // === CRUD OPERATIONS === + getTransactions(query: TransactionQuery): Observable { + let params = new HttpParams(); + + // Ajouter tous les paramètres de query + Object.keys(query).forEach(key => { + const value = query[key as keyof TransactionQuery]; + if (value !== undefined && value !== null) { + if (value instanceof Date) { + params = params.set(key, value.toISOString()); + } else { + params = params.set(key, value.toString()); + } + } + }); + + return this.http.get(`${this.apiUrl}`, { params }).pipe( + catchError(error => { + console.error('Error loading transactions:', error); + return throwError(() => error); + }) ); } - getTransactionDetails(id: string): Observable { - return this.http.get(`${environment.apiUrl}/transactions/${id}`); + getTransactionById(id: string): Observable { + return this.http.get(`${this.apiUrl}/${id}`).pipe( + catchError(error => { + console.error('Error loading transaction:', error); + return throwError(() => error); + }) + ); } - getTransactionStats(): Observable { - return this.http.get(`${environment.apiUrl}/transactions/stats`); + // === ACTIONS === + refundTransaction(refundRequest: RefundRequest): Observable<{ message: string; transaction: Transaction }> { + return this.http.post<{ message: string; transaction: Transaction }>( + `${this.apiUrl}/${refundRequest.transactionId}/refund`, + refundRequest + ); + } + + cancelTransaction(transactionId: string): Observable<{ message: string }> { + return this.http.post<{ message: string }>( + `${this.apiUrl}/${transactionId}/cancel`, + {} + ); + } + + retryTransaction(transactionId: string): Observable<{ message: string; transaction: Transaction }> { + return this.http.post<{ message: string; transaction: Transaction }>( + `${this.apiUrl}/${transactionId}/retry`, + {} + ); + } + + // === STATISTIQUES === + getTransactionStats(query?: Partial): Observable { + let params = new HttpParams(); + + if (query) { + Object.keys(query).forEach(key => { + const value = query[key as keyof TransactionQuery]; + if (value !== undefined && value !== null) { + if (value instanceof Date) { + params = params.set(key, value.toISOString()); + } else { + params = params.set(key, value.toString()); + } + } + }); + } + + return this.http.get(`${this.apiUrl}/stats`, { params }); + } + + // === EXPORT === + exportTransactions(exportRequest: any): Observable<{ url: string; filename: string }> { + return this.http.post<{ url: string; filename: string }>( + `${this.apiUrl}/export`, + exportRequest + ); + } + + // === MOCK DATA POUR LE DÉVELOPPEMENT === + getMockTransactions(): Transaction[] { + return [ + { + id: 'tx_001', + msisdn: '+33612345678', + operator: 'Orange', + operatorId: 'orange_fr', + country: 'FR', + amount: 4.99, + currency: 'EUR', + status: 'SUCCESS', + productId: 'prod_premium', + productName: 'Contenu Premium', + productCategory: 'ENTERTAINMENT', + transactionDate: new Date('2024-01-15T14:30:00'), + createdAt: new Date('2024-01-15T14:30:00'), + updatedAt: new Date('2024-01-15T14:30:00'), + externalId: 'ext_123456', + merchantName: 'MediaCorp' + }, + { + id: 'tx_002', + msisdn: '+33798765432', + operator: 'Free', + operatorId: 'free_fr', + country: 'FR', + amount: 2.99, + currency: 'EUR', + status: 'PENDING', + productId: 'prod_basic', + productName: 'Abonnement Basique', + productCategory: 'SUBSCRIPTION', + transactionDate: new Date('2024-01-15T14:25:00'), + createdAt: new Date('2024-01-15T14:25:00'), + updatedAt: new Date('2024-01-15T14:25:00'), + externalId: 'ext_123457' + }, + { + id: 'tx_003', + msisdn: '+33687654321', + operator: 'SFR', + operatorId: 'sfr_fr', + country: 'FR', + amount: 9.99, + currency: 'EUR', + status: 'FAILED', + productId: 'prod_pro', + productName: 'Pack Professionnel', + productCategory: 'BUSINESS', + transactionDate: new Date('2024-01-15T14:20:00'), + createdAt: new Date('2024-01-15T14:20:00'), + updatedAt: new Date('2024-01-15T14:20:00'), + errorCode: 'INSUFFICIENT_FUNDS', + errorMessage: 'Solde insuffisant' + }, + { + id: 'tx_004', + msisdn: '+33611223344', + operator: 'Bouygues', + operatorId: 'bouygues_fr', + country: 'FR', + amount: 1.99, + currency: 'EUR', + status: 'REFUNDED', + productId: 'prod_mini', + productName: 'Pack Découverte', + productCategory: 'GAMING', + transactionDate: new Date('2024-01-15T14:15:00'), + createdAt: new Date('2024-01-15T14:15:00'), + updatedAt: new Date('2024-01-15T16:30:00'), + merchantName: 'GameStudio' + } + ]; } } \ No newline at end of file diff --git a/src/app/modules/transactions/transactions.html b/src/app/modules/transactions/transactions.html index 199342b..7437551 100644 --- a/src/app/modules/transactions/transactions.html +++ b/src/app/modules/transactions/transactions.html @@ -1,49 +1,46 @@ -
    - - + +
    +
    + @if (activeView === 'list') { + + } @else if (activeView === 'details' && selectedTransactionId) { +
    + +
    Détails de la transaction
    +
    + + } +
    +
    +
    - - @switch (activeTab) { - @case ('list') { - - } - @case ('filters') { - - } - @case ('export') { - - } - } - \ No newline at end of file + + + + + + \ No newline at end of file diff --git a/src/app/modules/transactions/transactions.ts b/src/app/modules/transactions/transactions.ts index 590a524..b275365 100644 --- a/src/app/modules/transactions/transactions.ts +++ b/src/app/modules/transactions/transactions.ts @@ -1,24 +1,48 @@ -import { Component } from '@angular/core'; +import { Component, inject, TemplateRef, ViewChild } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; import { PageTitle } from '@app/components/page-title/page-title'; import { TransactionsList } from './list/list'; -import { TransactionsFilters } from './filters/filters'; -import { TransactionsDetails } from './details/details'; -import { TransactionsExport } from './export/export'; +import { TransactionDetails } from './details/details'; +import { NgIcon } from '@ng-icons/core'; @Component({ selector: 'app-transactions', - imports: [PageTitle, TransactionsList, TransactionsFilters, TransactionsDetails, TransactionsExport], + standalone: true, + imports: [ + CommonModule, + NgbModalModule, + PageTitle, + NgIcon, + TransactionsList, + TransactionDetails + ], templateUrl: './transactions.html', }) export class Transactions { - activeTab: string = 'list'; - - setActiveTab(tab: string) { - this.activeTab = tab; + private modalService = inject(NgbModal); + + activeView: 'list' | 'details' = 'list'; + selectedTransactionId: string | null = null; + + showListView() { + this.activeView = 'list'; + this.selectedTransactionId = null; } - onFiltersApplied(filters: any) { - console.log('Filters applied:', filters); - // Appliquer les filtres à la liste + showDetailsView(transactionId: string) { + this.activeView = 'details'; + this.selectedTransactionId = transactionId; } + + // Gestion des modals + openModal(content: TemplateRef, size: 'sm' | 'lg' | 'xl' = 'lg') { + this.modalService.open(content, { + size: size, + centered: true, + scrollable: true + }); + } + + @ViewChild('refundModal') refundModal!: TemplateRef; } \ No newline at end of file diff --git a/src/app/modules/users/audits/audits.html b/src/app/modules/users/audits/audits.html deleted file mode 100644 index 2d29124..0000000 --- a/src/app/modules/users/audits/audits.html +++ /dev/null @@ -1 +0,0 @@ -

    Users - Audits

    \ No newline at end of file diff --git a/src/app/modules/users/audits/audits.spec.ts b/src/app/modules/users/audits/audits.spec.ts deleted file mode 100644 index c575699..0000000 --- a/src/app/modules/users/audits/audits.spec.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { UsersAudits } from './audits'; -describe('UsersAudits', () => {}); \ No newline at end of file diff --git a/src/app/modules/users/audits/audits.ts b/src/app/modules/users/audits/audits.ts deleted file mode 100644 index 8bfbe47..0000000 --- a/src/app/modules/users/audits/audits.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-users-audits', - templateUrl: './audits.html', -}) -export class UsersAudits {} \ No newline at end of file diff --git a/src/app/modules/users/list/list.html b/src/app/modules/users/list/list.html index 8d80789..45a44ae 100644 --- a/src/app/modules/users/list/list.html +++ b/src/app/modules/users/list/list.html @@ -1 +1,243 @@ -

    Users - List

    \ No newline at end of file + + Gérez les accès utilisateurs de votre plateforme + + +
    + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + + + +
    +
    +
    + +
    +
    + +
    +
    +
    + + +
    +
    +
    + + + @if (loading) { +
    +
    + Chargement... +
    +

    Chargement des utilisateurs...

    +
    + } + + + @if (error && !loading) { + + } + + + @if (!loading && !error) { +
    + + + + + + + + + + + + + @for (user of displayedUsers; track user.id) { + + + + + + + + + } + @empty { + + + + } + +
    +
    + Utilisateur + +
    +
    +
    + Email + +
    +
    Rôles +
    + Statut + +
    +
    +
    + Créé le + +
    +
    Actions
    +
    +
    + + {{ getUserInitials(user) }} + +
    +
    + {{ getUserDisplayName(user) }} +
    @{{ user.username }}
    +
    +
    +
    +
    {{ user.email }}
    + @if (!user.emailVerified) { + + + Non vérifié + + } +
    + @for (role of user.clientRoles; track role) { + + {{ role }} + + } + @if (user.clientRoles.length === 0) { + Aucun rôle + } + + + {{ getStatusText(user) }} + + + + {{ formatTimestamp(user.createdTimestamp) }} + + +
    + + + @if (user.enabled) { + + } @else { + + } + +
    +
    + +

    Aucun utilisateur trouvé

    + +
    +
    + + +
    +
    + Affichage de {{ getStartIndex() }} à {{ getEndIndex() }} sur {{ totalItems }} utilisateurs +
    + +
    + } +
    + +
    \ No newline at end of file diff --git a/src/app/modules/users/list/list.ts b/src/app/modules/users/list/list.ts index dfb1ba6..e913f94 100644 --- a/src/app/modules/users/list/list.ts +++ b/src/app/modules/users/list/list.ts @@ -1,7 +1,258 @@ -import { Component } from '@angular/core'; +import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgIcon } from '@ng-icons/core'; +import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; +import { UsersService } from '../services/users.service'; +import { UserResponse } from '../models/user'; +import { UiCard } from '@app/components/ui-card'; @Component({ selector: 'app-users-list', + standalone: true, + imports: [ + CommonModule, + FormsModule, + NgIcon, + UiCard, + NgbPaginationModule + ], templateUrl: './list.html', }) -export class UsersList {} \ No newline at end of file +export class UsersList implements OnInit { + private usersService = inject(UsersService); + private cdRef = inject(ChangeDetectorRef); + + @Output() userSelected = new EventEmitter(); + @Output() openCreateModal = new EventEmitter(); + @Output() openResetPasswordModal = new EventEmitter(); + @Output() openDeleteUserModal = new EventEmitter(); + + + + // Données + allUsers: UserResponse[] = []; + filteredUsers: UserResponse[] = []; + displayedUsers: UserResponse[] = []; + + // États + loading = false; + error = ''; + + // Recherche et filtres + searchTerm = ''; + statusFilter: 'all' | 'enabled' | 'disabled' = 'all'; + emailVerifiedFilter: 'all' | 'verified' | 'not-verified' = 'all'; + + // Pagination + currentPage = 1; + itemsPerPage = 10; + totalItems = 0; + totalPages = 0; + + // Tri + sortField: keyof UserResponse = 'username'; + sortDirection: 'asc' | 'desc' = 'asc'; + + ngOnInit() { + this.loadUsers(); + } + + loadUsers() { + this.loading = true; + this.error = ''; + + this.usersService.findAllUsers().subscribe({ + next: (response) => { + this.allUsers = response.data; + this.applyFiltersAndPagination(); + this.loading = false; + this.cdRef.detectChanges(); + }, + error: (error) => { + this.error = 'Erreur lors du chargement des utilisateurs'; + this.loading = false; + this.cdRef.detectChanges(); + console.error('Error loading users:', error); + } + }); + } + + // Recherche et filtres + onSearch() { + this.currentPage = 1; + this.applyFiltersAndPagination(); + } + + onClearFilters() { + this.searchTerm = ''; + this.statusFilter = 'all'; + this.emailVerifiedFilter = 'all'; + this.currentPage = 1; + this.applyFiltersAndPagination(); + } + + applyFiltersAndPagination() { + // Appliquer les filtres + this.filteredUsers = this.allUsers.filter(user => { + // Filtre de recherche + const matchesSearch = !this.searchTerm || + user.username.toLowerCase().includes(this.searchTerm.toLowerCase()) || + user.email.toLowerCase().includes(this.searchTerm.toLowerCase()) || + user.firstName?.toLowerCase().includes(this.searchTerm.toLowerCase()) || + user.lastName?.toLowerCase().includes(this.searchTerm.toLowerCase()); + + // Filtre par statut + const matchesStatus = this.statusFilter === 'all' || + (this.statusFilter === 'enabled' && user.enabled) || + (this.statusFilter === 'disabled' && !user.enabled); + + // Filtre par email vérifié + const matchesEmailVerified = this.emailVerifiedFilter === 'all' || + (this.emailVerifiedFilter === 'verified' && user.emailVerified) || + (this.emailVerifiedFilter === 'not-verified' && !user.emailVerified); + + return matchesSearch && matchesStatus && matchesEmailVerified; + }); + + // Appliquer le tri + this.filteredUsers.sort((a, b) => { + const aValue = a[this.sortField]; + const bValue = b[this.sortField]; + + if (aValue === bValue) return 0; + + let comparison = 0; + if (typeof aValue === 'string' && typeof bValue === 'string') { + comparison = aValue.localeCompare(bValue); + } else if (typeof aValue === 'number' && typeof bValue === 'number') { + comparison = aValue - bValue; + } else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') { + comparison = (aValue === bValue) ? 0 : aValue ? -1 : 1; + } + + return this.sortDirection === 'asc' ? comparison : -comparison; + }); + + // Calculer la pagination + this.totalItems = this.filteredUsers.length; + this.totalPages = Math.ceil(this.totalItems / this.itemsPerPage); + + // Appliquer la pagination + const startIndex = (this.currentPage - 1) * this.itemsPerPage; + const endIndex = startIndex + this.itemsPerPage; + this.displayedUsers = this.filteredUsers.slice(startIndex, endIndex); + } + + // Tri + sort(field: keyof UserResponse) { + if (this.sortField === field) { + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + this.sortField = field; + this.sortDirection = 'asc'; + } + this.applyFiltersAndPagination(); + } + + getSortIcon(field: keyof UserResponse): string { + if (this.sortField !== field) return 'lucideArrowUpDown'; + return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown'; + } + + // Pagination + onPageChange(page: number) { + this.currentPage = page; + this.applyFiltersAndPagination(); + } + + getStartIndex(): number { + return (this.currentPage - 1) * this.itemsPerPage + 1; + } + + getEndIndex(): number { + return Math.min(this.currentPage * this.itemsPerPage, this.totalItems); + } + + // Actions + viewUserProfile(userId: string) { + this.userSelected.emit(userId); + } + + // Méthode pour réinitialiser le mot de passe + resetPassword(user: UserResponse) { + this.openResetPasswordModal.emit(user.id); + } + + // Méthode pour ouvrir le modal de suppression + deleteUser(user: UserResponse) { + this.openDeleteUserModal.emit(user.id); + } + + + enableUser(user: UserResponse) { + this.usersService.enableUser(user.id).subscribe({ + next: () => { + user.enabled = true; + this.applyFiltersAndPagination(); + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('Error enabling user:', error); + alert('Erreur lors de l\'activation de l\'utilisateur'); + } + }); + } + + disableUser(user: UserResponse) { + this.usersService.disableUser(user.id).subscribe({ + next: () => { + user.enabled = false; + this.applyFiltersAndPagination(); + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('Error disabling user:', error); + alert('Erreur lors de la désactivation de l\'utilisateur'); + } + }); + } + + // Utilitaires d'affichage + getStatusBadgeClass(user: UserResponse): string { + if (!user.enabled) return 'badge bg-danger'; + if (!user.emailVerified) return 'badge bg-warning'; + return 'badge bg-success'; + } + + getStatusText(user: UserResponse): string { + if (!user.enabled) return 'Désactivé'; + if (!user.emailVerified) return 'Email non vérifié'; + return 'Actif'; + } + + getRoleBadgeClass(role: string): string { + switch (role) { + case 'admin': return 'bg-danger'; + case 'merchant': return 'bg-success'; + case 'support': return 'bg-info'; + case 'user': return 'bg-secondary'; + default: return 'bg-secondary'; + } + } + + formatTimestamp(timestamp: number): string { + return new Date(timestamp).toLocaleDateString('fr-FR'); + } + + getUserInitials(user: UserResponse): string { + return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U'; + } + + getUserDisplayName(user: UserResponse): string { + if (user.firstName && user.lastName) { + return `${user.firstName} ${user.lastName}`; + } + return user.username; + } +} \ No newline at end of file diff --git a/src/app/modules/users/models/user.ts b/src/app/modules/users/models/user.ts new file mode 100644 index 0000000..fb4284c --- /dev/null +++ b/src/app/modules/users/models/user.ts @@ -0,0 +1,180 @@ +export class User { + id?: string; + username: string; + email: string; + firstName: string; + lastName: string; + enabled?: boolean; + emailVerified?: boolean; + attributes?: { [key: string]: any }; + createdTimestamp?: number; + + constructor(partial?: Partial) { + // Initialisation des propriétés obligatoires + this.username = ''; + this.email = ''; + this.firstName = ''; + this.lastName = ''; + + if (partial) { + Object.assign(this, partial); + } + + // Valeurs par défaut + this.enabled = this.enabled ?? true; + this.emailVerified = this.emailVerified ?? false; + } +} + +export class UserResponse { + id: string; + username: string; + email: string; + firstName: string; + lastName: string; + enabled: boolean; + emailVerified: boolean; + attributes?: { [key: string]: any }; + clientRoles: string[]; + createdTimestamp: number; + + constructor(user: any) { + // Initialisation avec valeurs par défaut + this.id = user?.id || ''; + this.username = user?.username || ''; + this.email = user?.email || ''; + this.firstName = user?.firstName || ''; + this.lastName = user?.lastName || ''; + this.enabled = user?.enabled ?? true; + this.emailVerified = user?.emailVerified ?? false; + this.attributes = user?.attributes || {}; + this.clientRoles = user?.clientRoles || []; + this.createdTimestamp = user?.createdTimestamp || Date.now(); + } +} + +export class CreateUserDto { + username: string = ''; + email: string = ''; + firstName: string = ''; + lastName: string = ''; + password: string = ''; + enabled: boolean = true; + emailVerified: boolean = false; + clientRoles: string[] = []; + + constructor(partial?: Partial) { + if (partial) { + Object.assign(this, partial); + } + } +} + +export class UpdateUserDto { + username?: string; + email?: string; + firstName?: string; + lastName?: string; + enabled?: boolean; + emailVerified?: boolean; + attributes?: { [key: string]: any }; + + constructor(partial?: Partial) { + if (partial) { + Object.assign(this, partial); + } + } +} + +export class ResetPasswordDto { + userId: string; + newPassword: string; + temporary?: boolean; + + constructor(partial?: Partial) { + // Initialisation des propriétés obligatoires + this.userId = ''; + this.newPassword = ''; + + if (partial) { + Object.assign(this, partial); + } + + // Valeur par défaut + this.temporary = this.temporary ?? false; + } +} + +export class UserQueryDto { + page?: number; + limit?: number; + search?: string; + enabled?: boolean; + emailVerified?: boolean; // 🔥 Ajouter ce champ + email?: string; + username?: string; + firstName?: string; + lastName?: string; + + constructor(partial?: Partial) { + if (partial) { + Object.assign(this, partial); + } + } +} + +export class PaginatedUserResponse { + data: UserResponse[]; + total: number; + page: number; + limit: number; + totalPages: number; + + constructor(data: UserResponse[], total: number, page: number, limit: number) { + this.data = data || []; + this.total = total || 0; + this.page = page || 1; + this.limit = limit || 10; + this.totalPages = Math.ceil(this.total / this.limit); + } +} + +export class UserCredentials { + type: string; + value: string; + temporary: boolean; + + constructor(type: string, value: string, temporary: boolean = false) { + this.type = type; + this.value = value; + this.temporary = temporary; + } +} + +// Types (restent des interfaces/type alias) +export type ClientRole = 'admin' | 'merchant' | 'support' | 'user'; + +export interface ApiResponse { + data: T; + message?: string; + status: string; +} + +export interface UserRoleMapping { + id: string; + name: string; + description?: string; + composite?: boolean; + clientRole?: boolean; + containerId?: string; +} + +export interface UserSession { + id: string; + username: string; + userId: string; + ipAddress: string; + start: number; + lastAccess: number; + clients: { [key: string]: string }; +} \ No newline at end of file diff --git a/src/app/modules/users/profile/profile.html b/src/app/modules/users/profile/profile.html new file mode 100644 index 0000000..dc9afa7 --- /dev/null +++ b/src/app/modules/users/profile/profile.html @@ -0,0 +1,355 @@ +
    + +
    +
    +
    +
    +

    + @if (user) { + {{ getUserDisplayName() }} + } @else { + Profil Utilisateur + } +

    + +
    + +
    + + @if (user && !isEditing) { + + + @if (user.enabled) { + + } @else { + + } + + + } +
    +
    +
    +
    + + + @if (error) { +
    + + {{ error }} +
    + } + + @if (success) { +
    + + {{ success }} +
    + } + +
    + + @if (loading) { +
    +
    + Chargement... +
    +

    Chargement du profil...

    +
    + } + + + @if (user && !loading) { + +
    + +
    +
    +
    Profil Keycloak
    +
    +
    + +
    +
    + {{ getUserInitials() }} +
    +
    + +
    {{ getUserDisplayName() }}
    +

    @{{ user.username }}

    + + + + {{ getStatusText() }} + + + +
    +
    + + {{ user.email }} +
    +
    + + Créé le {{ formatTimestamp(user.createdTimestamp) }} +
    +
    +
    +
    + + +
    +
    +
    Rôles Client
    + @if (!isEditing) { + + } +
    +
    +
    + @for (role of availableRoles; track role) { +
    +
    + + +
    +
    + } +
    + + + @if (user.clientRoles.length > 0) { +
    +
    Rôles assignés :
    + @for (role of user.clientRoles; track role) { + + {{ role }} + + } +
    + } +
    +
    +
    + + +
    +
    +
    +
    + @if (isEditing) { + Modification du Profil + } @else { + Détails du Compte + } +
    + + @if (isEditing) { +
    + + +
    + } +
    + +
    +
    + +
    + + @if (isEditing) { + + } @else { +
    + {{ user.firstName || 'Non défini' }} +
    + } +
    + + +
    + + @if (isEditing) { + + } @else { +
    + {{ user.lastName || 'Non défini' }} +
    + } +
    + + +
    + + @if (isEditing) { + + } @else { +
    + {{ user.username }} +
    + } +
    + + +
    + + @if (isEditing) { + + } @else { +
    + {{ user.email }} +
    + } +
    + + + @if (isEditing) { +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + } + + + @if (!isEditing) { +
    +
    +
    Informations Système
    +
    +
    + +
    + {{ user.id }} +
    +
    +
    + +
    + {{ formatTimestamp(user.createdTimestamp) }} +
    +
    +
    +
    + } +
    +
    +
    +
    + } +
    +
    \ No newline at end of file diff --git a/src/app/modules/users/profile/profile.spec.ts b/src/app/modules/users/profile/profile.spec.ts new file mode 100644 index 0000000..2e2cb81 --- /dev/null +++ b/src/app/modules/users/profile/profile.spec.ts @@ -0,0 +1,2 @@ +import { UsersProfile } from './profile'; +describe('UsersProfile', () => {}); \ No newline at end of file diff --git a/src/app/modules/users/profile/profile.ts b/src/app/modules/users/profile/profile.ts new file mode 100644 index 0000000..e2188fc --- /dev/null +++ b/src/app/modules/users/profile/profile.ts @@ -0,0 +1,240 @@ +import { Component, inject, OnInit, Input, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgIcon } from '@ng-icons/core'; +import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'; +import { UsersService } from '../services/users.service'; +import { UserResponse, UpdateUserDto, ClientRole } from '../models/user'; + +@Component({ + selector: 'app-user-profile', + standalone: true, + imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule], + templateUrl: './profile.html', + styles: [` + .avatar-lg { + width: 80px; + height: 80px; + } + .fs-24 { + font-size: 24px; + } + `] +}) +export class UserProfile implements OnInit { + private usersService = inject(UsersService); + private cdRef = inject(ChangeDetectorRef); + + @Input() userId!: string; + @Output() openResetPasswordModal = new EventEmitter(); + + user: UserResponse | null = null; + loading = false; + saving = false; + error = ''; + success = ''; + + // Édition + isEditing = false; + editedUser: UpdateUserDto = {}; + + // Gestion des rôles + availableRoles: ClientRole[] = ['admin', 'merchant', 'support', 'user']; + selectedRoles: ClientRole[] = []; + updatingRoles = false; + + ngOnInit() { + if (this.userId) { + this.loadUserProfile(); + } + } + + loadUserProfile() { + this.loading = true; + this.error = ''; + + this.usersService.getUserById(this.userId).subscribe({ + next: (user) => { + this.user = user; + this.selectedRoles = user.clientRoles + .filter((role): role is ClientRole => + this.availableRoles.includes(role as ClientRole) + ); + this.loading = false; + this.cdRef.detectChanges(); + }, + error: (error) => { + this.error = 'Erreur lors du chargement du profil utilisateur'; + this.loading = false; + this.cdRef.detectChanges(); + console.error('Error loading user profile:', error); + } + }); + } + + startEditing() { + this.isEditing = true; + this.editedUser = { + firstName: this.user?.firstName, + lastName: this.user?.lastName, + email: this.user?.email, + username: this.user?.username, + enabled: this.user?.enabled, + emailVerified: this.user?.emailVerified + }; + this.cdRef.detectChanges(); + } + + cancelEditing() { + this.isEditing = false; + this.editedUser = {}; + this.error = ''; + this.success = ''; + this.cdRef.detectChanges(); + } + + saveProfile() { + if (!this.user) return; + + this.saving = true; + this.error = ''; + this.success = ''; + + this.usersService.updateUser(this.user.id, this.editedUser).subscribe({ + next: (updatedUser) => { + this.user = updatedUser; + this.isEditing = false; + this.saving = false; + this.success = 'Profil mis à jour avec succès'; + this.editedUser = {}; + this.cdRef.detectChanges(); + }, + error: (error) => { + this.error = 'Erreur lors de la mise à jour du profil'; + this.saving = false; + this.cdRef.detectChanges(); + console.error('Error updating user:', error); + } + }); + } + + // Gestion des rôles + toggleRole(role: ClientRole) { + const index = this.selectedRoles.indexOf(role); + if (index > -1) { + this.selectedRoles.splice(index, 1); + } else { + this.selectedRoles.push(role); + } + this.cdRef.detectChanges(); + } + + isRoleSelected(role: ClientRole): boolean { + return this.selectedRoles.includes(role); + } + + updateUserRoles() { + if (!this.user) return; + + this.updatingRoles = true; + this.usersService.assignClientRoles(this.user.id, this.selectedRoles).subscribe({ + next: () => { + this.updatingRoles = false; + this.success = 'Rôles mis à jour avec succès'; + if (this.user) { + this.user.clientRoles = [...this.selectedRoles]; + } + this.cdRef.detectChanges(); + }, + error: (error) => { + this.updatingRoles = false; + this.error = 'Erreur lors de la mise à jour des rôles'; + this.cdRef.detectChanges(); + console.error('Error updating roles:', error); + } + }); + } + + // Gestion du statut + enableUser() { + if (!this.user) return; + + this.usersService.enableUser(this.user.id).subscribe({ + next: () => { + this.user!.enabled = true; + this.success = 'Utilisateur activé avec succès'; + this.cdRef.detectChanges(); + }, + error: (error) => { + this.error = 'Erreur lors de l\'activation de l\'utilisateur'; + this.cdRef.detectChanges(); + console.error('Error enabling user:', error); + } + }); + } + + disableUser() { + if (!this.user) return; + + this.usersService.disableUser(this.user.id).subscribe({ + next: () => { + this.user!.enabled = false; + this.success = 'Utilisateur désactivé avec succès'; + this.cdRef.detectChanges(); + }, + error: (error) => { + this.error = 'Erreur lors de la désactivation de l\'utilisateur'; + this.cdRef.detectChanges(); + console.error('Error disabling user:', error); + } + }); + } + + // Utilitaires d'affichage + getStatusBadgeClass(): string { + if (!this.user) return 'badge bg-secondary'; + if (!this.user.enabled) return 'badge bg-danger'; + if (!this.user.emailVerified) return 'badge bg-warning'; + return 'badge bg-success'; + } + + getStatusText(): string { + if (!this.user) return 'Inconnu'; + if (!this.user.enabled) return 'Désactivé'; + if (!this.user.emailVerified) return 'Email non vérifié'; + return 'Actif'; + } + + formatTimestamp(timestamp: number): string { + return new Date(timestamp).toLocaleDateString('fr-FR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } + + getUserInitials(): string { + if (!this.user) return 'U'; + return (this.user.firstName?.charAt(0) || '') + (this.user.lastName?.charAt(0) || '') || 'U'; + } + + getUserDisplayName(): string { + if (!this.user) return 'Utilisateur'; + if (this.user.firstName && this.user.lastName) { + return `${this.user.firstName} ${this.user.lastName}`; + } + return this.user.username; + } + + getRoleBadgeClass(role: string): string { + switch (role) { + case 'admin': return 'bg-danger'; + case 'merchant': return 'bg-success'; + case 'support': return 'bg-info'; + case 'user': return 'bg-secondary'; + default: return 'bg-secondary'; + } + } +} \ No newline at end of file diff --git a/src/app/modules/users/roles/roles.html b/src/app/modules/users/roles/roles.html deleted file mode 100644 index 5830f92..0000000 --- a/src/app/modules/users/roles/roles.html +++ /dev/null @@ -1 +0,0 @@ -

    Users - Roles

    \ No newline at end of file diff --git a/src/app/modules/users/roles/roles.spec.ts b/src/app/modules/users/roles/roles.spec.ts deleted file mode 100644 index c9ed38e..0000000 --- a/src/app/modules/users/roles/roles.spec.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { UsersRoles } from './roles'; -describe('UsersRoles', () => {}); \ No newline at end of file diff --git a/src/app/modules/users/roles/roles.ts b/src/app/modules/users/roles/roles.ts deleted file mode 100644 index d986358..0000000 --- a/src/app/modules/users/roles/roles.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { UiCard } from '@app/components/ui-card'; -import { CheckboxesAndRadios } from '@/app/modules/components/checkboxes-and-radios'; -import { InputFields } from '@/app/modules/components/input-fields'; -import { WizardWithProgress } from '@/app/modules/components/wizard-with-progress'; - -@Component({ - selector: 'app-roles', - imports: [FormsModule, UiCard, CheckboxesAndRadios, InputFields, WizardWithProgress], - templateUrl: './roles.html', -}) -export class UsersRoles { - role = { - name: '', - description: '', - permissions: { - transactions: ['read', 'export'], - merchants: ['read'], - operators: ['read'], - users: ['read'], - settings: [] - } as { [key: string]: string[] } - }; - - availablePermissions = { - transactions: ['read', 'create', 'update', 'delete', 'export'], - merchants: ['read', 'create', 'update', 'delete', 'config'], - operators: ['read', 'update', 'config'], - users: ['read', 'create', 'update', 'delete', 'roles'], - settings: ['read', 'update'] - }; - - togglePermission(module: string, permission: string) { - const index = this.role.permissions[module].indexOf(permission); - if (index > -1) { - this.role.permissions[module].splice(index, 1); - } else { - this.role.permissions[module].push(permission); - } - } - - createRole() { - console.log('Creating role:', this.role); - } -} \ No newline at end of file diff --git a/src/app/modules/users/services/list.service.ts b/src/app/modules/users/services/list.service.ts deleted file mode 100644 index 0ec0c9f..0000000 --- a/src/app/modules/users/services/list.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class UsersListService { - constructor() {} -} \ No newline at end of file diff --git a/src/app/modules/users/services/roles.service.ts b/src/app/modules/users/services/roles.service.ts deleted file mode 100644 index e40d0eb..0000000 --- a/src/app/modules/users/services/roles.service.ts +++ /dev/null @@ -1,42 +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 Role { - id: string; - name: string; - permissions: string[]; - description: string; - userCount: number; -} - -export interface Permission { - module: string; - actions: string[]; -} - -@Injectable({ providedIn: 'root' }) -export class UserRolesService { - private http = inject(HttpClient); - - getRoles(): Observable { - return this.http.get(`${environment.apiUrl}/users/roles`); - } - - createRole(role: Role): Observable { - return this.http.post(`${environment.apiUrl}/users/roles`, role); - } - - updateRole(id: string, role: Role): Observable { - return this.http.put(`${environment.apiUrl}/users/roles/${id}`, role); - } - - deleteRole(id: string): Observable { - return this.http.delete(`${environment.apiUrl}/users/roles/${id}`); - } - - getAvailablePermissions(): Observable { - return this.http.get(`${environment.apiUrl}/users/permissions`); - } -} \ No newline at end of file diff --git a/src/app/modules/users/services/users.service.ts b/src/app/modules/users/services/users.service.ts index 18e2908..872f883 100644 --- a/src/app/modules/users/services/users.service.ts +++ b/src/app/modules/users/services/users.service.ts @@ -1,50 +1,168 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { environment } from '@environments/environment'; -import { Observable } from 'rxjs'; +import { Observable, map, catchError, throwError } from 'rxjs'; +import { of } from 'rxjs'; -export interface User { - id: string; - username: string; - email: string; - firstName: string; - lastName: string; - role: string; - status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED'; - lastLogin?: Date; - createdAt: Date; -} +import { + UserResponse, + CreateUserDto, + UpdateUserDto, + ResetPasswordDto, + PaginatedUserResponse, + ClientRole +} from '../models/user'; @Injectable({ providedIn: 'root' }) -export class UserService { +export class UsersService { private http = inject(HttpClient); - private fb = inject(FormBuilder); + private apiUrl = `${environment.apiUrl}/users`; - createUserForm(): FormGroup { - return this.fb.group({ - username: ['', [Validators.required, Validators.minLength(3)]], - email: ['', [Validators.required, Validators.email]], - firstName: ['', Validators.required], - lastName: ['', Validators.required], - role: ['USER', Validators.required], - status: ['ACTIVE'] - }); + // === CRUD COMPLET === + createUser(createUserDto: CreateUserDto): Observable { + // Validation + if (!createUserDto.username || createUserDto.username.trim() === '') { + return throwError(() => 'Username is required and cannot be empty'); + } + + if (!createUserDto.email || createUserDto.email.trim() === '') { + return throwError(() => 'Email is required and cannot be empty'); + } + + if (!createUserDto.password || createUserDto.password.length < 8) { + return throwError(() => 'Password must be at least 8 characters'); + } + + // Nettoyage des données + const payload = { + username: createUserDto.username.trim(), + email: createUserDto.email.trim(), + firstName: (createUserDto.firstName || '').trim(), + lastName: (createUserDto.lastName || '').trim(), + password: createUserDto.password, + enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true, + emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : false, + clientRoles: createUserDto.clientRoles || [] + }; + + return this.http.post(`${this.apiUrl}`, payload).pipe( + catchError(error => throwError(() => error)) + ); } - getUsers(): Observable { - return this.http.get(`${environment.apiUrl}/users`); + // READ - Obtenir tous les utilisateurs + findAllUsers(): Observable { + return this.http.get<{ + users: any[]; + total: number; + page: number; + limit: number; + totalPages: number; + }>(`${this.apiUrl}`).pipe( + map(response => { + const users = response.users.map(user => new UserResponse(user)); + return new PaginatedUserResponse(users, response.total, response.page, response.limit); + }), + catchError(error => { + console.error('Error loading users:', error); + return of(new PaginatedUserResponse([], 0, 1, 10)); + }) + ); } - createUser(user: User): Observable { - return this.http.post(`${environment.apiUrl}/users`, user); + // READ - Obtenir un utilisateur par ID + getUserById(id: string): Observable { + return this.http.get(`${this.apiUrl}/${id}`).pipe( + map(response => new UserResponse(response)) + ); } - updateUser(id: string, user: User): Observable { - return this.http.put(`${environment.apiUrl}/users/${id}`, user); + // READ - Obtenir le profil de l'utilisateur connecté + getCurrentUserProfile(): Observable { + return this.http.get(`${this.apiUrl}/profile/me`).pipe( + map(response => new UserResponse(response)) + ); } - deleteUser(id: string): Observable { - return this.http.delete(`${environment.apiUrl}/users/${id}`); + // UPDATE - Mettre à jour un utilisateur + updateUser(id: string, updateUserDto: UpdateUserDto): Observable { + return this.http.put(`${this.apiUrl}/${id}`, updateUserDto).pipe( + map(response => new UserResponse(response)) + ); + } + + // UPDATE - Mettre à jour le profil de l'utilisateur connecté + updateCurrentUserProfile(updateUserDto: UpdateUserDto): Observable { + return this.http.put(`${this.apiUrl}/profile/me`, updateUserDto).pipe( + map(response => new UserResponse(response)) + ); + } + + // DELETE - Supprimer un utilisateur + deleteUser(id: string): Observable<{ message: string }> { + return this.http.delete<{ message: string }>(`${this.apiUrl}/${id}`); + } + + // === GESTION DES MOTS DE PASSE === + resetPassword(resetPasswordDto: ResetPasswordDto): Observable<{ message: string }> { + return this.http.put<{ message: string }>( + `${this.apiUrl}/${resetPasswordDto.userId}/password`, + resetPasswordDto + ); + } + + // === GESTION DU STATUT === + enableUser(id: string): Observable<{ message: string }> { + return this.http.put<{ message: string }>(`${this.apiUrl}/${id}/enable`, {}); + } + + disableUser(id: string): Observable<{ message: string }> { + return this.http.put<{ message: string }>(`${this.apiUrl}/${id}/disable`, {}); + } + + // === RECHERCHE ET VÉRIFICATION === + userExists(username: string): Observable<{ exists: boolean }> { + return this.http.get<{ exists: boolean }>(`${this.apiUrl}/check/${username}`); + } + + findUserByUsername(username: string): Observable { + return this.http.get(`${this.apiUrl}/search/username/${username}`).pipe( + map(users => users.map(user => new UserResponse(user))) + ); + } + + findUserByEmail(email: string): Observable { + return this.http.get(`${this.apiUrl}/search/email/${email}`).pipe( + map(users => users.map(user => new UserResponse(user))) + ); + } + + // === GESTION DES RÔLES === + getUserClientRoles(id: string): Observable<{ roles: string[] }> { + return this.http.get<{ roles: string[] }>(`${this.apiUrl}/${id}/roles`); + } + + assignClientRoles(userId: string, roles: ClientRole[]): Observable<{ message: string }> { + return this.http.put<{ message: string }>(`${this.apiUrl}/${userId}/roles`, { roles }); + } + + // === SESSIONS ET TOKENS === + getUserSessions(userId: string): Observable { + return this.http.get(`${this.apiUrl}/${userId}/sessions`); + } + + logoutUser(userId: string): Observable<{ message: string }> { + return this.http.post<{ message: string }>(`${this.apiUrl}/${userId}/logout`, {}); + } + + // === STATISTIQUES === + getUserStats(): Observable<{ + total: number; + enabled: number; + disabled: number; + emailVerified: number; + emailNotVerified: number; + }> { + return this.http.get(`${this.apiUrl}/stats`); } } \ No newline at end of file diff --git a/src/app/modules/users/structure.txt b/src/app/modules/users/structure.txt new file mode 100644 index 0000000..435eb3d --- /dev/null +++ b/src/app/modules/users/structure.txt @@ -0,0 +1,15 @@ +modules/users/ +├── components/ # Composants réutilisables +│ ├── users-list/ +│ │ ├── users-list.ts # Logique du tableau utilisateurs +│ │ └── users-list.html +│ ├── users-profile/ +│ │ ├── users-profile.ts # Logique création / modification +│ │ └── users-profile.html +│ +├── services/ +│ └── users.service.ts # Service API centralisé (NestJS) +│ +├── users.module.ts # Module principal +├── users.routes.ts # Gestion des routes +└── users.html # Template global du module diff --git a/src/app/modules/users/users.html b/src/app/modules/users/users.html index d479ca4..0d9ad50 100644 --- a/src/app/modules/users/users.html +++ b/src/app/modules/users/users.html @@ -1 +1,472 @@ -

    Users

    \ No newline at end of file +
    + + + +
    +
    + + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/modules/users/users.routes.ts b/src/app/modules/users/users.routes.ts new file mode 100644 index 0000000..9137039 --- /dev/null +++ b/src/app/modules/users/users.routes.ts @@ -0,0 +1,16 @@ +import { Routes } from '@angular/router'; +import { Users } from './users'; +import { authGuard } from '../../core/guards/auth.guard'; +import { roleGuard } from '../../core/guards/role.guard'; + +export const USERS_ROUTES: Routes = [ + { + path: 'users', + canActivate: [authGuard, roleGuard], + component: Users, + data: { + title: 'Gestion des Utilisateurs', + requiredRoles: ['admin'] // pour information + } + } +]; \ No newline at end of file diff --git a/src/app/modules/users/users.ts b/src/app/modules/users/users.ts index 0015fd2..60a0581 100644 --- a/src/app/modules/users/users.ts +++ b/src/app/modules/users/users.ts @@ -1,7 +1,340 @@ -import { Component } from '@angular/core'; +import { Component, inject, OnInit, TemplateRef, ViewChild, ChangeDetectorRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgIcon } from '@ng-icons/core'; +import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; +import { PageTitle } from '@app/components/page-title/page-title'; +import { UsersList } from './list/list'; +import { UserProfile } from './profile/profile'; +import { UsersService } from './services/users.service'; +import { CreateUserDto, ClientRole, UserResponse } from './models/user'; @Component({ selector: 'app-users', + standalone: true, + imports: [ + CommonModule, + FormsModule, + NgIcon, + NgbNavModule, + NgbModalModule, + PageTitle, + UsersList, + UserProfile + ], templateUrl: './users.html', }) -export class Users {} \ No newline at end of file +export class Users implements OnInit { + private modalService = inject(NgbModal); + private usersService = inject(UsersService); + private cdRef = inject(ChangeDetectorRef); + + activeTab: 'list' | 'profile' = 'list'; + selectedUserId: string | null = null; + + // Données pour la création d'utilisateur + newUser: CreateUserDto = { + username: '', + email: '', + firstName: '', + lastName: '', + password: '', + enabled: true, + emailVerified: false, + clientRoles: ['user'] + }; + + availableRoles: ClientRole[] = ['admin', 'merchant', 'support', 'user']; + selectedRoles: ClientRole[] = ['user']; + + creatingUser = false; + createUserError = ''; + + // Données pour la réinitialisation de mot de passe + selectedUserForReset: UserResponse | null = null; + newPassword = ''; + temporaryPassword = false; + resettingPassword = false; + resetPasswordError = ''; + resetPasswordSuccess = ''; + + selectedUserForDelete: UserResponse | null = null; + deletingUser = false; + deleteUserError = ''; + + ngOnInit() { + this.activeTab = 'list'; + this.synchronizeRoles(); + } + + private synchronizeRoles(): void { + // S'assurer que les rôles sont synchronisés + this.selectedRoles = [...this.newUser.clientRoles as ClientRole[]]; + } + + showTab(tab: 'list' | 'profile', userId?: string) { + this.activeTab = tab; + + if (userId) { + this.selectedUserId = userId; + } + } + + backToList() { + this.activeTab = 'list'; + this.selectedUserId = null; + } + + // Méthodes pour les modals + openModal(content: TemplateRef, size: 'sm' | 'lg' | 'xl' = 'lg') { + this.modalService.open(content, { + size: size, + centered: true, + scrollable: true + }); + } + + // Méthode pour ouvrir le modal de création d'utilisateur + openCreateUserModal() { + this.newUser = { + username: '', + email: '', + firstName: '', + lastName: '', + password: '', + enabled: true, + emailVerified: false, + clientRoles: ['user'] + }; + this.selectedRoles = ['user']; + this.createUserError = ''; + this.openModal(this.createUserModal); + } + + // Méthode pour ouvrir le modal de réinitialisation de mot de passe + openResetPasswordModal(userId: string) { + // Charger les données de l'utilisateur + this.usersService.getUserById(userId).subscribe({ + next: (user) => { + this.selectedUserForReset = user; + this.newPassword = ''; + this.temporaryPassword = false; + this.resetPasswordError = ''; + this.openModal(this.resetPasswordModal); + }, + error: (error) => { + console.error('Error loading user for password reset:', error); + } + }); + } + + // Gestion des rôles sélectionnés + toggleRole(role: ClientRole) { + const index = this.selectedRoles.indexOf(role); + if (index > -1) { + this.selectedRoles.splice(index, 1); + } else { + this.selectedRoles.push(role); + } + + // Mettre à jour les deux propriétés + this.newUser.clientRoles = [...this.selectedRoles]; + } + + isRoleSelected(role: ClientRole): boolean { + return this.selectedRoles.includes(role); + } + + createUser() { + const validation = this.validateUserForm(); + if (!validation.isValid) { + this.createUserError = validation.error!; + return; + } + + this.creatingUser = true; + this.createUserError = ''; + + const payload = { + username: this.newUser.username.trim(), + email: this.newUser.email.trim(), + firstName: this.newUser.firstName.trim(), + lastName: this.newUser.lastName.trim(), + password: this.newUser.password, + enabled: this.newUser.enabled, + emailVerified: this.newUser.emailVerified, + clientRoles: this.selectedRoles + }; + + this.usersService.createUser(payload).subscribe({ + next: (createdUser) => { + this.creatingUser = false; + this.modalService.dismissAll(); + + if(this.usersListComponent){ + this.usersListComponent.loadUsers(); + this.usersListComponent.onClearFilters(); + } + + this.showTab('list'); + }, + error: (error) => { + this.creatingUser = false; + this.createUserError = this.getErrorMessage(error); + } + }); + } + + // Réinitialiser le mot de passe + confirmResetPassword() { + if (!this.selectedUserForReset || !this.newPassword || this.newPassword.length < 8) { + this.resetPasswordError = 'Veuillez saisir un mot de passe valide (au moins 8 caractères).'; + return; + } + + this.resettingPassword = true; + this.resetPasswordError = ''; + this.resetPasswordSuccess = ''; + + const resetDto = { + userId: this.selectedUserForReset.id, + newPassword: this.newPassword, + temporary: this.temporaryPassword + }; + + this.usersService.resetPassword(resetDto).subscribe({ + next: () => { + this.resettingPassword = false; + this.resetPasswordSuccess = 'Mot de passe réinitialisé avec succès !'; + this.cdRef.detectChanges(); // Forcer la détection des changements + }, + error: (error) => { + this.resettingPassword = false; + this.resetPasswordError = this.getResetPasswordErrorMessage(error); + this.cdRef.detectChanges(); // Forcer la détection des changements + } + }); + } + + // Gestion des erreurs améliorée + private getErrorMessage(error: any): string { + if (error.error?.message) { + return error.error.message; + } + if (error.status === 400) { + return 'Données invalides. Vérifiez les champs du formulaire.'; + } + if (error.status === 409) { + return 'Un utilisateur avec ce nom ou email existe déjà.'; + } + return 'Erreur lors de la création de l\'utilisateur. Veuillez réessayer.'; + } + + private getResetPasswordErrorMessage(error: any): string { + if (error.error?.message) { + return error.error.message; + } + if (error.status === 404) { + return 'Utilisateur non trouvé.'; + } + if (error.status === 400) { + return 'Le mot de passe ne respecte pas les critères de sécurité.'; + } + return 'Erreur lors de la réinitialisation du mot de passe. Veuillez réessayer.'; + } + + // Méthode pour ouvrir le modal de suppression + openDeleteUserModal(userId: string) { + this.usersService.getUserById(userId).subscribe({ + next: (user) => { + this.selectedUserForDelete = user; + this.deleteUserError = ''; + this.openModal(this.deleteUserModal); + }, + error: (error) => { + console.error('Error loading user for password reset:', error); + } + }); + } + + @ViewChild(UsersList) usersListComponent!: UsersList; + + private refreshUsersList(): void { + if (this.usersListComponent && typeof this.usersListComponent.loadUsers === 'function') { + this.usersListComponent.loadUsers(); + } else { + console.warn('UsersList component not available for refresh'); + // Alternative: reload the current tab + this.showTab('list'); + } + } + + confirmDeleteUser() { + if (!this.selectedUserForDelete) return; + + this.deletingUser = true; + this.deleteUserError = ''; + + this.usersService.deleteUser(this.selectedUserForDelete.id).subscribe({ + next: () => { + this.deletingUser = false; + this.modalService.dismissAll(); + + this.refreshUsersList(); + + this.cdRef.detectChanges(); + }, + error: (error) => { + this.deletingUser = false; + this.deleteUserError = this.getDeleteErrorMessage(error); + this.cdRef.detectChanges(); + } + }); + } + + // Gestion des erreurs pour la suppression + private getDeleteErrorMessage(error: any): string { + if (error.error?.message) { + return error.error.message; + } + if (error.status === 404) { + return 'Utilisateur non trouvé.'; + } + if (error.status === 403) { + return 'Vous n\'avez pas les permissions pour supprimer cet utilisateur.'; + } + return 'Erreur lors de la suppression de l\'utilisateur. Veuillez réessayer.'; + } + + // Méthode pour afficher un message de succès + private showSuccessMessage(message: string) { + console.log('Success:', message); + } + + // Validation du formulaire + private validateUserForm(): { isValid: boolean; error?: string } { + const requiredFields = [ + { field: this.newUser.username?.trim(), name: 'Nom d\'utilisateur' }, + { field: this.newUser.email?.trim(), name: 'Email' }, + { field: this.newUser.firstName?.trim(), name: 'Prénom' }, + { field: this.newUser.lastName?.trim(), name: 'Nom' } + ]; + + for (const { field, name } of requiredFields) { + if (!field) { + return { isValid: false, error: `${name} est requis` }; + } + } + + if (!this.newUser.password || this.newUser.password.length < 8) { + return { isValid: false, error: 'Le mot de passe doit contenir au moins 8 caractères' }; + } + + return { isValid: true }; + } + + // Références aux templates de modals + @ViewChild('createUserModal') createUserModal!: TemplateRef; + @ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef; + @ViewChild('deleteUserModal') deleteUserModal!: TemplateRef; +} \ No newline at end of file diff --git a/src/app/types/layout.ts b/src/app/types/layout.ts index c8d1bf6..08942b0 100644 --- a/src/app/types/layout.ts +++ b/src/app/types/layout.ts @@ -54,6 +54,18 @@ export type MenuItemType = { isCollapsed?: boolean } +// types/layout.ts - Ajoutez ce type +export type UserDropdownItemType = { + label?: string; + icon?: string; + url?: string; + isDivider?: boolean; + isHeader?: boolean; + class?: string; + target?: string; + isDisabled?: boolean; +}; + export type LanguageOptionType = { code: string name: string diff --git a/src/environments/environment.ts b/src/environments/environment.ts index dff3660..ffda9e2 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,4 +1,90 @@ export const environment = { production: false, apiUrl: "http://localhost:3000/api/v1", -} + dcbApiUrl: 'https://api.paymenthub.com/v2', + + // Configuration DCB + dcb: { + // Opérateurs supportés + operators: { + orange: { + endpoint: 'https://api.orange.com/dcb/v2', + timeout: 30000, + retryAttempts: 3, + countries: ['CIV', 'SEN', 'CMR', 'MLI', 'BFA', 'GIN'] + }, + mtn: { + endpoint: 'https://api.mtn.com/dcb/v2', + timeout: 25000, + retryAttempts: 3, + countries: ['CIV', 'GHA', 'NGA', 'CMR', 'RWA'] + }, + airtel: { + endpoint: 'https://api.airtel.com/dcb/v2', + timeout: 30000, + retryAttempts: 3, + countries: ['COD', 'TZN', 'KEN', 'UGA', 'RWA'] + }, + moov: { + endpoint: 'https://api.moov.com/dcb/v2', + timeout: 25000, + retryAttempts: 3, + countries: ['CIV', 'BEN', 'TGO', 'NER', 'BFA'] + } + }, + + // Limitations + limits: { + maxAmount: 50, + minAmount: 0.5, + dailyLimit: 100, + monthlyLimit: 1000 + }, + + // Sécurité + security: { + webhookSecret: 'dcb_wh_secret_2024', + encryptionKey: 'dcb_enc_key_2024', + jwtExpiry: '24h' + }, + + // Monitoring + monitoring: { + healthCheckInterval: 60000, + alertThreshold: 0.1, // 10% d'erreur + performanceThreshold: 5000 // 5 secondes + } + }, + + // Configuration Merchants + merchants: { + onboarding: { + maxFileSize: 10 * 1024 * 1024, + allowedFileTypes: ['pdf', 'jpg', 'jpeg', 'png'], + autoApproveThreshold: 1000 + }, + payouts: { + defaultSchedule: 'monthly', + processingDays: [1, 15], + minPayoutAmount: 50, + fees: { + bankTransfer: 1.5, + mobileMoney: 2.0 + } + }, + kyc: { + requiredDocuments: ['registration_certificate', 'tax_certificate', 'id_document'], + autoExpireDays: 365 + } + }, + + // Configuration générale + app: { + name: 'Payment Aggregation Hub', + version: '2.0.0', + supportEmail: 'support@paymenthub.com', + defaultLanguage: 'fr', + currencies: ['XOF', 'XAF', 'USD', 'EUR', 'TND'], + countries: ['CIV', 'SEN', 'CMR', 'COD', 'TUN', 'BFA', 'MLI', 'GIN', 'NGA', 'GHA'] + } +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index fd49832..1a553eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "@app/components/*": ["./src/app/components/*"], "@common/*": ["./src/app/common/*"], "@core/*": ["./src/app/core/*"], + "@modules/*": ["./src/app/modules/*"], "@layouts/*": ["./src/app/layouts/*"], "@environments/*": ["./src/environments/*"], "@/*": ["./src/*"],