feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
84824e7c28
commit
13a317aab0
33
package-lock.json
generated
33
package-lock.json
generated
@ -32,6 +32,7 @@
|
|||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"choices.js": "^11.1.0",
|
"choices.js": "^11.1.0",
|
||||||
|
"class-validator": "^0.14.2",
|
||||||
"datatables": "^1.10.18",
|
"datatables": "^1.10.18",
|
||||||
"datatables.net": "^2.3.4",
|
"datatables.net": "^2.3.4",
|
||||||
"datatables.net-bs5": "^2.3.4",
|
"datatables.net-bs5": "^2.3.4",
|
||||||
@ -4376,6 +4377,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/validator": {
|
||||||
|
"version": "13.15.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.4.tgz",
|
||||||
|
"integrity": "sha512-LSFfpSnJJY9wbC0LQxgvfb+ynbHftFo0tMsFOl/J4wexLnYMmDSPaj2ZyDv3TkfL1UePxPrxOWJfbiRS8mQv7A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-basic-ssl": {
|
"node_modules/@vitejs/plugin-basic-ssl": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz",
|
||||||
@ -5017,6 +5024,17 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/class-validator": {
|
||||||
|
"version": "0.14.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz",
|
||||||
|
"integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/validator": "^13.11.8",
|
||||||
|
"libphonenumber-js": "^1.11.1",
|
||||||
|
"validator": "^13.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cli-cursor": {
|
"node_modules/cli-cursor": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||||
@ -7589,6 +7607,12 @@
|
|||||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/libphonenumber-js": {
|
||||||
|
"version": "1.12.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.25.tgz",
|
||||||
|
"integrity": "sha512-u90tUu/SEF8b+RaDKCoW7ZNFDakyBtFlX1ex3J+VH+ElWes/UaitJLt/w4jGu8uAE41lltV/s+kMVtywcMEg7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lie": {
|
"node_modules/lie": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
@ -10599,6 +10623,15 @@
|
|||||||
"node": "^18.17.0 || >=20.5.0"
|
"node": "^18.17.0 || >=20.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/validator": {
|
||||||
|
"version": "13.15.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz",
|
||||||
|
"integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|||||||
@ -35,6 +35,7 @@
|
|||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"choices.js": "^11.1.0",
|
"choices.js": "^11.1.0",
|
||||||
|
"class-validator": "^0.14.2",
|
||||||
"datatables": "^1.10.18",
|
"datatables": "^1.10.18",
|
||||||
"datatables.net": "^2.3.4",
|
"datatables.net": "^2.3.4",
|
||||||
"datatables.net-bs5": "^2.3.4",
|
"datatables.net-bs5": "^2.3.4",
|
||||||
|
|||||||
@ -1,37 +1,42 @@
|
|||||||
import { Routes } from '@angular/router'
|
import { Routes } from '@angular/router';
|
||||||
import { VerticalLayout } from '@layouts/vertical-layout/vertical-layout'
|
import { VerticalLayout } from '@layouts/vertical-layout/vertical-layout';
|
||||||
|
import { authGuard } from './core/guards/auth.guard';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
|
// Redirection racine
|
||||||
{ path: '', redirectTo: '/dcb-dashboard', pathMatch: 'full' },
|
{ path: '', redirectTo: '/dcb-dashboard', pathMatch: 'full' },
|
||||||
|
|
||||||
// Routes publiques (auth)
|
// ===== ROUTES D'ERREUR (publiques) =====
|
||||||
{
|
{
|
||||||
path: '',
|
path: 'error',
|
||||||
loadChildren: () =>
|
loadChildren: () => import('./modules/auth/error/error.routes').then(mod => mod.ERROR_PAGES_ROUTES),
|
||||||
import('./modules/auth/auth.route').then(mod => mod.Auth_ROUTES),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Routes d'erreur (publiques)
|
// ===== ROUTES PUBLIQUES =====
|
||||||
{
|
{
|
||||||
path: '',
|
path: 'auth',
|
||||||
loadChildren: () =>
|
loadChildren: () => import('./modules/auth/auth.routes').then(mod => mod.AUTH_ROUTES),
|
||||||
import('./modules/auth/error/error.route').then(mod => mod.ERROR_PAGES_ROUTES),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Routes protégées - SANS guards au niveau parent
|
// ===== ROUTES PROTÉGÉES - Layout Principal =====
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: VerticalLayout,
|
component: VerticalLayout,
|
||||||
loadChildren: () =>
|
canActivate: [authGuard],
|
||||||
import('./modules/modules.routes').then(
|
children: [
|
||||||
m => m.ModulesRoutes
|
{
|
||||||
),
|
path: '',
|
||||||
|
loadChildren: () => import('./modules/modules.routes').then(m => m.ModulesRoutes),
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// Redirections pour les erreurs courantes
|
// ===== REDIRECTIONS POUR LES ERREURS =====
|
||||||
{ path: '404', redirectTo: '/error/404' },
|
{ path: '404', redirectTo: '/error/404' },
|
||||||
{ path: '403', redirectTo: '/error/403' },
|
{ path: '403', redirectTo: '/error/403' },
|
||||||
|
{ path: '401', redirectTo: '/auth/sign-in' },
|
||||||
|
{ path: 'unauthorized', redirectTo: '/error/403' },
|
||||||
|
|
||||||
// Catch-all
|
// ===== CATCH-ALL =====
|
||||||
{ path: '**', redirectTo: '/error/404' },
|
{ path: '**', redirectTo: '/error/404' },
|
||||||
];
|
];
|
||||||
@ -1,20 +1,126 @@
|
|||||||
|
// src/app/core/guards/auth.guard.ts
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { CanActivateFn, Router } from '@angular/router';
|
import { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '../services/auth.service';
|
||||||
|
import { RoleService } from '../services/role.service';
|
||||||
|
import { map, catchError, of, tap, switchMap } from 'rxjs';
|
||||||
|
|
||||||
export const authGuard: CanActivateFn = (route, state) => {
|
export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||||
const authService = inject(AuthService);
|
const authService = inject(AuthService);
|
||||||
|
const roleService = inject(RoleService);
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
|
|
||||||
console.log('Guard check for:', state.url, 'isAuthenticated:', authService.isAuthenticated());
|
console.log('🔐 AuthGuard check for:', state.url);
|
||||||
|
|
||||||
if (authService.isAuthenticated()) {
|
// Attendre que l'initialisation soit terminée
|
||||||
|
return authService.getInitializedState().pipe(
|
||||||
|
switchMap(initialized => {
|
||||||
|
if (!initialized) {
|
||||||
|
console.log('⏳ AuthService pas encore initialisé, attente...');
|
||||||
|
return of(false); // Bloquer en attendant l'initialisation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier l'authentification
|
||||||
|
if (authService.isAuthenticated()) {
|
||||||
|
return of(checkRoleAccess(route, roleService, router, state.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tentative de rafraîchissement du token
|
||||||
|
const refreshToken = authService.getRefreshToken();
|
||||||
|
if (refreshToken) {
|
||||||
|
console.log('🔄 Token expiré, tentative de rafraîchissement...');
|
||||||
|
|
||||||
|
return authService.refreshToken().pipe(
|
||||||
|
tap(() => {
|
||||||
|
console.log('✅ Token rafraîchi avec succès');
|
||||||
|
roleService.refreshRoles();
|
||||||
|
}),
|
||||||
|
map(() => checkRoleAccess(route, roleService, router, state.url)),
|
||||||
|
catchError((error) => {
|
||||||
|
console.error('❌ Échec du rafraîchissement du token:', error);
|
||||||
|
authService.logout().subscribe();
|
||||||
|
return of(redirectToLogin(router, state.url, 'session_expired'));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirection vers login
|
||||||
|
console.log('🔒 Redirection vers login depuis:', state.url);
|
||||||
|
return of(redirectToLogin(router, state.url, 'not_authenticated'));
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('❌ Erreur dans le guard d\'auth:', error);
|
||||||
|
return of(redirectToLogin(router, state.url, 'not_authenticated'));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie l'accès basé sur les rôles requis
|
||||||
|
*/
|
||||||
|
function checkRoleAccess(
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
roleService: RoleService,
|
||||||
|
router: Router,
|
||||||
|
currentUrl: string
|
||||||
|
): boolean {
|
||||||
|
const requiredRoles = route.data?.['roles'] as string[];
|
||||||
|
|
||||||
|
if (!requiredRoles || requiredRoles.length === 0) {
|
||||||
|
console.log('✅ Accès autorisé (aucun rôle requis):', currentUrl);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rediriger vers login avec l'URL de retour
|
const hasRequiredRole = roleService.hasAnyRole(requiredRoles);
|
||||||
router.navigate(['/auth/sign-in'], {
|
const currentUserRoles = roleService.getCurrentUserRoles();
|
||||||
queryParams: { returnUrl: state.url }
|
|
||||||
|
if (hasRequiredRole) {
|
||||||
|
console.log('✅ Accès autorisé avec rôles:', currentUrl);
|
||||||
|
console.log(' Rôles requis:', requiredRoles);
|
||||||
|
console.log(' Rôles actuels:', currentUserRoles);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('❌ Accès refusé: rôles insuffisants');
|
||||||
|
console.warn(' URL demandée:', currentUrl);
|
||||||
|
console.warn(' Rôles requis:', requiredRoles);
|
||||||
|
console.warn(' Rôles actuels:', currentUserRoles);
|
||||||
|
|
||||||
|
// Rediriger vers la page non autorisée
|
||||||
|
router.navigate(['/unauthorized'], {
|
||||||
|
queryParams: {
|
||||||
|
requiredRoles: requiredRoles.join(','),
|
||||||
|
currentRoles: currentUserRoles.join(','),
|
||||||
|
attemptedUrl: currentUrl
|
||||||
|
},
|
||||||
|
replaceUrl: true
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirige vers la page de login avec les paramètres appropriés
|
||||||
|
*/
|
||||||
|
function redirectToLogin(
|
||||||
|
router: Router,
|
||||||
|
returnUrl: string,
|
||||||
|
reason: 'not_authenticated' | 'session_expired'
|
||||||
|
): boolean {
|
||||||
|
const queryParams: any = {
|
||||||
|
returnUrl: returnUrl,
|
||||||
|
reason: reason
|
||||||
|
};
|
||||||
|
|
||||||
|
// Message spécifique selon la raison
|
||||||
|
if (reason === 'session_expired') {
|
||||||
|
queryParams.message = 'Votre session a expiré. Veuillez vous reconnecter.';
|
||||||
|
}
|
||||||
|
|
||||||
|
router.navigate(['/auth/sign-in'], {
|
||||||
|
queryParams,
|
||||||
|
replaceUrl: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
40
src/app/core/guards/public.guard.ts
Normal file
40
src/app/core/guards/public.guard.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// src/app/core/guards/public.guard.ts
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { CanActivateFn, Router } from '@angular/router';
|
||||||
|
import { AuthService } from '../services/auth.service';
|
||||||
|
import { map, catchError, of } from 'rxjs';
|
||||||
|
|
||||||
|
export const publicGuard: CanActivateFn = () => {
|
||||||
|
const authService = inject(AuthService);
|
||||||
|
const router = inject(Router);
|
||||||
|
|
||||||
|
// Si l'utilisateur est déjà authentifié, le rediriger vers le dashboard
|
||||||
|
if (authService.isAuthenticated()) {
|
||||||
|
console.log('🔄 Utilisateur déjà authentifié, redirection vers le dashboard');
|
||||||
|
router.navigate(['/dcb-dashboard'], { replaceUrl: true });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si un refresh token est disponible
|
||||||
|
const refreshToken = authService.getRefreshToken();
|
||||||
|
if (refreshToken) {
|
||||||
|
console.log('🔄 Token de rafraîchissement détecté, tentative de reconnexion...');
|
||||||
|
|
||||||
|
return authService.refreshToken().pipe(
|
||||||
|
map(() => {
|
||||||
|
console.log('✅ Reconnexion automatique réussie');
|
||||||
|
router.navigate(['/dcb-dashboard'], { replaceUrl: true });
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
console.error('❌ Échec de la reconnexion automatique:', error);
|
||||||
|
// En cas d'erreur, autoriser l'accès à la page publique
|
||||||
|
return of(true);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// L'utilisateur n'est pas connecté, autoriser l'accès à la page publique
|
||||||
|
console.log('🌐 Accès public autorisé');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
@ -11,8 +11,11 @@ export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state) =
|
|||||||
// Vérifier d'abord l'authentification
|
// Vérifier d'abord l'authentification
|
||||||
if (!authService.isAuthenticated()) {
|
if (!authService.isAuthenticated()) {
|
||||||
console.log('RoleGuard: User not authenticated, redirecting to login');
|
console.log('RoleGuard: User not authenticated, redirecting to login');
|
||||||
router.navigate(['/auth/sign-in'], {
|
router.navigate(['/auth/login'], {
|
||||||
queryParams: { returnUrl: state.url }
|
queryParams: {
|
||||||
|
returnUrl: state.url,
|
||||||
|
reason: 'not_authenticated'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -22,13 +25,15 @@ export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state) =
|
|||||||
|
|
||||||
if (!userRoles || userRoles.length === 0) {
|
if (!userRoles || userRoles.length === 0) {
|
||||||
console.warn('RoleGuard: User has no roles');
|
console.warn('RoleGuard: User has no roles');
|
||||||
router.navigate(['/unauthorized']);
|
router.navigate(['/unauthorized'], {
|
||||||
|
queryParams: { reason: 'no_roles' }
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modulePath = getModulePath(route);
|
const modulePath = getModulePath(route);
|
||||||
|
|
||||||
console.log('RoleGuard check:', {
|
console.log('🔐 RoleGuard check:', {
|
||||||
module: modulePath,
|
module: modulePath,
|
||||||
userRoles: userRoles,
|
userRoles: userRoles,
|
||||||
url: state.url
|
url: state.url
|
||||||
@ -38,12 +43,18 @@ export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state) =
|
|||||||
const hasAccess = permissionsService.canAccessModule(modulePath, userRoles);
|
const hasAccess = permissionsService.canAccessModule(modulePath, userRoles);
|
||||||
|
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
console.warn('RoleGuard: Access denied for', modulePath, 'User roles:', userRoles);
|
console.warn('❌ RoleGuard: Access denied for', modulePath, 'User roles:', userRoles);
|
||||||
router.navigate(['/unauthorized']);
|
router.navigate(['/unauthorized'], {
|
||||||
|
queryParams: {
|
||||||
|
module: modulePath,
|
||||||
|
userRoles: userRoles.join(','),
|
||||||
|
requiredRoles: getRequiredRolesForModule(permissionsService, modulePath).join(',')
|
||||||
|
}
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('RoleGuard: Access granted for', modulePath);
|
console.log('✅ RoleGuard: Access granted for', modulePath);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -78,4 +89,12 @@ function buildPathFromUrl(route: ActivatedRouteSnapshot): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return segments.join('/');
|
return segments.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequiredRolesForModule(permissionsService: PermissionsService, modulePath: string): string[] {
|
||||||
|
// Cette fonction récupère les rôles requis pour un module donné
|
||||||
|
// Vous devrez peut-être l'adapter selon votre implémentation PermissionsService
|
||||||
|
const [mainModule] = modulePath.split('/');
|
||||||
|
const permission = (permissionsService as any).findPermission?.(mainModule);
|
||||||
|
return permission?.roles || [];
|
||||||
}
|
}
|
||||||
48
src/app/core/interceptors/api.service.ts
Normal file
48
src/app/core/interceptors/api.service.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// src/app/core/services/api.service.ts
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { environment } from '@environments/environment';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ApiService {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
protected get<T>(endpoint: string, params?: any): Observable<T> {
|
||||||
|
return this.http.get<T>(`${environment.iamApiUrl}/${endpoint}`, {
|
||||||
|
params: this.createParams(params)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected post<T>(endpoint: string, data: any): Observable<T> {
|
||||||
|
return this.http.post<T>(`${environment.iamApiUrl}/${endpoint}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected put<T>(endpoint: string, data: any): Observable<T> {
|
||||||
|
return this.http.put<T>(`${environment.iamApiUrl}/${endpoint}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected patch<T>(endpoint: string, data: any): Observable<T> {
|
||||||
|
return this.http.patch<T>(`${environment.iamApiUrl}/${endpoint}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected delete<T>(endpoint: string): Observable<T> {
|
||||||
|
return this.http.delete<T>(`${environment.iamApiUrl}/${endpoint}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createParams(params: any): HttpParams {
|
||||||
|
let httpParams = new HttpParams();
|
||||||
|
|
||||||
|
if (params) {
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
if (params[key] !== null && params[key] !== undefined) {
|
||||||
|
httpParams = httpParams.set(key, params[key].toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
|
// src/app/core/interceptors/auth.interceptor.ts
|
||||||
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn, HttpErrorResponse } from '@angular/common/http';
|
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn, HttpErrorResponse } from '@angular/common/http';
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService, LoginResponseDto } from '../services/auth.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { catchError, switchMap, throwError } from 'rxjs';
|
import { catchError, switchMap, throwError } from 'rxjs';
|
||||||
|
|
||||||
@ -8,21 +9,19 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
|||||||
const authService = inject(AuthService);
|
const authService = inject(AuthService);
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
|
|
||||||
// On ignore les requêtes d'authentification
|
// Exclusion des endpoints d'authentification
|
||||||
if (isAuthRequest(req)) {
|
if (isAuthRequest(req)) {
|
||||||
return next(req);
|
return next(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = authService.getToken();
|
const token = authService.getAccessToken();
|
||||||
|
|
||||||
// On ajoute le token uniquement si c'est une requête API et que le token existe
|
|
||||||
if (token && isApiRequest(req)) {
|
if (token && isApiRequest(req)) {
|
||||||
const cloned = addToken(req, token);
|
const cloned = addToken(req, token);
|
||||||
|
|
||||||
return next(cloned).pipe(
|
return next(cloned).pipe(
|
||||||
catchError((error: HttpErrorResponse) => {
|
catchError((error: HttpErrorResponse) => {
|
||||||
if (error.status === 401) {
|
if (error.status === 401 && !req.url.includes('/auth/refresh')) {
|
||||||
// Token expiré, on tente de le rafraîchir
|
|
||||||
return handle401Error(authService, router, req, next);
|
return handle401Error(authService, router, req, next);
|
||||||
}
|
}
|
||||||
return throwError(() => error);
|
return throwError(() => error);
|
||||||
@ -33,7 +32,8 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
|||||||
return next(req);
|
return next(req);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ajoute le token à la requête
|
// === FONCTIONS UTILITAIRES ===
|
||||||
|
|
||||||
function addToken(req: HttpRequest<any>, token: string): HttpRequest<any> {
|
function addToken(req: HttpRequest<any>, token: string): HttpRequest<any> {
|
||||||
return req.clone({
|
return req.clone({
|
||||||
setHeaders: {
|
setHeaders: {
|
||||||
@ -42,7 +42,6 @@ function addToken(req: HttpRequest<any>, token: string): HttpRequest<any> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gère les erreurs 401 (token expiré)
|
|
||||||
function handle401Error(
|
function handle401Error(
|
||||||
authService: AuthService,
|
authService: AuthService,
|
||||||
router: Router,
|
router: Router,
|
||||||
@ -50,32 +49,28 @@ function handle401Error(
|
|||||||
next: HttpHandlerFn
|
next: HttpHandlerFn
|
||||||
) {
|
) {
|
||||||
return authService.refreshToken().pipe(
|
return authService.refreshToken().pipe(
|
||||||
switchMap((response: any) => {
|
switchMap((response: LoginResponseDto) => {
|
||||||
// Nouveau token obtenu, on relance la requête originale
|
const newRequest = addToken(req, response.access_token);
|
||||||
const newToken = response.access_token;
|
|
||||||
const newRequest = addToken(req, newToken);
|
|
||||||
return next(newRequest);
|
return next(newRequest);
|
||||||
}),
|
}),
|
||||||
catchError((refreshError) => {
|
catchError((refreshError) => {
|
||||||
// Échec du rafraîchissement, on déconnecte
|
authService.logout().subscribe();
|
||||||
authService.logout();
|
|
||||||
router.navigate(['/auth/sign-in']);
|
router.navigate(['/auth/sign-in']);
|
||||||
return throwError(() => refreshError);
|
return throwError(() => refreshError);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Détermine si c'est une requête vers le backend API
|
|
||||||
function isApiRequest(req: HttpRequest<any>): boolean {
|
function isApiRequest(req: HttpRequest<any>): boolean {
|
||||||
return req.url.includes('/api/') || req.url.includes('/auth/');
|
return req.url.includes('/api/') || req.url.includes('/auth/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Liste des endpoints où le token ne doit pas être ajouté
|
|
||||||
function isAuthRequest(req: HttpRequest<any>): boolean {
|
function isAuthRequest(req: HttpRequest<any>): boolean {
|
||||||
const url = req.url;
|
const authEndpoints = [
|
||||||
return (
|
'/auth/login',
|
||||||
url.includes('/auth/login') ||
|
'/auth/refresh',
|
||||||
url.endsWith('/auth/refresh') ||
|
'/auth/logout'
|
||||||
url.endsWith('/auth/logout')
|
];
|
||||||
);
|
|
||||||
}
|
return authEndpoints.some(endpoint => req.url.includes(endpoint));
|
||||||
|
}
|
||||||
@ -1,282 +1,371 @@
|
|||||||
|
// src/app/core/services/auth.service.ts
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { environment } from '@environments/environment';
|
import { environment } from '@environments/environment';
|
||||||
import { BehaviorSubject, tap, catchError, Observable, throwError, map } from 'rxjs';
|
import { BehaviorSubject, Observable, throwError, tap, catchError, map, of } from 'rxjs';
|
||||||
import { jwtDecode } from "jwt-decode";
|
|
||||||
|
|
||||||
interface DecodedToken {
|
// Interfaces pour les DTOs de l'API
|
||||||
exp: number;
|
export interface LoginDto {
|
||||||
iat?: number;
|
username: string;
|
||||||
sub?: string;
|
password: string;
|
||||||
preferred_username?: string;
|
|
||||||
email?: string;
|
|
||||||
given_name?: string;
|
|
||||||
family_name?: string;
|
|
||||||
resource_access?: {
|
|
||||||
[key: string]: {
|
|
||||||
roles: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthResponse {
|
export interface RefreshTokenDto {
|
||||||
|
refresh_token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponseDto {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
expires_in?: number;
|
refresh_token: string;
|
||||||
refresh_token?: string;
|
expires_in: number;
|
||||||
token_type?: string;
|
token_type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
export interface LogoutResponseDto {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthStatusResponseDto {
|
||||||
|
authenticated: boolean;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserProfileDto {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
roles: string[];
|
||||||
|
emailVerified: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenValidationResponseDto {
|
||||||
|
valid: boolean;
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
roles: string[];
|
||||||
|
};
|
||||||
|
expires_in: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private readonly tokenKey = 'access_token';
|
|
||||||
private readonly refreshTokenKey = 'refresh_token';
|
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
|
private readonly tokenKey = 'access_token';
|
||||||
|
private readonly refreshTokenKey = 'refresh_token';
|
||||||
|
|
||||||
private authState$ = new BehaviorSubject<boolean>(this.isAuthenticated());
|
private authState$ = new BehaviorSubject<boolean>(this.isAuthenticated());
|
||||||
|
private userProfile$ = new BehaviorSubject<UserProfileDto | null>(null);
|
||||||
|
private initialized$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
private userRoles$ = new BehaviorSubject<string[]>(this.getRolesFromToken());
|
// === INITIALISATION DE L'APPLICATION ===
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère les rôles depuis le token JWT
|
* Initialise l'authentification au démarrage de l'application
|
||||||
*/
|
*/
|
||||||
private getRolesFromToken(): string[] {
|
async initialize(): Promise<boolean> {
|
||||||
const token = this.getToken();
|
console.log('🔄 Initialisation du service d\'authentification...');
|
||||||
if (!token) return [];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decoded: DecodedToken = jwtDecode(token);
|
const token = this.getAccessToken();
|
||||||
|
|
||||||
// Priorité 2: Rôles du client (resource_access)
|
if (!token) {
|
||||||
// Prendre tous les rôles de tous les clients
|
console.log('🔍 Aucun token trouvé, utilisateur non authentifié');
|
||||||
if (decoded.resource_access) {
|
this.initialized$.next(true);
|
||||||
const allClientRoles: string[] = [];
|
return false;
|
||||||
|
|
||||||
Object.values(decoded.resource_access).forEach(client => {
|
|
||||||
if (client?.roles) {
|
|
||||||
allClientRoles.push(...client.roles);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retourner les rôles uniques
|
|
||||||
return [...new Set(allClientRoles)];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isTokenExpired(token)) {
|
||||||
|
console.log('⚠️ Token expiré, tentative de rafraîchissement...');
|
||||||
|
const refreshSuccess = await this.tryRefreshToken();
|
||||||
|
this.initialized$.next(true);
|
||||||
|
return refreshSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token valide, vérifier le profil utilisateur
|
||||||
|
console.log('✅ Token valide détecté, vérification du profil...');
|
||||||
|
await this.loadUserProfile().toPromise();
|
||||||
|
|
||||||
return [];
|
this.authState$.next(true);
|
||||||
} catch {
|
this.initialized$.next(true);
|
||||||
return [];
|
|
||||||
|
console.log('🎯 Authentification initialisée avec succès');
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur lors de l\'initialisation de l\'auth:', error);
|
||||||
|
this.clearAuthData();
|
||||||
|
this.initialized$.next(true);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère les rôles de l'utilisateur courant
|
* Tente de rafraîchir le token de manière synchrone
|
||||||
*/
|
*/
|
||||||
getCurrentUserRoles(): string[] {
|
private async tryRefreshToken(): Promise<boolean> {
|
||||||
return this.userRoles$.value;
|
const refreshToken = this.getRefreshToken();
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
console.log('🔍 Aucun refresh token disponible');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convertir l'Observable en Promise pour l'initialisation
|
||||||
|
const response = await this.refreshToken().toPromise();
|
||||||
|
console.log('✅ Token rafraîchi avec succès lors de l\'initialisation');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Échec du rafraîchissement du token:', error);
|
||||||
|
this.clearAuthData();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable des rôles
|
* Observable pour suivre l'état d'initialisation
|
||||||
*/
|
*/
|
||||||
onRolesChange(): Observable<string[]> {
|
getInitializedState(): Observable<boolean> {
|
||||||
return this.userRoles$.asObservable();
|
return this.initialized$.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === MÉTHODES EXISTANTES AVEC AMÉLIORATIONS ===
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifications rapides par rôle
|
* Connexion utilisateur
|
||||||
*/
|
*/
|
||||||
isAdmin(): boolean {
|
login(credentials: LoginDto): Observable<LoginResponseDto> {
|
||||||
return this.getCurrentUserRoles().includes('admin');
|
return this.http.post<LoginResponseDto>(
|
||||||
}
|
`${environment.iamApiUrl}/auth/login`,
|
||||||
|
credentials
|
||||||
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<boolean> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const token = this.getToken();
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
resolve(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isTokenExpired(token)) {
|
|
||||||
this.refreshToken().subscribe({
|
|
||||||
next: () => resolve(true),
|
|
||||||
error: () => {
|
|
||||||
this.clearSession(false);
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authentifie l'utilisateur via le backend NestJS
|
|
||||||
*/
|
|
||||||
login(username: string, password: string): Observable<AuthResponse> {
|
|
||||||
return this.http.post<AuthResponse>(
|
|
||||||
`${environment.iamApiUrl}/auth/login`,
|
|
||||||
{ username, password }
|
|
||||||
).pipe(
|
).pipe(
|
||||||
tap(response => {
|
tap(response => {
|
||||||
this.handleLoginResponse(response);
|
this.handleLoginSuccess(response);
|
||||||
|
this.loadUserProfile().subscribe(); // Charger le profil après connexion
|
||||||
}),
|
}),
|
||||||
catchError((error: HttpErrorResponse) => {
|
catchError(error => this.handleLoginError(error))
|
||||||
console.error('Login failed:', error);
|
|
||||||
return throwError(() => this.getErrorMessage(error));
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rafraîchit le token d'accès
|
* Rafraîchissement du token
|
||||||
*/
|
*/
|
||||||
refreshToken(): Observable<AuthResponse> {
|
refreshToken(): Observable<LoginResponseDto> {
|
||||||
const refreshToken = localStorage.getItem(this.refreshTokenKey);
|
const refreshToken = this.getRefreshToken();
|
||||||
|
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
return throwError(() => 'No refresh token available');
|
return throwError(() => new Error('No refresh token available'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.http.post<AuthResponse>(
|
return this.http.post<LoginResponseDto>(
|
||||||
`${environment.iamApiUrl}/auth/refresh`,
|
`${environment.iamApiUrl}/auth/refresh`,
|
||||||
{ refresh_token: refreshToken }
|
{ refresh_token: refreshToken }
|
||||||
).pipe(
|
).pipe(
|
||||||
tap(response => {
|
tap(response => {
|
||||||
this.handleLoginResponse(response);
|
this.handleLoginSuccess(response);
|
||||||
|
console.log('🔄 Token rafraîchi avec succès');
|
||||||
}),
|
}),
|
||||||
catchError((error: HttpErrorResponse) => {
|
catchError(error => {
|
||||||
console.error('Token refresh failed:', error);
|
console.error('❌ Échec du rafraîchissement du token:', error);
|
||||||
this.clearSession();
|
this.clearAuthData();
|
||||||
return throwError(() => error);
|
return throwError(() => error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Déconnecte l'utilisateur
|
* Déconnexion utilisateur
|
||||||
*/
|
*/
|
||||||
logout(redirect = true): void {
|
logout(): Observable<LogoutResponseDto> {
|
||||||
this.clearSession(redirect);
|
return this.http.post<LogoutResponseDto>(
|
||||||
|
`${environment.iamApiUrl}/auth/logout`,
|
||||||
// Appel API optionnel (ne pas bloquer dessus)
|
{}
|
||||||
this.http.post(`${environment.iamApiUrl}/auth/logout`, {}).subscribe({
|
).pipe(
|
||||||
error: () => {} // Ignorer silencieusement les erreurs de logout
|
tap(() => {
|
||||||
});
|
this.clearAuthData();
|
||||||
|
console.log('👋 Déconnexion réussie');
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
this.clearAuthData(); // Nettoyer même en cas d'erreur
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearSession(redirect: boolean = true): void {
|
/**
|
||||||
localStorage.removeItem(this.tokenKey);
|
* Chargement du profil utilisateur
|
||||||
localStorage.removeItem(this.refreshTokenKey);
|
*/
|
||||||
this.authState$.next(false);
|
loadUserProfile(): Observable<UserProfileDto> {
|
||||||
this.userRoles$.next([]);
|
return this.http.get<UserProfileDto>(
|
||||||
|
`${environment.iamApiUrl}/auth/profile`
|
||||||
if (redirect) {
|
).pipe(
|
||||||
this.router.navigate(['/auth/sign-in']);
|
tap(profile => {
|
||||||
}
|
this.userProfile$.next(profile);
|
||||||
|
console.log('👤 Profil utilisateur chargé:', profile.username);
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('❌ Erreur lors du chargement du profil:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
private handleLoginResponse(response: AuthResponse): void {
|
* Gestion de la connexion réussie
|
||||||
if (response?.access_token) {
|
*/
|
||||||
|
private handleLoginSuccess(response: LoginResponseDto): void {
|
||||||
|
if (response.access_token) {
|
||||||
localStorage.setItem(this.tokenKey, response.access_token);
|
localStorage.setItem(this.tokenKey, response.access_token);
|
||||||
|
|
||||||
if (response.refresh_token) {
|
if (response.refresh_token) {
|
||||||
localStorage.setItem(this.refreshTokenKey, response.refresh_token);
|
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);
|
this.authState$.next(true);
|
||||||
|
console.log('✅ Connexion réussie');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getErrorMessage(error: HttpErrorResponse): string {
|
|
||||||
if (error?.error?.message) {
|
|
||||||
return error.error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.status === 401) {
|
|
||||||
return 'Invalid username or password';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Login failed. Please try again.';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retourne le token JWT stocké
|
* Nettoyage des données d'authentification
|
||||||
*/
|
*/
|
||||||
getToken(): string | null {
|
private clearAuthData(): void {
|
||||||
|
localStorage.removeItem(this.tokenKey);
|
||||||
|
localStorage.removeItem(this.refreshTokenKey);
|
||||||
|
this.authState$.next(false);
|
||||||
|
this.userProfile$.next(null);
|
||||||
|
console.log('🧹 Données d\'authentification nettoyées');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation du token
|
||||||
|
*/
|
||||||
|
validateToken(): Observable<TokenValidationResponseDto> {
|
||||||
|
return this.http.get<TokenValidationResponseDto>(
|
||||||
|
`${environment.iamApiUrl}/auth/validate`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === OBSERVABLES POUR COMPOSANTS ===
|
||||||
|
|
||||||
|
getAuthState(): Observable<boolean> {
|
||||||
|
return this.authState$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserProfile(): Observable<UserProfileDto | null> {
|
||||||
|
return this.userProfile$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les rôles de l'utilisateur courant
|
||||||
|
*/
|
||||||
|
getCurrentUserRoles(): string[] {
|
||||||
|
const token = this.getAccessToken();
|
||||||
|
if (!token) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||||
|
const decoded: any = payload;
|
||||||
|
|
||||||
|
// Récupérer tous les rôles de tous les clients
|
||||||
|
if (decoded.resource_access) {
|
||||||
|
const allRoles: string[] = [];
|
||||||
|
|
||||||
|
Object.values(decoded.resource_access).forEach((client: any) => {
|
||||||
|
if (client?.roles) {
|
||||||
|
allRoles.push(...client.roles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...new Set(allRoles)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable de l'état d'authentification
|
||||||
|
*/
|
||||||
|
onAuthState(): Observable<boolean> {
|
||||||
|
return this.authState$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le profil utilisateur
|
||||||
|
*/
|
||||||
|
getProfile(): Observable<any> {
|
||||||
|
return this.getUserProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur a un rôle spécifique
|
||||||
|
*/
|
||||||
|
hasRole(role: string): boolean {
|
||||||
|
return this.getCurrentUserRoles().includes(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur a un des rôles spécifiés
|
||||||
|
*/
|
||||||
|
hasAnyRole(roles: string[]): boolean {
|
||||||
|
const userRoles = this.getCurrentUserRoles();
|
||||||
|
return roles.some(role => userRoles.includes(role));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === GETTERS POUR LES TOKENS ===
|
||||||
|
|
||||||
|
getAccessToken(): string | null {
|
||||||
return localStorage.getItem(this.tokenKey);
|
return localStorage.getItem(this.tokenKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getRefreshToken(): string | null {
|
||||||
* Vérifie si le token est expiré
|
return localStorage.getItem(this.refreshTokenKey);
|
||||||
*/
|
}
|
||||||
isTokenExpired(token: string): boolean {
|
|
||||||
|
// === METHODES PRIVEES ===
|
||||||
|
|
||||||
|
private handleLoginError(error: HttpErrorResponse): Observable<never> {
|
||||||
|
let errorMessage = 'Login failed';
|
||||||
|
|
||||||
|
if (error.status === 401) {
|
||||||
|
errorMessage = 'Invalid username or password';
|
||||||
|
} else if (error.status === 403) {
|
||||||
|
errorMessage = 'Account is disabled or not fully set up';
|
||||||
|
} else if (error.error?.message) {
|
||||||
|
errorMessage = error.error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return throwError(() => new Error(errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === VERIFICATIONS D'ETAT ===
|
||||||
|
|
||||||
|
isAuthenticated(): boolean {
|
||||||
|
const token = this.getAccessToken();
|
||||||
|
return !!token && !this.isTokenExpired(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isTokenExpired(token: string): boolean {
|
||||||
try {
|
try {
|
||||||
const decoded: DecodedToken = jwtDecode(token);
|
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const expiry = payload.exp;
|
||||||
return decoded.exp < (now + 60); // Marge de sécurité de 60 secondes
|
return (Math.floor((new Date).getTime() / 1000)) >= expiry;
|
||||||
} catch {
|
} catch {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si l'utilisateur est authentifié
|
|
||||||
*/
|
|
||||||
isAuthenticated(): boolean {
|
|
||||||
const token = this.getToken();
|
|
||||||
if (!token) return false;
|
|
||||||
return !this.isTokenExpired(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAuthState() {
|
|
||||||
return this.authState$.asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère les infos utilisateur depuis le backend
|
|
||||||
*/
|
|
||||||
getProfile(): Observable<any> {
|
|
||||||
return this.http.get(`${environment.iamApiUrl}/users/profile/me`).pipe(
|
|
||||||
catchError(error => {
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -77,9 +77,9 @@ export class MenuService {
|
|||||||
url: '/transactions',
|
url: '/transactions',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Gestions Marchant',
|
label: 'Gestions Merchants/Partenaires',
|
||||||
icon: 'lucideStore',
|
icon: 'lucideStore',
|
||||||
url: '/merchants'
|
url: '/merchant-partners'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Opérateurs',
|
label: 'Opérateurs',
|
||||||
|
|||||||
@ -12,88 +12,150 @@ export class PermissionsService {
|
|||||||
// Dashboard
|
// Dashboard
|
||||||
{
|
{
|
||||||
module: 'dcb-dashboard',
|
module: 'dcb-dashboard',
|
||||||
roles: ['admin', 'merchant', 'support'],
|
roles: [
|
||||||
|
'dcb-admin',
|
||||||
|
'dcb-partner',
|
||||||
|
'dcb-support',
|
||||||
|
'dcb-partner-admin',
|
||||||
|
'dcb-partner-manager',
|
||||||
|
'dcb-partner-support'
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Transactions
|
// Transactions
|
||||||
{
|
{
|
||||||
module: 'transactions',
|
module: 'transactions',
|
||||||
roles: ['admin', 'merchant', 'support'],
|
roles: [
|
||||||
|
'dcb-admin',
|
||||||
|
'dcb-partner',
|
||||||
|
'dcb-support',
|
||||||
|
'dcb-partner-admin',
|
||||||
|
'dcb-partner-manager',
|
||||||
|
'dcb-partner-support'
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Merchants
|
// Merchants/Partners
|
||||||
{
|
{
|
||||||
module: 'merchants',
|
module: 'merchant-partners',
|
||||||
roles: ['admin', 'merchant', 'support'],
|
roles: [
|
||||||
|
'dcb-admin',
|
||||||
|
'dcb-partner',
|
||||||
|
'dcb-support',
|
||||||
|
'dcb-partner-admin',
|
||||||
|
'dcb-partner-manager',
|
||||||
|
'dcb-partner-support'],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Operators (Admin only)
|
// Operators (Admin only)
|
||||||
{
|
{
|
||||||
module: 'operators',
|
module: 'operators',
|
||||||
roles: ['admin'],
|
roles: ['dcb-admin'],
|
||||||
children: {
|
children: {
|
||||||
'config': ['admin'],
|
'config': ['dcb-admin'],
|
||||||
'stats': ['admin']
|
'stats': ['dcb-admin']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Webhooks
|
// Webhooks
|
||||||
{
|
{
|
||||||
module: 'webhooks',
|
module: 'webhooks',
|
||||||
roles: ['admin', 'merchant'],
|
roles: ['dcb-admin', 'dcb-partner'],
|
||||||
children: {
|
children: {
|
||||||
'history': ['admin', 'merchant'],
|
'history': ['dcb-admin', 'dcb-partner'],
|
||||||
'status': ['admin', 'merchant'],
|
'status': ['dcb-admin', 'dcb-partner'],
|
||||||
'retry': ['admin']
|
'retry': ['dcb-admin']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Users (Admin only)
|
// Users (Admin only)
|
||||||
{
|
{
|
||||||
module: 'users',
|
module: 'users',
|
||||||
roles: ['admin']
|
roles: ['dcb-admin', 'dcb-support']
|
||||||
},
|
},
|
||||||
|
|
||||||
// Support (All authenticated users)
|
// Support (All authenticated users)
|
||||||
{
|
{
|
||||||
module: 'settings',
|
module: 'settings',
|
||||||
roles: ['admin', 'merchant', 'support']
|
roles: [
|
||||||
|
'dcb-admin',
|
||||||
|
'dcb-partner',
|
||||||
|
'dcb-support',
|
||||||
|
'dcb-partner-admin',
|
||||||
|
'dcb-partner-manager',
|
||||||
|
'dcb-partner-support'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// Integrations (Admin only)
|
// Integrations (Admin only)
|
||||||
{
|
{
|
||||||
module: 'integrations',
|
module: 'integrations',
|
||||||
roles: ['admin']
|
roles: ['dcb-admin']
|
||||||
},
|
},
|
||||||
|
|
||||||
// Support (All authenticated users)
|
// Support (All authenticated users)
|
||||||
{
|
{
|
||||||
module: 'support',
|
module: 'support',
|
||||||
roles: ['admin', 'merchant', 'support']
|
roles: [
|
||||||
|
'dcb-admin',
|
||||||
|
'dcb-partner',
|
||||||
|
'dcb-support',
|
||||||
|
'dcb-partner-admin',
|
||||||
|
'dcb-partner-manager',
|
||||||
|
'dcb-partner-support'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// Profile (All authenticated users)
|
// Profile (All authenticated users)
|
||||||
{
|
{
|
||||||
module: 'profile',
|
module: 'profile',
|
||||||
roles: ['admin', 'merchant', 'support']
|
roles: [
|
||||||
|
'dcb-admin',
|
||||||
|
'dcb-partner',
|
||||||
|
'dcb-support',
|
||||||
|
'dcb-partner-admin',
|
||||||
|
'dcb-partner-manager',
|
||||||
|
'dcb-partner-support'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// Documentation (All authenticated users)
|
// Documentation (All authenticated users)
|
||||||
{
|
{
|
||||||
module: 'documentation',
|
module: 'documentation',
|
||||||
roles: ['admin', 'merchant', 'support']
|
roles: [
|
||||||
|
'dcb-admin',
|
||||||
|
'dcb-partner',
|
||||||
|
'dcb-support',
|
||||||
|
'dcb-partner-admin',
|
||||||
|
'dcb-partner-manager',
|
||||||
|
'dcb-partner-support'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// Help (All authenticated users)
|
// Help (All authenticated users)
|
||||||
{
|
{
|
||||||
module: 'help',
|
module: 'help',
|
||||||
roles: ['admin', 'merchant', 'support']
|
roles: [
|
||||||
|
'dcb-admin',
|
||||||
|
'dcb-partner',
|
||||||
|
'dcb-support',
|
||||||
|
'dcb-partner-admin',
|
||||||
|
'dcb-partner-manager',
|
||||||
|
'dcb-partner-support'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// About (All authenticated users)
|
// About (All authenticated users)
|
||||||
{
|
{
|
||||||
module: 'about',
|
module: 'about',
|
||||||
roles: ['admin', 'merchant', 'support']
|
roles: [
|
||||||
|
'dcb-admin',
|
||||||
|
'dcb-partner',
|
||||||
|
'dcb-support',
|
||||||
|
'dcb-partner-admin',
|
||||||
|
'dcb-partner-manager',
|
||||||
|
'dcb-partner-support'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
378
src/app/core/services/role-management.service.ts
Normal file
378
src/app/core/services/role-management.service.ts
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
// src/app/core/services/role-management.service.ts
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { HubUsersService, UserRole } from '../../modules/users/services/users.service';
|
||||||
|
import { BehaviorSubject, Observable, map, tap, of, catchError } from 'rxjs';
|
||||||
|
|
||||||
|
export interface RolePermission {
|
||||||
|
canCreateUsers: boolean;
|
||||||
|
canEditUsers: boolean;
|
||||||
|
canDeleteUsers: boolean;
|
||||||
|
canManageRoles: boolean;
|
||||||
|
canViewStats: boolean;
|
||||||
|
canManageMerchants: boolean;
|
||||||
|
canAccessAdmin: boolean;
|
||||||
|
canAccessSupport: boolean;
|
||||||
|
canAccessPartner: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface simplifiée pour la réponse API
|
||||||
|
export interface AvailableRoleResponse {
|
||||||
|
value: UserRole;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailableRolesResponse {
|
||||||
|
roles: AvailableRoleResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface étendue pour l'usage interne avec les permissions
|
||||||
|
export interface AvailableRole extends AvailableRoleResponse {
|
||||||
|
permissions: RolePermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailableRolesWithPermissions {
|
||||||
|
roles: AvailableRole[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class RoleManagementService {
|
||||||
|
private hubUsersService = inject(HubUsersService);
|
||||||
|
|
||||||
|
private availableRoles$ = new BehaviorSubject<AvailableRolesWithPermissions | null>(null);
|
||||||
|
private currentUserRole$ = new BehaviorSubject<UserRole | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge les rôles disponibles depuis l'API et les enrichit avec les permissions
|
||||||
|
*/
|
||||||
|
loadAvailableRoles(): Observable<AvailableRolesWithPermissions> {
|
||||||
|
return this.hubUsersService.getAvailableHubRoles().pipe(
|
||||||
|
map(apiResponse => {
|
||||||
|
// Enrichir les rôles de l'API avec les permissions
|
||||||
|
const rolesWithPermissions: AvailableRole[] = apiResponse.roles.map(role => ({
|
||||||
|
...role,
|
||||||
|
permissions: this.getPermissionsForRole(role.value)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result: AvailableRolesWithPermissions = {
|
||||||
|
roles: rolesWithPermissions
|
||||||
|
};
|
||||||
|
|
||||||
|
this.availableRoles$.next(result);
|
||||||
|
return result;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error loading available roles:', error);
|
||||||
|
// Fallback avec les rôles par défaut
|
||||||
|
const defaultRoles: AvailableRolesWithPermissions = {
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
value: UserRole.DCB_ADMIN,
|
||||||
|
label: 'DCB Admin',
|
||||||
|
description: 'Full administrative access to the entire system',
|
||||||
|
permissions: this.getPermissionsForRole(UserRole.DCB_ADMIN)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: UserRole.DCB_SUPPORT,
|
||||||
|
label: 'DCB Support',
|
||||||
|
description: 'Support access with limited administrative capabilities',
|
||||||
|
permissions: this.getPermissionsForRole(UserRole.DCB_SUPPORT)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: UserRole.DCB_PARTNER,
|
||||||
|
label: 'DCB Partner',
|
||||||
|
description: 'Merchant partner with access to their own merchant ecosystem',
|
||||||
|
permissions: this.getPermissionsForRole(UserRole.DCB_PARTNER)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
this.availableRoles$.next(defaultRoles);
|
||||||
|
return of(defaultRoles);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les rôles disponibles depuis le cache ou l'API
|
||||||
|
*/
|
||||||
|
getAvailableRoles(): Observable<AvailableRolesWithPermissions> {
|
||||||
|
const cached = this.availableRoles$.value;
|
||||||
|
if (cached) {
|
||||||
|
return of(cached);
|
||||||
|
}
|
||||||
|
return this.loadAvailableRoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les rôles disponibles sous forme simplifiée (pour les selects)
|
||||||
|
*/
|
||||||
|
getAvailableRolesSimple(): Observable<AvailableRoleResponse[]> {
|
||||||
|
return this.getAvailableRoles().pipe(
|
||||||
|
map(response => response.roles.map(role => ({
|
||||||
|
value: role.value,
|
||||||
|
label: role.label,
|
||||||
|
description: role.description
|
||||||
|
})))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... (le reste des méthodes reste identique)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Définit le rôle de l'utilisateur courant
|
||||||
|
*/
|
||||||
|
setCurrentUserRole(role: UserRole): void {
|
||||||
|
this.currentUserRole$.next(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le rôle de l'utilisateur courant
|
||||||
|
*/
|
||||||
|
getCurrentUserRole(): Observable<UserRole | null> {
|
||||||
|
return this.currentUserRole$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les permissions détaillées selon le rôle
|
||||||
|
*/
|
||||||
|
getPermissionsForRole(role: UserRole): RolePermission {
|
||||||
|
switch (role) {
|
||||||
|
case UserRole.DCB_ADMIN:
|
||||||
|
return {
|
||||||
|
canCreateUsers: true,
|
||||||
|
canEditUsers: true,
|
||||||
|
canDeleteUsers: true,
|
||||||
|
canManageRoles: true,
|
||||||
|
canViewStats: true,
|
||||||
|
canManageMerchants: true,
|
||||||
|
canAccessAdmin: true,
|
||||||
|
canAccessSupport: true,
|
||||||
|
canAccessPartner: true
|
||||||
|
};
|
||||||
|
|
||||||
|
case UserRole.DCB_SUPPORT:
|
||||||
|
return {
|
||||||
|
canCreateUsers: true,
|
||||||
|
canEditUsers: true,
|
||||||
|
canDeleteUsers: false,
|
||||||
|
canManageRoles: false,
|
||||||
|
canViewStats: true,
|
||||||
|
canManageMerchants: true,
|
||||||
|
canAccessAdmin: false,
|
||||||
|
canAccessSupport: true,
|
||||||
|
canAccessPartner: true
|
||||||
|
};
|
||||||
|
|
||||||
|
case UserRole.DCB_PARTNER:
|
||||||
|
return {
|
||||||
|
canCreateUsers: false,
|
||||||
|
canEditUsers: false,
|
||||||
|
canDeleteUsers: false,
|
||||||
|
canManageRoles: false,
|
||||||
|
canViewStats: false,
|
||||||
|
canManageMerchants: false,
|
||||||
|
canAccessAdmin: false,
|
||||||
|
canAccessSupport: false,
|
||||||
|
canAccessPartner: true
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
canCreateUsers: false,
|
||||||
|
canEditUsers: false,
|
||||||
|
canDeleteUsers: false,
|
||||||
|
canManageRoles: false,
|
||||||
|
canViewStats: false,
|
||||||
|
canManageMerchants: false,
|
||||||
|
canAccessAdmin: false,
|
||||||
|
canAccessSupport: false,
|
||||||
|
canAccessPartner: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un rôle peut être attribué par l'utilisateur courant
|
||||||
|
*/
|
||||||
|
canAssignRole(currentUserRole: UserRole | null, targetRole: UserRole): boolean {
|
||||||
|
if (!currentUserRole) return false;
|
||||||
|
|
||||||
|
// Seuls les admins peuvent attribuer tous les rôles
|
||||||
|
if (currentUserRole === UserRole.DCB_ADMIN) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Les supports ne peuvent créer que d'autres supports
|
||||||
|
if (currentUserRole === UserRole.DCB_SUPPORT) {
|
||||||
|
return targetRole === UserRole.DCB_SUPPORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur courant peut créer des utilisateurs
|
||||||
|
*/
|
||||||
|
canCreateUsers(currentUserRole: UserRole | null): boolean {
|
||||||
|
return currentUserRole ? this.getPermissionsForRole(currentUserRole).canCreateUsers : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur courant peut éditer des utilisateurs
|
||||||
|
*/
|
||||||
|
canEditUsers(currentUserRole: UserRole | null): boolean {
|
||||||
|
return currentUserRole ? this.getPermissionsForRole(currentUserRole).canEditUsers : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur courant peut supprimer des utilisateurs
|
||||||
|
*/
|
||||||
|
canDeleteUsers(currentUserRole: UserRole | null): boolean {
|
||||||
|
return currentUserRole ? this.getPermissionsForRole(currentUserRole).canDeleteUsers : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur courant peut gérer les rôles
|
||||||
|
*/
|
||||||
|
canManageRoles(currentUserRole: UserRole | null): boolean {
|
||||||
|
return currentUserRole ? this.getPermissionsForRole(currentUserRole).canManageRoles : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur courant peut accéder aux statistiques
|
||||||
|
*/
|
||||||
|
canViewStats(currentUserRole: UserRole | null): boolean {
|
||||||
|
return currentUserRole ? this.getPermissionsForRole(currentUserRole).canViewStats : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur courant peut gérer les merchants
|
||||||
|
*/
|
||||||
|
canManageMerchants(currentUserRole: UserRole | null): boolean {
|
||||||
|
return currentUserRole ? this.getPermissionsForRole(currentUserRole).canManageMerchants : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur courant peut accéder à l'admin
|
||||||
|
*/
|
||||||
|
canAccessAdmin(currentUserRole: UserRole | null): boolean {
|
||||||
|
return currentUserRole ? this.getPermissionsForRole(currentUserRole).canAccessAdmin : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur courant peut accéder au support
|
||||||
|
*/
|
||||||
|
canAccessSupport(currentUserRole: UserRole | null): boolean {
|
||||||
|
return currentUserRole ? this.getPermissionsForRole(currentUserRole).canAccessSupport : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur courant peut accéder à l'espace partenaire
|
||||||
|
*/
|
||||||
|
canAccessPartner(currentUserRole: UserRole | null): boolean {
|
||||||
|
return currentUserRole ? this.getPermissionsForRole(currentUserRole).canAccessPartner : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le libellé d'un rôle
|
||||||
|
*/
|
||||||
|
getRoleLabel(role: UserRole): string {
|
||||||
|
switch (role) {
|
||||||
|
case UserRole.DCB_ADMIN:
|
||||||
|
return 'Administrateur DCB';
|
||||||
|
case UserRole.DCB_SUPPORT:
|
||||||
|
return 'Support DCB';
|
||||||
|
case UserRole.DCB_PARTNER:
|
||||||
|
return 'Partenaire DCB';
|
||||||
|
default:
|
||||||
|
return 'Rôle inconnu';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la classe CSS pour un badge de rôle
|
||||||
|
*/
|
||||||
|
getRoleBadgeClass(role: UserRole): string {
|
||||||
|
switch (role) {
|
||||||
|
case UserRole.DCB_ADMIN:
|
||||||
|
return 'bg-danger';
|
||||||
|
case UserRole.DCB_SUPPORT:
|
||||||
|
return 'bg-info';
|
||||||
|
case UserRole.DCB_PARTNER:
|
||||||
|
return 'bg-success';
|
||||||
|
default:
|
||||||
|
return 'bg-secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'icône pour un rôle
|
||||||
|
*/
|
||||||
|
getRoleIcon(role: UserRole): string {
|
||||||
|
switch (role) {
|
||||||
|
case UserRole.DCB_ADMIN:
|
||||||
|
return 'lucideShield';
|
||||||
|
case UserRole.DCB_SUPPORT:
|
||||||
|
return 'lucideHeadphones';
|
||||||
|
case UserRole.DCB_PARTNER:
|
||||||
|
return 'lucideBuilding';
|
||||||
|
default:
|
||||||
|
return 'lucideUser';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un rôle est un rôle administrateur
|
||||||
|
*/
|
||||||
|
isAdminRole(role: UserRole): boolean {
|
||||||
|
return role === UserRole.DCB_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un rôle est un rôle support
|
||||||
|
*/
|
||||||
|
isSupportRole(role: UserRole): boolean {
|
||||||
|
return role === UserRole.DCB_SUPPORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un rôle est un rôle partenaire
|
||||||
|
*/
|
||||||
|
isPartnerRole(role: UserRole): boolean {
|
||||||
|
return role === UserRole.DCB_PARTNER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère tous les rôles disponibles sous forme de tableau
|
||||||
|
*/
|
||||||
|
getAllRoles(): UserRole[] {
|
||||||
|
return [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les rôles que l'utilisateur courant peut attribuer
|
||||||
|
*/
|
||||||
|
getAssignableRoles(currentUserRole: UserRole | null): UserRole[] {
|
||||||
|
if (!currentUserRole) return [];
|
||||||
|
|
||||||
|
if (currentUserRole === UserRole.DCB_ADMIN) {
|
||||||
|
return this.getAllRoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUserRole === UserRole.DCB_SUPPORT) {
|
||||||
|
return [UserRole.DCB_SUPPORT];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réinitialise le cache des rôles
|
||||||
|
*/
|
||||||
|
clearCache(): void {
|
||||||
|
this.availableRoles$.next(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/app/core/services/role.service.ts
Normal file
117
src/app/core/services/role.service.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// src/app/core/services/role.service.ts
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
|
||||||
|
interface DecodedToken {
|
||||||
|
exp: number;
|
||||||
|
sub?: string;
|
||||||
|
preferred_username?: string;
|
||||||
|
email?: string;
|
||||||
|
given_name?: string;
|
||||||
|
family_name?: string;
|
||||||
|
resource_access?: {
|
||||||
|
[key: string]: {
|
||||||
|
roles: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class RoleService {
|
||||||
|
private userRoles$ = new BehaviorSubject<string[]>(this.extractRolesFromToken());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les rôles actuels de l'utilisateur
|
||||||
|
*/
|
||||||
|
getCurrentUserRoles(): string[] {
|
||||||
|
return this.userRoles$.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable des changements de rôles
|
||||||
|
*/
|
||||||
|
onRolesChange(): Observable<string[]> {
|
||||||
|
return this.userRoles$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rafraîchit les rôles depuis le token
|
||||||
|
*/
|
||||||
|
refreshRoles(token?: string): void {
|
||||||
|
const roles = this.extractRolesFromToken(token);
|
||||||
|
this.userRoles$.next(roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === VERIFICATIONS SPECIFIQUES ===
|
||||||
|
|
||||||
|
isAdmin(): boolean {
|
||||||
|
return this.hasRole('dcb-admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
isPartner(): boolean {
|
||||||
|
return this.hasRole('dcb-partner');
|
||||||
|
}
|
||||||
|
|
||||||
|
isSupport(): boolean {
|
||||||
|
return this.hasRole('dcb-support');
|
||||||
|
}
|
||||||
|
|
||||||
|
isPartnerAdmin(): boolean {
|
||||||
|
return this.hasRole('dcb-partner-admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
isPartnerManager(): boolean {
|
||||||
|
return this.hasRole('dcb-partner-merchant');
|
||||||
|
}
|
||||||
|
|
||||||
|
isPartnerSupport(): boolean {
|
||||||
|
return this.hasRole('dcb-partner-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));
|
||||||
|
}
|
||||||
|
|
||||||
|
hasRole(role: string): boolean {
|
||||||
|
return this.getCurrentUserRoles().includes(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === METHODES PRIVEES ===
|
||||||
|
|
||||||
|
private extractRolesFromToken(token?: string): string[] {
|
||||||
|
const accessToken = token || localStorage.getItem('access_token');
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded: DecodedToken = jwtDecode(accessToken);
|
||||||
|
|
||||||
|
if (decoded.resource_access) {
|
||||||
|
const allRoles: string[] = [];
|
||||||
|
|
||||||
|
Object.values(decoded.resource_access).forEach(client => {
|
||||||
|
if (client?.roles) {
|
||||||
|
allRoles.push(...client.roles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...new Set(allRoles)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -66,9 +66,9 @@ export const menuItems: MenuItemType[] = [
|
|||||||
url: '/transactions',
|
url: '/transactions',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Gestions Marchant',
|
label: 'Gestions Merchants/Partners',
|
||||||
icon: 'lucideStore',
|
icon: 'lucideStore',
|
||||||
url: '/merchants'
|
url: '/merchant-partners'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Opérateurs',
|
label: 'Opérateurs',
|
||||||
|
|||||||
BIN
src/app/modules.zip
Normal file
BIN
src/app/modules.zip
Normal file
Binary file not shown.
@ -1,10 +0,0 @@
|
|||||||
import { Routes } from '@angular/router'
|
|
||||||
import { SignIn } from '@/app/modules/auth/sign-in'
|
|
||||||
|
|
||||||
export const Auth_ROUTES: Routes = [
|
|
||||||
{
|
|
||||||
path: 'auth/sign-in',
|
|
||||||
component: SignIn,
|
|
||||||
data: { title: 'Sign In' },
|
|
||||||
},
|
|
||||||
]
|
|
||||||
41
src/app/modules/auth/auth.routes.ts
Normal file
41
src/app/modules/auth/auth.routes.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Routes } from '@angular/router'
|
||||||
|
import { SignIn } from '@/app/modules/auth/sign-in'
|
||||||
|
import { ResetPassword } from '@/app/modules/auth/reset-password'
|
||||||
|
import { NewPassword } from '@/app/modules/auth/new-password'
|
||||||
|
import { publicGuard } from '@core/guards/public.guard';
|
||||||
|
|
||||||
|
export const AUTH_ROUTES: Routes = [
|
||||||
|
{
|
||||||
|
path: 'sign-in',
|
||||||
|
component: SignIn,
|
||||||
|
canActivate: [publicGuard],
|
||||||
|
data: {
|
||||||
|
title: 'Connexion - DCB',
|
||||||
|
module: 'auth'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reset-password',
|
||||||
|
component: ResetPassword,
|
||||||
|
title: 'Réinitialisation Mot de Passe - DCB',
|
||||||
|
canActivate: [publicGuard],
|
||||||
|
data: {
|
||||||
|
title: 'Réinitialisation Mot de Passe - DCB',
|
||||||
|
module: 'auth'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'forgot-password',
|
||||||
|
component: NewPassword,
|
||||||
|
canActivate: [publicGuard],
|
||||||
|
data: {
|
||||||
|
title: 'Mot de Passe Oublié - DCB',
|
||||||
|
module: 'auth'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'logout',
|
||||||
|
redirectTo: 'sign-in',
|
||||||
|
data: { module: 'auth' }
|
||||||
|
}
|
||||||
|
];
|
||||||
@ -4,18 +4,20 @@ import { Unauthorized } from '../unauthorized'
|
|||||||
|
|
||||||
export const ERROR_PAGES_ROUTES: Routes = [
|
export const ERROR_PAGES_ROUTES: Routes = [
|
||||||
{
|
{
|
||||||
path: 'error/404',
|
path: '404', //
|
||||||
component: Error404,
|
component: Error404,
|
||||||
data: { title: 'Page Non Trouvée' },
|
data: { title: 'Page Non Trouvée' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'error/403',
|
path: '403', //
|
||||||
component: Unauthorized,
|
component: Unauthorized,
|
||||||
data: { title: 'Accès Refusé' },
|
data: { title: 'Accès Refusé' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'unauthorized',
|
path: '500', //
|
||||||
component: Unauthorized,
|
component: Error404,
|
||||||
data: { title: 'Accès Refusé' },
|
data: { title: 'Erreur Serveur' },
|
||||||
}
|
},
|
||||||
|
// Redirection par défaut
|
||||||
|
{ path: '', redirectTo: '404', pathMatch: 'full' }
|
||||||
]
|
]
|
||||||
162
src/app/modules/auth/new-password.ts
Normal file
162
src/app/modules/auth/new-password.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { credits, currentYear } from '@/app/constants'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { PasswordStrengthBar } from '@app/components/password-strength-bar'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import { AppLogo } from '@app/components/app-logo'
|
||||||
|
import { NgOtpInputModule } from 'ng-otp-input'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-new-password',
|
||||||
|
imports: [
|
||||||
|
RouterLink,
|
||||||
|
PasswordStrengthBar,
|
||||||
|
FormsModule,
|
||||||
|
AppLogo,
|
||||||
|
NgOtpInputModule,
|
||||||
|
],
|
||||||
|
template: `
|
||||||
|
<div class="auth-box overflow-hidden align-items-center d-flex">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-xxl-4 col-md-6 col-sm-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="auth-brand mb-4">
|
||||||
|
<app-app-logo />
|
||||||
|
<p class="text-muted mt-3">
|
||||||
|
We've emailed you a 6-digit verification code. Please enter
|
||||||
|
it below to confirm your email address
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="userEmail" class="form-label"
|
||||||
|
>Email address <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
id="userEmail"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"
|
||||||
|
>Enter your 6-digit code
|
||||||
|
<span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<ng-otp-input
|
||||||
|
[config]="{
|
||||||
|
length: 6,
|
||||||
|
allowNumbersOnly: true,
|
||||||
|
inputClass: 'form-control text-center',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
</ng-otp-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" data-password="bar">
|
||||||
|
<label for="userPassword" class="form-label"
|
||||||
|
>Password <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
[(ngModel)]="password"
|
||||||
|
class="form-control"
|
||||||
|
id="userPassword"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<app-password-strength-bar [password]="password" />
|
||||||
|
<div class="password-bar my-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="userNewPassword" class="form-label"
|
||||||
|
>Confirm New Password
|
||||||
|
<span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="userNewPassword"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input form-check-input-light fs-14"
|
||||||
|
type="checkbox"
|
||||||
|
id="termAndPolicy"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="termAndPolicy"
|
||||||
|
>Agree the Terms & Policy</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary fw-semibold py-2"
|
||||||
|
>
|
||||||
|
Update Password
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="mt-4 text-muted text-center mb-4">
|
||||||
|
Don’t have a code?
|
||||||
|
<a
|
||||||
|
href="javascript:void(0);"
|
||||||
|
class="text-decoration-underline link-offset-2 fw-semibold"
|
||||||
|
>Resend</a
|
||||||
|
>
|
||||||
|
or
|
||||||
|
<a
|
||||||
|
href="javascript:void(0);"
|
||||||
|
class="text-decoration-underline link-offset-2 fw-semibold"
|
||||||
|
>Call Us</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p class="text-muted text-center mb-0">
|
||||||
|
Return to
|
||||||
|
<a
|
||||||
|
routerLink="/auth/sign-in"
|
||||||
|
class="text-decoration-underline link-offset-3 fw-semibold"
|
||||||
|
>Sign in</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center text-muted mt-4 mb-0">
|
||||||
|
© {{ currentYear }} Simple — by
|
||||||
|
<span class="fw-semibold">{{ credits.name }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class NewPassword {
|
||||||
|
password: string = ''
|
||||||
|
protected readonly currentYear = currentYear
|
||||||
|
protected readonly credits = credits
|
||||||
|
}
|
||||||
88
src/app/modules/auth/reset-password.ts
Normal file
88
src/app/modules/auth/reset-password.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { credits, currentYear } from '@/app/constants'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { AppLogo } from '@app/components/app-logo'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-reset-password',
|
||||||
|
imports: [RouterLink, AppLogo],
|
||||||
|
template: `
|
||||||
|
<div class="auth-box overflow-hidden align-items-center d-flex">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-xxl-4 col-md-6 col-sm-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="auth-brand mb-4">
|
||||||
|
<app-app-logo />
|
||||||
|
<p class="text-muted w-lg-75 mt-3">
|
||||||
|
Enter your email address and we'll send you a link to reset
|
||||||
|
your password.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="userEmail" class="form-label"
|
||||||
|
>Email address <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
id="userEmail"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input form-check-input-light fs-14"
|
||||||
|
type="checkbox"
|
||||||
|
id="termAndPolicy"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="termAndPolicy"
|
||||||
|
>Agree the Terms & Policy</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary fw-semibold py-2"
|
||||||
|
>
|
||||||
|
Send Request
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="text-muted text-center mt-4 mb-0">
|
||||||
|
Return to
|
||||||
|
<a
|
||||||
|
routerLink="/auth/sign-in"
|
||||||
|
class="text-decoration-underline link-offset-3 fw-semibold"
|
||||||
|
>Sign in</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center text-muted mt-4 mb-0">
|
||||||
|
© {{ currentYear }} Simple — by
|
||||||
|
<span class="fw-semibold">{{ credits.name }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class ResetPassword {
|
||||||
|
protected readonly currentYear = currentYear
|
||||||
|
protected readonly credits = credits
|
||||||
|
}
|
||||||
@ -1,8 +1,11 @@
|
|||||||
import { Component, inject, ChangeDetectorRef } from '@angular/core';
|
// src/app/modules/auth/sign-in/sign-in.ts
|
||||||
|
import { Component, inject, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule, NgForm } from '@angular/forms';
|
import { FormsModule, NgForm } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router, RouterLink } from '@angular/router';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
import { AuthService, LoginDto } from '@core/services/auth.service';
|
||||||
|
import { RoleService } from '@core/services/role.service';
|
||||||
import { AppLogo } from '@app/components/app-logo';
|
import { AppLogo } from '@app/components/app-logo';
|
||||||
import { PasswordStrengthBar } from '@app/components/password-strength-bar';
|
import { PasswordStrengthBar } from '@app/components/password-strength-bar';
|
||||||
import { appName, credits, currentYear } from '@/app/constants';
|
import { appName, credits, currentYear } from '@/app/constants';
|
||||||
@ -10,7 +13,7 @@ import { appName, credits, currentYear } from '@/app/constants';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-sign-in',
|
selector: 'app-sign-in',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [FormsModule, CommonModule, AppLogo, PasswordStrengthBar],
|
imports: [FormsModule, CommonModule, RouterLink, AppLogo, PasswordStrengthBar],
|
||||||
template: `
|
template: `
|
||||||
<div class="auth-box overflow-hidden align-items-center d-flex">
|
<div class="auth-box overflow-hidden align-items-center d-flex">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -25,7 +28,7 @@ import { appName, credits, currentYear } from '@/app/constants';
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm)" novalidate>
|
<form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm)" novalidate autocomplete="on">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">
|
<label for="username" class="form-label">
|
||||||
Username <span class="text-danger">*</span>
|
Username <span class="text-danger">*</span>
|
||||||
@ -35,14 +38,20 @@ import { appName, credits, currentYear } from '@/app/constants';
|
|||||||
id="username"
|
id="username"
|
||||||
name="username"
|
name="username"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Enter username"
|
placeholder="Enter your Username"
|
||||||
required
|
required
|
||||||
[(ngModel)]="username"
|
username
|
||||||
|
[(ngModel)]="credentials.username"
|
||||||
#usernameCtrl="ngModel"
|
#usernameCtrl="ngModel"
|
||||||
[class.is-invalid]="usernameCtrl.invalid && usernameCtrl.touched"
|
[class.is-invalid]="usernameCtrl.invalid && usernameCtrl.touched"
|
||||||
|
[attr.aria-describedby]="usernameCtrl.invalid && usernameCtrl.touched ? 'username-error' : null"
|
||||||
|
autocomplete="username"
|
||||||
/>
|
/>
|
||||||
<div class="invalid-feedback">
|
<div *ngIf="usernameCtrl.invalid && usernameCtrl.touched"
|
||||||
Username is required
|
id="username-error"
|
||||||
|
class="invalid-feedback">
|
||||||
|
<span *ngIf="usernameCtrl.errors?.['required']">Username is required</span>
|
||||||
|
<span *ngIf="usernameCtrl.errors?.['username']">Please enter a valid email address</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -57,16 +66,27 @@ import { appName, credits, currentYear } from '@/app/constants';
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
required
|
required
|
||||||
[(ngModel)]="password"
|
minlength="6"
|
||||||
|
[(ngModel)]="credentials.password"
|
||||||
#passwordCtrl="ngModel"
|
#passwordCtrl="ngModel"
|
||||||
[class.is-invalid]="passwordCtrl.invalid && passwordCtrl.touched"
|
[class.is-invalid]="passwordCtrl.invalid && passwordCtrl.touched"
|
||||||
|
[attr.aria-describedby]="passwordCtrl.invalid && passwordCtrl.touched ? 'password-error' : null"
|
||||||
|
autocomplete="current-password"
|
||||||
/>
|
/>
|
||||||
<div class="invalid-feedback">
|
<div *ngIf="passwordCtrl.invalid && passwordCtrl.touched"
|
||||||
Password is required
|
id="password-error"
|
||||||
|
class="invalid-feedback">
|
||||||
|
<span *ngIf="passwordCtrl.errors?.['required']">Password is required</span>
|
||||||
|
<span *ngIf="passwordCtrl.errors?.['minlength']">
|
||||||
|
Password must be at least 6 characters
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Password strength bar -->
|
<!-- Password strength bar -->
|
||||||
<app-password-strength-bar [password]="password || ''"></app-password-strength-bar>
|
<app-password-strength-bar
|
||||||
|
[password]="credentials.password || ''"
|
||||||
|
class="mt-2"
|
||||||
|
></app-password-strength-bar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Server-side error message -->
|
<!-- Server-side error message -->
|
||||||
@ -74,6 +94,7 @@ import { appName, credits, currentYear } from '@/app/constants';
|
|||||||
id="error-message"
|
id="error-message"
|
||||||
class="alert alert-danger mt-3"
|
class="alert alert-danger mt-3"
|
||||||
role="alert"
|
role="alert"
|
||||||
|
aria-live="polite"
|
||||||
tabindex="-1">
|
tabindex="-1">
|
||||||
<i class="ri-error-warning-line me-2"></i>
|
<i class="ri-error-warning-line me-2"></i>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
@ -105,7 +126,9 @@ import { appName, credits, currentYear } from '@/app/constants';
|
|||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-primary fw-semibold py-2"
|
class="btn btn-primary fw-semibold py-2"
|
||||||
[disabled]="loginForm.invalid || loading"
|
[disabled]="loginForm.invalid || loading"
|
||||||
|
[attr.aria-busy]="loading"
|
||||||
>
|
>
|
||||||
|
<span *ngIf="loading" class="spinner-border spinner-border-sm me-2" role="status"></span>
|
||||||
{{ loading ? 'Signing In...' : 'Sign In' }}
|
{{ loading ? 'Signing In...' : 'Sign In' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -124,50 +147,155 @@ import { appName, credits, currentYear } from '@/app/constants';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
styles: []
|
||||||
})
|
})
|
||||||
export class SignIn {
|
export class SignIn implements OnInit, OnDestroy {
|
||||||
protected readonly appName = appName;
|
protected readonly appName = appName;
|
||||||
protected readonly currentYear = currentYear;
|
protected readonly currentYear = currentYear;
|
||||||
protected readonly credits = credits;
|
protected readonly credits = credits;
|
||||||
|
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
|
private roleService = inject(RoleService);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
username: string = '';
|
credentials: LoginDto = {
|
||||||
password: string = '';
|
username: '',
|
||||||
|
password: ''
|
||||||
|
};
|
||||||
|
|
||||||
rememberMe: boolean = false;
|
rememberMe: boolean = false;
|
||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
errorMessage: string | null = null;
|
errorMessage: string | null = null;
|
||||||
|
|
||||||
onSubmit(form: NgForm) {
|
ngOnInit(): void {
|
||||||
|
// Vérifier si l'utilisateur est déjà connecté
|
||||||
|
this.checkExistingAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur a déjà une session valide
|
||||||
|
*/
|
||||||
|
private checkExistingAuth(): void {
|
||||||
|
if (this.authService.isAuthenticated()) {
|
||||||
|
this.redirectBasedOnRoles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soumission du formulaire de connexion
|
||||||
|
*/
|
||||||
|
onSubmit(form: NgForm): void {
|
||||||
|
// Marquer tous les champs comme touchés pour afficher les erreurs
|
||||||
if (form.invalid) {
|
if (form.invalid) {
|
||||||
form.control.markAllAsTouched();
|
this.markFormGroupTouched(form);
|
||||||
|
this.focusFirstInvalidField();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.errorMessage = null;
|
this.errorMessage = null;
|
||||||
|
|
||||||
this.authService.login(this.username, this.password).subscribe({
|
this.authService.login(this.credentials)
|
||||||
next: (res) => {
|
.pipe(takeUntil(this.destroy$))
|
||||||
this.router.navigate(['/dcb-dashboard']);
|
.subscribe({
|
||||||
this.loading = false;
|
next: () => {
|
||||||
},
|
this.handleLoginSuccess();
|
||||||
error: (err) => {
|
},
|
||||||
this.errorMessage = err.error?.message || 'Login failed';
|
error: (error) => {
|
||||||
this.loading = false;
|
this.handleLoginError(error);
|
||||||
|
}
|
||||||
// Forcer la mise à jour de la vue
|
});
|
||||||
this.cdRef.detectChanges();
|
}
|
||||||
|
|
||||||
// Scroll et focus sur l'erreur
|
/**
|
||||||
this.scrollToError();
|
* Gestion de la connexion réussie
|
||||||
},
|
*/
|
||||||
|
private handleLoginSuccess(): void {
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
// Rafraîchir les rôles après connexion
|
||||||
|
this.roleService.refreshRoles();
|
||||||
|
|
||||||
|
// Rediriger en fonction des rôles
|
||||||
|
this.redirectBasedOnRoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestion des erreurs de connexion
|
||||||
|
*/
|
||||||
|
private handleLoginError(error: any): void {
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
// Messages d'erreur spécifiques selon le type d'erreur
|
||||||
|
if (error.status === 401) {
|
||||||
|
this.errorMessage = 'Invalid email or password. Please try again.';
|
||||||
|
} else if (error.status === 403) {
|
||||||
|
this.errorMessage = 'Your account is disabled or not fully set up. Please contact support.';
|
||||||
|
} else if (error.status === 0 || error.status === 500) {
|
||||||
|
this.errorMessage = 'Server is unavailable. Please try again later.';
|
||||||
|
} else {
|
||||||
|
this.errorMessage = error.message || 'An unexpected error occurred. Please try again.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forcer la mise à jour de la vue
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
|
||||||
|
// Scroll et focus sur l'erreur
|
||||||
|
this.scrollToError();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirection basée sur les rôles de l'utilisateur
|
||||||
|
*/
|
||||||
|
private redirectBasedOnRoles(): void {
|
||||||
|
const userRoles = this.roleService.getCurrentUserRoles();
|
||||||
|
|
||||||
|
// Logique de redirection selon les rôles
|
||||||
|
if (userRoles.includes('dcb-admin')) {
|
||||||
|
this.router.navigate(['/admin/dcb-dashboard']);
|
||||||
|
} else if (userRoles.includes('dcb-partner-admin')) {
|
||||||
|
this.router.navigate(['/partner/dcb-dashboard']);
|
||||||
|
} else if (userRoles.includes('dcb-support')) {
|
||||||
|
this.router.navigate(['/support/dcb-dashboard']);
|
||||||
|
} else {
|
||||||
|
// Route par défaut
|
||||||
|
this.router.navigate(['/dcb-dashboard']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marque tous les champs du formulaire comme touchés
|
||||||
|
*/
|
||||||
|
private markFormGroupTouched(form: NgForm): void {
|
||||||
|
Object.keys(form.controls).forEach(key => {
|
||||||
|
const control = form.controls[key];
|
||||||
|
control.markAsTouched();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private scrollToError() {
|
/**
|
||||||
|
* Focus sur le premier champ invalide
|
||||||
|
*/
|
||||||
|
private focusFirstInvalidField(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const firstInvalidElement = document.querySelector('.is-invalid') as HTMLElement;
|
||||||
|
if (firstInvalidElement) {
|
||||||
|
firstInvalidElement.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll vers le message d'erreur
|
||||||
|
*/
|
||||||
|
private scrollToError(): void {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const errorElement = document.getElementById('error-message');
|
const errorElement = document.getElementById('error-message');
|
||||||
if (errorElement) {
|
if (errorElement) {
|
||||||
@ -179,4 +307,14 @@ export class SignIn {
|
|||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réinitialise le formulaire
|
||||||
|
*/
|
||||||
|
resetForm(form: NgForm): void {
|
||||||
|
form.resetForm();
|
||||||
|
this.credentials = { username: '', password: '' };
|
||||||
|
this.errorMessage = null;
|
||||||
|
this.rememberMe = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
92
src/app/modules/auth/two-factor.ts
Normal file
92
src/app/modules/auth/two-factor.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { AppLogo } from '@app/components/app-logo'
|
||||||
|
import { NgOtpInputComponent } from 'ng-otp-input'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { credits, currentYear } from '@/app/constants'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-two-factor',
|
||||||
|
imports: [AppLogo, NgOtpInputComponent, RouterLink],
|
||||||
|
template: `
|
||||||
|
<div class="auth-box overflow-hidden align-items-center d-flex">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-xxl-4 col-md-6 col-sm-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="auth-brand mb-4">
|
||||||
|
<app-app-logo />
|
||||||
|
<p class="text-muted w-lg-75 mt-3">
|
||||||
|
We've emailed you a 6-digit verification code we sent to
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<div class="fw-bold fs-4">+ (12) ******6789</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<label class="form-label"
|
||||||
|
>Enter your 6-digit code
|
||||||
|
<span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<ngx-otp-input
|
||||||
|
[config]="{
|
||||||
|
length: 6,
|
||||||
|
allowNumbersOnly: true,
|
||||||
|
inputClass: 'form-control text-center mb-3',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
</ngx-otp-input>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary fw-semibold py-2"
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="mt-4 text-muted text-center mb-4">
|
||||||
|
Don’t have a code?
|
||||||
|
<a
|
||||||
|
href="javascript:void(0);"
|
||||||
|
class="text-decoration-underline link-offset-2 fw-semibold"
|
||||||
|
>Resend</a
|
||||||
|
>
|
||||||
|
or
|
||||||
|
<a
|
||||||
|
href="javascript:void(0);"
|
||||||
|
class="text-decoration-underline link-offset-2 fw-semibold"
|
||||||
|
>Call Us</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p class="text-muted text-center mb-0">
|
||||||
|
Return to
|
||||||
|
<a
|
||||||
|
routerLink="/auth/sign-in"
|
||||||
|
class="text-decoration-underline link-offset-3 fw-semibold"
|
||||||
|
>Sign in</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center text-muted mt-4 mb-0">
|
||||||
|
©
|
||||||
|
{{ currentYear }}
|
||||||
|
Simple — by <span class="fw-semibold">{{ credits.name }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class TwoFactor {
|
||||||
|
protected readonly currentYear = currentYear
|
||||||
|
protected readonly credits = credits
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { UiCard } from '@app/components/ui-card'
|
import { UiCard } from '@app/components/ui-card'
|
||||||
import { wizardSteps } from '@/app/modules/merchants/data'
|
import { wizardSteps } from '@/app/modules/components/data'
|
||||||
import { NgIcon } from '@ng-icons/core'
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@ -1,253 +1,34 @@
|
|||||||
export const paginationIcons = {
|
import { WizardStepType } from '@/app/modules/components/types'
|
||||||
first: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevrons-left"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M11 7l-5 5l5 5" /><path d="M17 7l-5 5l5 5" /></svg>`,
|
|
||||||
previous: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 6l-6 6l6 6" /></svg>`,
|
|
||||||
next: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 6l6 6l-6 6" /></svg>`,
|
|
||||||
last: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevrons-right"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7l5 5l-5 5" /><path d="M13 7l5 5l-5 5" /></svg>`,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const states = [
|
export const wizardSteps: WizardStepType[] = [
|
||||||
'Alabama',
|
|
||||||
'Alaska',
|
|
||||||
'American Samoa',
|
|
||||||
'Arizona',
|
|
||||||
'Arkansas',
|
|
||||||
'California',
|
|
||||||
'Colorado',
|
|
||||||
'Connecticut',
|
|
||||||
'Delaware',
|
|
||||||
'District Of Columbia',
|
|
||||||
'Federated States Of Micronesia',
|
|
||||||
'Florida',
|
|
||||||
'Georgia',
|
|
||||||
'Guam',
|
|
||||||
'Hawaii',
|
|
||||||
'Idaho',
|
|
||||||
'Illinois',
|
|
||||||
'Indiana',
|
|
||||||
'Iowa',
|
|
||||||
'Kansas',
|
|
||||||
'Kentucky',
|
|
||||||
'Louisiana',
|
|
||||||
'Maine',
|
|
||||||
'Marshall Islands',
|
|
||||||
'Maryland',
|
|
||||||
'Massachusetts',
|
|
||||||
'Michigan',
|
|
||||||
'Minnesota',
|
|
||||||
'Mississippi',
|
|
||||||
'Missouri',
|
|
||||||
'Montana',
|
|
||||||
'Nebraska',
|
|
||||||
'Nevada',
|
|
||||||
'New Hampshire',
|
|
||||||
'New Jersey',
|
|
||||||
'New Mexico',
|
|
||||||
'New York',
|
|
||||||
'North Carolina',
|
|
||||||
'North Dakota',
|
|
||||||
'Northern Mariana Islands',
|
|
||||||
'Ohio',
|
|
||||||
'Oklahoma',
|
|
||||||
'Oregon',
|
|
||||||
'Palau',
|
|
||||||
'Pennsylvania',
|
|
||||||
'Puerto Rico',
|
|
||||||
'Rhode Island',
|
|
||||||
'South Carolina',
|
|
||||||
'South Dakota',
|
|
||||||
'Tennessee',
|
|
||||||
'Texas',
|
|
||||||
'Utah',
|
|
||||||
'Vermont',
|
|
||||||
'Virgin Islands',
|
|
||||||
'Virginia',
|
|
||||||
'Washington',
|
|
||||||
'West Virginia',
|
|
||||||
'Wisconsin',
|
|
||||||
'Wyoming',
|
|
||||||
]
|
|
||||||
|
|
||||||
export const statesWithFlags: { name: string; flag: string }[] = [
|
|
||||||
{
|
{
|
||||||
name: 'Alabama',
|
id: 'stuInfo',
|
||||||
flag: '5/5c/Flag_of_Alabama.svg/45px-Flag_of_Alabama.svg.png',
|
icon: 'tablerUserCircle',
|
||||||
|
title: 'Student Info',
|
||||||
|
subtitle: 'Personal details',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Alaska',
|
id: 'addrInfo',
|
||||||
flag: 'e/e6/Flag_of_Alaska.svg/43px-Flag_of_Alaska.svg.png',
|
icon: 'tablerMapPin',
|
||||||
|
title: 'Address Info',
|
||||||
|
subtitle: 'Where you live',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Arizona',
|
id: 'courseInfo',
|
||||||
flag: '9/9d/Flag_of_Arizona.svg/45px-Flag_of_Arizona.svg.png',
|
icon: 'tablerBook',
|
||||||
|
title: 'Course Info',
|
||||||
|
subtitle: 'Select your course',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Arkansas',
|
id: 'parentInfo',
|
||||||
flag: '9/9d/Flag_of_Arkansas.svg/45px-Flag_of_Arkansas.svg.png',
|
icon: 'tablerUsers',
|
||||||
|
title: 'Parent Info',
|
||||||
|
subtitle: 'Guardian details',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'California',
|
id: 'documents',
|
||||||
flag: '0/01/Flag_of_California.svg/45px-Flag_of_California.svg.png',
|
icon: 'tablerFolder',
|
||||||
},
|
title: 'Documents',
|
||||||
{
|
subtitle: 'Upload certificates',
|
||||||
name: 'Colorado',
|
|
||||||
flag: '4/46/Flag_of_Colorado.svg/45px-Flag_of_Colorado.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Connecticut',
|
|
||||||
flag: '9/96/Flag_of_Connecticut.svg/39px-Flag_of_Connecticut.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Delaware',
|
|
||||||
flag: 'c/c6/Flag_of_Delaware.svg/45px-Flag_of_Delaware.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Florida',
|
|
||||||
flag: 'f/f7/Flag_of_Florida.svg/45px-Flag_of_Florida.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Georgia',
|
|
||||||
flag: '5/54/Flag_of_Georgia_%28U.S._state%29.svg/46px-Flag_of_Georgia_%28U.S._state%29.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Hawaii',
|
|
||||||
flag: 'e/ef/Flag_of_Hawaii.svg/46px-Flag_of_Hawaii.svg.png',
|
|
||||||
},
|
|
||||||
{ name: 'Idaho', flag: 'a/a4/Flag_of_Idaho.svg/38px-Flag_of_Idaho.svg.png' },
|
|
||||||
{
|
|
||||||
name: 'Illinois',
|
|
||||||
flag: '0/01/Flag_of_Illinois.svg/46px-Flag_of_Illinois.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Indiana',
|
|
||||||
flag: 'a/ac/Flag_of_Indiana.svg/45px-Flag_of_Indiana.svg.png',
|
|
||||||
},
|
|
||||||
{ name: 'Iowa', flag: 'a/aa/Flag_of_Iowa.svg/44px-Flag_of_Iowa.svg.png' },
|
|
||||||
{
|
|
||||||
name: 'Kansas',
|
|
||||||
flag: 'd/da/Flag_of_Kansas.svg/46px-Flag_of_Kansas.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Kentucky',
|
|
||||||
flag: '8/8d/Flag_of_Kentucky.svg/46px-Flag_of_Kentucky.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Louisiana',
|
|
||||||
flag: 'e/e0/Flag_of_Louisiana.svg/46px-Flag_of_Louisiana.svg.png',
|
|
||||||
},
|
|
||||||
{ name: 'Maine', flag: '3/35/Flag_of_Maine.svg/45px-Flag_of_Maine.svg.png' },
|
|
||||||
{
|
|
||||||
name: 'Maryland',
|
|
||||||
flag: 'a/a0/Flag_of_Maryland.svg/45px-Flag_of_Maryland.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Massachusetts',
|
|
||||||
flag: 'f/f2/Flag_of_Massachusetts.svg/46px-Flag_of_Massachusetts.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Michigan',
|
|
||||||
flag: 'b/b5/Flag_of_Michigan.svg/45px-Flag_of_Michigan.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Minnesota',
|
|
||||||
flag: 'b/b9/Flag_of_Minnesota.svg/46px-Flag_of_Minnesota.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Mississippi',
|
|
||||||
flag: '4/42/Flag_of_Mississippi.svg/45px-Flag_of_Mississippi.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Missouri',
|
|
||||||
flag: '5/5a/Flag_of_Missouri.svg/46px-Flag_of_Missouri.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Montana',
|
|
||||||
flag: 'c/cb/Flag_of_Montana.svg/45px-Flag_of_Montana.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Nebraska',
|
|
||||||
flag: '4/4d/Flag_of_Nebraska.svg/46px-Flag_of_Nebraska.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Nevada',
|
|
||||||
flag: 'f/f1/Flag_of_Nevada.svg/45px-Flag_of_Nevada.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'New Hampshire',
|
|
||||||
flag: '2/28/Flag_of_New_Hampshire.svg/45px-Flag_of_New_Hampshire.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'New Jersey',
|
|
||||||
flag: '9/92/Flag_of_New_Jersey.svg/45px-Flag_of_New_Jersey.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'New Mexico',
|
|
||||||
flag: 'c/c3/Flag_of_New_Mexico.svg/45px-Flag_of_New_Mexico.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'New York',
|
|
||||||
flag: '1/1a/Flag_of_New_York.svg/46px-Flag_of_New_York.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'North Carolina',
|
|
||||||
flag: 'b/bb/Flag_of_North_Carolina.svg/45px-Flag_of_North_Carolina.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'North Dakota',
|
|
||||||
flag: 'e/ee/Flag_of_North_Dakota.svg/38px-Flag_of_North_Dakota.svg.png',
|
|
||||||
},
|
|
||||||
{ name: 'Ohio', flag: '4/4c/Flag_of_Ohio.svg/46px-Flag_of_Ohio.svg.png' },
|
|
||||||
{
|
|
||||||
name: 'Oklahoma',
|
|
||||||
flag: '6/6e/Flag_of_Oklahoma.svg/45px-Flag_of_Oklahoma.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Oregon',
|
|
||||||
flag: 'b/b9/Flag_of_Oregon.svg/46px-Flag_of_Oregon.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Pennsylvania',
|
|
||||||
flag: 'f/f7/Flag_of_Pennsylvania.svg/45px-Flag_of_Pennsylvania.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Rhode Island',
|
|
||||||
flag: 'f/f3/Flag_of_Rhode_Island.svg/32px-Flag_of_Rhode_Island.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'South Carolina',
|
|
||||||
flag: '6/69/Flag_of_South_Carolina.svg/45px-Flag_of_South_Carolina.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'South Dakota',
|
|
||||||
flag: '1/1a/Flag_of_South_Dakota.svg/46px-Flag_of_South_Dakota.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Tennessee',
|
|
||||||
flag: '9/9e/Flag_of_Tennessee.svg/46px-Flag_of_Tennessee.svg.png',
|
|
||||||
},
|
|
||||||
{ name: 'Texas', flag: 'f/f7/Flag_of_Texas.svg/45px-Flag_of_Texas.svg.png' },
|
|
||||||
{ name: 'Utah', flag: 'f/f6/Flag_of_Utah.svg/45px-Flag_of_Utah.svg.png' },
|
|
||||||
{
|
|
||||||
name: 'Vermont',
|
|
||||||
flag: '4/49/Flag_of_Vermont.svg/46px-Flag_of_Vermont.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Virginia',
|
|
||||||
flag: '4/47/Flag_of_Virginia.svg/44px-Flag_of_Virginia.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Washington',
|
|
||||||
flag: '5/54/Flag_of_Washington.svg/46px-Flag_of_Washington.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'West Virginia',
|
|
||||||
flag: '2/22/Flag_of_West_Virginia.svg/46px-Flag_of_West_Virginia.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Wisconsin',
|
|
||||||
flag: '2/22/Flag_of_Wisconsin.svg/45px-Flag_of_Wisconsin.svg.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Wyoming',
|
|
||||||
flag: 'b/bc/Flag_of_Wyoming.svg/43px-Flag_of_Wyoming.svg.png',
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,254 +0,0 @@
|
|||||||
import { Component, ViewChild } from '@angular/core'
|
|
||||||
import { UiCard } from '@app/components/ui-card'
|
|
||||||
import { NgIcon } from '@ng-icons/core'
|
|
||||||
import { FormsModule } from '@angular/forms'
|
|
||||||
import { NgbTypeahead, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import {
|
|
||||||
debounceTime,
|
|
||||||
distinctUntilChanged,
|
|
||||||
filter,
|
|
||||||
map,
|
|
||||||
merge,
|
|
||||||
Observable,
|
|
||||||
OperatorFunction,
|
|
||||||
Subject,
|
|
||||||
} from 'rxjs'
|
|
||||||
import {
|
|
||||||
states,
|
|
||||||
statesWithFlags,
|
|
||||||
} from '@/app/modules/components/data'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-typeaheds',
|
|
||||||
imports: [UiCard, NgIcon, FormsModule, NgbTypeaheadModule],
|
|
||||||
template: `
|
|
||||||
<app-ui-card title="Typeahead" bodyClass="p-0">
|
|
||||||
<div card-body>
|
|
||||||
<div class="card-body rounded-bottom-0">
|
|
||||||
<p class="text-muted mb-2">
|
|
||||||
A flexible JavaScript library that provides a strong foundation for
|
|
||||||
building robust typeaheads
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<a
|
|
||||||
class="btn btn-link shadow-none p-0 fw-semibold"
|
|
||||||
href="https://twitter.github.io/typeahead.js/"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
View Official Website
|
|
||||||
<ng-icon name="tablerChevronRight" class="ms-1" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body rounded-top-0 border-top-0">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<h5 class="fw-semibold mb-1">Basic</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<input
|
|
||||||
class="form-control typeahead"
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="basicTypeahead"
|
|
||||||
[ngbTypeahead]="search"
|
|
||||||
placeholder="Enter states from USA"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-4 border-top border-dashed"></div>
|
|
||||||
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<h5 class="fw-semibold mb-1">Open on focus</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<input
|
|
||||||
class="form-control bloodhound-typeahead"
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="focusTypeahead"
|
|
||||||
[ngbTypeahead]="searchFocusTypeahead"
|
|
||||||
(focus)="focus$.next($any($event).target.value)"
|
|
||||||
(click)="click$.next($any($event).target.value)"
|
|
||||||
#instance="ngbTypeahead"
|
|
||||||
placeholder="Enter states from USA"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-4 border-top border-dashed"></div>
|
|
||||||
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<h5 class="fw-semibold mb-1">Formatted results</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="formattedTypeahead"
|
|
||||||
[ngbTypeahead]="formatterSearch"
|
|
||||||
[resultFormatter]="formatter"
|
|
||||||
placeholder="Enter states from USA"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-4 border-top border-dashed"></div>
|
|
||||||
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<h5 class="fw-semibold mb-1">Select on exact</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="exactSearchTypeahead"
|
|
||||||
[ngbTypeahead]="searchExact"
|
|
||||||
[inputFormatter]="exactFormatter"
|
|
||||||
[resultFormatter]="exactFormatter"
|
|
||||||
[selectOnExact]="true"
|
|
||||||
placeholder="Search Exact"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-4 border-top border-dashed"></div>
|
|
||||||
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<h5 class="fw-semibold mb-1">Custom Template</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<ng-template #rt let-r="result" let-t="term">
|
|
||||||
<img
|
|
||||||
[src]="
|
|
||||||
'https://upload.wikimedia.org/wikipedia/commons/thumb/' +
|
|
||||||
r['flag']
|
|
||||||
"
|
|
||||||
class="me-1"
|
|
||||||
style="width: 16px"
|
|
||||||
/>
|
|
||||||
<ngb-highlight [result]="r.name" [term]="t"></ngb-highlight>
|
|
||||||
</ng-template>
|
|
||||||
<input
|
|
||||||
class="form-control custom-template-typeahead"
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="customTypeahead"
|
|
||||||
[ngbTypeahead]="searchWithFlags"
|
|
||||||
[resultTemplate]="rt"
|
|
||||||
[inputFormatter]="nameFormatter"
|
|
||||||
placeholder="custom template"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</app-ui-card>
|
|
||||||
`,
|
|
||||||
styles: ``,
|
|
||||||
})
|
|
||||||
export class Typeaheds {
|
|
||||||
basicTypeahead: any
|
|
||||||
focusTypeahead: any
|
|
||||||
formattedTypeahead: any
|
|
||||||
exactSearchTypeahead: any
|
|
||||||
customTypeahead: any
|
|
||||||
|
|
||||||
search: OperatorFunction<string, readonly string[]> = (
|
|
||||||
text$: Observable<string>
|
|
||||||
) =>
|
|
||||||
text$.pipe(
|
|
||||||
debounceTime(200),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
map((term) =>
|
|
||||||
term.length < 2
|
|
||||||
? []
|
|
||||||
: states
|
|
||||||
.filter((v) => v.toLowerCase().indexOf(term.toLowerCase()) > -1)
|
|
||||||
.slice(0, 10)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@ViewChild('instance', { static: true }) instance!: NgbTypeahead
|
|
||||||
|
|
||||||
focus$ = new Subject<string>()
|
|
||||||
click$ = new Subject<string>()
|
|
||||||
|
|
||||||
searchFocusTypeahead: OperatorFunction<string, readonly string[]> = (
|
|
||||||
text$: Observable<string>
|
|
||||||
) => {
|
|
||||||
const debouncedText$ = text$.pipe(debounceTime(200), distinctUntilChanged())
|
|
||||||
const clicksWithClosedPopup$ = this.click$.pipe(
|
|
||||||
filter(() => !this.instance.isPopupOpen())
|
|
||||||
)
|
|
||||||
const inputFocus$ = this.focus$
|
|
||||||
|
|
||||||
return merge(debouncedText$, inputFocus$, clicksWithClosedPopup$).pipe(
|
|
||||||
map((term) =>
|
|
||||||
(term === ''
|
|
||||||
? states
|
|
||||||
: states.filter(
|
|
||||||
(v) => v.toLowerCase().indexOf(term.toLowerCase()) > -1
|
|
||||||
)
|
|
||||||
).slice(0, 10)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
formatter = (result: string) => result.toUpperCase()
|
|
||||||
|
|
||||||
formatterSearch: OperatorFunction<string, readonly string[]> = (
|
|
||||||
text$: Observable<string>
|
|
||||||
) =>
|
|
||||||
text$.pipe(
|
|
||||||
debounceTime(200),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
map((term) =>
|
|
||||||
term === ''
|
|
||||||
? []
|
|
||||||
: states
|
|
||||||
.filter((v) => v.toLowerCase().indexOf(term.toLowerCase()) > -1)
|
|
||||||
.slice(0, 10)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
searchExact: OperatorFunction<string, readonly string[]> = (
|
|
||||||
text$: Observable<string>
|
|
||||||
) =>
|
|
||||||
text$.pipe(
|
|
||||||
debounceTime(200),
|
|
||||||
map((term) =>
|
|
||||||
term === ''
|
|
||||||
? []
|
|
||||||
: states
|
|
||||||
.filter((v) => v.toLowerCase().indexOf(term.toLowerCase()) > -1)
|
|
||||||
.slice(0, 10)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
exactFormatter = (x: string) => x
|
|
||||||
|
|
||||||
searchWithFlags: OperatorFunction<
|
|
||||||
string,
|
|
||||||
readonly {
|
|
||||||
name: string
|
|
||||||
flag: string
|
|
||||||
}[]
|
|
||||||
> = (text$: Observable<string>) =>
|
|
||||||
text$.pipe(
|
|
||||||
debounceTime(200),
|
|
||||||
map((term) =>
|
|
||||||
term === ''
|
|
||||||
? []
|
|
||||||
: statesWithFlags
|
|
||||||
.filter(
|
|
||||||
(v) => v.name.toLowerCase().indexOf(term.toLowerCase()) > -1
|
|
||||||
)
|
|
||||||
.slice(0, 10)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
nameFormatter = (x: { name: string }) => x.name
|
|
||||||
}
|
|
||||||
@ -2,7 +2,7 @@ import { Component } from '@angular/core'
|
|||||||
import { UiCard } from '@app/components/ui-card'
|
import { UiCard } from '@app/components/ui-card'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgIcon } from '@ng-icons/core'
|
import { NgIcon } from '@ng-icons/core'
|
||||||
import { wizardSteps } from '@/app/modules/merchants/data'
|
import { wizardSteps } from '@/app/modules/components/data'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-vertical-wizard',
|
selector: 'app-vertical-wizard',
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { NgIcon } from '@ng-icons/core'
|
|||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { UiCard } from '@app/components/ui-card'
|
import { UiCard } from '@app/components/ui-card'
|
||||||
import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { wizardSteps } from '@/app/modules/merchants/data'
|
import { wizardSteps } from '@/app/modules/components/data'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-wizard-with-progress',
|
selector: 'app-wizard-with-progress',
|
||||||
|
|||||||
@ -7,7 +7,11 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
<app-basic-wizard />
|
||||||
|
|
||||||
<app-wizard-with-progress />
|
<app-wizard-with-progress />
|
||||||
|
|
||||||
|
<app-vertical-wizard />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { PageTitle } from '@app/components/page-title/page-title'
|
import { PageTitle } from '@app/components/page-title/page-title'
|
||||||
|
import { BasicWizard } from '@/app/modules/components/basic-wizard'
|
||||||
import { WizardWithProgress } from '@/app/modules/components/wizard-with-progress'
|
import { WizardWithProgress } from '@/app/modules/components/wizard-with-progress'
|
||||||
|
import { VerticalWizard } from '@/app/modules/components/vertical-wizard'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-merchant-wizard',
|
selector: 'app-wizard',
|
||||||
imports: [PageTitle, WizardWithProgress],
|
imports: [PageTitle, BasicWizard, WizardWithProgress, VerticalWizard],
|
||||||
templateUrl: './wizard.html',
|
templateUrl: './wizard.html',
|
||||||
styles: ``,
|
styles: ``,
|
||||||
})
|
})
|
||||||
@ -25,7 +25,7 @@ interface Transaction {
|
|||||||
<div class="card-header justify-content-between align-items-center border-dashed">
|
<div class="card-header justify-content-between align-items-center border-dashed">
|
||||||
<h4 class="card-title mb-0">Transactions Récentes</h4>
|
<h4 class="card-title mb-0">Transactions Récentes</h4>
|
||||||
<a href="javascript:void(0);" class="btn btn-sm btn-primary">
|
<a href="javascript:void(0);" class="btn btn-sm btn-primary">
|
||||||
<ng-icon name="lucideFileExport" class="me-1" />
|
<ng-icon name="lucideFile" class="me-1" />
|
||||||
Exporter
|
Exporter
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
436
src/app/modules/merchant-partners/config/config.html
Normal file
436
src/app/modules/merchant-partners/config/config.html
Normal file
@ -0,0 +1,436 @@
|
|||||||
|
<app-ui-card title="Configuration Partenaire DCB">
|
||||||
|
<span helper-text class="badge badge-soft-success badge-label fs-xxs py-1">
|
||||||
|
Payment Hub DCB
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="ins-wizard" card-body>
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<ngb-progressbar
|
||||||
|
class="mb-4"
|
||||||
|
[value]="progressValue"
|
||||||
|
type="primary"
|
||||||
|
height="6px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Navigation Steps -->
|
||||||
|
<ul class="nav nav-tabs wizard-tabs" role="tablist">
|
||||||
|
@for (step of wizardSteps; track $index; let i = $index) {
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
href="javascript:void(0);"
|
||||||
|
[class.active]="i === currentStep"
|
||||||
|
class="nav-link"
|
||||||
|
[class.disabled]="!isStepAccessible(i)"
|
||||||
|
[class.wizard-item-done]="i < currentStep"
|
||||||
|
(click)="goToStep(i)"
|
||||||
|
>
|
||||||
|
<span class="d-flex align-items-center">
|
||||||
|
<ng-icon [name]="step.icon" class="fs-32" />
|
||||||
|
<span class="flex-grow-1 ms-2 text-truncate">
|
||||||
|
<span class="mb-0 lh-base d-block fw-semibold text-body fs-base">
|
||||||
|
{{ step.title }}
|
||||||
|
</span>
|
||||||
|
<span class="mb-0 fw-normal">{{ step.subtitle }}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
@if (configError) {
|
||||||
|
<div class="alert alert-danger mt-3">
|
||||||
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
|
{{ configError }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (configSuccess) {
|
||||||
|
<div class="alert alert-success mt-3">
|
||||||
|
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||||
|
{{ configSuccess }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Contenu des Steps -->
|
||||||
|
<div class="tab-content pt-3">
|
||||||
|
@for (step of wizardSteps; track $index; let i = $index) {
|
||||||
|
<div
|
||||||
|
class="tab-pane fade"
|
||||||
|
[class.show]="currentStep === i"
|
||||||
|
[class.active]="currentStep === i"
|
||||||
|
>
|
||||||
|
<form [formGroup]="partnerForm">
|
||||||
|
<!-- Step 1: Informations Société -->
|
||||||
|
@if (i === 0) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Nom commercial *</label>
|
||||||
|
<div formGroupName="companyInfo">
|
||||||
|
<input type="text" class="form-control" formControlName="name"
|
||||||
|
placeholder="Nom commercial" />
|
||||||
|
@if (companyInfo.get('name')?.invalid && companyInfo.get('name')?.touched) {
|
||||||
|
<div class="text-danger small">Le nom commercial est requis</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Raison sociale *</label>
|
||||||
|
<div formGroupName="companyInfo">
|
||||||
|
<input type="text" class="form-control" formControlName="legalName"
|
||||||
|
placeholder="Raison sociale" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Email *</label>
|
||||||
|
<div formGroupName="companyInfo">
|
||||||
|
<input type="email" class="form-control" formControlName="email"
|
||||||
|
placeholder="email@entreprise.com" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Téléphone *</label>
|
||||||
|
<div formGroupName="companyInfo">
|
||||||
|
<input type="tel" class="form-control" formControlName="phone"
|
||||||
|
placeholder="+225 XX XX XX XX" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Site web</label>
|
||||||
|
<div formGroupName="companyInfo">
|
||||||
|
<input type="url" class="form-control" formControlName="website"
|
||||||
|
placeholder="https://..." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Catégorie *</label>
|
||||||
|
<div formGroupName="companyInfo">
|
||||||
|
<select class="form-select" formControlName="category">
|
||||||
|
@for (cat of categories; track cat.value) {
|
||||||
|
<option [value]="cat.value">{{ cat.label }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Pays *</label>
|
||||||
|
<div formGroupName="companyInfo">
|
||||||
|
<select class="form-select" formControlName="country">
|
||||||
|
@for (country of countries; track country.code) {
|
||||||
|
<option [value]="country.code">{{ country.name }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Devise *</label>
|
||||||
|
<div formGroupName="companyInfo">
|
||||||
|
<select class="form-select" formControlName="currency">
|
||||||
|
@for (currency of currencies; track currency.code) {
|
||||||
|
<option [value]="currency.code">{{ currency.name }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Fuseau horaire *</label>
|
||||||
|
<div formGroupName="companyInfo">
|
||||||
|
<select class="form-select" formControlName="timezone">
|
||||||
|
@for (tz of timezones; track tz.value) {
|
||||||
|
<option [value]="tz.value">{{ tz.label }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Step 2: Adresse et Contact -->
|
||||||
|
@if (i === 1) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mb-4">
|
||||||
|
<h6 class="border-bottom pb-2">Adresse de l'entreprise</h6>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<label class="form-label">Rue *</label>
|
||||||
|
<div formGroupName="addressInfo">
|
||||||
|
<input type="text" class="form-control" formControlName="street"
|
||||||
|
placeholder="Adresse complète" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Ville *</label>
|
||||||
|
<div formGroupName="addressInfo">
|
||||||
|
<input type="text" class="form-control" formControlName="city"
|
||||||
|
placeholder="Ville" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Région *</label>
|
||||||
|
<div formGroupName="addressInfo">
|
||||||
|
<input type="text" class="form-control" formControlName="state"
|
||||||
|
placeholder="Région" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Code postal *</label>
|
||||||
|
<div formGroupName="addressInfo">
|
||||||
|
<input type="text" class="form-control" formControlName="postalCode"
|
||||||
|
placeholder="Code postal" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 mb-4 mt-4">
|
||||||
|
<h6 class="border-bottom pb-2">Contact technique</h6>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Nom complet *</label>
|
||||||
|
<div formGroupName="technicalContact">
|
||||||
|
<input type="text" class="form-control" formControlName="name"
|
||||||
|
placeholder="Nom du contact technique" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Email *</label>
|
||||||
|
<div formGroupName="technicalContact">
|
||||||
|
<input type="email" class="form-control" formControlName="email"
|
||||||
|
placeholder="contact@entreprise.com" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<label class="form-label">Téléphone *</label>
|
||||||
|
<div formGroupName="technicalContact">
|
||||||
|
<input type="tel" class="form-control" formControlName="phone"
|
||||||
|
placeholder="+225 XX XX XX XX" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Step 3: Configuration Paiements -->
|
||||||
|
@if (i === 2) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Taux de commission (%) *</label>
|
||||||
|
<div formGroupName="paymentConfig">
|
||||||
|
<input type="number" class="form-control" formControlName="commissionRate"
|
||||||
|
min="0" max="100" step="0.1" />
|
||||||
|
<div class="form-text">Pourcentage prélevé sur chaque transaction</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Limite quotidienne (XOF) *</label>
|
||||||
|
<div formGroupName="paymentConfig">
|
||||||
|
<input type="number" class="form-control" formControlName="dailyLimit"
|
||||||
|
min="1000" />
|
||||||
|
<div class="form-text">Plafond total des transactions par jour</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Limite par transaction (XOF) *</label>
|
||||||
|
<div formGroupName="paymentConfig">
|
||||||
|
<input type="number" class="form-control" formControlName="transactionLimit"
|
||||||
|
min="100" max="500000" />
|
||||||
|
<div class="form-text">Montant maximum par transaction</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Montant minimum (XOF) *</label>
|
||||||
|
<div formGroupName="paymentConfig">
|
||||||
|
<input type="number" class="form-control" formControlName="minAmount"
|
||||||
|
min="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<label class="form-label">Montant maximum (XOF) *</label>
|
||||||
|
<div formGroupName="paymentConfig">
|
||||||
|
<input type="number" class="form-control" formControlName="maxAmount"
|
||||||
|
min="100" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Step 4: Webhooks -->
|
||||||
|
@if (i === 3) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mb-4">
|
||||||
|
<h6 class="border-bottom pb-2">Header Enrichment</h6>
|
||||||
|
<div formGroupName="webhookConfig">
|
||||||
|
<div formGroupName="headerEnrichment">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label">URL de callback</label>
|
||||||
|
<input type="url" class="form-control" formControlName="url"
|
||||||
|
placeholder="https://votre-domaine.com/api/header-enrichment" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Méthode HTTP</label>
|
||||||
|
<select class="form-select" formControlName="method">
|
||||||
|
@for (method of httpMethods; track method.value) {
|
||||||
|
<option [value]="method.value">{{ method.label }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<label class="form-label">Headers HTTP</label>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="addHeader()">
|
||||||
|
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
|
||||||
|
Ajouter un header
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@for (header of headerEnrichmentHeaders.controls; track $index; let idx = $index) {
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-md-5">
|
||||||
|
<!-- CORRECTION ICI : Utilisation de getHeaderControl -->
|
||||||
|
<input type="text" class="form-control"
|
||||||
|
[formControl]="getHeaderControl(header, 'key')"
|
||||||
|
placeholder="Clé (ex: Authorization)" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<!-- CORRECTION ICI : Utilisation de getHeaderControl -->
|
||||||
|
<input type="text" class="form-control"
|
||||||
|
[formControl]="getHeaderControl(header, 'value')"
|
||||||
|
placeholder="Valeur" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger w-100"
|
||||||
|
(click)="removeHeader(idx)">
|
||||||
|
<ng-icon name="lucideTrash2"></ng-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 mb-4">
|
||||||
|
<h6 class="border-bottom pb-2">Webhooks Abonnements</h6>
|
||||||
|
<div formGroupName="webhookConfig">
|
||||||
|
<div formGroupName="subscription">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<label class="form-label">Création d'abonnement</label>
|
||||||
|
<input type="url" class="form-control" formControlName="onCreate"
|
||||||
|
placeholder="https://votre-domaine.com/webhooks/subscription-created" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<label class="form-label">Renouvellement</label>
|
||||||
|
<input type="url" class="form-control" formControlName="onRenew"
|
||||||
|
placeholder="https://votre-domaine.com/webhooks/subscription-renewed" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<label class="form-label">Annulation</label>
|
||||||
|
<input type="url" class="form-control" formControlName="onCancel"
|
||||||
|
placeholder="https://votre-domaine.com/webhooks/subscription-cancelled" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<label class="form-label">Expiration</label>
|
||||||
|
<input type="url" class="form-control" formControlName="onExpire"
|
||||||
|
placeholder="https://votre-domaine.com/webhooks/subscription-expired" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 mb-4">
|
||||||
|
<h6 class="border-bottom pb-2">Webhooks Paiements</h6>
|
||||||
|
<div formGroupName="webhookConfig">
|
||||||
|
<div formGroupName="payment">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-2">
|
||||||
|
<label class="form-label">Paiement réussi</label>
|
||||||
|
<input type="url" class="form-control" formControlName="onSuccess"
|
||||||
|
placeholder="https://votre-domaine.com/webhooks/payment-success" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-2">
|
||||||
|
<label class="form-label">Paiement échoué</label>
|
||||||
|
<input type="url" class="form-control" formControlName="onFailure"
|
||||||
|
placeholder="https://votre-domaine.com/webhooks/payment-failed" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-2">
|
||||||
|
<label class="form-label">Remboursement</label>
|
||||||
|
<input type="url" class="form-control" formControlName="onRefund"
|
||||||
|
placeholder="https://votre-domaine.com/webhooks/payment-refunded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Step 5: Validation -->
|
||||||
|
@if (i === 4) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||||
|
Vérifiez les informations avant de créer le partenaire
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Récapitulatif</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Informations Société:</strong><br>
|
||||||
|
{{ companyInfo.value.name || 'Non renseigné' }}<br>
|
||||||
|
{{ companyInfo.value.legalName || 'Non renseigné' }}<br>
|
||||||
|
{{ companyInfo.value.email || 'Non renseigné' }}<br>
|
||||||
|
{{ companyInfo.value.phone || 'Non renseigné' }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Configuration:</strong><br>
|
||||||
|
Commission: {{ paymentConfig.value.commissionRate || 0 }}%<br>
|
||||||
|
Limite quotidienne: {{ (paymentConfig.value.dailyLimit || 0) | number }} XOF<br>
|
||||||
|
Limite transaction: {{ (paymentConfig.value.transactionLimit || 0) | number }} XOF
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Navigation Buttons -->
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
@if (i > 0) {
|
||||||
|
<button type="button" class="btn btn-secondary" (click)="previousStep()">
|
||||||
|
← Précédent
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<div></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (i < wizardSteps.length - 1) {
|
||||||
|
<button type="button" class="btn btn-primary" (click)="nextStep()"
|
||||||
|
[disabled]="!isStepValid(i)">
|
||||||
|
Suivant →
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button type="button" class="btn btn-success"
|
||||||
|
(click)="submitForm()" [disabled]="configLoading">
|
||||||
|
@if (configLoading) {
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
Créer le Partenaire
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-ui-card>
|
||||||
2
src/app/modules/merchant-partners/config/config.spec.ts
Normal file
2
src/app/modules/merchant-partners/config/config.spec.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { PartnerConfig } from './config';
|
||||||
|
describe('PartnerConfig', () => {});
|
||||||
317
src/app/modules/merchant-partners/config/config.ts
Normal file
317
src/app/modules/merchant-partners/config/config.ts
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormGroup, FormControl } from '@angular/forms';
|
||||||
|
import { NgIcon } from '@ng-icons/core';
|
||||||
|
import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { UiCard } from '@app/components/ui-card';
|
||||||
|
import { PartnerConfigService } from '../services/partner-config.service';
|
||||||
|
import { CreatePartnerDto, PartnerCategory } from '../models/partners-config.model';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-partner-config',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgIcon,
|
||||||
|
NgbProgressbarModule,
|
||||||
|
UiCard
|
||||||
|
],
|
||||||
|
templateUrl: './config.html'
|
||||||
|
})
|
||||||
|
export class PartnerConfig implements OnInit {
|
||||||
|
private fb = inject(FormBuilder);
|
||||||
|
private PartnerConfigService = inject(PartnerConfigService);
|
||||||
|
|
||||||
|
// Configuration wizard
|
||||||
|
currentStep = 0;
|
||||||
|
wizardSteps = [
|
||||||
|
{ id: 'company-info', icon: 'lucideBuilding', title: 'Informations Société', subtitle: 'Détails entreprise' },
|
||||||
|
{ id: 'contact-info', icon: 'lucideUser', title: 'Contact Principal', subtitle: 'Personne de contact' },
|
||||||
|
{ id: 'payment-config', icon: 'lucideCreditCard', title: 'Configuration Paiements', subtitle: 'Paramètres DCB' },
|
||||||
|
{ id: 'webhooks', icon: 'lucideWebhook', title: 'Webhooks', subtitle: 'Notifications et retours' },
|
||||||
|
{ id: 'review', icon: 'lucideCheckCircle', title: 'Validation', subtitle: 'Vérification finale' }
|
||||||
|
];
|
||||||
|
|
||||||
|
configLoading = false;
|
||||||
|
configError = '';
|
||||||
|
configSuccess = '';
|
||||||
|
|
||||||
|
// Formulaires
|
||||||
|
partnerForm = this.fb.group({
|
||||||
|
companyInfo: this.fb.group({
|
||||||
|
name: ['', [Validators.required, Validators.minLength(2)]],
|
||||||
|
legalName: ['', [Validators.required]],
|
||||||
|
email: ['', [Validators.required, Validators.email]],
|
||||||
|
phone: ['', [Validators.required]],
|
||||||
|
website: [''],
|
||||||
|
category: ['E_COMMERCE', [Validators.required]],
|
||||||
|
country: ['CIV', [Validators.required]],
|
||||||
|
currency: ['XOF', [Validators.required]],
|
||||||
|
timezone: ['Africa/Abidjan', [Validators.required]]
|
||||||
|
}),
|
||||||
|
addressInfo: this.fb.group({
|
||||||
|
street: ['', [Validators.required]],
|
||||||
|
city: ['', [Validators.required]],
|
||||||
|
state: ['', [Validators.required]],
|
||||||
|
postalCode: ['', [Validators.required]],
|
||||||
|
country: ['CIV', [Validators.required]]
|
||||||
|
}),
|
||||||
|
technicalContact: this.fb.group({
|
||||||
|
name: ['', [Validators.required]],
|
||||||
|
email: ['', [Validators.required, Validators.email]],
|
||||||
|
phone: ['', [Validators.required]]
|
||||||
|
}),
|
||||||
|
paymentConfig: this.fb.group({
|
||||||
|
commissionRate: [2.5, [Validators.required, Validators.min(0), Validators.max(100)]],
|
||||||
|
dailyLimit: [1000000, [Validators.required, Validators.min(1000)]],
|
||||||
|
transactionLimit: [50000, [Validators.required, Validators.min(100), Validators.max(500000)]],
|
||||||
|
minAmount: [100, [Validators.required, Validators.min(1)]],
|
||||||
|
maxAmount: [500000, [Validators.required, Validators.min(100)]]
|
||||||
|
}),
|
||||||
|
webhookConfig: this.fb.group({
|
||||||
|
headerEnrichment: this.fb.group({
|
||||||
|
url: ['', [Validators.pattern('https?://.+')]],
|
||||||
|
method: ['POST'],
|
||||||
|
headers: this.fb.array([])
|
||||||
|
}),
|
||||||
|
subscription: this.fb.group({
|
||||||
|
onCreate: ['', [Validators.pattern('https?://.+')]],
|
||||||
|
onRenew: ['', [Validators.pattern('https?://.+')]],
|
||||||
|
onCancel: ['', [Validators.pattern('https?://.+')]],
|
||||||
|
onExpire: ['', [Validators.pattern('https?://.+')]]
|
||||||
|
}),
|
||||||
|
payment: this.fb.group({
|
||||||
|
onSuccess: ['', [Validators.pattern('https?://.+')]],
|
||||||
|
onFailure: ['', [Validators.pattern('https?://.+')]],
|
||||||
|
onRefund: ['', [Validators.pattern('https?://.+')]]
|
||||||
|
}),
|
||||||
|
authentication: this.fb.group({
|
||||||
|
onSuccess: ['', [Validators.pattern('https?://.+')]],
|
||||||
|
onFailure: ['', [Validators.pattern('https?://.+')]]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Données partagées
|
||||||
|
countries = [
|
||||||
|
{ code: 'CIV', name: 'Côte d\'Ivoire' },
|
||||||
|
{ code: 'SEN', name: 'Sénégal' },
|
||||||
|
{ code: 'CMR', name: 'Cameroun' },
|
||||||
|
{ code: 'GHA', name: 'Ghana' },
|
||||||
|
{ code: 'NGA', name: 'Nigeria' }
|
||||||
|
];
|
||||||
|
|
||||||
|
categories = [
|
||||||
|
{ value: 'E_COMMERCE', label: 'E-Commerce' },
|
||||||
|
{ value: 'GAMING', label: 'Jeux & Gaming' },
|
||||||
|
{ value: 'ENTERTAINMENT', label: 'Divertissement' },
|
||||||
|
{ value: 'UTILITIES', label: 'Services Publics' },
|
||||||
|
{ value: 'DIGITAL_CONTENT', label: 'Contenu Digital' },
|
||||||
|
{ value: 'SERVICES', label: 'Services' },
|
||||||
|
{ value: 'OTHER', label: 'Autre' }
|
||||||
|
];
|
||||||
|
|
||||||
|
currencies = [
|
||||||
|
{ code: 'XOF', name: 'Franc CFA' },
|
||||||
|
{ code: 'EUR', name: 'Euro' },
|
||||||
|
{ code: 'USD', name: 'Dollar US' }
|
||||||
|
];
|
||||||
|
|
||||||
|
timezones = [
|
||||||
|
{ value: 'Africa/Abidjan', label: 'Abidjan (GMT)' },
|
||||||
|
{ value: 'Africa/Lagos', label: 'Lagos (WAT)' },
|
||||||
|
{ value: 'Africa/Johannesburg', label: 'Johannesburg (SAST)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
httpMethods = [
|
||||||
|
{ value: 'GET', label: 'GET' },
|
||||||
|
{ value: 'POST', label: 'POST' }
|
||||||
|
];
|
||||||
|
|
||||||
|
ngOnInit() {}
|
||||||
|
|
||||||
|
// Navigation du wizard
|
||||||
|
get progressValue(): number {
|
||||||
|
return ((this.currentStep + 1) / this.wizardSteps.length) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextStep() {
|
||||||
|
if (this.currentStep < this.wizardSteps.length - 1 && this.isStepValid(this.currentStep)) {
|
||||||
|
this.currentStep++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previousStep() {
|
||||||
|
if (this.currentStep > 0) {
|
||||||
|
this.currentStep--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
goToStep(index: number) {
|
||||||
|
if (this.isStepAccessible(index)) {
|
||||||
|
this.currentStep = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isStepAccessible(index: number): boolean {
|
||||||
|
if (index === 0) return true;
|
||||||
|
|
||||||
|
for (let i = 0; i < index; i++) {
|
||||||
|
if (!this.isStepValid(i)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isStepValid(stepIndex: number): boolean {
|
||||||
|
switch (stepIndex) {
|
||||||
|
case 0: // Company Info
|
||||||
|
return this.companyInfo.valid;
|
||||||
|
case 1: // Contact Info
|
||||||
|
return this.addressInfo.valid && this.technicalContact.valid;
|
||||||
|
case 2: // Payment Config
|
||||||
|
return this.paymentConfig.valid;
|
||||||
|
case 3: // Webhooks (toujours valide car optionnel)
|
||||||
|
return true;
|
||||||
|
case 4: // Review
|
||||||
|
return this.partnerForm.valid;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion des headers dynamiques - CORRECTION ICI
|
||||||
|
get headerEnrichmentHeaders(): FormArray {
|
||||||
|
return this.headerEnrichment.get('headers') as FormArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour obtenir un FormControl sécurisé - NOUVELLE MÉTHODE
|
||||||
|
getHeaderControl(header: any, field: string): FormControl {
|
||||||
|
return header.get(field) as FormControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
addHeader() {
|
||||||
|
const headerGroup = this.fb.group({
|
||||||
|
key: ['', Validators.required],
|
||||||
|
value: ['', Validators.required]
|
||||||
|
});
|
||||||
|
this.headerEnrichmentHeaders.push(headerGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeHeader(index: number) {
|
||||||
|
this.headerEnrichmentHeaders.removeAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soumission du formulaire
|
||||||
|
async submitForm() {
|
||||||
|
if (this.partnerForm.valid) {
|
||||||
|
this.configLoading = true;
|
||||||
|
this.configError = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = this.partnerForm.value;
|
||||||
|
|
||||||
|
const createPartnerDto: CreatePartnerDto = {
|
||||||
|
name: this.safeString(formData.companyInfo?.name) || '',
|
||||||
|
legalName: this.safeString(formData.companyInfo?.legalName) || '',
|
||||||
|
email: this.safeString(formData.companyInfo?.email) || '',
|
||||||
|
phone: this.safeString(formData.companyInfo?.phone) || '',
|
||||||
|
website: this.safeString(formData.companyInfo?.website) || '',
|
||||||
|
category: (this.safeString(formData.companyInfo?.category) as PartnerCategory) || 'OTHER',
|
||||||
|
country: this.safeString(formData.companyInfo?.country) || 'CIV',
|
||||||
|
currency: this.safeString(formData.companyInfo?.currency) || 'XOF',
|
||||||
|
timezone: this.safeString(formData.companyInfo?.timezone) || 'Africa/Abidjan',
|
||||||
|
commissionRate: this.safeNumber(formData.paymentConfig?.commissionRate) || 0,
|
||||||
|
dailyLimit: this.safeNumber(formData.paymentConfig?.dailyLimit) || 0,
|
||||||
|
transactionLimit: this.safeNumber(formData.paymentConfig?.transactionLimit) || 0,
|
||||||
|
minAmount: this.safeNumber(formData.paymentConfig?.minAmount) || 0,
|
||||||
|
maxAmount: this.safeNumber(formData.paymentConfig?.maxAmount) || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.PartnerConfigService.createPartnerConfig(createPartnerDto)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
this.configSuccess = `Partenaire créé avec succès! ID: ${response.data.id}`;
|
||||||
|
this.partnerForm.reset();
|
||||||
|
this.currentStep = 0;
|
||||||
|
} else {
|
||||||
|
this.configError = response.error || 'Erreur lors de la création du partenaire';
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.configError = 'Erreur lors de la création du partenaire';
|
||||||
|
console.error('Error creating partner:', error);
|
||||||
|
} finally {
|
||||||
|
this.configLoading = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.configError = 'Veuillez corriger les erreurs dans le formulaire';
|
||||||
|
this.markAllFieldsAsTouched();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes utilitaires
|
||||||
|
private safeString(value: string | null | undefined): string {
|
||||||
|
return value || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private safeNumber(value: number | null | undefined): number {
|
||||||
|
return value || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private markAllFieldsAsTouched() {
|
||||||
|
Object.keys(this.partnerForm.controls).forEach(key => {
|
||||||
|
const control = this.partnerForm.get(key);
|
||||||
|
if (control instanceof FormGroup) {
|
||||||
|
Object.keys(control.controls).forEach(subKey => {
|
||||||
|
control.get(subKey)?.markAsTouched();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
control?.markAsTouched();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters pour les formulaires - CORRECTION ICI (suppression des ?)
|
||||||
|
get companyInfo() {
|
||||||
|
return this.partnerForm.get('companyInfo') as FormGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
get addressInfo() {
|
||||||
|
return this.partnerForm.get('addressInfo') as FormGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
get technicalContact() {
|
||||||
|
return this.partnerForm.get('technicalContact') as FormGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
get paymentConfig() {
|
||||||
|
return this.partnerForm.get('paymentConfig') as FormGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
get webhookConfig() {
|
||||||
|
return this.partnerForm.get('webhookConfig') as FormGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
get headerEnrichment() {
|
||||||
|
return this.webhookConfig.get('headerEnrichment') as FormGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
get subscription() {
|
||||||
|
return this.webhookConfig.get('subscription') as FormGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
get payment() {
|
||||||
|
return this.webhookConfig.get('payment') as FormGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
get authentication() {
|
||||||
|
return this.webhookConfig.get('authentication') as FormGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
399
src/app/modules/merchant-partners/list/list.html
Normal file
399
src/app/modules/merchant-partners/list/list.html
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
<!-- src/app/modules/merchant-users/list/list.html -->
|
||||||
|
<app-ui-card title="Équipe Marchande">
|
||||||
|
<a
|
||||||
|
helper-text
|
||||||
|
href="javascript:void(0);"
|
||||||
|
class="icon-link icon-link-hover link-primary fw-semibold"
|
||||||
|
>
|
||||||
|
@if (canViewAllMerchants) {
|
||||||
|
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||||
|
Vue administrative - Tous les utilisateurs marchands
|
||||||
|
} @else if (isDcbPartner) {
|
||||||
|
<ng-icon name="lucideUsers" class="me-1"></ng-icon>
|
||||||
|
Votre équipe marchande
|
||||||
|
} @else {
|
||||||
|
<ng-icon name="lucideBuilding" class="me-1"></ng-icon>
|
||||||
|
Utilisateurs de votre partenaire marchand
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div card-body>
|
||||||
|
<!-- Indicateur de contexte -->
|
||||||
|
@if (canViewAllMerchants) {
|
||||||
|
<div class="alert alert-info mb-3">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideShield" class="me-2"></ng-icon>
|
||||||
|
<div>
|
||||||
|
<strong>Vue administrative DCB :</strong> Vous visualisez tous les utilisateurs marchands de la plateforme
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else if (isDcbPartner) {
|
||||||
|
<div class="alert alert-primary mb-3">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideBuilding" class="me-2"></ng-icon>
|
||||||
|
<div>
|
||||||
|
<strong>Vue partenaire marchand :</strong> Vous gérez les utilisateurs de votre propre équipe
|
||||||
|
<small class="d-block text-muted">Merchant Partner ID: {{ currentMerchantPartnerId }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Barre d'actions supérieure -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<!-- Statistiques rapides -->
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-primary"
|
||||||
|
[class.active]="roleFilter === 'all'"
|
||||||
|
(click)="filterByRole('all')"
|
||||||
|
>
|
||||||
|
Tous ({{ allUsers.length }})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger"
|
||||||
|
[class.active]="roleFilter === UserRole.DCB_PARTNER_ADMIN"
|
||||||
|
(click)="filterByRole(UserRole.DCB_PARTNER_ADMIN)"
|
||||||
|
>
|
||||||
|
Admins ({{ getUsersCountByRole(UserRole.DCB_PARTNER_ADMIN) }})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-warning text-dark"
|
||||||
|
[class.active]="roleFilter === UserRole.DCB_PARTNER_MANAGER"
|
||||||
|
(click)="filterByRole(UserRole.DCB_PARTNER_MANAGER)"
|
||||||
|
>
|
||||||
|
Managers ({{ getUsersCountByRole(UserRole.DCB_PARTNER_MANAGER) }})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-info"
|
||||||
|
[class.active]="roleFilter === UserRole.DCB_PARTNER_SUPPORT"
|
||||||
|
(click)="filterByRole(UserRole.DCB_PARTNER_SUPPORT)"
|
||||||
|
>
|
||||||
|
Support ({{ getUsersCountByRole(UserRole.DCB_PARTNER_SUPPORT) }})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
@if (!canViewAllMerchants) {
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
(click)="openCreateModal.emit()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
||||||
|
Nouvel Utilisateur
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Barre de recherche et filtres -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<ng-icon name="lucideSearch"></ng-icon>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Nom, email, username..."
|
||||||
|
[(ngModel)]="searchTerm"
|
||||||
|
(keyup.enter)="onSearch()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<select class="form-select" [(ngModel)]="statusFilter" (change)="onSearch()">
|
||||||
|
<option value="all">Tous les statuts</option>
|
||||||
|
<option value="enabled">Activés ({{ getEnabledUsersCount() }})</option>
|
||||||
|
<option value="disabled">Désactivés ({{ getDisabledUsersCount() }})</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<select class="form-select" [(ngModel)]="emailVerifiedFilter" (change)="onSearch()">
|
||||||
|
<option value="all">Tous les emails</option>
|
||||||
|
<option value="verified">Email vérifié</option>
|
||||||
|
<option value="not-verified">Email non vérifié</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<select class="form-select" [(ngModel)]="roleFilter" (change)="onSearch()">
|
||||||
|
@for (role of availableRoles; track role.value) {
|
||||||
|
<option [value]="role.value">{{ role.label }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button class="btn btn-outline-primary" (click)="onSearch()">
|
||||||
|
<ng-icon name="lucideFilter" class="me-1"></ng-icon>
|
||||||
|
Appliquer
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" (click)="onClearFilters()">
|
||||||
|
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||||
|
Réinitialiser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
@if (loading) {
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-muted">Chargement des utilisateurs marchands...</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
@if (error && !loading) {
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
|
<div>{{ error }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Users Table -->
|
||||||
|
@if (!loading && !error) {
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-striped">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<!-- Colonne Merchant Partner uniquement pour les admins -->
|
||||||
|
@if (canViewAllMerchants) {
|
||||||
|
<th (click)="sort('merchantPartnerId')" class="cursor-pointer">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span>Merchant Partner</span>
|
||||||
|
<ng-icon [name]="getSortIcon('merchantPartnerId')" class="ms-1 fs-12"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
}
|
||||||
|
<th (click)="sort('username')" class="cursor-pointer">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span>Utilisateur</span>
|
||||||
|
<ng-icon [name]="getSortIcon('username')" class="ms-1 fs-12"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th (click)="sort('email')" class="cursor-pointer">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span>Email</span>
|
||||||
|
<ng-icon [name]="getSortIcon('email')" class="ms-1 fs-12"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th (click)="sort('role')" class="cursor-pointer">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span>Rôle</span>
|
||||||
|
<ng-icon [name]="getSortIcon('role')" class="ms-1 fs-12"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th (click)="sort('enabled')" class="cursor-pointer">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span>Statut</span>
|
||||||
|
<ng-icon [name]="getSortIcon('enabled')" class="ms-1 fs-12"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th (click)="sort('createdTimestamp')" class="cursor-pointer">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span>Créé le</span>
|
||||||
|
<ng-icon [name]="getSortIcon('createdTimestamp')" class="ms-1 fs-12"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th width="180">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (user of displayedUsers; track user.id) {
|
||||||
|
<tr>
|
||||||
|
<!-- Colonne Merchant Partner uniquement pour les admins -->
|
||||||
|
@if (canViewAllMerchants) {
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="avatar-sm bg-secondary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||||
|
<ng-icon name="lucideBuilding" class="text-secondary fs-12"></ng-icon>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<small class="text-muted font-monospace" [title]="user.merchantPartnerId || 'N/A'">
|
||||||
|
{{ (user.merchantPartnerId || 'N/A').substring(0, 8) }}...
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||||
|
<span class="text-primary fw-semibold small">
|
||||||
|
{{ getUserInitials(user) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong class="d-block">{{ getUserDisplayName(user) }}</strong>
|
||||||
|
<small class="text-muted">@{{ user.username }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
{{ user.email }}
|
||||||
|
@if (!user.emailVerified) {
|
||||||
|
<ng-icon
|
||||||
|
name="lucideAlertTriangle"
|
||||||
|
class="ms-1 text-warning"
|
||||||
|
size="16"
|
||||||
|
title="Email non vérifié"
|
||||||
|
></ng-icon>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge d-flex align-items-center" [ngClass]="getRoleBadgeClass(user.role)">
|
||||||
|
<ng-icon [name]="getRoleIcon(user.role)" class="me-1" size="14"></ng-icon>
|
||||||
|
{{ getRoleDisplayName(user.role) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span [class]="getStatusBadgeClass(user)">
|
||||||
|
{{ getStatusText(user) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ formatTimestamp(user.createdTimestamp) }}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-primary btn-sm"
|
||||||
|
(click)="viewUserProfile(user.id)"
|
||||||
|
title="Voir le profil"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideEye"></ng-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-warning btn-sm"
|
||||||
|
(click)="resetPassword(user)"
|
||||||
|
title="Réinitialiser le mot de passe"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideKey"></ng-icon>
|
||||||
|
</button>
|
||||||
|
@if (user.enabled) {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary btn-sm"
|
||||||
|
(click)="disableUser(user)"
|
||||||
|
title="Désactiver l'utilisateur"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideUserX"></ng-icon>
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-success btn-sm"
|
||||||
|
(click)="enableUser(user)"
|
||||||
|
title="Activer l'utilisateur"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideUserCheck"></ng-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (!canViewAllMerchants) {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-danger btn-sm"
|
||||||
|
(click)="deleteUser(user)"
|
||||||
|
title="Supprimer l'utilisateur"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideTrash2"></ng-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@empty {
|
||||||
|
<tr>
|
||||||
|
<td [attr.colspan]="canViewAllMerchants ? 7 : 6" class="text-center py-4">
|
||||||
|
<div class="text-muted">
|
||||||
|
<ng-icon name="lucideUsers" class="fs-1 mb-3 opacity-50"></ng-icon>
|
||||||
|
<h5 class="mb-2">Aucun utilisateur marchand trouvé</h5>
|
||||||
|
<p class="mb-3">Aucun utilisateur ne correspond à vos critères de recherche.</p>
|
||||||
|
@if (!canViewAllMerchants) {
|
||||||
|
<button class="btn btn-primary" (click)="openCreateModal.emit()">
|
||||||
|
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
||||||
|
Créer le premier utilisateur
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
@if (totalPages > 1) {
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
|
<div class="text-muted">
|
||||||
|
Affichage de {{ getStartIndex() }} à {{ getEndIndex() }} sur {{ totalItems }} utilisateurs
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<ngb-pagination
|
||||||
|
[collectionSize]="totalItems"
|
||||||
|
[page]="currentPage"
|
||||||
|
[pageSize]="itemsPerPage"
|
||||||
|
[maxSize]="5"
|
||||||
|
[rotate]="true"
|
||||||
|
[boundaryLinks]="true"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Résumé des résultats -->
|
||||||
|
@if (displayedUsers.length > 0) {
|
||||||
|
<div class="mt-3 pt-3 border-top">
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>Total :</strong> {{ allUsers.length }} utilisateurs
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>Actifs :</strong> {{ getEnabledUsersCount() }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>Admins :</strong> {{ getUsersCountByRole(UserRole.DCB_PARTNER_ADMIN) }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>Managers :</strong> {{ getUsersCountByRole(UserRole.DCB_PARTNER_MANAGER) }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>Support :</strong> {{ getUsersCountByRole(UserRole.DCB_PARTNER_SUPPORT) }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</app-ui-card>
|
||||||
2
src/app/modules/merchant-partners/list/list.spec.ts
Normal file
2
src/app/modules/merchant-partners/list/list.spec.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { PartnerTeamList } from './list';
|
||||||
|
describe('PartnerTeamList', () => {});
|
||||||
559
src/app/modules/merchant-partners/list/list.ts
Normal file
559
src/app/modules/merchant-partners/list/list.ts
Normal file
@ -0,0 +1,559 @@
|
|||||||
|
// src/app/modules/merchant-users/list/list.ts
|
||||||
|
import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { NgIcon } from '@ng-icons/core';
|
||||||
|
import { NgbPaginationModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { catchError, map, of, Subject, takeUntil } from 'rxjs';
|
||||||
|
import {
|
||||||
|
MerchantUsersService,
|
||||||
|
MerchantUserResponse,
|
||||||
|
UserRole,
|
||||||
|
} from '../services/merchant-partners.service';
|
||||||
|
|
||||||
|
import { HubUsersService, UserRole as HubUserRole } from '../../users/services/users.service';
|
||||||
|
|
||||||
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
import { UiCard } from '@app/components/ui-card';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-merchant-users-list',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
NgIcon,
|
||||||
|
UiCard,
|
||||||
|
NgbPaginationModule,
|
||||||
|
NgbDropdownModule
|
||||||
|
],
|
||||||
|
templateUrl: './list.html',
|
||||||
|
})
|
||||||
|
export class MerchantUsersList implements OnInit, OnDestroy {
|
||||||
|
private merchantUsersService = inject(MerchantUsersService);
|
||||||
|
private hubUsersService = inject(HubUsersService);
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
readonly UserRole = UserRole;
|
||||||
|
readonly HubUserRole = HubUserRole;
|
||||||
|
|
||||||
|
@Output() userSelected = new EventEmitter<string>();
|
||||||
|
@Output() openCreateModal = new EventEmitter<void>();
|
||||||
|
@Output() openResetPasswordModal = new EventEmitter<string>();
|
||||||
|
@Output() openDeleteUserModal = new EventEmitter<string>();
|
||||||
|
|
||||||
|
// Données
|
||||||
|
allUsers: MerchantUserResponse[] = [];
|
||||||
|
filteredUsers: MerchantUserResponse[] = [];
|
||||||
|
displayedUsers: MerchantUserResponse[] = [];
|
||||||
|
|
||||||
|
// États
|
||||||
|
loading = false;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
// Recherche et filtres
|
||||||
|
searchTerm = '';
|
||||||
|
statusFilter: 'all' | 'enabled' | 'disabled' = 'all';
|
||||||
|
emailVerifiedFilter: 'all' | 'verified' | 'not-verified' = 'all';
|
||||||
|
roleFilter: UserRole | 'all' = 'all';
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
currentPage = 1;
|
||||||
|
itemsPerPage = 10;
|
||||||
|
totalItems = 0;
|
||||||
|
totalPages = 0;
|
||||||
|
|
||||||
|
// Tri
|
||||||
|
sortField: keyof MerchantUserResponse = 'username';
|
||||||
|
sortDirection: 'asc' | 'desc' = 'asc';
|
||||||
|
|
||||||
|
// Rôles disponibles pour le filtre
|
||||||
|
availableRoles: { value: UserRole | 'all'; label: string }[] = [
|
||||||
|
{ value: 'all', label: 'Tous les rôles' },
|
||||||
|
{ value: UserRole.DCB_PARTNER_ADMIN, label: 'Administrateurs' },
|
||||||
|
{ value: UserRole.DCB_PARTNER_MANAGER, label: 'Managers' },
|
||||||
|
{ value: UserRole.DCB_PARTNER_SUPPORT, label: 'Support' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ID du merchant partner courant et permissions
|
||||||
|
currentMerchantPartnerId: string = '';
|
||||||
|
currentUserRole: HubUserRole | null = null;
|
||||||
|
isHubAdminOrSupport = false;
|
||||||
|
canViewAllMerchants = false;
|
||||||
|
isDcbPartner = false;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.loadCurrentUserPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadCurrentUserPermissions() {
|
||||||
|
this.authService.getProfile().subscribe({
|
||||||
|
next: (user: any) => {
|
||||||
|
// Méthode robuste pour récupérer le rôle
|
||||||
|
this.currentUserRole = this.extractUserRole(user);
|
||||||
|
|
||||||
|
// Déterminer le type d'utilisateur
|
||||||
|
this.isHubAdminOrSupport = this.currentUserRole === HubUserRole.DCB_ADMIN ||
|
||||||
|
this.currentUserRole === HubUserRole.DCB_SUPPORT;
|
||||||
|
this.isDcbPartner = this.currentUserRole === HubUserRole.DCB_PARTNER;
|
||||||
|
this.canViewAllMerchants = this.isHubAdminOrSupport;
|
||||||
|
|
||||||
|
// Déterminer le merchantPartnerId
|
||||||
|
this.currentMerchantPartnerId = this.extractMerchantPartnerId(user);
|
||||||
|
|
||||||
|
console.log('🎯 Final Permissions:', {
|
||||||
|
currentUserRole: this.currentUserRole,
|
||||||
|
isHubAdminOrSupport: this.isHubAdminOrSupport,
|
||||||
|
isDcbPartner: this.isDcbPartner,
|
||||||
|
canViewAllMerchants: this.canViewAllMerchants,
|
||||||
|
currentMerchantPartnerId: this.currentMerchantPartnerId
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loadUsers();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('❌ Error loading current user permissions:', error);
|
||||||
|
this.loadUsers(); // Charger quand même les utilisateurs
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrait le rôle de l'utilisateur de manière robuste
|
||||||
|
*/
|
||||||
|
private extractUserRole(user: any): HubUserRole | null {
|
||||||
|
// Essayer différentes sources possibles pour le rôle
|
||||||
|
if (user.roles && user.roles.length > 0) {
|
||||||
|
return user.roles[0] as HubUserRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.role) {
|
||||||
|
return user.role as HubUserRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('No role found in user profile');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrait le merchantPartnerId de manière robuste
|
||||||
|
*/
|
||||||
|
private extractMerchantPartnerId(user: any): string {
|
||||||
|
if (this.isDcbPartner) {
|
||||||
|
// Pour DCB_PARTNER, utiliser son ID comme merchantPartnerId
|
||||||
|
return user.id || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour les autres, chercher le merchantPartnerId dans différentes sources
|
||||||
|
if (user.merchantPartnerId) {
|
||||||
|
return user.merchantPartnerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.attributes?.merchantPartnerId?.[0]) {
|
||||||
|
return user.attributes.merchantPartnerId[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.attributes?.partnerId?.[0]) {
|
||||||
|
return user.attributes.partnerId[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('No merchantPartnerId found in user profile');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUsers() {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
|
||||||
|
console.log('🚀 Loading users with permissions:', {
|
||||||
|
canViewAllMerchants: this.canViewAllMerchants,
|
||||||
|
currentMerchantPartnerId: this.currentMerchantPartnerId,
|
||||||
|
currentUserRole: this.currentUserRole
|
||||||
|
});
|
||||||
|
|
||||||
|
let usersObservable;
|
||||||
|
|
||||||
|
if (this.canViewAllMerchants) {
|
||||||
|
// Admin/Support DCB : charger tous les utilisateurs via HubUsersService
|
||||||
|
console.log('📊 Loading ALL merchant users (DCB Admin view)');
|
||||||
|
|
||||||
|
usersObservable = this.hubUsersService.findAllMerchantUsers(1, 1000).pipe(
|
||||||
|
map((response: any) => {
|
||||||
|
console.log('📦 Hub Users API Response:', response);
|
||||||
|
|
||||||
|
// Adapter selon la structure de votre API
|
||||||
|
if (response && response.users) {
|
||||||
|
return response.users;
|
||||||
|
} else if (Array.isArray(response)) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('❌ Error loading hub users:', error);
|
||||||
|
return of([]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (this.currentMerchantPartnerId) {
|
||||||
|
// Utilisateur marchand (DCB_PARTNER) : charger seulement ses utilisateurs
|
||||||
|
console.log('🏢 Loading merchant users for partner:', this.currentMerchantPartnerId);
|
||||||
|
|
||||||
|
usersObservable = this.merchantUsersService.getMyMerchantUsers().pipe(
|
||||||
|
catchError(error => {
|
||||||
|
console.error('❌ Error loading merchant users:', error);
|
||||||
|
return of([]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.error = 'Impossible de déterminer les permissions de chargement';
|
||||||
|
this.loading = false;
|
||||||
|
console.error('❌ No valid permission scenario');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
usersObservable
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (users: any[]) => {
|
||||||
|
this.allUsers = users || [];
|
||||||
|
|
||||||
|
this.applyFiltersAndPagination();
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error: any) => {
|
||||||
|
this.error = 'Erreur lors du chargement des utilisateurs marchands';
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
this.allUsers = [];
|
||||||
|
this.filteredUsers = [];
|
||||||
|
this.displayedUsers = [];
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
console.error('❌ Error in users subscription:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recherche et filtres
|
||||||
|
onSearch() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.applyFiltersAndPagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
onClearFilters() {
|
||||||
|
this.searchTerm = '';
|
||||||
|
this.statusFilter = 'all';
|
||||||
|
this.emailVerifiedFilter = 'all';
|
||||||
|
this.roleFilter = 'all';
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.applyFiltersAndPagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFiltersAndPagination() {
|
||||||
|
// Vérifier que allUsers est défini
|
||||||
|
if (!this.allUsers) {
|
||||||
|
this.allUsers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 && user.firstName.toLowerCase().includes(this.searchTerm.toLowerCase())) ||
|
||||||
|
(user.lastName && 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);
|
||||||
|
|
||||||
|
// Filtre par rôle
|
||||||
|
const matchesRole = this.roleFilter === 'all' || user.role === this.roleFilter;
|
||||||
|
|
||||||
|
return matchesSearch && matchesStatus && matchesEmailVerified && matchesRole;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 MerchantUserResponse) {
|
||||||
|
if (this.sortField === field) {
|
||||||
|
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
this.sortField = field;
|
||||||
|
this.sortDirection = 'asc';
|
||||||
|
}
|
||||||
|
this.applyFiltersAndPagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSortIcon(field: keyof MerchantUserResponse): 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: MerchantUserResponse) {
|
||||||
|
this.openResetPasswordModal.emit(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour ouvrir le modal de suppression
|
||||||
|
deleteUser(user: MerchantUserResponse) {
|
||||||
|
this.openDeleteUserModal.emit(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activer un utilisateur
|
||||||
|
enableUser(user: MerchantUserResponse) {
|
||||||
|
this.merchantUsersService.enableMerchantUser(user.id)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (updatedUser) => {
|
||||||
|
user.enabled = updatedUser.enabled;
|
||||||
|
this.applyFiltersAndPagination();
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error enabling merchant user:', error);
|
||||||
|
this.error = 'Erreur lors de l\'activation de l\'utilisateur';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Désactiver un utilisateur
|
||||||
|
disableUser(user: MerchantUserResponse) {
|
||||||
|
this.merchantUsersService.disableMerchantUser(user.id)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (updatedUser) => {
|
||||||
|
user.enabled = updatedUser.enabled;
|
||||||
|
this.applyFiltersAndPagination();
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error disabling merchant user:', error);
|
||||||
|
this.error = 'Erreur lors de la désactivation de l\'utilisateur';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== UTILITAIRES D'AFFICHAGE ====================
|
||||||
|
|
||||||
|
getStatusBadgeClass(user: MerchantUserResponse): string {
|
||||||
|
if (!user.enabled) return 'badge bg-danger';
|
||||||
|
if (!user.emailVerified) return 'badge bg-warning';
|
||||||
|
return 'badge bg-success';
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatusText(user: MerchantUserResponse): string {
|
||||||
|
if (!user.enabled) return 'Désactivé';
|
||||||
|
if (!user.emailVerified) return 'Email non vérifié';
|
||||||
|
return 'Actif';
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleBadgeClass(role: UserRole): string {
|
||||||
|
switch (role) {
|
||||||
|
case UserRole.DCB_PARTNER_ADMIN:
|
||||||
|
return 'bg-danger';
|
||||||
|
case UserRole.DCB_PARTNER_MANAGER:
|
||||||
|
return 'bg-warning text-dark';
|
||||||
|
case UserRole.DCB_PARTNER_SUPPORT:
|
||||||
|
return 'bg-info text-white';
|
||||||
|
default:
|
||||||
|
return 'bg-secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleDisplayName(role: UserRole): string {
|
||||||
|
const roleNames = {
|
||||||
|
[UserRole.DCB_PARTNER_ADMIN]: 'Administrateur',
|
||||||
|
[UserRole.DCB_PARTNER_MANAGER]: 'Manager',
|
||||||
|
[UserRole.DCB_PARTNER_SUPPORT]: 'Support'
|
||||||
|
};
|
||||||
|
return roleNames[role] || role;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleIcon(role: UserRole): string {
|
||||||
|
switch (role) {
|
||||||
|
case UserRole.DCB_PARTNER_ADMIN:
|
||||||
|
return 'lucideShield';
|
||||||
|
case UserRole.DCB_PARTNER_MANAGER:
|
||||||
|
return 'lucideUserCog';
|
||||||
|
case UserRole.DCB_PARTNER_SUPPORT:
|
||||||
|
return 'lucideHeadphones';
|
||||||
|
default:
|
||||||
|
return 'lucideUser';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatTimestamp(timestamp: number): string {
|
||||||
|
if (!timestamp) return 'Non disponible';
|
||||||
|
return new Date(timestamp).toLocaleDateString('fr-FR', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserInitials(user: MerchantUserResponse): string {
|
||||||
|
return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U';
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserDisplayName(user: MerchantUserResponse): string {
|
||||||
|
if (user.firstName && user.lastName) {
|
||||||
|
return `${user.firstName} ${user.lastName}`;
|
||||||
|
}
|
||||||
|
return user.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== STATISTIQUES ====================
|
||||||
|
|
||||||
|
getUsersCountByRole(role: UserRole): number {
|
||||||
|
return this.allUsers.filter(user => user.role === role).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEnabledUsersCount(): number {
|
||||||
|
return this.allUsers.filter(user => user.enabled).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisabledUsersCount(): number {
|
||||||
|
return this.allUsers.filter(user => !user.enabled).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEmailVerifiedCount(): number {
|
||||||
|
return this.allUsers.filter(user => user.emailVerified).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotalUsersCount(): number {
|
||||||
|
return this.allUsers.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== VÉRIFICATIONS DE PERMISSIONS ====================
|
||||||
|
|
||||||
|
canManageUser(user: MerchantUserResponse): boolean {
|
||||||
|
// Logique pour déterminer si l'utilisateur connecté peut gérer cet utilisateur
|
||||||
|
// À adapter selon votre logique métier
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeleteUser(user: MerchantUserResponse): boolean {
|
||||||
|
// Empêcher la suppression de soi-même
|
||||||
|
// À adapter avec l'ID de l'utilisateur connecté
|
||||||
|
return user.id !== 'current-user-id';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MÉTHODES UTILITAIRES ====================
|
||||||
|
|
||||||
|
hasRole(user: MerchantUserResponse, role: UserRole): boolean {
|
||||||
|
return user.role === role;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin(user: MerchantUserResponse): boolean {
|
||||||
|
return this.hasRole(user, UserRole.DCB_PARTNER_ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
isManager(user: MerchantUserResponse): boolean {
|
||||||
|
return this.hasRole(user, UserRole.DCB_PARTNER_MANAGER);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSupport(user: MerchantUserResponse): boolean {
|
||||||
|
return this.hasRole(user, UserRole.DCB_PARTNER_SUPPORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recherche rapide par rôle
|
||||||
|
filterByRole(role: UserRole | 'all') {
|
||||||
|
this.roleFilter = role;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.applyFiltersAndPagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recherche via le service (pour des recherches plus complexes)
|
||||||
|
searchUsers() {
|
||||||
|
if (this.searchTerm.trim()) {
|
||||||
|
this.loading = true;
|
||||||
|
this.merchantUsersService.searchMerchantUsers({
|
||||||
|
query: this.searchTerm,
|
||||||
|
role: this.roleFilter !== 'all' ? this.roleFilter as UserRole : undefined,
|
||||||
|
enabled: this.statusFilter !== 'all' ? this.statusFilter === 'enabled' : undefined
|
||||||
|
})
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (users) => {
|
||||||
|
this.allUsers = users;
|
||||||
|
this.applyFiltersAndPagination();
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error: any) => {
|
||||||
|
console.error('Error searching users:', error);
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.loadUsers(); // Recharger tous les utilisateurs si la recherche est vide
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
519
src/app/modules/merchant-partners/merchant-partners.html
Normal file
519
src/app/modules/merchant-partners/merchant-partners.html
Normal file
@ -0,0 +1,519 @@
|
|||||||
|
<!-- src/app/modules/merchant-users/merchant-users.html -->
|
||||||
|
<div class="container-fluid">
|
||||||
|
<app-page-title
|
||||||
|
title="Gestion des Utilisateurs Marchands"
|
||||||
|
subTitle="Administrez les utilisateurs de votre écosystème marchand"
|
||||||
|
[badge]="{icon:'lucideUsers', text:'Merchant Users'}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Navigation par onglets avec style bordered -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<ul
|
||||||
|
ngbNav
|
||||||
|
#merchantUsersNav="ngbNav"
|
||||||
|
[activeId]="activeTab"
|
||||||
|
[destroyOnHide]="false"
|
||||||
|
class="nav nav-tabs nav-justified nav-bordered nav-bordered-primary mb-3"
|
||||||
|
>
|
||||||
|
<li [ngbNavItem]="'list'">
|
||||||
|
<a ngbNavLink (click)="showTab('list')">
|
||||||
|
<ng-icon name="lucideUsers" class="fs-lg me-md-1 d-inline-flex align-middle" />
|
||||||
|
<span class="d-none d-md-inline-block align-middle">Équipe Marchande</span>
|
||||||
|
</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<app-merchant-users-list
|
||||||
|
(userSelected)="onUserSelected($event)"
|
||||||
|
(openCreateModal)="openCreateUserModal()"
|
||||||
|
(openResetPasswordModal)="onResetPasswordRequested($event)"
|
||||||
|
(openDeleteUserModal)="onDeleteUserRequested($event)"
|
||||||
|
/>
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li [ngbNavItem]="'profile'" [hidden]="activeTab !== 'profile'">
|
||||||
|
<a ngbNavLink (click)="showTab('profile')">
|
||||||
|
<ng-icon name="lucideUser" class="fs-lg me-md-1 d-inline-flex align-middle" />
|
||||||
|
<span class="d-none d-md-inline-block align-middle">Profil Utilisateur</span>
|
||||||
|
</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
@if (selectedUserId) {
|
||||||
|
<app-merchant-user-profile
|
||||||
|
[userId]="selectedUserId"
|
||||||
|
(back)="backToList()"
|
||||||
|
(openResetPasswordModal)="onResetPasswordRequested($event)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" [ngbNavOutlet]="merchantUsersNav"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de création d'utilisateur marchand -->
|
||||||
|
<ng-template #createUserModal let-modal>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">
|
||||||
|
<ng-icon name="lucideUserPlus" class="me-2"></ng-icon>
|
||||||
|
Créer un nouvel utilisateur marchand
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
(click)="modal.dismiss()"
|
||||||
|
[disabled]="creatingUser"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- Message d'erreur -->
|
||||||
|
@if (createUserError) {
|
||||||
|
<div class="alert alert-danger d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
|
<div>{{ createUserError }}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<form (ngSubmit)="createMerchantUser()" #userForm="ngForm">
|
||||||
|
<div class="row g-3">
|
||||||
|
<!-- Informations de base -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">
|
||||||
|
Prénom <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Entrez le prénom"
|
||||||
|
[(ngModel)]="newMerchantUser.firstName"
|
||||||
|
name="firstName"
|
||||||
|
required
|
||||||
|
[disabled]="creatingUser"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">
|
||||||
|
Nom <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Entrez le nom"
|
||||||
|
[(ngModel)]="newMerchantUser.lastName"
|
||||||
|
name="lastName"
|
||||||
|
required
|
||||||
|
[disabled]="creatingUser"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">
|
||||||
|
Nom d'utilisateur <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Nom d'utilisateur unique"
|
||||||
|
[(ngModel)]="newMerchantUser.username"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
[disabled]="creatingUser"
|
||||||
|
>
|
||||||
|
<div class="form-text">Doit être unique dans le système</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">
|
||||||
|
Email <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="email@exemple.com"
|
||||||
|
[(ngModel)]="newMerchantUser.email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
[disabled]="creatingUser"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">
|
||||||
|
Mot de passe <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Mot de passe sécurisé"
|
||||||
|
[(ngModel)]="newMerchantUser.password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
minlength="8"
|
||||||
|
[disabled]="creatingUser"
|
||||||
|
>
|
||||||
|
<div class="form-text">
|
||||||
|
Le mot de passe doit contenir au moins 8 caractères.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sélection du rôle -->
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">
|
||||||
|
Rôle <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
class="form-select"
|
||||||
|
[(ngModel)]="newMerchantUser.role"
|
||||||
|
name="role"
|
||||||
|
required
|
||||||
|
[disabled]="creatingUser"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Sélectionnez un rôle</option>
|
||||||
|
@if (availableRoles) {
|
||||||
|
@for (role of availableRoles.roles; track role.value) {
|
||||||
|
<option
|
||||||
|
[value]="role.value"
|
||||||
|
[disabled]="!role.allowedForCreation"
|
||||||
|
>
|
||||||
|
{{ role.label }} - {{ role.description }}
|
||||||
|
@if (!role.allowedForCreation) {
|
||||||
|
(Non autorisé)
|
||||||
|
}
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<div class="form-text">
|
||||||
|
Sélectionnez le rôle à assigner à cet utilisateur marchand
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aperçu du rôle sélectionné -->
|
||||||
|
@if (newMerchantUser.role) {
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon
|
||||||
|
[name]="getRoleIcon(newMerchantUser.role)"
|
||||||
|
class="me-2"
|
||||||
|
></ng-icon>
|
||||||
|
<div>
|
||||||
|
<strong>Rôle sélectionné :</strong>
|
||||||
|
<span class="badge ms-2" [ngClass]="getRoleBadgeClass(newMerchantUser.role)">
|
||||||
|
{{ getRoleDisplayName(newMerchantUser.role) }}
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ getRoleDescription(newMerchantUser.role) }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Configuration du compte -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="enabledSwitch"
|
||||||
|
[(ngModel)]="newMerchantUser.enabled"
|
||||||
|
name="enabled"
|
||||||
|
[disabled]="creatingUser"
|
||||||
|
checked
|
||||||
|
>
|
||||||
|
<label class="form-check-label" for="enabledSwitch">
|
||||||
|
Compte activé
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">L'utilisateur peut se connecter immédiatement</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="emailVerifiedSwitch"
|
||||||
|
[(ngModel)]="newMerchantUser.emailVerified"
|
||||||
|
name="emailVerified"
|
||||||
|
[disabled]="creatingUser"
|
||||||
|
>
|
||||||
|
<label class="form-check-label" for="emailVerifiedSwitch">
|
||||||
|
Email vérifié
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">L'utilisateur n'aura pas à vérifier son email</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informations système (lecture seule) -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-light">
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>Informations système :</strong><br>
|
||||||
|
• Merchant Partner ID : {{ currentMerchantPartnerId || 'Chargement...' }}<br>
|
||||||
|
• Type d'utilisateur : MERCHANT<br>
|
||||||
|
• Créé par : Utilisateur courant
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer mt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-light"
|
||||||
|
(click)="modal.dismiss()"
|
||||||
|
[disabled]="creatingUser"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
[disabled]="!userForm.form.valid || creatingUser || !isRoleAllowedForCreation(newMerchantUser.role)"
|
||||||
|
>
|
||||||
|
@if (creatingUser) {
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
Création...
|
||||||
|
} @else {
|
||||||
|
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
||||||
|
Créer l'utilisateur
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- Modal de réinitialisation de mot de passe -->
|
||||||
|
<ng-template #resetPasswordModal let-modal>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">
|
||||||
|
<ng-icon name="lucideKey" class="me-2"></ng-icon>
|
||||||
|
Réinitialiser le mot de passe
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
(click)="modal.dismiss()"
|
||||||
|
[disabled]="resettingPassword"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- Message de succès -->
|
||||||
|
@if (resetPasswordSuccess) {
|
||||||
|
<div class="alert alert-success d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||||
|
<div>{{ resetPasswordSuccess }}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Message d'erreur -->
|
||||||
|
@if (resetPasswordError) {
|
||||||
|
<div class="alert alert-danger d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
|
<div>{{ resetPasswordError }}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!resetPasswordSuccess && selectedUserForReset) {
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3">
|
||||||
|
<span class="text-primary fw-bold">{{ getUserInitials(selectedUserForReset) }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>{{ selectedUserForReset.username }}</strong>
|
||||||
|
@if (selectedUserForReset.firstName || selectedUserForReset.lastName) {
|
||||||
|
<br>
|
||||||
|
{{ selectedUserForReset.firstName }} {{ selectedUserForReset.lastName }}
|
||||||
|
}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
<span class="badge" [ngClass]="getRoleBadgeClass(selectedUserForReset.role)">
|
||||||
|
{{ getRoleDisplayName(selectedUserForReset.role) }}
|
||||||
|
</span>
|
||||||
|
• Merchant Partner: {{ selectedUserForReset.merchantPartnerId }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form (ngSubmit)="confirmResetPassword()" #resetForm="ngForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">
|
||||||
|
Nouveau mot de passe <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Entrez le nouveau mot de passe"
|
||||||
|
[(ngModel)]="newPassword"
|
||||||
|
name="newPassword"
|
||||||
|
required
|
||||||
|
minlength="8"
|
||||||
|
[disabled]="resettingPassword"
|
||||||
|
>
|
||||||
|
<div class="form-text">
|
||||||
|
Le mot de passe doit contenir au moins 8 caractères.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="temporaryPassword"
|
||||||
|
[(ngModel)]="temporaryPassword"
|
||||||
|
name="temporaryPassword"
|
||||||
|
[disabled]="resettingPassword"
|
||||||
|
checked
|
||||||
|
>
|
||||||
|
<label class="form-check-label" for="temporaryPassword">
|
||||||
|
Mot de passe temporaire
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
L'utilisateur devra changer son mot de passe à la prochaine connexion.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
@if (resetPasswordSuccess) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-success"
|
||||||
|
(click)="modal.close()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideCheck" class="me-1"></ng-icon>
|
||||||
|
Fermer
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-light"
|
||||||
|
(click)="modal.dismiss()"
|
||||||
|
[disabled]="resettingPassword"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
(click)="confirmResetPassword()"
|
||||||
|
[disabled]="!newPassword || newPassword.length < 8 || resettingPassword"
|
||||||
|
>
|
||||||
|
@if (resettingPassword) {
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
Réinitialisation...
|
||||||
|
} @else {
|
||||||
|
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
||||||
|
Réinitialiser le mot de passe
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- Modal de confirmation de suppression -->
|
||||||
|
<ng-template #deleteUserModal let-modal>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title text-danger">
|
||||||
|
<ng-icon name="lucideTrash2" class="me-2"></ng-icon>
|
||||||
|
Confirmer la suppression
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
(click)="modal.dismiss()"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="avatar-lg mx-auto mb-3 bg-danger bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
||||||
|
<ng-icon name="lucideUserX" class="text-danger" style="font-size: 2rem;"></ng-icon>
|
||||||
|
</div>
|
||||||
|
<h5 class="text-danger mb-2">Êtes-vous sûr de vouloir supprimer cet utilisateur ?</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Cette action est irréversible. Toutes les données de cet utilisateur marchand seront définitivement perdues.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (selectedUserForDelete) {
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<ng-icon name="lucideAlertTriangle" class="me-2 mt-1 text-warning"></ng-icon>
|
||||||
|
<div>
|
||||||
|
<strong>Utilisateur :</strong> {{ selectedUserForDelete.username }}
|
||||||
|
@if (selectedUserForDelete.firstName || selectedUserForDelete.lastName) {
|
||||||
|
<br>
|
||||||
|
<strong>Nom :</strong> {{ selectedUserForDelete.firstName }} {{ selectedUserForDelete.lastName }}
|
||||||
|
}
|
||||||
|
<br>
|
||||||
|
<strong>Email :</strong> {{ selectedUserForDelete.email }}
|
||||||
|
<br>
|
||||||
|
<strong>Rôle :</strong>
|
||||||
|
<span class="badge" [ngClass]="getRoleBadgeClass(selectedUserForDelete.role)">
|
||||||
|
{{ getRoleDisplayName(selectedUserForDelete.role) }}
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
|
<strong>Merchant Partner :</strong> {{ selectedUserForDelete.merchantPartnerId }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Message d'erreur -->
|
||||||
|
@if (deleteUserError) {
|
||||||
|
<div class="alert alert-danger d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
|
<div>{{ deleteUserError }}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-light"
|
||||||
|
(click)="modal.dismiss()"
|
||||||
|
[disabled]="deletingUser"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-danger"
|
||||||
|
(click)="confirmDeleteUser()"
|
||||||
|
[disabled]="deletingUser"
|
||||||
|
>
|
||||||
|
@if (deletingUser) {
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status">
|
||||||
|
<span class="visually-hidden">Suppression...</span>
|
||||||
|
</div>
|
||||||
|
Suppression...
|
||||||
|
} @else {
|
||||||
|
<ng-icon name="lucideTrash2" class="me-1"></ng-icon>
|
||||||
|
Supprimer définitivement
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
import { MerchantPartners } from './merchant-partners';
|
||||||
|
describe('Merchant Partners', () => {});
|
||||||
486
src/app/modules/merchant-partners/merchant-partners.ts
Normal file
486
src/app/modules/merchant-partners/merchant-partners.ts
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
import { Component, inject, OnInit, TemplateRef, ViewChild, ChangeDetectorRef, OnDestroy } 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 { Subject, takeUntil } from 'rxjs';
|
||||||
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
|
import { MerchantUsersList } from './list/list';
|
||||||
|
import { MerchantUserProfile } from './profile/profile';
|
||||||
|
import {
|
||||||
|
MerchantUsersService,
|
||||||
|
CreateMerchantUserDto,
|
||||||
|
MerchantUserResponse,
|
||||||
|
UserRole,
|
||||||
|
AvailableRolesResponse,
|
||||||
|
} from './services/merchant-partners.service';
|
||||||
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-merchant-partners',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
NgIcon,
|
||||||
|
NgbNavModule,
|
||||||
|
NgbModalModule,
|
||||||
|
PageTitle,
|
||||||
|
MerchantUsersList,
|
||||||
|
MerchantUserProfile
|
||||||
|
],
|
||||||
|
templateUrl: './merchant-partners.html',
|
||||||
|
})
|
||||||
|
export class MerchantPartners implements OnInit, OnDestroy {
|
||||||
|
private modalService = inject(NgbModal);
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
private merchantUsersService = inject(MerchantUsersService);
|
||||||
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
activeTab: 'list' | 'stats' | 'profile' = 'list';
|
||||||
|
selectedUserId: string | null = null;
|
||||||
|
currentMerchantPartnerId: string = '';
|
||||||
|
|
||||||
|
// Données pour la création d'utilisateur marchand
|
||||||
|
newMerchantUser: CreateMerchantUserDto = {
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
password: '',
|
||||||
|
role: UserRole.DCB_PARTNER_SUPPORT,
|
||||||
|
merchantPartnerId: '',
|
||||||
|
enabled: true,
|
||||||
|
emailVerified: false
|
||||||
|
};
|
||||||
|
|
||||||
|
availableRoles: AvailableRolesResponse | null = null;
|
||||||
|
creatingUser = false;
|
||||||
|
createUserError = '';
|
||||||
|
|
||||||
|
// Données pour la réinitialisation de mot de passe
|
||||||
|
selectedUserForReset: MerchantUserResponse | null = null;
|
||||||
|
newPassword = '';
|
||||||
|
temporaryPassword = true;
|
||||||
|
resettingPassword = false;
|
||||||
|
resetPasswordError = '';
|
||||||
|
resetPasswordSuccess = '';
|
||||||
|
|
||||||
|
selectedUserForDelete: MerchantUserResponse | null = null;
|
||||||
|
deletingUser = false;
|
||||||
|
deleteUserError = '';
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.activeTab = 'list';
|
||||||
|
this.loadCurrentMerchantPartnerId();
|
||||||
|
this.loadAvailableRoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadCurrentMerchantPartnerId() {
|
||||||
|
this.authService.getProfile().subscribe({
|
||||||
|
next: (user) => {
|
||||||
|
this.currentMerchantPartnerId = user.merchantPartnerId || '';
|
||||||
|
this.newMerchantUser.merchantPartnerId = this.currentMerchantPartnerId;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading current merchant partner ID:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadAvailableRoles() {
|
||||||
|
this.merchantUsersService.getAvailableMerchantRoles()
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (roles) => {
|
||||||
|
this.availableRoles = roles;
|
||||||
|
// Sélectionner le premier rôle disponible par défaut
|
||||||
|
const firstAllowedRole = roles.roles.find(role => role.allowedForCreation);
|
||||||
|
if (firstAllowedRole) {
|
||||||
|
this.newMerchantUser.role = firstAllowedRole.value as any;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading available roles:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showTab(tab: 'list' | 'stats' | 'profile', userId?: string) {
|
||||||
|
this.activeTab = tab;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
this.selectedUserId = userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backToList() {
|
||||||
|
this.activeTab = 'list';
|
||||||
|
this.selectedUserId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes de gestion des événements du composant enfant
|
||||||
|
onUserSelected(userId: string) {
|
||||||
|
this.showTab('profile', userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
onResetPasswordRequested(userId: string) {
|
||||||
|
this.openResetPasswordModal(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteUserRequested(userId: string) {
|
||||||
|
this.openDeleteUserModal(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes pour les modals
|
||||||
|
openModal(content: TemplateRef<any>, 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.resetUserForm();
|
||||||
|
this.createUserError = '';
|
||||||
|
this.openModal(this.createUserModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Réinitialiser le formulaire de création
|
||||||
|
private resetUserForm() {
|
||||||
|
this.newMerchantUser = {
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
password: '',
|
||||||
|
role: UserRole.DCB_PARTNER_SUPPORT,
|
||||||
|
merchantPartnerId: this.currentMerchantPartnerId,
|
||||||
|
enabled: true,
|
||||||
|
emailVerified: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour ouvrir le modal de réinitialisation de mot de passe
|
||||||
|
openResetPasswordModal(userId: string) {
|
||||||
|
this.merchantUsersService.getMerchantUserById(userId)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (user) => {
|
||||||
|
this.selectedUserForReset = user;
|
||||||
|
this.newPassword = '';
|
||||||
|
this.temporaryPassword = true;
|
||||||
|
this.resetPasswordError = '';
|
||||||
|
this.resetPasswordSuccess = '';
|
||||||
|
this.openModal(this.resetPasswordModal);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading user for password reset:', error);
|
||||||
|
this.resetPasswordError = 'Erreur lors du chargement de l\'utilisateur';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour ouvrir le modal de suppression
|
||||||
|
openDeleteUserModal(userId: string) {
|
||||||
|
this.merchantUsersService.getMerchantUserById(userId)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (user) => {
|
||||||
|
this.selectedUserForDelete = user;
|
||||||
|
this.deleteUserError = '';
|
||||||
|
this.openModal(this.deleteUserModal);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading user for deletion:', error);
|
||||||
|
this.deleteUserError = 'Erreur lors du chargement de l\'utilisateur';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer un utilisateur marchand
|
||||||
|
createMerchantUser() {
|
||||||
|
const validation = this.validateUserForm();
|
||||||
|
if (!validation.isValid) {
|
||||||
|
this.createUserError = validation.error!;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.creatingUser = true;
|
||||||
|
this.createUserError = '';
|
||||||
|
|
||||||
|
// S'assurer que le merchantPartnerId est défini
|
||||||
|
this.newMerchantUser.merchantPartnerId = this.currentMerchantPartnerId;
|
||||||
|
|
||||||
|
this.merchantUsersService.createMerchantUser(this.newMerchantUser)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (createdUser) => {
|
||||||
|
this.creatingUser = false;
|
||||||
|
this.modalService.dismissAll();
|
||||||
|
this.refreshUsersList();
|
||||||
|
this.showSuccessMessage(`Utilisateur "${createdUser.username}" créé avec succès`);
|
||||||
|
},
|
||||||
|
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 = '';
|
||||||
|
|
||||||
|
this.merchantUsersService.resetMerchantUserPassword(
|
||||||
|
this.selectedUserForReset.id,
|
||||||
|
{
|
||||||
|
newPassword: this.newPassword,
|
||||||
|
temporary: this.temporaryPassword
|
||||||
|
}
|
||||||
|
).pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
this.resettingPassword = false;
|
||||||
|
this.resetPasswordSuccess = 'Mot de passe réinitialisé avec succès !';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
|
||||||
|
// Fermer le modal après 2 secondes
|
||||||
|
setTimeout(() => {
|
||||||
|
this.modalService.dismissAll();
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.resettingPassword = false;
|
||||||
|
this.resetPasswordError = this.getResetPasswordErrorMessage(error);
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDeleteUser() {
|
||||||
|
if (!this.selectedUserForDelete) return;
|
||||||
|
|
||||||
|
this.deletingUser = true;
|
||||||
|
this.deleteUserError = '';
|
||||||
|
|
||||||
|
this.merchantUsersService.deleteMerchantUser(this.selectedUserForDelete.id)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.deletingUser = false;
|
||||||
|
this.modalService.dismissAll();
|
||||||
|
this.refreshUsersList();
|
||||||
|
this.showSuccessMessage(`Utilisateur "${this.selectedUserForDelete?.username}" supprimé avec succès`);
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.deletingUser = false;
|
||||||
|
this.deleteUserError = this.getDeleteErrorMessage(error);
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewChild(MerchantUsersList) usersListComponent!: MerchantUsersList;
|
||||||
|
|
||||||
|
private refreshUsersList(): void {
|
||||||
|
if (this.usersListComponent && typeof this.usersListComponent.loadUsers === 'function') {
|
||||||
|
this.usersListComponent.loadUsers();
|
||||||
|
} else {
|
||||||
|
console.warn('MerchantUsersList component not available for refresh');
|
||||||
|
this.showTab('list');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== GESTION DES ERREURS ====================
|
||||||
|
|
||||||
|
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 d\'utilisateur ou email existe déjà.';
|
||||||
|
}
|
||||||
|
if (error.status === 403) {
|
||||||
|
return 'Vous n\'avez pas les permissions pour créer cet utilisateur.';
|
||||||
|
}
|
||||||
|
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é.';
|
||||||
|
}
|
||||||
|
if (error.status === 403) {
|
||||||
|
return 'Vous n\'avez pas les permissions pour réinitialiser ce mot de passe.';
|
||||||
|
}
|
||||||
|
return 'Erreur lors de la réinitialisation du mot de passe. Veuillez réessayer.';
|
||||||
|
}
|
||||||
|
|
||||||
|
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.';
|
||||||
|
}
|
||||||
|
if (error.status === 409) {
|
||||||
|
return 'Impossible de supprimer cet utilisateur car il est associé à des données.';
|
||||||
|
}
|
||||||
|
return 'Erreur lors de la suppression de l\'utilisateur. Veuillez réessayer.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== VALIDATION DU FORMULAIRE ====================
|
||||||
|
|
||||||
|
private validateUserForm(): { isValid: boolean; error?: string } {
|
||||||
|
const requiredFields = [
|
||||||
|
{ field: this.newMerchantUser.username?.trim(), name: 'Nom d\'utilisateur' },
|
||||||
|
{ field: this.newMerchantUser.email?.trim(), name: 'Email' },
|
||||||
|
{ field: this.newMerchantUser.firstName?.trim(), name: 'Prénom' },
|
||||||
|
{ field: this.newMerchantUser.lastName?.trim(), name: 'Nom' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { field, name } of requiredFields) {
|
||||||
|
if (!field) {
|
||||||
|
return { isValid: false, error: `${name} est requis` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation email
|
||||||
|
const email = this.newMerchantUser.email?.trim();
|
||||||
|
if (!email) {
|
||||||
|
return { isValid: false, error: 'Email est requis' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
return { isValid: false, error: 'Format d\'email invalide' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newMerchantUser.password || this.newMerchantUser.password.length < 8) {
|
||||||
|
return { isValid: false, error: 'Le mot de passe doit contenir au moins 8 caractères' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newMerchantUser.role) {
|
||||||
|
return { isValid: false, error: 'Le rôle est requis' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newMerchantUser.merchantPartnerId) {
|
||||||
|
return { isValid: false, error: 'Merchant Partner ID est requis' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isValid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MESSAGES DE SUCCÈS ====================
|
||||||
|
|
||||||
|
private showSuccessMessage(message: string) {
|
||||||
|
// Vous pouvez implémenter un service de notification ici
|
||||||
|
console.log('Success:', message);
|
||||||
|
// Exemple avec un toast:
|
||||||
|
// this.notificationService.success(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MÉTHODES UTILITAIRES ====================
|
||||||
|
|
||||||
|
getRoleDisplayName(role: UserRole): string {
|
||||||
|
const roleNames = {
|
||||||
|
[UserRole.DCB_PARTNER_ADMIN]: 'Administrateur Partenaire',
|
||||||
|
[UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire',
|
||||||
|
[UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire'
|
||||||
|
};
|
||||||
|
return roleNames[role] || role;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleDescription(role: UserRole): string {
|
||||||
|
if (!this.availableRoles) return '';
|
||||||
|
|
||||||
|
const roleInfo = this.availableRoles.roles.find(r => r.value === role);
|
||||||
|
return roleInfo?.description || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
isRoleAllowedForCreation(role: UserRole): boolean {
|
||||||
|
if (!this.availableRoles) return false;
|
||||||
|
|
||||||
|
const roleInfo = this.availableRoles.roles.find(r => r.value === role);
|
||||||
|
return roleInfo?.allowedForCreation || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes utilitaires pour le template
|
||||||
|
getUserInitials(user: MerchantUserResponse): string {
|
||||||
|
return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U';
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserType(user: MerchantUserResponse): string {
|
||||||
|
switch (user.role) {
|
||||||
|
case UserRole.DCB_PARTNER_ADMIN:
|
||||||
|
return 'Administrateur';
|
||||||
|
case UserRole.DCB_PARTNER_MANAGER:
|
||||||
|
return 'Manager';
|
||||||
|
case UserRole.DCB_PARTNER_SUPPORT:
|
||||||
|
return 'Support';
|
||||||
|
default:
|
||||||
|
return 'Utilisateur';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleBadgeClass(role: UserRole): string {
|
||||||
|
switch (role) {
|
||||||
|
case UserRole.DCB_PARTNER_ADMIN:
|
||||||
|
return 'bg-danger text-white';
|
||||||
|
case UserRole.DCB_PARTNER_MANAGER:
|
||||||
|
return 'bg-warning text-dark';
|
||||||
|
case UserRole.DCB_PARTNER_SUPPORT:
|
||||||
|
return 'bg-info text-white';
|
||||||
|
default:
|
||||||
|
return 'bg-secondary text-white';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleIcon(role: UserRole): string {
|
||||||
|
switch (role) {
|
||||||
|
case UserRole.DCB_PARTNER_ADMIN:
|
||||||
|
return 'lucideShield';
|
||||||
|
case UserRole.DCB_PARTNER_MANAGER:
|
||||||
|
return 'lucideUserCog';
|
||||||
|
case UserRole.DCB_PARTNER_SUPPORT:
|
||||||
|
return 'lucideHeadphones';
|
||||||
|
default:
|
||||||
|
return 'lucideUser';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== RÉFÉRENCES AUX TEMPLATES ====================
|
||||||
|
|
||||||
|
@ViewChild('createUserModal') createUserModal!: TemplateRef<any>;
|
||||||
|
@ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef<any>;
|
||||||
|
@ViewChild('deleteUserModal') deleteUserModal!: TemplateRef<any>;
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
export enum UserRole {
|
||||||
|
DCB_PARTNER_ADMIN = 'DCB_PARTNER_ADMIN',
|
||||||
|
DCB_PARTNER_MANAGER = 'DCB_PARTNER_MANAGER',
|
||||||
|
DCB_PARTNER_SUPPORT = 'DCB_PARTNER_SUPPORT',
|
||||||
|
DCB_PARTNER = 'DCB_PARTNER',
|
||||||
|
DCB_ADMIN = 'DCB_ADMIN',
|
||||||
|
DCB_SUPPORT = 'DCB_SUPPORT'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateMerchantUserDto {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
password: string;
|
||||||
|
role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
|
||||||
|
enabled?: boolean;
|
||||||
|
emailVerified?: boolean;
|
||||||
|
merchantPartnerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateMerchantUserDto {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
email?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResetPasswordDto {
|
||||||
|
newPassword: string;
|
||||||
|
temporary?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MerchantUserResponse {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
|
||||||
|
enabled: boolean;
|
||||||
|
emailVerified: boolean;
|
||||||
|
merchantPartnerId: string;
|
||||||
|
createdBy: string;
|
||||||
|
createdByUsername: string;
|
||||||
|
createdTimestamp: number;
|
||||||
|
lastLogin?: number;
|
||||||
|
userType: 'MERCHANT';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MerchantUsersStatsResponse {
|
||||||
|
totalAdmins: number;
|
||||||
|
totalManagers: number;
|
||||||
|
totalSupport: number;
|
||||||
|
totalUsers: number;
|
||||||
|
activeUsers: number;
|
||||||
|
inactiveUsers: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailableRole {
|
||||||
|
value: UserRole;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
allowedForCreation: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailableRolesResponse {
|
||||||
|
roles: AvailableRole[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchMerchantUsersParams {
|
||||||
|
query?: string;
|
||||||
|
role?: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleOperationResponse {
|
||||||
|
message: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
@ -0,0 +1,531 @@
|
|||||||
|
import { IsString, IsEmail, IsBoolean, IsOptional, IsArray, MinLength, IsNumber, IsEnum } from 'class-validator';
|
||||||
|
|
||||||
|
// ==================== TYPES AND ENUMS ====================
|
||||||
|
|
||||||
|
export type PartnerStatus =
|
||||||
|
| 'ACTIVE'
|
||||||
|
| 'INACTIVE'
|
||||||
|
|
||||||
|
export type PartnerCategory =
|
||||||
|
| 'E_COMMERCE'
|
||||||
|
| 'GAMING'
|
||||||
|
| 'ENTERTAINMENT'
|
||||||
|
| 'UTILITIES'
|
||||||
|
| 'DIGITAL_CONTENT'
|
||||||
|
| 'SERVICES'
|
||||||
|
| 'OTHER';
|
||||||
|
|
||||||
|
// ==================== CALLBACK CONFIGURATION ====================
|
||||||
|
|
||||||
|
export interface CallbackConfiguration {
|
||||||
|
headerEnrichment?: {
|
||||||
|
url?: string;
|
||||||
|
method?: 'GET' | 'POST';
|
||||||
|
headers?: { [key: string]: string };
|
||||||
|
};
|
||||||
|
subscription?: {
|
||||||
|
onCreate?: string;
|
||||||
|
onRenew?: string;
|
||||||
|
onCancel?: string;
|
||||||
|
onExpire?: string;
|
||||||
|
};
|
||||||
|
payment?: {
|
||||||
|
onSuccess?: string;
|
||||||
|
onFailure?: string;
|
||||||
|
onRefund?: string;
|
||||||
|
};
|
||||||
|
authentication?: {
|
||||||
|
onSuccess?: string;
|
||||||
|
onFailure?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CallbackConfigurationImpl implements CallbackConfiguration {
|
||||||
|
headerEnrichment?: {
|
||||||
|
url?: string;
|
||||||
|
method?: 'GET' | 'POST';
|
||||||
|
headers?: { [key: string]: string };
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
subscription?: {
|
||||||
|
onCreate?: string;
|
||||||
|
onRenew?: string;
|
||||||
|
onCancel?: string;
|
||||||
|
onExpire?: string;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
payment?: {
|
||||||
|
onSuccess?: string;
|
||||||
|
onFailure?: string;
|
||||||
|
onRefund?: string;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
authentication?: {
|
||||||
|
onSuccess?: string;
|
||||||
|
onFailure?: string;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
constructor(partial?: Partial<CallbackConfiguration>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CORE PARTNER MODELS ====================
|
||||||
|
|
||||||
|
export class PartnerAddress {
|
||||||
|
@IsString()
|
||||||
|
street: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
city: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
state: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
postalCode: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
country: string = '';
|
||||||
|
|
||||||
|
constructor(partial?: Partial<PartnerAddress>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TechnicalContact {
|
||||||
|
@IsString()
|
||||||
|
name: string = '';
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
phone: string = '';
|
||||||
|
|
||||||
|
constructor(partial?: Partial<TechnicalContact>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PartnerStats {
|
||||||
|
@IsNumber()
|
||||||
|
totalTransactions: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
totalRevenue: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
successRate: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
refundRate: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
averageAmount: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
todayTransactions: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
todayRevenue: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
activeProducts: number = 0;
|
||||||
|
|
||||||
|
lastTransactionDate?: Date;
|
||||||
|
|
||||||
|
constructor(partial?: Partial<PartnerStats>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Partner {
|
||||||
|
id: string = '';
|
||||||
|
name: string = '';
|
||||||
|
legalName: string = '';
|
||||||
|
email: string = '';
|
||||||
|
phone: string = '';
|
||||||
|
website: string = '';
|
||||||
|
|
||||||
|
@IsEnum(['ACTIVE', 'INACTIVE'])
|
||||||
|
status: PartnerStatus = 'ACTIVE';
|
||||||
|
|
||||||
|
@IsEnum(['E_COMMERCE', 'GAMING', 'ENTERTAINMENT', 'UTILITIES', 'DIGITAL_CONTENT', 'SERVICES', 'OTHER'])
|
||||||
|
category: PartnerCategory = 'OTHER';
|
||||||
|
|
||||||
|
country: string = '';
|
||||||
|
currency: string = '';
|
||||||
|
timezone: string = '';
|
||||||
|
|
||||||
|
// Configuration technique
|
||||||
|
apiKey: string = '';
|
||||||
|
secretKey: string = '';
|
||||||
|
webhookUrl: string = '';
|
||||||
|
|
||||||
|
callbacks: CallbackConfiguration = new CallbackConfigurationImpl();
|
||||||
|
|
||||||
|
// Limites et commissions
|
||||||
|
@IsNumber()
|
||||||
|
commissionRate: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
dailyLimit: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
transactionLimit: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
minAmount: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
maxAmount: number = 0;
|
||||||
|
|
||||||
|
// Adresse
|
||||||
|
address: PartnerAddress = new PartnerAddress();
|
||||||
|
|
||||||
|
// Contact technique
|
||||||
|
technicalContact: TechnicalContact = new TechnicalContact();
|
||||||
|
|
||||||
|
// Statistiques
|
||||||
|
stats: PartnerStats = new PartnerStats();
|
||||||
|
|
||||||
|
// Métadonnées
|
||||||
|
createdAt: Date = new Date();
|
||||||
|
updatedAt: Date = new Date();
|
||||||
|
createdBy: string = '';
|
||||||
|
|
||||||
|
constructor(partial?: Partial<Partner>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PARTNER DTOs ====================
|
||||||
|
|
||||||
|
export class CreatePartnerDto {
|
||||||
|
@IsString()
|
||||||
|
name: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
legalName: string = '';
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
phone: string = '';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
website: string = '';
|
||||||
|
|
||||||
|
@IsEnum(['E_COMMERCE', 'GAMING', 'ENTERTAINMENT', 'UTILITIES', 'DIGITAL_CONTENT', 'SERVICES', 'OTHER'])
|
||||||
|
category: PartnerCategory = 'OTHER';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
country: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
currency: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
timezone: string = '';
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
commissionRate: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
dailyLimit: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
transactionLimit: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
minAmount: number = 0;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
maxAmount: number = 0;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
address?: PartnerAddress;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
technicalContact?: TechnicalContact;
|
||||||
|
|
||||||
|
constructor(partial?: Partial<CreatePartnerDto>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdatePartnerDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
legalName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
website?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(['ACTIVE', 'INACTIVE', 'SUSPENDED', 'PENDING_VERIFICATION', 'BLOCKED'])
|
||||||
|
status?: PartnerStatus;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(['E_COMMERCE', 'GAMING', 'ENTERTAINMENT', 'UTILITIES', 'DIGITAL_CONTENT', 'SERVICES', 'OTHER'])
|
||||||
|
category?: PartnerCategory;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
commissionRate?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
dailyLimit?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
transactionLimit?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
minAmount?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
maxAmount?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
address?: PartnerAddress;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
technicalContact?: TechnicalContact;
|
||||||
|
|
||||||
|
constructor(partial?: Partial<UpdatePartnerDto>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateCallbacksDto {
|
||||||
|
@IsOptional()
|
||||||
|
callbacks?: CallbackConfiguration;
|
||||||
|
|
||||||
|
constructor(partial?: Partial<UpdateCallbacksDto>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== QUERY AND PAGINATION ====================
|
||||||
|
|
||||||
|
export class PartnerQuery {
|
||||||
|
@IsOptional()
|
||||||
|
page: number = 1;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
limit: number = 10;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(['ACTIVE', 'INACTIVE', 'SUSPENDED', 'PENDING_VERIFICATION', 'BLOCKED'])
|
||||||
|
status?: PartnerStatus;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(['E_COMMERCE', 'GAMING', 'ENTERTAINMENT', 'UTILITIES', 'DIGITAL_CONTENT', 'SERVICES', 'OTHER'])
|
||||||
|
category?: PartnerCategory;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
country?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
sortBy?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(['asc', 'desc'])
|
||||||
|
sortOrder: 'asc' | 'desc' = 'desc';
|
||||||
|
|
||||||
|
constructor(partial?: Partial<PartnerQuery>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PaginatedPartners {
|
||||||
|
data: Partner[] = [];
|
||||||
|
total: number = 0;
|
||||||
|
page: number = 1;
|
||||||
|
limit: number = 10;
|
||||||
|
totalPages: number = 0;
|
||||||
|
|
||||||
|
constructor(data: Partner[] = [], total: number = 0, page: number = 1, limit: number = 10) {
|
||||||
|
this.data = data;
|
||||||
|
this.total = total;
|
||||||
|
this.page = page;
|
||||||
|
this.limit = limit;
|
||||||
|
this.totalPages = Math.ceil(total / limit) || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== API RESPONSES ====================
|
||||||
|
|
||||||
|
export class ApiResponse<T> {
|
||||||
|
success: boolean = false;
|
||||||
|
data?: T = undefined;
|
||||||
|
error?: string = '';
|
||||||
|
message?: string = '';
|
||||||
|
|
||||||
|
constructor(partial?: Partial<ApiResponse<T>>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiKeyResponse {
|
||||||
|
apiKey: string = '';
|
||||||
|
secretKey: string = '';
|
||||||
|
partnerId: string = '';
|
||||||
|
createdAt: Date = new Date();
|
||||||
|
|
||||||
|
constructor(partial?: Partial<ApiKeyResponse>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PARTNER FORM DATA ====================
|
||||||
|
|
||||||
|
export interface PartnerFormData {
|
||||||
|
companyInfo: {
|
||||||
|
name: string;
|
||||||
|
legalName: string;
|
||||||
|
taxId: string;
|
||||||
|
address: string;
|
||||||
|
country: string;
|
||||||
|
};
|
||||||
|
contactInfo: {
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
};
|
||||||
|
paymentConfig: {
|
||||||
|
supportedOperators: string[];
|
||||||
|
defaultCurrency: string;
|
||||||
|
maxTransactionAmount: number;
|
||||||
|
};
|
||||||
|
webhookConfig: CallbackConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PartnerFormDataImpl implements PartnerFormData {
|
||||||
|
companyInfo: {
|
||||||
|
name: string;
|
||||||
|
legalName: string;
|
||||||
|
taxId: string;
|
||||||
|
address: string;
|
||||||
|
country: string;
|
||||||
|
} = {
|
||||||
|
name: '',
|
||||||
|
legalName: '',
|
||||||
|
taxId: '',
|
||||||
|
address: '',
|
||||||
|
country: 'CIV'
|
||||||
|
};
|
||||||
|
|
||||||
|
contactInfo: {
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
} = {
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
paymentConfig: {
|
||||||
|
supportedOperators: string[];
|
||||||
|
defaultCurrency: string;
|
||||||
|
maxTransactionAmount: number;
|
||||||
|
} = {
|
||||||
|
supportedOperators: [],
|
||||||
|
defaultCurrency: 'XOF',
|
||||||
|
maxTransactionAmount: 50000
|
||||||
|
};
|
||||||
|
|
||||||
|
webhookConfig: CallbackConfiguration = new CallbackConfigurationImpl();
|
||||||
|
|
||||||
|
constructor(partial?: Partial<PartnerFormData>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PARTNER PRODUCT ====================
|
||||||
|
|
||||||
|
export class PartnerProduct {
|
||||||
|
id: string = '';
|
||||||
|
partnerId: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
description: string = '';
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
price: number = 0;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
currency: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
category: string = '';
|
||||||
|
|
||||||
|
@IsEnum(['ACTIVE', 'INACTIVE', 'SUSPENDED'])
|
||||||
|
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED' = 'ACTIVE';
|
||||||
|
|
||||||
|
createdAt: Date = new Date();
|
||||||
|
updatedAt: Date = new Date();
|
||||||
|
|
||||||
|
constructor(partial?: Partial<PartnerProduct>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
511
src/app/modules/merchant-partners/profile/profile.html
Normal file
511
src/app/modules/merchant-partners/profile/profile.html
Normal file
@ -0,0 +1,511 @@
|
|||||||
|
<!-- src/app/modules/merchant-users/profile/profile.html -->
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- En-tête avec navigation -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-1">
|
||||||
|
@if (user) {
|
||||||
|
{{ getUserDisplayName() }}
|
||||||
|
} @else {
|
||||||
|
Profil Utilisateur Marchand
|
||||||
|
}
|
||||||
|
</h4>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb mb-0">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="javascript:void(0)" (click)="back.emit()" class="text-decoration-none cursor-pointer">
|
||||||
|
Équipe Marchande
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">
|
||||||
|
@if (user) {
|
||||||
|
{{ getUserDisplayName() }}
|
||||||
|
} @else {
|
||||||
|
Profil
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<!-- Bouton de réinitialisation de mot de passe -->
|
||||||
|
@if (user && !isEditing) {
|
||||||
|
<button
|
||||||
|
class="btn btn-warning"
|
||||||
|
(click)="resetPassword()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
||||||
|
Réinitialiser MDP
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Bouton activation/désactivation -->
|
||||||
|
@if (user.enabled) {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-warning"
|
||||||
|
(click)="disableUser()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
|
||||||
|
Désactiver
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-success"
|
||||||
|
(click)="enableUser()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideUserCheck" class="me-1"></ng-icon>
|
||||||
|
Activer
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Bouton modification -->
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
(click)="startEditing()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messages d'alerte -->
|
||||||
|
@if (error) {
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
|
<div>{{ error }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (success) {
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||||
|
<div>{{ success }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Loading State -->
|
||||||
|
@if (loading) {
|
||||||
|
<div class="col-12 text-center py-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-muted">Chargement du profil...</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- User Profile -->
|
||||||
|
@if (user && !loading) {
|
||||||
|
<!-- Colonne de gauche - Informations de base -->
|
||||||
|
<div class="col-xl-4 col-lg-5">
|
||||||
|
<!-- Carte profil -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="card-title mb-0">Profil Utilisateur Marchand</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<!-- Avatar -->
|
||||||
|
<div class="avatar-lg mx-auto mb-3">
|
||||||
|
<div class="avatar-title bg-primary bg-opacity-10 rounded-circle text-primary fs-24">
|
||||||
|
{{ getUserInitials() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>{{ getUserDisplayName() }}</h5>
|
||||||
|
<p class="text-muted mb-2">@{{ user.username }}</p>
|
||||||
|
|
||||||
|
<!-- Rôle principal -->
|
||||||
|
<span class="badge d-flex align-items-center justify-content-center mx-auto mb-3"
|
||||||
|
[ngClass]="getRoleBadgeClass(user.role)" style="max-width: 150px;">
|
||||||
|
<ng-icon [name]="getRoleIcon(user.role)" class="me-1"></ng-icon>
|
||||||
|
{{ getRoleDisplayName(user.role) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Statut -->
|
||||||
|
<span [class]="getStatusBadgeClass()" class="mb-3">
|
||||||
|
{{ getStatusText() }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Informations rapides -->
|
||||||
|
<div class="mt-4 text-start">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<ng-icon name="lucideMail" class="me-2 text-muted"></ng-icon>
|
||||||
|
<small>{{ user.email }}</small>
|
||||||
|
@if (!user.emailVerified) {
|
||||||
|
<ng-icon name="lucideAlertTriangle" class="ms-1 text-warning" size="14" title="Email non vérifié"></ng-icon>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<ng-icon name="lucideBuilding" class="me-2 text-muted"></ng-icon>
|
||||||
|
<small class="text-truncate" title="Merchant Partner ID">
|
||||||
|
{{ user.merchantPartnerId }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
||||||
|
<small>Créé le {{ getCreationDate() }}</small>
|
||||||
|
</div>
|
||||||
|
@if (user.lastLogin) {
|
||||||
|
<div class="d-flex align-items-center mt-2">
|
||||||
|
<ng-icon name="lucideLogIn" class="me-2 text-muted"></ng-icon>
|
||||||
|
<small>Dernière connexion : {{ getLastLoginDate() }}</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Carte rôle utilisateur -->
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="card-title mb-0">Rôle Utilisateur</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Rôle actuel -->
|
||||||
|
<div class="text-center mb-3">
|
||||||
|
<span class="badge d-flex align-items-center justify-content-center"
|
||||||
|
[ngClass]="getRoleBadgeClass(user.role)">
|
||||||
|
<ng-icon [name]="getRoleIcon(user.role)" class="me-2"></ng-icon>
|
||||||
|
{{ getRoleDisplayName(user.role) }}
|
||||||
|
</span>
|
||||||
|
<small class="text-muted d-block mt-2">
|
||||||
|
{{ getRoleDescription(user.role) }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Information sur le rôle -->
|
||||||
|
<div class="alert alert-light mt-3">
|
||||||
|
<small>
|
||||||
|
<strong>Information :</strong> Le rôle de l'utilisateur marchand ne peut pas être modifié directement.
|
||||||
|
Pour changer le rôle, vous devez recréer l'utilisateur avec le nouveau rôle souhaité.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informations de création -->
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="card-title mb-0">Informations de Création</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-2 small">
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Créé par :</strong>
|
||||||
|
<div class="text-muted">{{ getCreatorName() }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Date de création :</strong>
|
||||||
|
<div class="text-muted">{{ getCreationDate() }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Type d'utilisateur :</strong>
|
||||||
|
<div class="text-muted">
|
||||||
|
<span class="badge bg-primary">{{ user.userType }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Merchant Partner :</strong>
|
||||||
|
<div class="text-muted font-monospace small">
|
||||||
|
{{ user.merchantPartnerId }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonne de droite - Détails et édition -->
|
||||||
|
<div class="col-xl-8 col-lg-7">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
@if (isEditing) {
|
||||||
|
<ng-icon name="lucideEdit" class="me-2"></ng-icon>
|
||||||
|
Modification du Profil
|
||||||
|
} @else {
|
||||||
|
<ng-icon name="lucideUser" class="me-2"></ng-icon>
|
||||||
|
Détails du Compte
|
||||||
|
}
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
@if (isEditing) {
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary btn-sm"
|
||||||
|
(click)="cancelEditing()"
|
||||||
|
[disabled]="saving"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-success btn-sm"
|
||||||
|
(click)="saveProfile()"
|
||||||
|
[disabled]="saving || !isFormValid()"
|
||||||
|
>
|
||||||
|
@if (saving) {
|
||||||
|
<div class="spinner-border spinner-border-sm me-1" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<ng-icon name="lucideCheck" class="me-1"></ng-icon>
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<!-- Prénom -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Prénom <span class="text-danger">*</span></label>
|
||||||
|
@if (isEditing) {
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="editedUser.firstName"
|
||||||
|
placeholder="Entrez le prénom"
|
||||||
|
[disabled]="saving"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
} @else {
|
||||||
|
<div class="form-control-plaintext">
|
||||||
|
{{ user.firstName || 'Non renseigné' }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nom -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Nom <span class="text-danger">*</span></label>
|
||||||
|
@if (isEditing) {
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="editedUser.lastName"
|
||||||
|
placeholder="Entrez le nom"
|
||||||
|
[disabled]="saving"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
} @else {
|
||||||
|
<div class="form-control-plaintext">
|
||||||
|
{{ user.lastName || 'Non renseigné' }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nom d'utilisateur -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Nom d'utilisateur</label>
|
||||||
|
<div class="form-control-plaintext font-monospace">
|
||||||
|
{{ user.username }}
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
Le nom d'utilisateur ne peut pas être modifié
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Email <span class="text-danger">*</span></label>
|
||||||
|
@if (isEditing) {
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="editedUser.email"
|
||||||
|
placeholder="email@exemple.com"
|
||||||
|
[disabled]="saving"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
@if (editedUser.email && !isValidEmail(editedUser.email)) {
|
||||||
|
<div class="text-danger small mt-1">
|
||||||
|
Format d'email invalide
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
<div class="form-control-plaintext">
|
||||||
|
{{ user.email }}
|
||||||
|
@if (!user.emailVerified) {
|
||||||
|
<span class="badge bg-warning ms-2">Non vérifié</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rôle (lecture seule) -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Rôle</label>
|
||||||
|
<div class="form-control-plaintext">
|
||||||
|
<span class="badge" [ngClass]="getRoleBadgeClass(user.role)">
|
||||||
|
{{ getRoleDisplayName(user.role) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
Rôle assigné à la création
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Merchant Partner ID (lecture seule) -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Merchant Partner ID</label>
|
||||||
|
<div class="form-control-plaintext font-monospace small">
|
||||||
|
{{ user.merchantPartnerId }}
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
Identifiant du partenaire marchand
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statut activé -->
|
||||||
|
@if (isEditing) {
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="enabledSwitch"
|
||||||
|
[(ngModel)]="editedUser.enabled"
|
||||||
|
[disabled]="saving"
|
||||||
|
>
|
||||||
|
<label class="form-check-label" for="enabledSwitch">
|
||||||
|
Compte activé
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
L'utilisateur peut se connecter si activé
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Statut du compte</label>
|
||||||
|
<div class="form-control-plaintext">
|
||||||
|
<span [class]="getStatusBadgeClass()">
|
||||||
|
{{ getStatusText() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Informations système -->
|
||||||
|
@if (!isEditing) {
|
||||||
|
<div class="col-12">
|
||||||
|
<hr>
|
||||||
|
<h6 class="mb-3">
|
||||||
|
<ng-icon name="lucideSettings" class="me-2"></ng-icon>
|
||||||
|
Informations Système
|
||||||
|
</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">ID Utilisateur</label>
|
||||||
|
<div class="form-control-plaintext font-monospace small text-truncate">
|
||||||
|
{{ user.id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Date de création</label>
|
||||||
|
<div class="form-control-plaintext">
|
||||||
|
{{ getCreationDate() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Créé par</label>
|
||||||
|
<div class="form-control-plaintext">
|
||||||
|
{{ getCreatorName() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Type d'utilisateur</label>
|
||||||
|
<div class="form-control-plaintext">
|
||||||
|
<span class="badge bg-primary">{{ user.userType }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (user.lastLogin) {
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Dernière connexion</label>
|
||||||
|
<div class="form-control-plaintext">
|
||||||
|
{{ getLastLoginDate() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions supplémentaires -->
|
||||||
|
@if (!isEditing) {
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="card-title mb-0">Actions de Gestion</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-warning w-100"
|
||||||
|
(click)="resetPassword()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
||||||
|
Réinitialiser MDP
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
@if (user.enabled) {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary w-100"
|
||||||
|
(click)="disableUser()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
|
||||||
|
Désactiver
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-success w-100"
|
||||||
|
(click)="enableUser()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideUserCheck" class="me-1"></ng-icon>
|
||||||
|
Activer
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-primary w-100"
|
||||||
|
(click)="startEditing()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Avertissement pour la suppression -->
|
||||||
|
<div class="alert alert-light mt-3 mb-0">
|
||||||
|
<small>
|
||||||
|
<strong>Note :</strong> Pour supprimer cet utilisateur, utilisez l'action de suppression
|
||||||
|
disponible dans la liste des utilisateurs marchands.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
import { PartnerTeamProfile } from './profile';
|
||||||
|
describe('PartnerTeamProfile', () => {});
|
||||||
440
src/app/modules/merchant-partners/profile/profile.ts
Normal file
440
src/app/modules/merchant-partners/profile/profile.ts
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
// src/app/modules/merchant-users/profile/profile.ts
|
||||||
|
import { Component, inject, OnInit, Input, Output, EventEmitter, ChangeDetectorRef, OnDestroy } 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 { Subject, takeUntil } from 'rxjs';
|
||||||
|
import {
|
||||||
|
MerchantUsersService,
|
||||||
|
MerchantUserResponse,
|
||||||
|
UserRole,
|
||||||
|
UpdateMerchantUserDto
|
||||||
|
} from '../services/merchant-partners.service';
|
||||||
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-merchant-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 MerchantUserProfile implements OnInit, OnDestroy {
|
||||||
|
private merchantUsersService = inject(MerchantUsersService);
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
@Input() userId!: string;
|
||||||
|
@Output() back = new EventEmitter<void>();
|
||||||
|
@Output() openResetPasswordModal = new EventEmitter<string>();
|
||||||
|
|
||||||
|
user: MerchantUserResponse | null = null;
|
||||||
|
loading = false;
|
||||||
|
saving = false;
|
||||||
|
error = '';
|
||||||
|
success = '';
|
||||||
|
|
||||||
|
// Édition
|
||||||
|
isEditing = false;
|
||||||
|
editedUser: UpdateMerchantUserDto = {};
|
||||||
|
|
||||||
|
// Gestion des rôles
|
||||||
|
availableRoles: UserRole[] = [
|
||||||
|
UserRole.DCB_PARTNER_ADMIN,
|
||||||
|
UserRole.DCB_PARTNER_MANAGER,
|
||||||
|
UserRole.DCB_PARTNER_SUPPORT
|
||||||
|
];
|
||||||
|
updatingRole = false;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (this.userId) {
|
||||||
|
this.loadUserProfile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUserProfile() {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
|
||||||
|
this.merchantUsersService.getMerchantUserById(this.userId)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (user) => {
|
||||||
|
this.user = user;
|
||||||
|
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 merchant user profile:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startEditing() {
|
||||||
|
this.isEditing = true;
|
||||||
|
this.editedUser = {
|
||||||
|
firstName: this.user?.firstName,
|
||||||
|
lastName: this.user?.lastName,
|
||||||
|
email: this.user?.email,
|
||||||
|
enabled: this.user?.enabled
|
||||||
|
};
|
||||||
|
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.merchantUsersService.updateMerchantUser(this.user.id, this.editedUser)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.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 = this.getErrorMessage(error);
|
||||||
|
this.saving = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
console.error('Error updating merchant user:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion des rôles
|
||||||
|
updateUserRole(newRole: UserRole) {
|
||||||
|
if (!this.user) return;
|
||||||
|
|
||||||
|
this.updatingRole = true;
|
||||||
|
this.error = '';
|
||||||
|
this.success = '';
|
||||||
|
|
||||||
|
// Pour changer le rôle, on doit recréer l'utilisateur ou utiliser une méthode spécifique
|
||||||
|
// Pour l'instant, on utilise la mise à jour standard
|
||||||
|
const updateData: UpdateMerchantUserDto = {
|
||||||
|
...this.editedUser
|
||||||
|
// Note: Le rôle n'est pas modifiable via update dans l'API actuelle
|
||||||
|
// Vous devrez peut-être implémenter une méthode spécifique dans le service
|
||||||
|
};
|
||||||
|
|
||||||
|
this.merchantUsersService.updateMerchantUser(this.user.id, updateData)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (updatedUser) => {
|
||||||
|
this.user = updatedUser;
|
||||||
|
this.updatingRole = false;
|
||||||
|
this.success = 'Profil mis à jour avec succès';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.updatingRole = false;
|
||||||
|
this.error = this.getErrorMessage(error);
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion du statut
|
||||||
|
enableUser() {
|
||||||
|
if (!this.user) return;
|
||||||
|
|
||||||
|
this.error = '';
|
||||||
|
this.success = '';
|
||||||
|
|
||||||
|
this.merchantUsersService.enableMerchantUser(this.user.id)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (updatedUser) => {
|
||||||
|
this.user = updatedUser;
|
||||||
|
this.success = 'Utilisateur activé avec succès';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.error = this.getErrorMessage(error);
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
console.error('Error enabling merchant user:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disableUser() {
|
||||||
|
if (!this.user) return;
|
||||||
|
|
||||||
|
this.error = '';
|
||||||
|
this.success = '';
|
||||||
|
|
||||||
|
this.merchantUsersService.disableMerchantUser(this.user.id)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (updatedUser) => {
|
||||||
|
this.user = updatedUser;
|
||||||
|
this.success = 'Utilisateur désactivé avec succès';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.error = this.getErrorMessage(error);
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
console.error('Error disabling merchant user:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Réinitialisation du mot de passe
|
||||||
|
resetPassword() {
|
||||||
|
if (this.user) {
|
||||||
|
this.openResetPasswordModal.emit(this.user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 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 {
|
||||||
|
if (!timestamp) return 'Non disponible';
|
||||||
|
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: UserRole): string {
|
||||||
|
switch (role) {
|
||||||
|
case UserRole.DCB_PARTNER_ADMIN:
|
||||||
|
return 'bg-danger';
|
||||||
|
case UserRole.DCB_PARTNER_MANAGER:
|
||||||
|
return 'bg-warning text-dark';
|
||||||
|
case UserRole.DCB_PARTNER_SUPPORT:
|
||||||
|
return 'bg-info text-white';
|
||||||
|
default:
|
||||||
|
return 'bg-secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleDisplayName(role: UserRole): string {
|
||||||
|
const roleNames = {
|
||||||
|
[UserRole.DCB_PARTNER_ADMIN]: 'Administrateur',
|
||||||
|
[UserRole.DCB_PARTNER_MANAGER]: 'Manager',
|
||||||
|
[UserRole.DCB_PARTNER_SUPPORT]: 'Support'
|
||||||
|
};
|
||||||
|
return roleNames[role] || role;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleIcon(role: UserRole): string {
|
||||||
|
switch (role) {
|
||||||
|
case UserRole.DCB_PARTNER_ADMIN:
|
||||||
|
return 'lucideShield';
|
||||||
|
case UserRole.DCB_PARTNER_MANAGER:
|
||||||
|
return 'lucideUserCog';
|
||||||
|
case UserRole.DCB_PARTNER_SUPPORT:
|
||||||
|
return 'lucideHeadphones';
|
||||||
|
default:
|
||||||
|
return 'lucideUser';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleDescription(role: UserRole): string {
|
||||||
|
const descriptions = {
|
||||||
|
[UserRole.DCB_PARTNER_ADMIN]: 'Accès administratif complet au sein du partenaire marchand',
|
||||||
|
[UserRole.DCB_PARTNER_MANAGER]: 'Accès de gestion avec capacités administratives limitées',
|
||||||
|
[UserRole.DCB_PARTNER_SUPPORT]: 'Rôle support avec accès en lecture seule et opérations de base'
|
||||||
|
};
|
||||||
|
return descriptions[role] || 'Description non disponible';
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserType(): string {
|
||||||
|
if (!this.user) return 'Utilisateur';
|
||||||
|
|
||||||
|
switch (this.user.role) {
|
||||||
|
case UserRole.DCB_PARTNER_ADMIN:
|
||||||
|
return 'Administrateur';
|
||||||
|
case UserRole.DCB_PARTNER_MANAGER:
|
||||||
|
return 'Manager';
|
||||||
|
case UserRole.DCB_PARTNER_SUPPORT:
|
||||||
|
return 'Support';
|
||||||
|
default:
|
||||||
|
return 'Utilisateur';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserTypeBadgeClass(): string {
|
||||||
|
const userType = this.getUserType();
|
||||||
|
switch (userType) {
|
||||||
|
case 'Administrateur': return 'bg-danger';
|
||||||
|
case 'Manager': return 'bg-success';
|
||||||
|
case 'Support': return 'bg-info';
|
||||||
|
default: return 'bg-secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== GESTION DES ERREURS ====================
|
||||||
|
|
||||||
|
private getErrorMessage(error: any): string {
|
||||||
|
if (error.error?.message) {
|
||||||
|
return error.error.message;
|
||||||
|
}
|
||||||
|
if (error.status === 400) {
|
||||||
|
return 'Données invalides. Vérifiez les informations saisies.';
|
||||||
|
}
|
||||||
|
if (error.status === 403) {
|
||||||
|
return 'Vous n\'avez pas les permissions pour effectuer cette action.';
|
||||||
|
}
|
||||||
|
if (error.status === 404) {
|
||||||
|
return 'Utilisateur non trouvé.';
|
||||||
|
}
|
||||||
|
if (error.status === 409) {
|
||||||
|
return 'Conflit de données. Cet utilisateur existe peut-être déjà.';
|
||||||
|
}
|
||||||
|
return 'Erreur lors de l\'opération. Veuillez réessayer.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== VÉRIFICATIONS DE PERMISSIONS ====================
|
||||||
|
|
||||||
|
canEditUser(): boolean {
|
||||||
|
// Logique pour déterminer si l'utilisateur connecté peut éditer cet utilisateur
|
||||||
|
return true; // Temporaire - à implémenter
|
||||||
|
}
|
||||||
|
|
||||||
|
canManageRoles(): boolean {
|
||||||
|
// Logique pour déterminer si l'utilisateur connecté peut gérer les rôles
|
||||||
|
return true; // Temporaire - à implémenter
|
||||||
|
}
|
||||||
|
|
||||||
|
canEnableDisableUser(): boolean {
|
||||||
|
// Empêcher la désactivation de soi-même
|
||||||
|
return this.user?.id !== 'current-user-id';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MÉTHODES DE NAVIGATION ====================
|
||||||
|
|
||||||
|
goBack() {
|
||||||
|
this.back.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MÉTHODES DE VALIDATION ====================
|
||||||
|
|
||||||
|
isFormValid(): boolean {
|
||||||
|
if (!this.editedUser.firstName?.trim() || !this.editedUser.lastName?.trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.editedUser.email?.trim() || !this.isValidEmail(this.editedUser.email)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isValidEmail(email: string): boolean {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MÉTHODES UTILITAIRES ====================
|
||||||
|
|
||||||
|
isAdmin(): boolean {
|
||||||
|
return this.user?.role === UserRole.DCB_PARTNER_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
isManager(): boolean {
|
||||||
|
return this.user?.role === UserRole.DCB_PARTNER_MANAGER;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSupport(): boolean {
|
||||||
|
return this.user?.role === UserRole.DCB_PARTNER_SUPPORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.loadUserProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMessages() {
|
||||||
|
this.error = '';
|
||||||
|
this.success = '';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifie si c'est le profil de l'utilisateur courant
|
||||||
|
isCurrentUserProfile(): boolean {
|
||||||
|
// Implémentez cette logique selon votre système d'authentification
|
||||||
|
// Exemple: return this.authService.getCurrentUserId() === this.user?.id;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour obtenir la date de création formatée
|
||||||
|
getCreationDate(): string {
|
||||||
|
if (!this.user?.createdTimestamp) return 'Non disponible';
|
||||||
|
return this.formatTimestamp(this.user.createdTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour obtenir la date de dernière connexion formatée
|
||||||
|
getLastLoginDate(): string {
|
||||||
|
if (!this.user?.lastLogin) return 'Jamais connecté';
|
||||||
|
return this.formatTimestamp(this.user.lastLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour obtenir le nom du créateur
|
||||||
|
getCreatorName(): string {
|
||||||
|
if (!this.user?.createdByUsername) return 'Non disponible';
|
||||||
|
return this.user.createdByUsername;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,347 @@
|
|||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { environment } from '@environments/environment';
|
||||||
|
import { Observable, map, catchError, throwError, of } from 'rxjs';
|
||||||
|
|
||||||
|
// Interfaces alignées avec le contrôleur MerchantUsersController
|
||||||
|
export interface MerchantUserResponse {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
|
||||||
|
enabled: boolean;
|
||||||
|
emailVerified: boolean;
|
||||||
|
merchantPartnerId: string;
|
||||||
|
createdBy: string;
|
||||||
|
createdByUsername: string;
|
||||||
|
createdTimestamp: number;
|
||||||
|
lastLogin?: number;
|
||||||
|
userType: 'MERCHANT';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateMerchantUserDto {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
password: string;
|
||||||
|
role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
|
||||||
|
enabled?: boolean;
|
||||||
|
emailVerified?: boolean;
|
||||||
|
merchantPartnerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateMerchantUserDto {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
email?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResetPasswordDto {
|
||||||
|
newPassword: string;
|
||||||
|
temporary?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MerchantPartnerStatsResponse {
|
||||||
|
totalAdmins: number;
|
||||||
|
totalManagers: number;
|
||||||
|
totalSupport: number;
|
||||||
|
totalUsers: number;
|
||||||
|
activeUsers: number;
|
||||||
|
inactiveUsers: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailableRole {
|
||||||
|
value: UserRole;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
allowedForCreation: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailableRolesResponse {
|
||||||
|
roles: AvailableRole[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchMerchantUsersParams {
|
||||||
|
query?: string;
|
||||||
|
role?: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserRole {
|
||||||
|
DCB_PARTNER_ADMIN = 'DCB_PARTNER_ADMIN',
|
||||||
|
DCB_PARTNER_MANAGER = 'DCB_PARTNER_MANAGER',
|
||||||
|
DCB_PARTNER_SUPPORT = 'DCB_PARTNER_SUPPORT'
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class MerchantUsersService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private apiUrl = `${environment.iamApiUrl}/merchant-users`;
|
||||||
|
|
||||||
|
// === RÉCUPÉRATION D'UTILISATEURS ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les utilisateurs marchands de l'utilisateur courant
|
||||||
|
*/
|
||||||
|
getMyMerchantUsers(): Observable<MerchantUserResponse[]> {
|
||||||
|
return this.http.get<MerchantUserResponse[]>(this.apiUrl).pipe(
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error loading my merchant users:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les utilisateurs marchands par ID de partenaire
|
||||||
|
*/
|
||||||
|
getMerchantUsersByPartner(partnerId: string): Observable<MerchantUserResponse[]> {
|
||||||
|
return this.http.get<MerchantUserResponse[]>(`${this.apiUrl}/partner/${partnerId}`).pipe(
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error loading merchant users for partner ${partnerId}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un utilisateur marchand par ID
|
||||||
|
*/
|
||||||
|
getMerchantUserById(id: string): Observable<MerchantUserResponse> {
|
||||||
|
return this.http.get<MerchantUserResponse>(`${this.apiUrl}/${id}`).pipe(
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error loading merchant user ${id}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === CRÉATION D'UTILISATEURS ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un nouvel utilisateur marchand
|
||||||
|
*/
|
||||||
|
createMerchantUser(createUserDto: CreateMerchantUserDto): Observable<MerchantUserResponse> {
|
||||||
|
// Validation
|
||||||
|
if (!createUserDto.username?.trim()) {
|
||||||
|
return throwError(() => 'Username is required and cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!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');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createUserDto.role) {
|
||||||
|
return throwError(() => 'Role is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createUserDto.merchantPartnerId?.trim()) {
|
||||||
|
return throwError(() => 'Merchant Partner ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
role: createUserDto.role,
|
||||||
|
merchantPartnerId: createUserDto.merchantPartnerId.trim(),
|
||||||
|
enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true,
|
||||||
|
emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.http.post<MerchantUserResponse>(this.apiUrl, payload).pipe(
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error creating merchant user:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MISE À JOUR D'UTILISATEURS ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour un utilisateur marchand
|
||||||
|
*/
|
||||||
|
updateMerchantUser(id: string, updateUserDto: UpdateMerchantUserDto): Observable<MerchantUserResponse> {
|
||||||
|
return this.http.put<MerchantUserResponse>(`${this.apiUrl}/${id}`, updateUserDto).pipe(
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error updating merchant user ${id}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === SUPPRESSION D'UTILISATEURS ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un utilisateur marchand
|
||||||
|
*/
|
||||||
|
deleteMerchantUser(id: string): Observable<{ message: string }> {
|
||||||
|
return this.http.delete<{ message: string }>(`${this.apiUrl}/${id}`).pipe(
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error deleting merchant user ${id}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === GESTION DES MOTS DE PASSE ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réinitialise le mot de passe d'un utilisateur marchand
|
||||||
|
*/
|
||||||
|
resetMerchantUserPassword(id: string, resetPasswordDto: ResetPasswordDto): Observable<{ message: string }> {
|
||||||
|
return this.http.post<{ message: string }>(
|
||||||
|
`${this.apiUrl}/${id}/reset-password`,
|
||||||
|
resetPasswordDto
|
||||||
|
).pipe(
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error resetting password for merchant user ${id}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === STATISTIQUES ET RAPPORTS ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les statistiques des utilisateurs marchands
|
||||||
|
*/
|
||||||
|
getMerchantUsersStats(): Observable<MerchantPartnerStatsResponse> {
|
||||||
|
return this.http.get<MerchantPartnerStatsResponse>(`${this.apiUrl}/stats/overview`).pipe(
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error loading merchant users stats:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === RECHERCHE ET FILTRES ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche des utilisateurs marchands avec filtres
|
||||||
|
*/
|
||||||
|
searchMerchantUsers(params: SearchMerchantUsersParams): Observable<MerchantUserResponse[]> {
|
||||||
|
let httpParams = new HttpParams();
|
||||||
|
|
||||||
|
if (params.query) {
|
||||||
|
httpParams = httpParams.set('query', params.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.role) {
|
||||||
|
httpParams = httpParams.set('role', params.role);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.enabled !== undefined) {
|
||||||
|
httpParams = httpParams.set('enabled', params.enabled.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<MerchantUserResponse[]>(`${this.apiUrl}/search`, { params: httpParams }).pipe(
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error searching merchant users:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === GESTION DES RÔLES ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les rôles marchands disponibles
|
||||||
|
*/
|
||||||
|
getAvailableMerchantRoles(): Observable<AvailableRolesResponse> {
|
||||||
|
return this.http.get<AvailableRolesResponse>(`${this.apiUrl}/roles/available`).pipe(
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error loading available merchant roles:', error);
|
||||||
|
// Fallback en cas d'erreur
|
||||||
|
return of({
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
value: UserRole.DCB_PARTNER_ADMIN,
|
||||||
|
label: 'Partner Admin',
|
||||||
|
description: 'Full administrative access within the merchant partner',
|
||||||
|
allowedForCreation: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: UserRole.DCB_PARTNER_MANAGER,
|
||||||
|
label: 'Partner Manager',
|
||||||
|
description: 'Manager access with limited administrative capabilities',
|
||||||
|
allowedForCreation: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: UserRole.DCB_PARTNER_SUPPORT,
|
||||||
|
label: 'Partner Support',
|
||||||
|
description: 'Support role with read-only and basic operational access',
|
||||||
|
allowedForCreation: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === GESTION DU STATUT ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active un utilisateur marchand
|
||||||
|
*/
|
||||||
|
enableMerchantUser(id: string): Observable<MerchantUserResponse> {
|
||||||
|
return this.updateMerchantUser(id, { enabled: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Désactive un utilisateur marchand
|
||||||
|
*/
|
||||||
|
disableMerchantUser(id: string): Observable<MerchantUserResponse> {
|
||||||
|
return this.updateMerchantUser(id, { enabled: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// === UTILITAIRES ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un nom d'utilisateur existe parmi les utilisateurs marchands
|
||||||
|
*/
|
||||||
|
merchantUserExists(username: string): Observable<{ exists: boolean }> {
|
||||||
|
return this.getMyMerchantUsers().pipe(
|
||||||
|
map(users => ({
|
||||||
|
exists: users.some(user => user.username === username)
|
||||||
|
})),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error checking if merchant user exists:', error);
|
||||||
|
return of({ exists: false });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les utilisateurs par rôle spécifique
|
||||||
|
*/
|
||||||
|
getMerchantUsersByRole(role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT): Observable<MerchantUserResponse[]> {
|
||||||
|
return this.searchMerchantUsers({ role });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère uniquement les utilisateurs actifs
|
||||||
|
*/
|
||||||
|
getActiveMerchantUsers(): Observable<MerchantUserResponse[]> {
|
||||||
|
return this.searchMerchantUsers({ enabled: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère uniquement les utilisateurs inactifs
|
||||||
|
*/
|
||||||
|
getInactiveMerchantUsers(): Observable<MerchantUserResponse[]> {
|
||||||
|
return this.searchMerchantUsers({ enabled: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,233 @@
|
|||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { environment } from '@environments/environment';
|
||||||
|
import { catchError, Observable, of, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Partner,
|
||||||
|
CreatePartnerDto,
|
||||||
|
UpdatePartnerDto,
|
||||||
|
PartnerQuery,
|
||||||
|
PaginatedPartners,
|
||||||
|
ApiResponse,
|
||||||
|
CallbackConfiguration,
|
||||||
|
UpdateCallbacksDto,
|
||||||
|
ApiKeyResponse,
|
||||||
|
PartnerStats
|
||||||
|
} from '../models/partners-config.model';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class PartnerConfigService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private apiUrl = `${environment.localServiceTestApiUrl}/partners/config`;
|
||||||
|
|
||||||
|
// ==================== GESTION DES MARCHANDS (ADMIN) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Créer une config marchand
|
||||||
|
*/
|
||||||
|
createPartnerConfig(createPartnerDto: CreatePartnerDto): Observable<ApiResponse<Partner>> {
|
||||||
|
return this.http.post<ApiResponse<Partner>>(`${this.apiUrl}`, createPartnerDto).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir toutes les une config marchands avec pagination
|
||||||
|
*/
|
||||||
|
findAllPartnersConfig(query: PartnerQuery = new PartnerQuery()): Observable<PaginatedPartners> {
|
||||||
|
const params = this.buildQueryParams(query);
|
||||||
|
|
||||||
|
return this.http.get<PaginatedPartners>(`${this.apiUrl}`, { params }).pipe(
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error loading merchants:', error);
|
||||||
|
return of(new PaginatedPartners());
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir une config marchand par son ID
|
||||||
|
*/
|
||||||
|
getPartnerConfigById(merchantId: string): Observable<ApiResponse<Partner>> {
|
||||||
|
return this.http.get<ApiResponse<Partner>>(`${this.apiUrl}/${merchantId}`).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mettre à jour une config marchand
|
||||||
|
*/
|
||||||
|
updatePartnerConfig(merchantId: string, updateData: UpdatePartnerDto): Observable<ApiResponse<Partner>> {
|
||||||
|
return this.http.put<ApiResponse<Partner>>(`${this.apiUrl}/${merchantId}`, updateData).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprimer une config marchand
|
||||||
|
*/
|
||||||
|
deletePartnerConfig(merchantId: string): Observable<ApiResponse<void>> {
|
||||||
|
return this.http.delete<ApiResponse<void>>(`${this.apiUrl}/${merchantId}`).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== GESTION DES CALLBACKS ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mettre à jour la configuration des callbacks d'un marchand
|
||||||
|
*/
|
||||||
|
updateCallbacksConfig(merchantId: string, updateData: UpdateCallbacksDto): Observable<ApiResponse<Partner>> {
|
||||||
|
return this.http.put<ApiResponse<Partner>>(
|
||||||
|
`${this.apiUrl}/${merchantId}/callbacks`,
|
||||||
|
updateData
|
||||||
|
).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir la configuration des callbacks d'un marchand
|
||||||
|
*/
|
||||||
|
getCallbacksConfig(merchantId: string): Observable<ApiResponse<CallbackConfiguration>> {
|
||||||
|
return this.http.get<ApiResponse<CallbackConfiguration>>(
|
||||||
|
`${this.apiUrl}/${merchantId}/callbacks`
|
||||||
|
).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tester un webhook spécifique
|
||||||
|
*/
|
||||||
|
testWebhookConfig(merchantId: string, webhookType: string, payload: any = {}): Observable<ApiResponse<any>> {
|
||||||
|
return this.http.post<ApiResponse<any>>(
|
||||||
|
`${this.apiUrl}/${merchantId}/callbacks/test/${webhookType}`,
|
||||||
|
payload
|
||||||
|
).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les logs des webhooks d'un marchand
|
||||||
|
*/
|
||||||
|
getWebhookLogsConfig(merchantId: string, query: any = {}): Observable<ApiResponse<any>> {
|
||||||
|
const params = this.buildQueryParams(query);
|
||||||
|
return this.http.get<ApiResponse<any>>(
|
||||||
|
`${this.apiUrl}/${merchantId}/callbacks/logs`,
|
||||||
|
{ params }
|
||||||
|
).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== GESTION DES STATISTIQUES ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les statistiques d'un marchand
|
||||||
|
*/
|
||||||
|
getPartnerStats(merchantId: string): Observable<ApiResponse<PartnerStats>> {
|
||||||
|
return this.http.get<ApiResponse<PartnerStats>>(
|
||||||
|
`${this.apiUrl}/${merchantId}/stats`
|
||||||
|
).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir mes statistiques (marchand connecté)
|
||||||
|
*/
|
||||||
|
getMyStats(): Observable<ApiResponse<PartnerStats>> {
|
||||||
|
return this.http.get<ApiResponse<PartnerStats>>(
|
||||||
|
`${this.apiUrl}/me/stats`
|
||||||
|
).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les statistiques globales (admin seulement)
|
||||||
|
*/
|
||||||
|
getGlobalStats(): Observable<ApiResponse<any>> {
|
||||||
|
return this.http.get<ApiResponse<any>>(
|
||||||
|
`${this.apiUrl}/stats/global`
|
||||||
|
).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== GESTION DES CLÉS API ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Générer de nouvelles clés API pour un marchand
|
||||||
|
*/
|
||||||
|
generateApiKeys(merchantId: string): Observable<ApiResponse<ApiKeyResponse>> {
|
||||||
|
return this.http.post<ApiResponse<ApiKeyResponse>>(
|
||||||
|
`${this.apiUrl}/${merchantId}/api-keys`,
|
||||||
|
{}
|
||||||
|
).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Révoker les clés API d'un marchand
|
||||||
|
*/
|
||||||
|
revokeApiKeys(merchantId: string): Observable<ApiResponse<void>> {
|
||||||
|
return this.http.delete<ApiResponse<void>>(
|
||||||
|
`${this.apiUrl}/${merchantId}/api-keys`
|
||||||
|
).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Régénérer la clé secrète d'un marchand
|
||||||
|
*/
|
||||||
|
regenerateSecretKey(merchantId: string): Observable<ApiResponse<ApiKeyResponse>> {
|
||||||
|
return this.http.put<ApiResponse<ApiKeyResponse>>(
|
||||||
|
`${this.apiUrl}/${merchantId}/api-keys/regenerate-secret`,
|
||||||
|
{}
|
||||||
|
).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les clés API d'un marchand
|
||||||
|
*/
|
||||||
|
getApiKeys(merchantId: string): Observable<ApiResponse<ApiKeyResponse>> {
|
||||||
|
return this.http.get<ApiResponse<ApiKeyResponse>>(
|
||||||
|
`${this.apiUrl}/${merchantId}/api-keys`
|
||||||
|
).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MÉTHODES UTILITAIRES ====================
|
||||||
|
|
||||||
|
private buildQueryParams(query: any): { [key: string]: string } {
|
||||||
|
const params: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
Object.keys(query).forEach(key => {
|
||||||
|
if (query[key] !== undefined && query[key] !== null && query[key] !== '') {
|
||||||
|
params[key] = query[key].toString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valider la configuration d'un marchand
|
||||||
|
*/
|
||||||
|
validatePartnerConfig(merchantId: string): Observable<ApiResponse<{ isValid: boolean; errors: string[] }>> {
|
||||||
|
return this.http.get<ApiResponse<{ isValid: boolean; errors: string[] }>>(
|
||||||
|
`${this.apiUrl}/${merchantId}/validate`
|
||||||
|
).pipe(
|
||||||
|
catchError(error => throwError(() => error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
249
src/app/modules/merchant-partners/stats/stats.html
Normal file
249
src/app/modules/merchant-partners/stats/stats.html
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
<!-- src/app/modules/merchant-users/stats/stats.html -->
|
||||||
|
<app-ui-card title="Statistiques de l'Équipe Marchande">
|
||||||
|
<a
|
||||||
|
helper-text
|
||||||
|
href="javascript:void(0);"
|
||||||
|
class="icon-link icon-link-hover link-primary fw-semibold"
|
||||||
|
>Vue d'ensemble des utilisateurs de votre écosystème marchand
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div card-body>
|
||||||
|
@if (!stats) {
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-muted">Chargement des statistiques...</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (stats) {
|
||||||
|
<div class="row">
|
||||||
|
<!-- KPI Cards -->
|
||||||
|
<div class="col-xl-3 col-md-6">
|
||||||
|
<div class="card card-animate">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-uppercase fw-medium text-muted mb-0">Total Utilisateurs</p>
|
||||||
|
<h4 class="mt-2 mb-0 text-primary">{{ stats.totalUsers }}</h4>
|
||||||
|
<p class="mb-0">
|
||||||
|
<span class="badge bg-success-subtle text-success mt-1">
|
||||||
|
<ng-icon name="lucideUsers" class="me-1"></ng-icon>
|
||||||
|
Équipe complète
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
||||||
|
<ng-icon name="lucideUsers" class="text-primary fs-20"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-3 col-md-6">
|
||||||
|
<div class="card card-animate">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-uppercase fw-medium text-muted mb-0">Administrateurs</p>
|
||||||
|
<h4 class="mt-2 mb-0 text-danger">{{ stats.totalAdmins }}</h4>
|
||||||
|
<p class="mb-0">
|
||||||
|
<span class="badge bg-danger-subtle text-danger mt-1">
|
||||||
|
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||||
|
Accès complet
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="avatar-sm bg-danger bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
||||||
|
<ng-icon name="lucideShield" class="text-danger fs-20"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-3 col-md-6">
|
||||||
|
<div class="card card-animate">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-uppercase fw-medium text-muted mb-0">Managers</p>
|
||||||
|
<h4 class="mt-2 mb-0 text-warning">{{ stats.totalManagers }}</h4>
|
||||||
|
<p class="mb-0">
|
||||||
|
<span class="badge bg-warning-subtle text-warning mt-1">
|
||||||
|
<ng-icon name="lucideUserCog" class="me-1"></ng-icon>
|
||||||
|
Gestion opérationnelle
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="avatar-sm bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
||||||
|
<ng-icon name="lucideUserCog" class="text-warning fs-20"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-3 col-md-6">
|
||||||
|
<div class="card card-animate">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-uppercase fw-medium text-muted mb-0">Support</p>
|
||||||
|
<h4 class="mt-2 mb-0 text-info">{{ stats.totalSupport }}</h4>
|
||||||
|
<p class="mb-0">
|
||||||
|
<span class="badge bg-info-subtle text-info mt-1">
|
||||||
|
<ng-icon name="lucideHeadphones" class="me-1"></ng-icon>
|
||||||
|
Assistance client
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="avatar-sm bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
||||||
|
<ng-icon name="lucideHeadphones" class="text-info fs-20"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistiques d'activité -->
|
||||||
|
<div class="col-xl-4 col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h6 class="card-title mb-3">Utilisateurs Actifs</h6>
|
||||||
|
<div class="mb-3">
|
||||||
|
<h2 class="text-success">{{ stats.activeUsers }}</h2>
|
||||||
|
<p class="text-muted mb-0">Comptes activés</p>
|
||||||
|
</div>
|
||||||
|
<div class="progress mb-2">
|
||||||
|
<div class="progress-bar bg-success"
|
||||||
|
[style.width]="(stats.activeUsers / stats.totalUsers * 100) + '%'">
|
||||||
|
{{ (stats.activeUsers / stats.totalUsers * 100).toFixed(1) }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ stats.activeUsers }} sur {{ stats.totalUsers }} utilisateurs
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-4 col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h6 class="card-title mb-3">Utilisateurs Inactifs</h6>
|
||||||
|
<div class="mb-3">
|
||||||
|
<h2 class="text-danger">{{ stats.inactiveUsers }}</h2>
|
||||||
|
<p class="text-muted mb-0">Comptes désactivés</p>
|
||||||
|
</div>
|
||||||
|
<div class="progress mb-2">
|
||||||
|
<div class="progress-bar bg-danger"
|
||||||
|
[style.width]="(stats.inactiveUsers / stats.totalUsers * 100) + '%'">
|
||||||
|
{{ (stats.inactiveUsers / stats.totalUsers * 100).toFixed(1) }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ stats.inactiveUsers }} sur {{ stats.totalUsers }} utilisateurs
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-4 col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h6 class="card-title mb-3">Répartition des Rôles</h6>
|
||||||
|
<div class="d-flex justify-content-around text-center">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-danger mb-1">{{ stats.totalAdmins }}</h4>
|
||||||
|
<small class="text-muted">Admins</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-warning mb-1">{{ stats.totalManagers }}</h4>
|
||||||
|
<small class="text-muted">Managers</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-info mb-1">{{ stats.totalSupport }}</h4>
|
||||||
|
<small class="text-muted">Support</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="progress" style="height: 8px;">
|
||||||
|
<div class="progress-bar bg-danger"
|
||||||
|
[style.width]="(stats.totalAdmins / stats.totalUsers * 100) + '%'"
|
||||||
|
title="Administrateurs">
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar bg-warning"
|
||||||
|
[style.width]="(stats.totalManagers / stats.totalUsers * 100) + '%'"
|
||||||
|
title="Managers">
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar bg-info"
|
||||||
|
[style.width]="(stats.totalSupport / stats.totalUsers * 100) + '%'"
|
||||||
|
title="Support">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Résumé textuel -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title mb-3">Synthèse de l'Équipe</h6>
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="border-end">
|
||||||
|
<h5 class="text-primary mb-1">{{ stats.totalUsers }}</h5>
|
||||||
|
<small class="text-muted">Total Membres</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="border-end">
|
||||||
|
<h5 class="text-success mb-1">{{ stats.activeUsers }}</h5>
|
||||||
|
<small class="text-muted">Actifs</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="border-end">
|
||||||
|
<h5 class="text-danger mb-1">{{ stats.inactiveUsers }}</h5>
|
||||||
|
<small class="text-muted">Inactifs</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<h5 class="text-info mb-1">{{ stats.totalAdmins + stats.totalManagers + stats.totalSupport }}</h5>
|
||||||
|
<small class="text-muted">Avec Rôle Défini</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message d'information -->
|
||||||
|
<div class="alert alert-light mt-3 mb-0">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideInfo" class="me-2 text-info"></ng-icon>
|
||||||
|
<div>
|
||||||
|
<small>
|
||||||
|
Votre équipe marchande est composée de <strong>{{ stats.totalAdmins }} administrateurs</strong>,
|
||||||
|
<strong>{{ stats.totalManagers }} managers</strong> et <strong>{{ stats.totalSupport }} agents de support</strong>.
|
||||||
|
<strong>{{ stats.activeUsers }} utilisateurs</strong> sont actuellement actifs.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</app-ui-card>
|
||||||
2
src/app/modules/merchant-partners/stats/stats.spec.ts
Normal file
2
src/app/modules/merchant-partners/stats/stats.spec.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { MerchantPartnerStats } from './stats';
|
||||||
|
describe('Merchant Partner Stats', () => {});
|
||||||
15
src/app/modules/merchant-partners/stats/stats.ts
Normal file
15
src/app/modules/merchant-partners/stats/stats.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgIcon } from '@ng-icons/core';
|
||||||
|
import { UiCard } from '@app/components/ui-card';
|
||||||
|
import { MerchantPartnerStatsResponse } from '../services/merchant-partners.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-merchant-users-stats',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, NgIcon, UiCard],
|
||||||
|
templateUrl: './stats.html'
|
||||||
|
})
|
||||||
|
export class MerchantPartnerStats {
|
||||||
|
@Input() stats: MerchantPartnerStatsResponse | null = null;
|
||||||
|
}
|
||||||
6
src/app/modules/merchant-partners/types.ts
Normal file
6
src/app/modules/merchant-partners/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export type WizardStepType = {
|
||||||
|
id: string
|
||||||
|
icon: string
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
}
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import { WizardStepType } from './types'
|
|
||||||
|
|
||||||
export const wizardSteps: WizardStepType[] = [
|
|
||||||
{
|
|
||||||
id: 'stuInfo',
|
|
||||||
icon: 'tablerUserCircle',
|
|
||||||
title: 'Student Info',
|
|
||||||
subtitle: 'Personal details',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'addrInfo',
|
|
||||||
icon: 'tablerMapPin',
|
|
||||||
title: 'Address Info',
|
|
||||||
subtitle: 'Where you live',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'courseInfo',
|
|
||||||
icon: 'tablerBook',
|
|
||||||
title: 'Course Info',
|
|
||||||
subtitle: 'Select your course',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'parentInfo',
|
|
||||||
icon: 'tablerUsers',
|
|
||||||
title: 'Parent Info',
|
|
||||||
subtitle: 'Guardian details',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'documents',
|
|
||||||
icon: 'tablerFolder',
|
|
||||||
title: 'Documents',
|
|
||||||
subtitle: 'Upload certificates',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@ -1,588 +0,0 @@
|
|||||||
<div class="container-fluid">
|
|
||||||
<app-page-title
|
|
||||||
title="Gestion des Merchants"
|
|
||||||
subTitle="Administrez vos partenaires marchands DCB - Liste, Configuration et Statistiques"
|
|
||||||
[badge]="{icon:'lucideStore', text:'Partner Management'}"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Navigation par onglets -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav nav-tabs nav-bordered">
|
|
||||||
<li [ngbNavItem]="'list'">
|
|
||||||
<a ngbNavLink (click)="onTabChange('list')">
|
|
||||||
<ng-icon name="lucideList" class="me-1"></ng-icon>
|
|
||||||
Liste des Merchants
|
|
||||||
</a>
|
|
||||||
<ng-template ngbNavContent>
|
|
||||||
<!-- CONTENU LISTE -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button class="btn btn-primary" (click)="activeTab = 'config'">
|
|
||||||
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
|
|
||||||
Nouveau Merchant
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary" (click)="loadMerchants()">
|
|
||||||
<ng-icon name="lucideRefreshCw" class="me-1"></ng-icon>
|
|
||||||
Actualiser
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="row g-2">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<input type="text" class="form-control" placeholder="Rechercher..."
|
|
||||||
[(ngModel)]="searchTerm" (input)="currentPage = 1">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<select class="form-select" [(ngModel)]="statusFilter" (change)="currentPage = 1">
|
|
||||||
<option value="all">Tous les statuts</option>
|
|
||||||
<option value="ACTIVE">Actifs</option>
|
|
||||||
<option value="PENDING">En attente</option>
|
|
||||||
<option value="SUSPENDED">Suspendus</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<select class="form-select" [(ngModel)]="countryFilter" (change)="currentPage = 1">
|
|
||||||
@for (country of countries; track country.code) {
|
|
||||||
<option [value]="country.code">{{ country.name }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading State -->
|
|
||||||
@if (loading) {
|
|
||||||
<div class="text-center py-5">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden">Chargement...</span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-muted">Chargement des merchants...</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Error State -->
|
|
||||||
@if (error && !loading) {
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Tableau des Merchants -->
|
|
||||||
@if (!loading && !error) {
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body p-0">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-centered table-hover mb-0">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<th>Merchant</th>
|
|
||||||
<th>Contact</th>
|
|
||||||
<th>Pays</th>
|
|
||||||
<th>Statut</th>
|
|
||||||
<th>Date création</th>
|
|
||||||
<th width="120">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (merchant of displayedMerchants; track merchant.partnerId) {
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="avatar-sm bg-primary rounded-circle d-flex align-items-center justify-content-center me-2">
|
|
||||||
<ng-icon name="lucideStore" class="text-white fs-14"></ng-icon>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>{{ merchant.name }}</strong>
|
|
||||||
@if (merchant.companyInfo && merchant.companyInfo.legalName) {
|
|
||||||
<div class="text-muted small">{{ merchant.companyInfo.legalName }}</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div>{{ merchant.email }}</div>
|
|
||||||
<small class="text-muted">{{ merchant.companyInfo?.address }}</small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-light text-dark">{{ getCountryName(merchant.country) }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span [class]="'badge ' + getStatusBadgeClass(merchant.status)">
|
|
||||||
{{ getStatusText(merchant.status) }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<small class="text-muted">
|
|
||||||
{{ merchant.createdAt | date:'dd/MM/yyyy' }}
|
|
||||||
</small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="btn-group btn-group-sm">
|
|
||||||
<button class="btn btn-outline-primary" title="Voir détails">
|
|
||||||
<ng-icon name="lucideEye"></ng-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if (merchant.status === 'ACTIVE') {
|
|
||||||
<button class="btn btn-outline-warning"
|
|
||||||
(click)="suspendMerchant(merchant)"
|
|
||||||
title="Suspendre">
|
|
||||||
<ng-icon name="lucidePause"></ng-icon>
|
|
||||||
</button>
|
|
||||||
} @else if (merchant.status === 'SUSPENDED') {
|
|
||||||
<button class="btn btn-outline-success"
|
|
||||||
(click)="activateMerchant(merchant)"
|
|
||||||
title="Activer">
|
|
||||||
<ng-icon name="lucidePlay"></ng-icon>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<button class="btn btn-outline-info" title="API Keys">
|
|
||||||
<ng-icon name="lucideKey"></ng-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
@empty {
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="text-center py-4">
|
|
||||||
<ng-icon name="lucideStore" class="text-muted fs-1 mb-2"></ng-icon>
|
|
||||||
<p class="text-muted">Aucun merchant trouvé</p>
|
|
||||||
<button class="btn btn-primary" (click)="activeTab = 'config'">
|
|
||||||
Créer le premier merchant
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
@if (filteredMerchants.length > 0) {
|
|
||||||
<div class="card-footer">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div class="text-muted">
|
|
||||||
Affichage de {{ (currentPage - 1) * itemsPerPage + 1 }} à
|
|
||||||
{{ mathMin(currentPage * itemsPerPage, filteredMerchants.length) }} sur
|
|
||||||
{{ filteredMerchants.length }} merchants
|
|
||||||
</div>
|
|
||||||
<ngb-pagination
|
|
||||||
[collectionSize]="filteredMerchants.length"
|
|
||||||
[page]="currentPage"
|
|
||||||
[pageSize]="itemsPerPage"
|
|
||||||
[maxSize]="5"
|
|
||||||
(pageChange)="onPageChange($event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</ng-template>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li [ngbNavItem]="'config'">
|
|
||||||
<a ngbNavLink (click)="onTabChange('config')">
|
|
||||||
<ng-icon name="lucideSettings" class="me-1"></ng-icon>
|
|
||||||
Configuration
|
|
||||||
</a>
|
|
||||||
<ng-template ngbNavContent>
|
|
||||||
<!-- CONTENU CONFIGURATION -->
|
|
||||||
<app-ui-card title="Création d'un Nouveau Merchant">
|
|
||||||
<span helper-text class="badge badge-soft-success badge-label fs-xxs py-1">
|
|
||||||
Payment Hub DCB
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="ins-wizard" card-body>
|
|
||||||
<!-- Progress Bar -->
|
|
||||||
<ngb-progressbar
|
|
||||||
class="mb-4"
|
|
||||||
[value]="progressValue"
|
|
||||||
type="primary"
|
|
||||||
height="6px"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Navigation Steps -->
|
|
||||||
<ul class="nav nav-tabs wizard-tabs" role="tablist">
|
|
||||||
@for (step of wizardSteps; track $index; let i = $index) {
|
|
||||||
<li class="nav-item">
|
|
||||||
<a
|
|
||||||
href="javascript:void(0);"
|
|
||||||
[class.active]="i === currentStep"
|
|
||||||
class="nav-link"
|
|
||||||
[class]="i < currentStep ? 'wizard-item-done' : ''"
|
|
||||||
(click)="goToStep(i)"
|
|
||||||
>
|
|
||||||
<span class="d-flex align-items-center">
|
|
||||||
<ng-icon [name]="step.icon" class="fs-32" />
|
|
||||||
<span class="flex-grow-1 ms-2 text-truncate">
|
|
||||||
<span class="mb-0 lh-base d-block fw-semibold text-body fs-base">
|
|
||||||
{{ step.title }}
|
|
||||||
</span>
|
|
||||||
<span class="mb-0 fw-normal">{{ step.subtitle }}</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- Messages -->
|
|
||||||
@if (configError) {
|
|
||||||
<div class="alert alert-danger mt-3">
|
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
|
||||||
{{ configError }}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (configSuccess) {
|
|
||||||
<div class="alert alert-success mt-3">
|
|
||||||
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
|
||||||
{{ configSuccess }}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Contenu des Steps -->
|
|
||||||
<div class="tab-content pt-3">
|
|
||||||
@for (step of wizardSteps; track $index; let i = $index) {
|
|
||||||
<div
|
|
||||||
class="tab-pane fade"
|
|
||||||
[class.show]="currentStep === i"
|
|
||||||
[class.active]="currentStep === i"
|
|
||||||
>
|
|
||||||
<form [formGroup]="merchantForm">
|
|
||||||
<!-- Step 1: Informations Société -->
|
|
||||||
@if (i === 0) {
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Nom de l'entreprise *</label>
|
|
||||||
<div formGroupName="companyInfo">
|
|
||||||
<input type="text" class="form-control" formControlName="name"
|
|
||||||
placeholder="Nom commercial" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Nom légal *</label>
|
|
||||||
<div formGroupName="companyInfo">
|
|
||||||
<input type="text" class="form-control" formControlName="legalName"
|
|
||||||
placeholder="Raison sociale" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Numéro fiscal</label>
|
|
||||||
<div formGroupName="companyInfo">
|
|
||||||
<input type="text" class="form-control" formControlName="taxId"
|
|
||||||
placeholder="NIF/RC" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Pays *</label>
|
|
||||||
<div formGroupName="companyInfo">
|
|
||||||
<select class="form-select" formControlName="country">
|
|
||||||
@for (country of countries; track country.code) {
|
|
||||||
@if (country.code !== 'all') {
|
|
||||||
<option [value]="country.code">{{ country.name }}</option>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 mb-3">
|
|
||||||
<label class="form-label">Adresse *</label>
|
|
||||||
<div formGroupName="companyInfo">
|
|
||||||
<textarea class="form-control" formControlName="address"
|
|
||||||
placeholder="Adresse complète" rows="2"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Step 2: Contact Principal -->
|
|
||||||
@if (i === 1) {
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Email *</label>
|
|
||||||
<!-- CORRECTION : Ajouter formGroupName="contactInfo" -->
|
|
||||||
<div formGroupName="contactInfo">
|
|
||||||
<input type="email" class="form-control" formControlName="email"
|
|
||||||
placeholder="email@entreprise.com" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Téléphone *</label>
|
|
||||||
<div formGroupName="contactInfo">
|
|
||||||
<input type="tel" class="form-control" formControlName="phone"
|
|
||||||
placeholder="+225 XX XX XX XX" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Prénom *</label>
|
|
||||||
<div formGroupName="contactInfo">
|
|
||||||
<input type="text" class="form-control" formControlName="firstName"
|
|
||||||
placeholder="Prénom du contact" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Nom *</label>
|
|
||||||
<div formGroupName="contactInfo">
|
|
||||||
<input type="text" class="form-control" formControlName="lastName"
|
|
||||||
placeholder="Nom du contact" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Dans la section Configuration - Step 3: Configuration Paiements -->
|
|
||||||
@if (i === 2) {
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 mb-3">
|
|
||||||
<label class="form-label">Opérateurs supportés</label>
|
|
||||||
<!-- CORRECTION : Ajouter formGroupName="paymentConfig" -->
|
|
||||||
<div formGroupName="paymentConfig">
|
|
||||||
<div class="row">
|
|
||||||
@for (operator of operators; track operator; let idx = $index) {
|
|
||||||
<div class="col-md-3 mb-2">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox"
|
|
||||||
[formControl]="supportedOperatorsArray[idx]"
|
|
||||||
[id]="'operator-' + idx">
|
|
||||||
<label class="form-check-label" [for]="'operator-' + idx">
|
|
||||||
{{ operator }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Devise par défaut</label>
|
|
||||||
<div formGroupName="paymentConfig">
|
|
||||||
<select class="form-select" formControlName="defaultCurrency">
|
|
||||||
<option value="XOF">XOF (Franc CFA)</option>
|
|
||||||
<option value="XAF">XAF (Franc CFA)</option>
|
|
||||||
<option value="USD">USD (Dollar US)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">Montant max par transaction (XOF)</label>
|
|
||||||
<div formGroupName="paymentConfig">
|
|
||||||
<input type="number" class="form-control" formControlName="maxTransactionAmount"
|
|
||||||
min="1000" max="1000000" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Dans la section Configuration - Step 4: Webhooks -->
|
|
||||||
@if (i === 3) {
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 mb-3">
|
|
||||||
<h6>Webhooks Abonnements</h6>
|
|
||||||
<!-- CORRECTION : Ajouter formGroupName="webhookConfig" -->
|
|
||||||
<div formGroupName="webhookConfig">
|
|
||||||
<div formGroupName="subscription">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-2">
|
|
||||||
<label class="form-label">Création d'abonnement</label>
|
|
||||||
<input type="url" class="form-control"
|
|
||||||
formControlName="onCreate"
|
|
||||||
placeholder="https://votre-domaine.com/webhooks/subscription-created" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-2">
|
|
||||||
<label class="form-label">Renouvellement</label>
|
|
||||||
<input type="url" class="form-control"
|
|
||||||
formControlName="onRenew"
|
|
||||||
placeholder="https://votre-domaine.com/webhooks/subscription-renewed" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-2">
|
|
||||||
<label class="form-label">Annulation</label>
|
|
||||||
<input type="url" class="form-control"
|
|
||||||
formControlName="onCancel"
|
|
||||||
placeholder="https://votre-domaine.com/webhooks/subscription-cancelled" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-2">
|
|
||||||
<label class="form-label">Expiration</label>
|
|
||||||
<input type="url" class="form-control"
|
|
||||||
formControlName="onExpire"
|
|
||||||
placeholder="https://votre-domaine.com/webhooks/subscription-expired" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 mb-3">
|
|
||||||
<h6>Webhooks Paiements</h6>
|
|
||||||
<div formGroupName="webhookConfig">
|
|
||||||
<div formGroupName="payment">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4 mb-2">
|
|
||||||
<label class="form-label">Paiement réussi</label>
|
|
||||||
<input type="url" class="form-control"
|
|
||||||
formControlName="onSuccess"
|
|
||||||
placeholder="https://votre-domaine.com/webhooks/payment-success" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 mb-2">
|
|
||||||
<label class="form-label">Paiement échoué</label>
|
|
||||||
<input type="url" class="form-control"
|
|
||||||
formControlName="onFailure"
|
|
||||||
placeholder="https://votre-domaine.com/webhooks/payment-failed" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 mb-2">
|
|
||||||
<label class="form-label">Remboursement</label>
|
|
||||||
<input type="url" class="form-control"
|
|
||||||
formControlName="onRefund"
|
|
||||||
placeholder="https://votre-domaine.com/webhooks/payment-refunded" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Navigation Buttons -->
|
|
||||||
<div class="d-flex justify-content-between mt-4">
|
|
||||||
@if (i > 0) {
|
|
||||||
<button type="button" class="btn btn-secondary" (click)="previousStep()">
|
|
||||||
← Précédent
|
|
||||||
</button>
|
|
||||||
} @else {
|
|
||||||
<div></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (i < wizardSteps.length - 1) {
|
|
||||||
<button type="button" class="btn btn-primary" (click)="nextStep()">
|
|
||||||
Suivant →
|
|
||||||
</button>
|
|
||||||
} @else {
|
|
||||||
<button type="button" class="btn btn-success"
|
|
||||||
(click)="submitForm()" [disabled]="configLoading">
|
|
||||||
@if (configLoading) {
|
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status">
|
|
||||||
<span class="visually-hidden">Chargement...</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
Créer le Merchant
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</app-ui-card>
|
|
||||||
</ng-template>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li [ngbNavItem]="'stats'">
|
|
||||||
<a ngbNavLink (click)="onTabChange('stats')">
|
|
||||||
<ng-icon name="lucideBarChart3" class="me-1"></ng-icon>
|
|
||||||
Statistiques
|
|
||||||
</a>
|
|
||||||
<ng-template ngbNavContent>
|
|
||||||
<!-- CONTENU STATISTIQUES -->
|
|
||||||
<!-- Loading State -->
|
|
||||||
@if (statsLoading) {
|
|
||||||
<div class="text-center py-5">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden">Chargement...</span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-muted">Chargement des statistiques...</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Statistiques Globales -->
|
|
||||||
@if (stats && !statsLoading) {
|
|
||||||
<div class="row g-4">
|
|
||||||
<!-- KPI Cards -->
|
|
||||||
<div class="col-xl-3 col-md-6">
|
|
||||||
<div class="card border-primary">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<ng-icon name="lucideCreditCard" class="text-primary fs-24 mb-2"></ng-icon>
|
|
||||||
<h3 class="text-primary">{{ formatNumber(stats.totalTransactions) }}</h3>
|
|
||||||
<p class="text-muted mb-0">Transactions totales</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xl-3 col-md-6">
|
|
||||||
<div class="card border-success">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<ng-icon name="lucideTrendingUp" class="text-success fs-24 mb-2"></ng-icon>
|
|
||||||
<h3 class="text-success">{{ stats.successRate }}%</h3>
|
|
||||||
<p class="text-muted mb-0">Taux de succès</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xl-3 col-md-6">
|
|
||||||
<div class="card border-info">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<ng-icon name="lucideUsers" class="text-info fs-24 mb-2"></ng-icon>
|
|
||||||
<h3 class="text-info">{{ formatNumber(stats.activeSubscriptions) }}</h3>
|
|
||||||
<p class="text-muted mb-0">Abonnements actifs</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xl-3 col-md-6">
|
|
||||||
<div class="card border-warning">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<ng-icon name="lucideDollarSign" class="text-warning fs-24 mb-2"></ng-icon>
|
|
||||||
<h3 class="text-warning">{{ formatCurrency(stats.totalRevenue) }}</h3>
|
|
||||||
<p class="text-muted mb-0">Revenue total</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Détails des performances -->
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title mb-0">Détails des Performances</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row text-center">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="border-end">
|
|
||||||
<h4 class="text-success">{{ formatNumber(stats.successfulTransactions) }}</h4>
|
|
||||||
<p class="text-muted mb-0">Transactions réussies</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="border-end">
|
|
||||||
<h4 class="text-danger">{{ formatNumber(stats.failedTransactions) }}</h4>
|
|
||||||
<p class="text-muted mb-0">Transactions échouées</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-info">+{{ stats.monthlyGrowth }}%</h4>
|
|
||||||
<p class="text-muted mb-0">Croissance mensuelle</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</ng-template>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { Merchants } from './merchants';
|
|
||||||
describe('Merchants', () => {});
|
|
||||||
@ -1,339 +0,0 @@
|
|||||||
import { Component, inject, OnInit } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
import { NgIconComponent } from '@ng-icons/core';
|
|
||||||
import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormControl } from '@angular/forms';
|
|
||||||
import { NgbNavModule, NgbProgressbarModule, NgbPaginationModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
import { firstValueFrom } from 'rxjs'; // ← AJOUT IMPORT
|
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
|
||||||
import { UiCard } from '@app/components/ui-card';
|
|
||||||
import { MerchantsService } from './services/merchants.service';
|
|
||||||
import { MerchantResponse, MerchantStats, MerchantFormData } from './models/merchant.models';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-merchant',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
RouterModule,
|
|
||||||
NgIconComponent,
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
NgbNavModule,
|
|
||||||
NgbProgressbarModule,
|
|
||||||
NgbPaginationModule,
|
|
||||||
NgbDropdownModule,
|
|
||||||
PageTitle,
|
|
||||||
UiCard
|
|
||||||
],
|
|
||||||
templateUrl: './merchants.html',
|
|
||||||
})
|
|
||||||
export class Merchants implements OnInit {
|
|
||||||
private fb = inject(FormBuilder);
|
|
||||||
private merchantsService = inject(MerchantsService);
|
|
||||||
|
|
||||||
// Navigation par onglets
|
|
||||||
activeTab: 'list' | 'config' | 'stats' = 'list';
|
|
||||||
|
|
||||||
// === DONNÉES LISTE ===
|
|
||||||
merchants: MerchantResponse[] = [];
|
|
||||||
loading = false;
|
|
||||||
error = '';
|
|
||||||
|
|
||||||
// Pagination et filtres
|
|
||||||
currentPage = 1;
|
|
||||||
itemsPerPage = 10;
|
|
||||||
searchTerm = '';
|
|
||||||
statusFilter: 'all' | 'ACTIVE' | 'PENDING' | 'SUSPENDED' = 'all';
|
|
||||||
countryFilter = 'all';
|
|
||||||
|
|
||||||
// === DONNÉES CONFIG (WIZARD) ===
|
|
||||||
currentStep = 0;
|
|
||||||
wizardSteps = [
|
|
||||||
{ id: 'company-info', icon: 'lucideBuilding', title: 'Informations Société', subtitle: 'Détails entreprise' },
|
|
||||||
{ id: 'contact-info', icon: 'lucideUser', title: 'Contact Principal', subtitle: 'Personne de contact' },
|
|
||||||
{ id: 'payment-config', icon: 'lucideCreditCard', title: 'Configuration Paiements', subtitle: 'Paramètres DCB' },
|
|
||||||
{ id: 'webhooks', icon: 'lucideWebhook', title: 'Webhooks', subtitle: 'Notifications et retours' },
|
|
||||||
{ id: 'review', icon: 'lucideCheckCircle', title: 'Validation', subtitle: 'Vérification finale' }
|
|
||||||
];
|
|
||||||
configLoading = false;
|
|
||||||
configError = '';
|
|
||||||
configSuccess = '';
|
|
||||||
|
|
||||||
// === DONNÉES STATS ===
|
|
||||||
stats: MerchantStats | null = null;
|
|
||||||
statsLoading = false;
|
|
||||||
|
|
||||||
supportedOperatorsArray: FormControl<boolean | null>[] = [
|
|
||||||
this.fb.control(false), // Orange
|
|
||||||
this.fb.control(false), // MTN
|
|
||||||
this.fb.control(false), // Airtel
|
|
||||||
this.fb.control(false) // Moov
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
// === FORMULAIRE ===
|
|
||||||
merchantForm = this.fb.group({
|
|
||||||
companyInfo: this.fb.group({
|
|
||||||
name: ['', [Validators.required, Validators.minLength(2)]],
|
|
||||||
legalName: ['', [Validators.required]],
|
|
||||||
taxId: [''],
|
|
||||||
address: ['', [Validators.required]],
|
|
||||||
country: ['CIV', [Validators.required]]
|
|
||||||
}),
|
|
||||||
contactInfo: this.fb.group({
|
|
||||||
email: ['', [Validators.required, Validators.email]],
|
|
||||||
phone: ['', [Validators.required]],
|
|
||||||
firstName: ['', [Validators.required]],
|
|
||||||
lastName: ['', [Validators.required]]
|
|
||||||
}),
|
|
||||||
paymentConfig: this.fb.group({
|
|
||||||
// CORRECTION : Utiliser l'array déclaré séparément
|
|
||||||
supportedOperators: this.fb.array(this.supportedOperatorsArray),
|
|
||||||
defaultCurrency: ['XOF', [Validators.required]],
|
|
||||||
maxTransactionAmount: [50000, [Validators.min(1000), Validators.max(1000000)]]
|
|
||||||
}),
|
|
||||||
webhookConfig: this.fb.group({
|
|
||||||
subscription: this.fb.group({
|
|
||||||
onCreate: ['', [Validators.pattern('https?://.+')]],
|
|
||||||
onRenew: ['', [Validators.pattern('https?://.+')]],
|
|
||||||
onCancel: ['', [Validators.pattern('https?://.+')]],
|
|
||||||
onExpire: ['', [Validators.pattern('https?://.+')]]
|
|
||||||
}),
|
|
||||||
payment: this.fb.group({
|
|
||||||
onSuccess: ['', [Validators.pattern('https?://.+')]],
|
|
||||||
onFailure: ['', [Validators.pattern('https?://.+')]],
|
|
||||||
onRefund: ['', [Validators.pattern('https?://.+')]]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// Données partagées
|
|
||||||
countries = [
|
|
||||||
{ code: 'all', name: 'Tous les pays' },
|
|
||||||
{ code: 'CIV', name: 'Côte d\'Ivoire' },
|
|
||||||
{ code: 'SEN', name: 'Sénégal' },
|
|
||||||
{ code: 'CMR', name: 'Cameroun' },
|
|
||||||
{ code: 'COD', name: 'RDC' },
|
|
||||||
{ code: 'TUN', name: 'Tunisie' },
|
|
||||||
{ code: 'BFA', name: 'Burkina Faso' },
|
|
||||||
{ code: 'MLI', name: 'Mali' },
|
|
||||||
{ code: 'GIN', name: 'Guinée' }
|
|
||||||
];
|
|
||||||
|
|
||||||
operators = ['Orange', 'MTN', 'Airtel', 'Moov'];
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.loadMerchants();
|
|
||||||
this.loadStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
// === MÉTHODES LISTE ===
|
|
||||||
loadMerchants() {
|
|
||||||
this.loading = true;
|
|
||||||
this.merchantsService.getAllMerchants().subscribe({
|
|
||||||
next: (merchants) => {
|
|
||||||
this.merchants = merchants;
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
this.error = 'Erreur lors du chargement des merchants';
|
|
||||||
this.loading = false;
|
|
||||||
console.error('Error loading merchants:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get filteredMerchants(): MerchantResponse[] {
|
|
||||||
return this.merchants.filter(merchant => {
|
|
||||||
const matchesSearch = !this.searchTerm ||
|
|
||||||
merchant.name.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
|
|
||||||
merchant.email.toLowerCase().includes(this.searchTerm.toLowerCase());
|
|
||||||
|
|
||||||
const matchesStatus = this.statusFilter === 'all' || merchant.status === this.statusFilter;
|
|
||||||
const matchesCountry = this.countryFilter === 'all' || merchant.country === this.countryFilter;
|
|
||||||
|
|
||||||
return matchesSearch && matchesStatus && matchesCountry;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get displayedMerchants(): MerchantResponse[] {
|
|
||||||
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
|
||||||
return this.filteredMerchants.slice(startIndex, startIndex + this.itemsPerPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatusBadgeClass(status: string): string {
|
|
||||||
switch (status) {
|
|
||||||
case 'ACTIVE': return 'bg-success';
|
|
||||||
case 'PENDING': return 'bg-warning';
|
|
||||||
case 'SUSPENDED': return 'bg-danger';
|
|
||||||
default: return 'bg-secondary';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatusText(status: string): string {
|
|
||||||
switch (status) {
|
|
||||||
case 'ACTIVE': return 'Actif';
|
|
||||||
case 'PENDING': return 'En attente';
|
|
||||||
case 'SUSPENDED': return 'Suspendu';
|
|
||||||
default: return 'Inconnu';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getCountryName(code: string): string {
|
|
||||||
const country = this.countries.find(c => c.code === code);
|
|
||||||
return country ? country.name : code;
|
|
||||||
}
|
|
||||||
|
|
||||||
suspendMerchant(merchant: MerchantResponse) {
|
|
||||||
if (confirm(`Êtes-vous sûr de vouloir suspendre ${merchant.name} ?`)) {
|
|
||||||
this.merchantsService.updateMerchantStatus(merchant.partnerId, 'SUSPENDED').subscribe({
|
|
||||||
next: () => {
|
|
||||||
merchant.status = 'SUSPENDED';
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Error suspending merchant:', error);
|
|
||||||
alert('Erreur lors de la suspension du merchant');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activateMerchant(merchant: MerchantResponse) {
|
|
||||||
this.merchantsService.updateMerchantStatus(merchant.partnerId, 'ACTIVE').subscribe({
|
|
||||||
next: () => {
|
|
||||||
merchant.status = 'ACTIVE';
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Error activating merchant:', error);
|
|
||||||
alert('Erreur lors de l\'activation du merchant');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPageChange(page: number) {
|
|
||||||
this.currentPage = page;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearFilters() {
|
|
||||||
this.searchTerm = '';
|
|
||||||
this.statusFilter = 'all';
|
|
||||||
this.countryFilter = 'all';
|
|
||||||
this.currentPage = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// === MÉTHODES CONFIG (WIZARD) ===
|
|
||||||
get progressValue(): number {
|
|
||||||
return ((this.currentStep + 1) / this.wizardSteps.length) * 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextStep() {
|
|
||||||
if (this.currentStep < this.wizardSteps.length - 1) {
|
|
||||||
this.currentStep++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
previousStep() {
|
|
||||||
if (this.currentStep > 0) {
|
|
||||||
this.currentStep--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
goToStep(index: number) {
|
|
||||||
this.currentStep = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedOperators(): string[] {
|
|
||||||
return this.supportedOperatorsArray
|
|
||||||
.map((control, index) => control.value ? this.operators[index] : null)
|
|
||||||
.filter(op => op !== null) as string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitForm() {
|
|
||||||
if (this.merchantForm.valid) {
|
|
||||||
this.configLoading = true;
|
|
||||||
this.configError = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formData = this.merchantForm.value as unknown as MerchantFormData;
|
|
||||||
|
|
||||||
const registrationData = {
|
|
||||||
name: formData.companyInfo.name,
|
|
||||||
email: formData.contactInfo.email,
|
|
||||||
country: formData.companyInfo.country,
|
|
||||||
companyInfo: {
|
|
||||||
legalName: formData.companyInfo.legalName,
|
|
||||||
taxId: formData.companyInfo.taxId,
|
|
||||||
address: formData.companyInfo.address
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// CORRECTION : Utilisation de firstValueFrom au lieu de toPromise()
|
|
||||||
const partnerResponse = await firstValueFrom(
|
|
||||||
this.merchantsService.registerMerchant(registrationData)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (partnerResponse) {
|
|
||||||
// CORRECTION : Utilisation de firstValueFrom au lieu de toPromise()
|
|
||||||
await firstValueFrom(
|
|
||||||
this.merchantsService.updateCallbacks(partnerResponse.partnerId, formData.webhookConfig)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.configSuccess = `Merchant créé avec succès! API Key: ${partnerResponse.apiKey}`;
|
|
||||||
this.merchantForm.reset();
|
|
||||||
this.currentStep = 0;
|
|
||||||
this.loadMerchants(); // Recharger la liste
|
|
||||||
this.activeTab = 'list'; // Retourner à la liste
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.configError = 'Erreur lors de la création du merchant';
|
|
||||||
console.error('Error creating merchant:', error);
|
|
||||||
} finally {
|
|
||||||
this.configLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === MÉTHODES STATS ===
|
|
||||||
loadStats() {
|
|
||||||
this.statsLoading = true;
|
|
||||||
// Données mockées pour l'exemple
|
|
||||||
this.stats = {
|
|
||||||
totalTransactions: 12543,
|
|
||||||
successfulTransactions: 12089,
|
|
||||||
failedTransactions: 454,
|
|
||||||
totalRevenue: 45875000,
|
|
||||||
activeSubscriptions: 8450,
|
|
||||||
successRate: 96.4,
|
|
||||||
monthlyGrowth: 12.3
|
|
||||||
};
|
|
||||||
this.statsLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatNumber(num: number): string {
|
|
||||||
return new Intl.NumberFormat('fr-FR').format(num);
|
|
||||||
}
|
|
||||||
|
|
||||||
formatCurrency(amount: number): string {
|
|
||||||
return new Intl.NumberFormat('fr-FR', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'XOF'
|
|
||||||
}).format(amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode utilitaire pour Math.min dans le template
|
|
||||||
mathMin(a: number, b: number): number {
|
|
||||||
return Math.min(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
// === MÉTHODES COMMUNES ===
|
|
||||||
onTabChange(tab: 'list' | 'config' | 'stats') {
|
|
||||||
this.activeTab = tab;
|
|
||||||
if (tab === 'list') {
|
|
||||||
this.loadMerchants();
|
|
||||||
} else if (tab === 'stats') {
|
|
||||||
this.loadStats();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
export interface MerchantRegistration {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
country: string;
|
|
||||||
companyInfo?: {
|
|
||||||
legalName?: string;
|
|
||||||
taxId?: string;
|
|
||||||
address?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MerchantResponse {
|
|
||||||
partnerId: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
country: string;
|
|
||||||
apiKey: string;
|
|
||||||
secretKey: string;
|
|
||||||
status: 'PENDING' | 'ACTIVE' | 'SUSPENDED';
|
|
||||||
createdAt: string;
|
|
||||||
companyInfo?: {
|
|
||||||
legalName?: string;
|
|
||||||
taxId?: string;
|
|
||||||
address?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallbackConfiguration {
|
|
||||||
headerEnrichment?: {
|
|
||||||
url?: string;
|
|
||||||
method?: 'GET' | 'POST';
|
|
||||||
headers?: { [key: string]: string };
|
|
||||||
};
|
|
||||||
subscription?: {
|
|
||||||
onCreate?: string;
|
|
||||||
onRenew?: string;
|
|
||||||
onCancel?: string;
|
|
||||||
onExpire?: string;
|
|
||||||
};
|
|
||||||
payment?: {
|
|
||||||
onSuccess?: string;
|
|
||||||
onFailure?: string;
|
|
||||||
onRefund?: string;
|
|
||||||
};
|
|
||||||
authentication?: {
|
|
||||||
onSuccess?: string;
|
|
||||||
onFailure?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MerchantStats {
|
|
||||||
totalTransactions: number;
|
|
||||||
successfulTransactions: number;
|
|
||||||
failedTransactions: number;
|
|
||||||
totalRevenue: number;
|
|
||||||
activeSubscriptions: number;
|
|
||||||
successRate: number;
|
|
||||||
monthlyGrowth: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MerchantFormData {
|
|
||||||
companyInfo: {
|
|
||||||
name: string;
|
|
||||||
legalName: string;
|
|
||||||
taxId: string;
|
|
||||||
address: string;
|
|
||||||
country: string;
|
|
||||||
};
|
|
||||||
contactInfo: {
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
};
|
|
||||||
paymentConfig: {
|
|
||||||
supportedOperators: string[];
|
|
||||||
defaultCurrency: string;
|
|
||||||
maxTransactionAmount: number;
|
|
||||||
};
|
|
||||||
webhookConfig: CallbackConfiguration;
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { environment } from '@environments/environment';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import {
|
|
||||||
MerchantRegistration,
|
|
||||||
MerchantResponse,
|
|
||||||
CallbackConfiguration,
|
|
||||||
MerchantStats
|
|
||||||
} from '../models/merchant.models';
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class MerchantsService {
|
|
||||||
private http = inject(HttpClient);
|
|
||||||
private apiUrl = `${environment.localServiceTestApiUrl}/partners`;
|
|
||||||
|
|
||||||
// Enregistrement d'un nouveau merchant
|
|
||||||
registerMerchant(registration: MerchantRegistration): Observable<MerchantResponse> {
|
|
||||||
return this.http.post<MerchantResponse>(`${this.apiUrl}/register`, registration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration des webhooks
|
|
||||||
updateCallbacks(partnerId: string, callbacks: CallbackConfiguration): Observable<CallbackConfiguration> {
|
|
||||||
return this.http.put<CallbackConfiguration>(`${this.apiUrl}/${partnerId}/callbacks`, callbacks);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupération de tous les merchants
|
|
||||||
getAllMerchants(): Observable<MerchantResponse[]> {
|
|
||||||
return this.http.get<MerchantResponse[]>(`${this.apiUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupération d'un merchant par ID
|
|
||||||
getMerchantById(partnerId: string): Observable<MerchantResponse> {
|
|
||||||
return this.http.get<MerchantResponse>(`${this.apiUrl}/${partnerId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Statistiques d'un merchant
|
|
||||||
getMerchantStats(partnerId: string): Observable<MerchantStats> {
|
|
||||||
return this.http.get<MerchantStats>(`${this.apiUrl}/${partnerId}/stats`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test des webhooks
|
|
||||||
testWebhook(url: string, event: string): Observable<{ success: boolean; response: any; responseTime: number }> {
|
|
||||||
return this.http.post<{ success: boolean; response: any; responseTime: number }>(
|
|
||||||
`${environment.iamApiUrl}/webhooks/test`,
|
|
||||||
{ url, event }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suspension/Activation d'un merchant
|
|
||||||
updateMerchantStatus(partnerId: string, status: 'ACTIVE' | 'SUSPENDED'): Observable<MerchantResponse> {
|
|
||||||
return this.http.patch<MerchantResponse>(`${this.apiUrl}/${partnerId}`, { status });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,312 +0,0 @@
|
|||||||
import { Component } from '@angular/core'
|
|
||||||
import { NgIcon } from '@ng-icons/core'
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { UiCard } from '@app/components/ui-card'
|
|
||||||
import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { wizardSteps } from '@/app/modules/merchants/data'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-wizard-with-progress',
|
|
||||||
imports: [
|
|
||||||
NgIcon,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
FormsModule,
|
|
||||||
UiCard,
|
|
||||||
NgbProgressbarModule,
|
|
||||||
],
|
|
||||||
template: `
|
|
||||||
<app-ui-card title="Progressbar Support">
|
|
||||||
<span helper-text class="badge badge-soft-success badge-label fs-xxs py-1"
|
|
||||||
>Exclusive</span
|
|
||||||
>
|
|
||||||
<div class="ins-wizard" card-body>
|
|
||||||
<ngb-progressbar
|
|
||||||
class="mb-4"
|
|
||||||
[value]="progressValue"
|
|
||||||
type="primary"
|
|
||||||
height="6px"
|
|
||||||
>
|
|
||||||
</ngb-progressbar>
|
|
||||||
<ul class="nav nav-tabs wizard-tabs" role="tablist">
|
|
||||||
@for (step of wizardSteps; track $index; let i = $index) {
|
|
||||||
<li class="nav-item">
|
|
||||||
<a
|
|
||||||
href="javascript:void(0);"
|
|
||||||
[class.active]="i === currentStep"
|
|
||||||
class="nav-link"
|
|
||||||
[class]="i < currentStep ? 'wizard-item-done' : ''"
|
|
||||||
(click)="goToStep(i)"
|
|
||||||
>
|
|
||||||
<span class="d-flex align-items-center">
|
|
||||||
<ng-icon [name]="step.icon" class="fs-32" />
|
|
||||||
<span class="flex-grow-1 ms-2 text-truncate">
|
|
||||||
<span
|
|
||||||
class="mb-0 lh-base d-block fw-semibold text-body fs-base"
|
|
||||||
>{{ step.title }}</span
|
|
||||||
>
|
|
||||||
<span class="mb-0 fw-normal">{{ step.subtitle }}</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="tab-content pt-3">
|
|
||||||
@for (step of wizardSteps; track $index; let i = $index) {
|
|
||||||
<div
|
|
||||||
class="tab-pane fade"
|
|
||||||
[class.show]="currentStep === i"
|
|
||||||
[class.active]="currentStep === i"
|
|
||||||
>
|
|
||||||
@switch (i) {
|
|
||||||
@case (0) {
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Full Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="Enter your full name"
|
|
||||||
name="fullname"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Email</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="Enter your email"
|
|
||||||
name="email"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Phone Number</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
class="form-control"
|
|
||||||
name="phone"
|
|
||||||
placeholder="Enter your phone number"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Date of Birth</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
data-provider="flatpickr"
|
|
||||||
data-date-format="d M, Y"
|
|
||||||
placeholder="Select your DOB"
|
|
||||||
class="form-control"
|
|
||||||
name="dob"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@case (1) {
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Street Address</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="street"
|
|
||||||
placeholder="123 Main St"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">City</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="city"
|
|
||||||
placeholder="e.g., New York"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">State</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="state"
|
|
||||||
placeholder="e.g., California"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Zip Code</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="zip"
|
|
||||||
placeholder="e.g., 10001"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@case (2) {
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Choose Course</label>
|
|
||||||
<select class="form-select" name="course" required>
|
|
||||||
<option value="">Select</option>
|
|
||||||
<option value="Engineering">Engineering</option>
|
|
||||||
<option value="Medical">Medical</option>
|
|
||||||
<option value="Business">Business</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Enrollment Type</label>
|
|
||||||
<select class="form-select" name="enrollment" required>
|
|
||||||
<option value="">Select</option>
|
|
||||||
<option value="Full Time">Full Time</option>
|
|
||||||
<option value="Part Time">Part Time</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Preferred Batch Time</label>
|
|
||||||
<select class="form-select" name="batch_time" required>
|
|
||||||
<option value="">Select Time</option>
|
|
||||||
<option value="Morning">Morning (8am – 12pm)</option>
|
|
||||||
<option value="Afternoon">Afternoon (1pm – 5pm)</option>
|
|
||||||
<option value="Evening">Evening (6pm – 9pm)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Mode of Study</label>
|
|
||||||
<select class="form-select" name="mode" required>
|
|
||||||
<option value="">Select Mode</option>
|
|
||||||
<option value="Offline">Offline</option>
|
|
||||||
<option value="Online">Online</option>
|
|
||||||
<option value="Hybrid">Hybrid</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@case (3) {
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Parent/Guardian Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="parent_name"
|
|
||||||
placeholder="e.g., John Doe"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Relation</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="relation"
|
|
||||||
placeholder="e.g., Father, Mother"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Parent Phone</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
class="form-control"
|
|
||||||
name="parent_phone"
|
|
||||||
placeholder="e.g., +1 555 123 4567"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-6 mb-3">
|
|
||||||
<label class="form-label">Parent Email</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
class="form-control"
|
|
||||||
name="parent_email"
|
|
||||||
placeholder="e.g., parent@example.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@case (4) {
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Upload ID Proof</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
class="form-control"
|
|
||||||
name="id_proof"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Upload Previous Marksheet</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
class="form-control"
|
|
||||||
name="marksheet"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mt-3">
|
|
||||||
@if (i > 0) {
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
(click)="previousStep()"
|
|
||||||
>
|
|
||||||
← Back:
|
|
||||||
{{ step.title }}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
@if (i < wizardSteps.length - 1) {
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary ms-auto"
|
|
||||||
(click)="nextStep()"
|
|
||||||
>
|
|
||||||
Next: {{ step.title }} →
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
@if (i === wizardSteps.length - 1) {
|
|
||||||
<button type="submit" class="btn btn-success">
|
|
||||||
Submit Application
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</app-ui-card>
|
|
||||||
`,
|
|
||||||
styles: ``,
|
|
||||||
})
|
|
||||||
export class WizardWithProgress {
|
|
||||||
currentStep = 0
|
|
||||||
|
|
||||||
nextStep() {
|
|
||||||
if (this.currentStep < wizardSteps.length - 1) this.currentStep++
|
|
||||||
}
|
|
||||||
|
|
||||||
previousStep() {
|
|
||||||
if (this.currentStep > 0) this.currentStep--
|
|
||||||
}
|
|
||||||
|
|
||||||
goToStep(index: number) {
|
|
||||||
this.currentStep = index
|
|
||||||
}
|
|
||||||
get progressValue(): number {
|
|
||||||
const totalSteps = this.wizardSteps.length
|
|
||||||
return ((this.currentStep + 1) / totalSteps) * 100
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly wizardSteps = wizardSteps
|
|
||||||
}
|
|
||||||
@ -8,7 +8,7 @@ import { Users } from '@modules/users/users';
|
|||||||
import { DcbDashboard } from './dcb-dashboard/dcb-dashboard';
|
import { DcbDashboard } from './dcb-dashboard/dcb-dashboard';
|
||||||
import { Team } from './team/team';
|
import { Team } from './team/team';
|
||||||
import { Transactions } from './transactions/transactions';
|
import { Transactions } from './transactions/transactions';
|
||||||
import { Merchants } from './merchants/merchants';
|
import { MerchantPartners } from './merchant-partners/merchant-partners';
|
||||||
import { OperatorsConfig } from './operators/config/config';
|
import { OperatorsConfig } from './operators/config/config';
|
||||||
import { OperatorsStats } from './operators/stats/stats';
|
import { OperatorsStats } from './operators/stats/stats';
|
||||||
import { WebhooksHistory } from './webhooks/history/history';
|
import { WebhooksHistory } from './webhooks/history/history';
|
||||||
@ -85,16 +85,23 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Merchants
|
// Partners
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
{
|
{
|
||||||
path: 'merchants',
|
path: 'merchant-partners',
|
||||||
component: Merchants,
|
component: MerchantPartners,
|
||||||
canActivate: [authGuard, roleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
title: 'Gestion des Merchants',
|
title: 'Gestion Partners/Marchants',
|
||||||
module: 'merchants',
|
module: 'merchant-partners',
|
||||||
requiredRoles: ['admin', 'support']
|
requiredRoles: [
|
||||||
|
'dcb-admin',
|
||||||
|
'dcb-support',
|
||||||
|
'dcb-partner',
|
||||||
|
'dcb-partner-admin',
|
||||||
|
'dcb-partner-manager',
|
||||||
|
'dcb-partner-suport',
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -190,12 +197,12 @@ const routes: Routes = [
|
|||||||
// Support & Profile (Tous les utilisateurs authentifiés)
|
// Support & Profile (Tous les utilisateurs authentifiés)
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
{
|
{
|
||||||
path: 'support',
|
path: 'dcb-support',
|
||||||
component: Support,
|
component: Support,
|
||||||
canActivate: [authGuard, roleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
title: 'Support',
|
title: 'Support',
|
||||||
module: 'support'
|
module: 'dcb-support'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
<!-- my-profile.html -->
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- En-tête avec navigation -->
|
<!-- En-tête avec navigation -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
@ -7,20 +6,60 @@
|
|||||||
<div>
|
<div>
|
||||||
<h4 class="mb-1">
|
<h4 class="mb-1">
|
||||||
@if (user) {
|
@if (user) {
|
||||||
Mon Profil - {{ getUserDisplayName() }}
|
{{ getUserDisplayName() }}
|
||||||
} @else {
|
} @else {
|
||||||
Mon Profil
|
Profil Utilisateur
|
||||||
}
|
}
|
||||||
</h4>
|
</h4>
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb mb-0">
|
<ol class="breadcrumb mb-0">
|
||||||
<li class="breadcrumb-item active">Mon Profil</li>
|
<li class="breadcrumb-item">
|
||||||
|
<a href="javascript:void(0)" (click)="back.emit()" class="text-decoration-none cursor-pointer">
|
||||||
|
Utilisateurs
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">
|
||||||
|
@if (user) {
|
||||||
|
{{ getUserDisplayName() }}
|
||||||
|
} @else {
|
||||||
|
Profil
|
||||||
|
}
|
||||||
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
@if (user && !isEditing) {
|
<!-- Bouton de réinitialisation de mot de passe -->
|
||||||
|
@if (user && canEditUsers && !isEditing) {
|
||||||
|
<button
|
||||||
|
class="btn btn-warning"
|
||||||
|
(click)="resetPassword()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
||||||
|
Réinitialiser MDP
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Bouton activation/désactivation -->
|
||||||
|
@if (user.enabled) {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-warning"
|
||||||
|
(click)="disableUser()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucidePause" class="me-1"></ng-icon>
|
||||||
|
Désactiver
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-success"
|
||||||
|
(click)="enableUser()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucidePlay" class="me-1"></ng-icon>
|
||||||
|
Activer
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Bouton modification -->
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
(click)="startEditing()"
|
(click)="startEditing()"
|
||||||
@ -34,18 +73,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Indicateur de permissions -->
|
||||||
|
@if (currentUserRole && !canEditUsers) {
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideShield" class="me-2"></ng-icon>
|
||||||
|
<div>
|
||||||
|
<strong>Permissions limitées :</strong> Vous ne pouvez que consulter ce profil
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Messages d'alerte -->
|
<!-- Messages d'alerte -->
|
||||||
@if (error) {
|
@if (error) {
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
<div class="d-flex align-items-center">
|
||||||
{{ error }}
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
|
<div>{{ error }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (success) {
|
@if (success) {
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success">
|
||||||
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
<div class="d-flex align-items-center">
|
||||||
{{ success }}
|
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||||
|
<div>{{ success }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +115,7 @@
|
|||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Chargement...</span>
|
<span class="visually-hidden">Chargement...</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-muted">Chargement de votre profil...</p>
|
<p class="mt-2 text-muted">Chargement du profil...</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,12 +126,12 @@
|
|||||||
<!-- Carte profil -->
|
<!-- Carte profil -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header bg-light">
|
||||||
<h5 class="card-title mb-0">Mon Profil</h5>
|
<h5 class="card-title mb-0">Profil Utilisateur Hub</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<!-- Avatar -->
|
<!-- Avatar -->
|
||||||
<div class="avatar-lg mx-auto mb-3">
|
<div class="avatar-lg mx-auto mb-3">
|
||||||
<div class="avatar-title bg-primary rounded-circle text-white fs-24">
|
<div class="avatar-title bg-primary bg-opacity-10 rounded-circle text-primary fs-24">
|
||||||
{{ getUserInitials() }}
|
{{ getUserInitials() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -90,43 +149,112 @@
|
|||||||
<div class="d-flex align-items-center mb-2">
|
<div class="d-flex align-items-center mb-2">
|
||||||
<ng-icon name="lucideMail" class="me-2 text-muted"></ng-icon>
|
<ng-icon name="lucideMail" class="me-2 text-muted"></ng-icon>
|
||||||
<small>{{ user.email }}</small>
|
<small>{{ user.email }}</small>
|
||||||
|
@if (!user.emailVerified) {
|
||||||
|
<ng-icon name="lucideAlertTriangle" class="ms-1 text-warning" size="14" title="Email non vérifié"></ng-icon>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
||||||
<small>Membre depuis {{ formatTimestamp(user.createdTimestamp) }}</small>
|
<small>Créé le {{ formatTimestamp(user.createdTimestamp) }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center mt-2">
|
@if (user.lastLogin) {
|
||||||
<ng-icon name="lucideShield" class="me-2 text-muted"></ng-icon>
|
<div class="d-flex align-items-center mt-2">
|
||||||
<small class="text-info">Vous pouvez modifier votre mot de passe ici</small>
|
<ng-icon name="lucideLogIn" class="me-2 text-muted"></ng-icon>
|
||||||
</div>
|
<small>Dernière connexion : {{ formatTimestamp(user.lastLogin) }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<!-- Bouton de sécurité -->
|
|
||||||
<div class="mt-4">
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-warning btn-sm"
|
|
||||||
(click)="openResetPasswordModal()"
|
|
||||||
[disabled]="resettingPassword"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
|
||||||
Changer mon mot de passe
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Carte rôles -->
|
<!-- Carte rôle utilisateur -->
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title mb-0">Mes Rôles</h5>
|
<h5 class="card-title mb-0">Rôle Utilisateur</h5>
|
||||||
|
@if (canManageRoles && !isEditing) {
|
||||||
|
<span class="badge bg-info">Modifiable</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex flex-wrap gap-1">
|
<!-- Rôle actuel -->
|
||||||
@for (role of user.clientRoles; track role) {
|
<div class="text-center mb-3">
|
||||||
<span class="badge" [ngClass]="getRoleBadgeClass(role)">
|
<span class="badge d-flex align-items-center justify-content-center" [ngClass]="getRoleBadgeClass(user.role)">
|
||||||
{{ role }}
|
<ng-icon [name]="getRoleIcon(user.role)" class="me-2"></ng-icon>
|
||||||
</span>
|
{{ getRoleLabel(user.role) }}
|
||||||
}
|
</span>
|
||||||
|
<small class="text-muted d-block mt-1">
|
||||||
|
{{ getRoleDescription(user.role) }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Changement de rôle -->
|
||||||
|
@if (canManageRoles && !isEditing) {
|
||||||
|
<div class="mt-3">
|
||||||
|
<label class="form-label fw-semibold">Changer le rôle</label>
|
||||||
|
<select
|
||||||
|
class="form-select"
|
||||||
|
[value]="user.role"
|
||||||
|
(change)="updateUserRole($any($event.target).value)"
|
||||||
|
[disabled]="updatingRoles"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Sélectionnez un nouveau rôle</option>
|
||||||
|
@for (role of availableRoles; track role.value) {
|
||||||
|
<option
|
||||||
|
[value]="role.value"
|
||||||
|
[disabled]="!canAssignRole(role.value) || role.value === user.role"
|
||||||
|
>
|
||||||
|
{{ role.label }}
|
||||||
|
@if (!canAssignRole(role.value)) {
|
||||||
|
(Non autorisé)
|
||||||
|
} @else if (role.value === user.role) {
|
||||||
|
(Actuel)
|
||||||
|
}
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<div class="form-text">
|
||||||
|
@if (updatingRoles) {
|
||||||
|
<div class="spinner-border spinner-border-sm me-1" role="status">
|
||||||
|
<span class="visually-hidden">Mise à jour...</span>
|
||||||
|
</div>
|
||||||
|
Mise à jour en cours...
|
||||||
|
} @else {
|
||||||
|
Sélectionnez un nouveau rôle pour cet utilisateur
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else if (!canManageRoles) {
|
||||||
|
<div class="alert alert-info mt-3">
|
||||||
|
<small>
|
||||||
|
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||||
|
Vous n'avez pas la permission de modifier les rôles
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informations de création -->
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="card-title mb-0">Informations de Création</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-2 small">
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Créé par :</strong>
|
||||||
|
<div class="text-muted">{{ user.createdByUsername || 'Système' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Date de création :</strong>
|
||||||
|
<div class="text-muted">{{ formatTimestamp(user.createdTimestamp) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Type d'utilisateur :</strong>
|
||||||
|
<div class="text-muted">
|
||||||
|
<span class="badge bg-secondary">{{ user.userType }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -135,14 +263,44 @@
|
|||||||
<!-- Colonne de droite - Détails et édition -->
|
<!-- Colonne de droite - Détails et édition -->
|
||||||
<div class="col-xl-8 col-lg-7">
|
<div class="col-xl-8 col-lg-7">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title mb-0">
|
<h5 class="card-title mb-0">
|
||||||
@if (isEditing) {
|
@if (isEditing) {
|
||||||
|
<ng-icon name="lucideEdit" class="me-2"></ng-icon>
|
||||||
Modification du Profil
|
Modification du Profil
|
||||||
} @else {
|
} @else {
|
||||||
Mes Informations
|
<ng-icon name="lucideUser" class="me-2"></ng-icon>
|
||||||
|
Détails du Compte
|
||||||
}
|
}
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
|
@if (isEditing) {
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary btn-sm"
|
||||||
|
(click)="cancelEditing()"
|
||||||
|
[disabled]="saving"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-success btn-sm"
|
||||||
|
(click)="saveProfile()"
|
||||||
|
[disabled]="saving"
|
||||||
|
>
|
||||||
|
@if (saving) {
|
||||||
|
<div class="spinner-border spinner-border-sm me-1" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<ng-icon name="lucideCheck" class="me-1"></ng-icon>
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -155,7 +313,8 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
[(ngModel)]="editedUser.firstName"
|
[(ngModel)]="editedUser.firstName"
|
||||||
placeholder="Votre prénom"
|
placeholder="Entrez le prénom"
|
||||||
|
[disabled]="saving"
|
||||||
>
|
>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
@ -172,7 +331,8 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
[(ngModel)]="editedUser.lastName"
|
[(ngModel)]="editedUser.lastName"
|
||||||
placeholder="Votre nom"
|
placeholder="Entrez le nom"
|
||||||
|
[disabled]="saving"
|
||||||
>
|
>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
@ -184,10 +344,12 @@
|
|||||||
<!-- Nom d'utilisateur -->
|
<!-- Nom d'utilisateur -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Nom d'utilisateur</label>
|
<label class="form-label">Nom d'utilisateur</label>
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext font-monospace">
|
||||||
{{ user.username }}
|
{{ user.username }}
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">Le nom d'utilisateur ne peut pas être modifié</small>
|
<div class="form-text">
|
||||||
|
Le nom d'utilisateur ne peut pas être modifié
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Email -->
|
<!-- Email -->
|
||||||
@ -198,56 +360,45 @@
|
|||||||
type="email"
|
type="email"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
[(ngModel)]="editedUser.email"
|
[(ngModel)]="editedUser.email"
|
||||||
placeholder="votre@email.com"
|
placeholder="email@exemple.com"
|
||||||
|
[disabled]="saving"
|
||||||
>
|
>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
{{ user.email }}
|
{{ user.email }}
|
||||||
|
@if (!user.emailVerified) {
|
||||||
|
<span class="badge bg-warning ms-2">Non vérifié</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Section Sécurité -->
|
<!-- Statut activé -->
|
||||||
<div class="col-12">
|
@if (isEditing) {
|
||||||
<hr>
|
<div class="col-md-6">
|
||||||
<h6>
|
<div class="form-check form-switch">
|
||||||
<ng-icon name="lucideShield" class="me-2 text-warning"></ng-icon>
|
<input
|
||||||
Sécurité du Compte
|
class="form-check-input"
|
||||||
</h6>
|
type="checkbox"
|
||||||
<div class="alert alert-info">
|
id="enabledSwitch"
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
[(ngModel)]="editedUser.enabled"
|
||||||
<div>
|
[disabled]="saving"
|
||||||
<strong>Mot de passe</strong>
|
>
|
||||||
<br>
|
<label class="form-check-label" for="enabledSwitch">
|
||||||
<small class="text-muted">Vous pouvez changer votre mot de passe à tout moment</small>
|
Compte activé
|
||||||
</div>
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
L'utilisateur peut se connecter si activé
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
} @else {
|
||||||
|
<div class="col-md-6">
|
||||||
<!-- Actions d'édition -->
|
<label class="form-label">Statut du compte</label>
|
||||||
@if (isEditing) {
|
<div class="form-control-plaintext">
|
||||||
<div class="col-12">
|
<span [class]="getStatusBadgeClass()">
|
||||||
<div class="d-flex gap-2 justify-content-end mt-4">
|
{{ getStatusText() }}
|
||||||
<button
|
</span>
|
||||||
type="button"
|
|
||||||
class="btn btn-light"
|
|
||||||
(click)="cancelEditing()"
|
|
||||||
[disabled]="saving">
|
|
||||||
Annuler
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-success"
|
|
||||||
(click)="saveProfile()"
|
|
||||||
[disabled]="saving">
|
|
||||||
@if (saving) {
|
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status">
|
|
||||||
<span class="visually-hidden">Chargement...</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
Enregistrer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -256,11 +407,14 @@
|
|||||||
@if (!isEditing) {
|
@if (!isEditing) {
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<hr>
|
<hr>
|
||||||
<h6>Informations Système</h6>
|
<h6 class="mb-3">
|
||||||
|
<ng-icon name="lucideSettings" class="me-2"></ng-icon>
|
||||||
|
Informations Système
|
||||||
|
</h6>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">ID Utilisateur</label>
|
<label class="form-label">ID Utilisateur</label>
|
||||||
<div class="form-control-plaintext font-monospace small">
|
<div class="form-control-plaintext font-monospace small text-truncate">
|
||||||
{{ user.id }}
|
{{ user.id }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -270,136 +424,75 @@
|
|||||||
{{ formatTimestamp(user.createdTimestamp) }}
|
{{ formatTimestamp(user.createdTimestamp) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Créé par</label>
|
||||||
|
<div class="form-control-plaintext">
|
||||||
|
{{ user.createdByUsername || 'Système' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Type d'utilisateur</label>
|
||||||
|
<div class="form-control-plaintext">
|
||||||
|
<span class="badge bg-secondary">{{ user.userType }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal de réinitialisation de mot de passe -->
|
<!-- Actions supplémentaires -->
|
||||||
<!-- Modal de réinitialisation de mot de passe -->
|
@if (!isEditing && canEditUsers) {
|
||||||
<ng-template #resetPasswordModal let-modal>
|
<div class="card mt-3">
|
||||||
<div class="modal-header">
|
<div class="card-header bg-light">
|
||||||
<h4 class="modal-title">
|
<h6 class="card-title mb-0">Actions de Gestion</h6>
|
||||||
<ng-icon name="lucideKey" class="me-2"></ng-icon>
|
</div>
|
||||||
Réinitialiser votre mot de passe
|
<div class="card-body">
|
||||||
</h4>
|
<div class="row g-2">
|
||||||
<button
|
<div class="col-md-4">
|
||||||
type="button"
|
<button
|
||||||
class="btn-close"
|
class="btn btn-outline-warning w-100"
|
||||||
(click)="modal.dismiss()"
|
(click)="resetPassword()"
|
||||||
[disabled]="resettingPassword"
|
>
|
||||||
></button>
|
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
||||||
</div>
|
Réinitialiser MDP
|
||||||
|
</button>
|
||||||
<div class="modal-body">
|
</div>
|
||||||
<!-- Message de succès -->
|
<div class="col-md-4">
|
||||||
@if (resetPasswordSuccess) {
|
@if (user.enabled) {
|
||||||
<div class="alert alert-success d-flex align-items-center">
|
<button
|
||||||
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
class="btn btn-outline-secondary w-100"
|
||||||
<div>{{ resetPasswordSuccess }}</div>
|
(click)="disableUser()"
|
||||||
</div>
|
>
|
||||||
}
|
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
|
||||||
|
Désactiver
|
||||||
<!-- Message d'erreur -->
|
</button>
|
||||||
@if (resetPasswordError) {
|
} @else {
|
||||||
<div class="alert alert-danger d-flex align-items-center">
|
<button
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
class="btn btn-outline-success w-100"
|
||||||
<div>{{ resetPasswordError }}</div>
|
(click)="enableUser()"
|
||||||
</div>
|
>
|
||||||
}
|
<ng-icon name="lucideUserCheck" class="me-1"></ng-icon>
|
||||||
|
Activer
|
||||||
@if (!resetPasswordSuccess && user) {
|
</button>
|
||||||
<div class="alert alert-info">
|
}
|
||||||
<strong>Utilisateur :</strong> {{ user.username }}
|
</div>
|
||||||
@if (user.firstName || user.lastName) {
|
<div class="col-md-4">
|
||||||
<br>
|
<button
|
||||||
<strong>Nom :</strong> {{ user.firstName }} {{ user.lastName }}
|
class="btn btn-outline-primary w-100"
|
||||||
|
(click)="startEditing()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form (ngSubmit)="confirmResetPassword()" #resetForm="ngForm">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">
|
|
||||||
Nouveau mot de passe <span class="text-danger">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="Entrez votre nouveau mot de passe"
|
|
||||||
[(ngModel)]="newPassword"
|
|
||||||
name="newPassword"
|
|
||||||
required
|
|
||||||
minlength="8"
|
|
||||||
[disabled]="resettingPassword"
|
|
||||||
>
|
|
||||||
<div class="form-text">
|
|
||||||
Le mot de passe doit contenir au moins 8 caractères.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
id="temporaryPassword"
|
|
||||||
[(ngModel)]="temporaryPassword"
|
|
||||||
name="temporaryPassword"
|
|
||||||
[disabled]="resettingPassword"
|
|
||||||
>
|
|
||||||
<label class="form-check-label" for="temporaryPassword">
|
|
||||||
Mot de passe temporaire
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-text">
|
|
||||||
Vous devrez changer votre mot de passe à la prochaine connexion.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal-footer">
|
|
||||||
@if (resetPasswordSuccess) {
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-success"
|
|
||||||
(click)="modal.close()"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideCheck" class="me-1"></ng-icon>
|
|
||||||
Fermer
|
|
||||||
</button>
|
|
||||||
} @else {
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-light"
|
|
||||||
(click)="modal.dismiss()"
|
|
||||||
[disabled]="resettingPassword"
|
|
||||||
>
|
|
||||||
Annuler
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary"
|
|
||||||
(click)="confirmResetPassword()"
|
|
||||||
[disabled]="!newPassword || newPassword.length < 8 || resettingPassword"
|
|
||||||
>
|
|
||||||
@if (resettingPassword) {
|
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status">
|
|
||||||
<span class="visually-hidden">Chargement...</span>
|
|
||||||
</div>
|
|
||||||
Réinitialisation...
|
|
||||||
} @else {
|
|
||||||
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
|
||||||
Réinitialiser le mot de passe
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
@ -1,2 +1,2 @@
|
|||||||
import { Profile } from './profile';
|
import { MyProfile } from './profile';
|
||||||
describe('Profile', () => {});
|
describe('MyProfile', () => {});
|
||||||
@ -1,16 +1,18 @@
|
|||||||
import { Component, inject, OnInit, TemplateRef, ViewChild, ChangeDetectorRef } from '@angular/core';
|
// src/app/modules/users/profile/personal-profile.ts
|
||||||
|
import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbAlertModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { UsersService } from '@modules/users/services/users.service';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
import { HubUsersService, UserRole, UpdateHubUserDto } from '../users/services/users.service';
|
||||||
|
import { RoleManagementService } from '@core/services/role-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { UserResponse, UpdateUserDto } from '@modules/users/models/user';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-my-profile',
|
selector: 'app-my-profile',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule, NgbModalModule],
|
imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule],
|
||||||
templateUrl: './profile.html',
|
templateUrl: './profile.html',
|
||||||
styles: [`
|
styles: [`
|
||||||
.avatar-lg {
|
.avatar-lg {
|
||||||
@ -22,129 +24,120 @@ import { UserResponse, UpdateUserDto } from '@modules/users/models/user';
|
|||||||
}
|
}
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class MyProfile implements OnInit {
|
export class MyProfile implements OnInit, OnDestroy {
|
||||||
private usersService = inject(UsersService);
|
private usersService = inject(HubUsersService);
|
||||||
|
private roleService = inject(RoleManagementService);
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
private modalService = inject(NgbModal);
|
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
@ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef<any>;
|
@Output() back = new EventEmitter<void>();
|
||||||
|
@Output() openResetPasswordModal = new EventEmitter<string>();
|
||||||
|
|
||||||
user: UserResponse | null = null;
|
user: any | null = null;
|
||||||
loading = false;
|
loading = false;
|
||||||
saving = false;
|
saving = false;
|
||||||
error = '';
|
error = '';
|
||||||
success = '';
|
success = '';
|
||||||
|
|
||||||
|
// Gestion des permissions (toujours true pour le profil personnel)
|
||||||
|
currentUserRole: UserRole | null = null;
|
||||||
|
canEditUsers = true; // Toujours vrai pour son propre profil
|
||||||
|
canManageRoles = false; // Jamais vrai pour le profil personnel
|
||||||
|
canDeleteUsers = false; // Jamais vrai pour le profil personnel
|
||||||
|
|
||||||
// Édition
|
// Édition
|
||||||
isEditing = false;
|
isEditing = false;
|
||||||
editedUser: UpdateUserDto = {};
|
editedUser: UpdateHubUserDto = {};
|
||||||
|
|
||||||
// Réinitialisation mot de passe
|
// Gestion des rôles (simplifiée pour profil personnel)
|
||||||
newPassword = '';
|
availableRoles: { value: UserRole; label: string; description: string }[] = [];
|
||||||
temporaryPassword = false;
|
updatingRoles = false;
|
||||||
resettingPassword = false;
|
|
||||||
resetPasswordError = '';
|
|
||||||
resetPasswordSuccess = '';
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadMyProfile();
|
this.initializeUserPermissions();
|
||||||
|
this.loadAvailableRoles();
|
||||||
|
this.loadUserProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMyProfile() {
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les permissions de l'utilisateur courant
|
||||||
|
*/
|
||||||
|
private initializeUserPermissions(): void {
|
||||||
|
this.authService.loadUserProfile()
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (profile) => {
|
||||||
|
this.currentUserRole = profile?.roles?.[0] as UserRole || null;
|
||||||
|
// Pour le profil personnel, on peut toujours éditer son propre profil
|
||||||
|
this.canEditUsers = true;
|
||||||
|
this.canManageRoles = false; // On ne peut pas gérer les rôles de son propre profil
|
||||||
|
this.canDeleteUsers = false; // On ne peut pas se supprimer soi-même
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading user permissions:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge les rôles disponibles (lecture seule pour profil personnel)
|
||||||
|
*/
|
||||||
|
private loadAvailableRoles(): void {
|
||||||
|
this.roleService.getAvailableRolesSimple()
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (roles) => {
|
||||||
|
this.availableRoles = roles;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading available roles:', error);
|
||||||
|
// Fallback
|
||||||
|
this.availableRoles = [
|
||||||
|
{ value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' },
|
||||||
|
{ value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' },
|
||||||
|
{ value: UserRole.DCB_PARTNER, label: 'DCB Partner', description: 'Partenaire commercial' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUserProfile() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
|
|
||||||
this.usersService.getCurrentUserProfile().subscribe({
|
this.usersService.getUserById(this.user.id)
|
||||||
next: (user) => {
|
.pipe(takeUntil(this.destroy$))
|
||||||
this.user = user;
|
.subscribe({
|
||||||
this.loading = false;
|
next: (user) => {
|
||||||
this.cdRef.detectChanges();
|
this.user = user;
|
||||||
},
|
this.loading = false;
|
||||||
error: (error) => {
|
this.cdRef.detectChanges();
|
||||||
this.error = 'Erreur lors du chargement de votre profil';
|
},
|
||||||
this.loading = false;
|
error: (error) => {
|
||||||
this.cdRef.detectChanges();
|
this.error = 'Erreur lors du chargement de votre profil';
|
||||||
console.error('Error loading my profile:', error);
|
this.loading = false;
|
||||||
}
|
this.cdRef.detectChanges();
|
||||||
});
|
console.error('Error loading user 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() {
|
startEditing() {
|
||||||
|
// Pas de vérification de permission pour le profil personnel
|
||||||
this.isEditing = true;
|
this.isEditing = true;
|
||||||
this.editedUser = {
|
this.editedUser = {
|
||||||
firstName: this.user?.firstName,
|
firstName: this.user?.firstName,
|
||||||
lastName: this.user?.lastName,
|
lastName: this.user?.lastName,
|
||||||
email: this.user?.email
|
email: this.user?.email
|
||||||
|
// On ne permet pas de modifier 'enabled' sur son propre profil
|
||||||
};
|
};
|
||||||
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelEditing() {
|
cancelEditing() {
|
||||||
@ -152,6 +145,7 @@ export class MyProfile implements OnInit {
|
|||||||
this.editedUser = {};
|
this.editedUser = {};
|
||||||
this.error = '';
|
this.error = '';
|
||||||
this.success = '';
|
this.success = '';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
saveProfile() {
|
saveProfile() {
|
||||||
@ -161,23 +155,70 @@ export class MyProfile implements OnInit {
|
|||||||
this.error = '';
|
this.error = '';
|
||||||
this.success = '';
|
this.success = '';
|
||||||
|
|
||||||
this.usersService.updateCurrentUserProfile(this.editedUser).subscribe({
|
this.usersService.updateUser(this.user.id, this.editedUser)
|
||||||
next: (updatedUser) => {
|
.pipe(takeUntil(this.destroy$))
|
||||||
this.user = updatedUser;
|
.subscribe({
|
||||||
this.isEditing = false;
|
next: (updatedUser) => {
|
||||||
this.saving = false;
|
this.user = updatedUser;
|
||||||
this.success = 'Profil mis à jour avec succès';
|
this.isEditing = false;
|
||||||
this.editedUser = {};
|
this.saving = false;
|
||||||
},
|
this.success = 'Profil mis à jour avec succès';
|
||||||
error: (error) => {
|
this.editedUser = {};
|
||||||
this.error = 'Erreur lors de la mise à jour du profil';
|
this.cdRef.detectChanges();
|
||||||
this.saving = false;
|
},
|
||||||
console.error('Error updating profile:', error);
|
error: (error) => {
|
||||||
}
|
this.error = this.getErrorMessage(error);
|
||||||
});
|
this.saving = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utilitaires d'affichage
|
// Gestion des rôles - désactivée pour profil personnel
|
||||||
|
updateUserRole(newRole: UserRole) {
|
||||||
|
// Non autorisé pour le profil personnel
|
||||||
|
this.error = 'Vous ne pouvez pas modifier votre propre rôle';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion du statut - désactivée pour profil personnel
|
||||||
|
enableUser() {
|
||||||
|
// Non autorisé pour le profil personnel
|
||||||
|
this.error = 'Vous ne pouvez pas vous activer/désactiver vous-même';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
disableUser() {
|
||||||
|
// Non autorisé pour le profil personnel
|
||||||
|
this.error = 'Vous ne pouvez pas vous activer/désactiver vous-même';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Réinitialisation du mot de passe
|
||||||
|
resetPassword() {
|
||||||
|
if (this.user) {
|
||||||
|
this.openResetPasswordModal.emit(this.user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion des erreurs - même méthode que le premier composant
|
||||||
|
private getErrorMessage(error: any): string {
|
||||||
|
if (error.error?.message) {
|
||||||
|
return error.error.message;
|
||||||
|
}
|
||||||
|
if (error.status === 403) {
|
||||||
|
return 'Vous n\'avez pas les permissions nécessaires pour cette action';
|
||||||
|
}
|
||||||
|
if (error.status === 404) {
|
||||||
|
return 'Utilisateur non trouvé';
|
||||||
|
}
|
||||||
|
if (error.status === 400) {
|
||||||
|
return 'Données invalides';
|
||||||
|
}
|
||||||
|
return 'Une erreur est survenue. Veuillez réessayer.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilitaires d'affichage - mêmes méthodes que le premier composant
|
||||||
getStatusBadgeClass(): string {
|
getStatusBadgeClass(): string {
|
||||||
if (!this.user) return 'badge bg-secondary';
|
if (!this.user) return 'badge bg-secondary';
|
||||||
if (!this.user.enabled) return 'badge bg-danger';
|
if (!this.user.enabled) return 'badge bg-danger';
|
||||||
@ -193,6 +234,7 @@ export class MyProfile implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatTimestamp(timestamp: number): string {
|
formatTimestamp(timestamp: number): string {
|
||||||
|
if (!timestamp) return 'Non disponible';
|
||||||
return new Date(timestamp).toLocaleDateString('fr-FR', {
|
return new Date(timestamp).toLocaleDateString('fr-FR', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
@ -215,13 +257,30 @@ export class MyProfile implements OnInit {
|
|||||||
return this.user.username;
|
return this.user.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleBadgeClass(role: string): string {
|
getRoleBadgeClass(role: UserRole): string {
|
||||||
switch (role) {
|
return this.roleService.getRoleBadgeClass(role);
|
||||||
case 'admin': return 'bg-danger';
|
}
|
||||||
case 'merchant': return 'bg-success';
|
|
||||||
case 'support': return 'bg-info';
|
getRoleLabel(role: UserRole): string {
|
||||||
case 'user': return 'bg-secondary';
|
return this.roleService.getRoleLabel(role);
|
||||||
default: return 'bg-secondary';
|
}
|
||||||
}
|
|
||||||
|
getRoleIcon(role: UserRole): string {
|
||||||
|
return this.roleService.getRoleIcon(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleDescription(role: UserRole): string {
|
||||||
|
const roleInfo = this.availableRoles.find(r => r.value === role);
|
||||||
|
return roleInfo?.description || 'Description non disponible';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification des permissions pour les actions - toujours false pour les actions sensibles
|
||||||
|
canAssignRole(targetRole: UserRole): boolean {
|
||||||
|
return false; // Jamais autorisé pour le profil personnel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifie si c'est le profil de l'utilisateur courant - toujours true
|
||||||
|
isCurrentUserProfile(): boolean {
|
||||||
|
return true; // Toujours vrai pour le profil personnel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,2 +1,2 @@
|
|||||||
import { TransactionsDetails } from './details';
|
import { TransactionDetails } from './details';
|
||||||
describe('TransactionsDetails', () => {});
|
describe('TransactionDetails', () => {});
|
||||||
@ -1,30 +1,71 @@
|
|||||||
<app-ui-card title="Liste des Utilisateurs">
|
<app-ui-card title="Liste des Utilisateurs Hub">
|
||||||
<a
|
<a
|
||||||
helper-text
|
helper-text
|
||||||
href="javascript:void(0);"
|
href="javascript:void(0);"
|
||||||
class="icon-link icon-link-hover link-primary fw-semibold"
|
class="icon-link icon-link-hover link-primary fw-semibold"
|
||||||
>Gérez les accès utilisateurs de votre plateforme
|
>Gérez les accès utilisateurs de votre plateforme DCB
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div card-body>
|
<div card-body>
|
||||||
<!-- Barre d'actions supérieure -->
|
<!-- Barre d'actions supérieure -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="d-flex justify-right-end gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<button
|
<!-- Statistiques rapides -->
|
||||||
class="btn btn-primary"
|
<div class="btn-group btn-group-sm">
|
||||||
(click)="openCreateModal.emit()"
|
<button
|
||||||
>
|
type="button"
|
||||||
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
class="btn btn-outline-primary"
|
||||||
Nouvel Utilisateur
|
[class.active]="roleFilter === 'all'"
|
||||||
</button>
|
(click)="filterByRole('all')"
|
||||||
|
>
|
||||||
|
Tous ({{ allUsers.length }})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger"
|
||||||
|
[class.active]="roleFilter === UserRole.DCB_ADMIN"
|
||||||
|
(click)="filterByRole(UserRole.DCB_ADMIN)"
|
||||||
|
>
|
||||||
|
Admins ({{ getUsersCountByRole(UserRole.DCB_ADMIN) }})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-info"
|
||||||
|
[class.active]="roleFilter === UserRole.DCB_SUPPORT"
|
||||||
|
(click)="filterByRole(UserRole.DCB_SUPPORT)"
|
||||||
|
>
|
||||||
|
Support ({{ getUsersCountByRole(UserRole.DCB_SUPPORT) }})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-success"
|
||||||
|
[class.active]="roleFilter === UserRole.DCB_PARTNER"
|
||||||
|
(click)="filterByRole(UserRole.DCB_PARTNER)"
|
||||||
|
>
|
||||||
|
Partenaires ({{ getUsersCountByRole(UserRole.DCB_PARTNER) }})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
@if (canCreateUsers) {
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
(click)="openCreateModal.emit()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
||||||
|
Nouvel Utilisateur
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Barre de recherche et filtres -->
|
<!-- Barre de recherche et filtres -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-3">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<ng-icon name="lucideSearch"></ng-icon>
|
<ng-icon name="lucideSearch"></ng-icon>
|
||||||
@ -32,20 +73,20 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Rechercher par nom, email, username..."
|
placeholder="Nom, email, username..."
|
||||||
[(ngModel)]="searchTerm"
|
[(ngModel)]="searchTerm"
|
||||||
(keyup.enter)="onSearch()"
|
(keyup.enter)="onSearch()"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
<select class="form-select" [(ngModel)]="statusFilter" (change)="onSearch()">
|
<select class="form-select" [(ngModel)]="statusFilter" (change)="onSearch()">
|
||||||
<option value="all">Tous les statuts</option>
|
<option value="all">Tous les statuts</option>
|
||||||
<option value="enabled">Activés</option>
|
<option value="enabled">Activés ({{ getEnabledUsersCount() }})</option>
|
||||||
<option value="disabled">Désactivés</option>
|
<option value="disabled">Désactivés ({{ getDisabledUsersCount() }})</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
<select class="form-select" [(ngModel)]="emailVerifiedFilter" (change)="onSearch()">
|
<select class="form-select" [(ngModel)]="emailVerifiedFilter" (change)="onSearch()">
|
||||||
<option value="all">Tous les emails</option>
|
<option value="all">Tous les emails</option>
|
||||||
<option value="verified">Email vérifié</option>
|
<option value="verified">Email vérifié</option>
|
||||||
@ -53,12 +94,21 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
|
<select class="form-select" [(ngModel)]="roleFilter" (change)="onSearch()">
|
||||||
|
@for (role of availableRoles; track role.value) {
|
||||||
|
<option [value]="role.value">{{ role.label }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button class="btn btn-outline-primary" (click)="onSearch()">
|
<button class="btn btn-outline-primary" (click)="onSearch()">
|
||||||
<ng-icon name="lucideFilter"></ng-icon>
|
<ng-icon name="lucideFilter" class="me-1"></ng-icon>
|
||||||
|
Appliquer
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-secondary" (click)="onClearFilters()">
|
<button class="btn btn-outline-secondary" (click)="onClearFilters()">
|
||||||
<ng-icon name="lucideX"></ng-icon>
|
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||||
|
Réinitialiser
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -77,8 +127,10 @@
|
|||||||
<!-- Error State -->
|
<!-- Error State -->
|
||||||
@if (error && !loading) {
|
@if (error && !loading) {
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="alert alert-danger" role="alert">
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
<div class="d-flex align-items-center">
|
||||||
{{ error }}
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
|
<div>{{ error }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +152,12 @@
|
|||||||
<ng-icon [name]="getSortIcon('email')" class="ms-1 fs-12"></ng-icon>
|
<ng-icon [name]="getSortIcon('email')" class="ms-1 fs-12"></ng-icon>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th>Rôles</th>
|
<th (click)="sort('role')" class="cursor-pointer">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span>Rôle</span>
|
||||||
|
<ng-icon [name]="getSortIcon('role')" class="ms-1 fs-12"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
<th (click)="sort('enabled')" class="cursor-pointer">
|
<th (click)="sort('enabled')" class="cursor-pointer">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span>Statut</span>
|
<span>Statut</span>
|
||||||
@ -113,7 +170,7 @@
|
|||||||
<ng-icon [name]="getSortIcon('createdTimestamp')" class="ms-1 fs-12"></ng-icon>
|
<ng-icon [name]="getSortIcon('createdTimestamp')" class="ms-1 fs-12"></ng-icon>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th width="150">Actions</th>
|
<th width="180">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -121,35 +178,35 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="avatar-sm bg-primary rounded-circle d-flex align-items-center justify-content-center me-2">
|
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||||
<span class="text-white fw-bold small">
|
<span class="text-primary fw-semibold small">
|
||||||
{{ getUserInitials(user) }}
|
{{ getUserInitials(user) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ getUserDisplayName(user) }}</strong>
|
<strong class="d-block">{{ getUserDisplayName(user) }}</strong>
|
||||||
<div class="text-muted small">@{{ user.username }}</div>
|
<small class="text-muted">@{{ user.username }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div>{{ user.email }}</div>
|
<div class="d-flex align-items-center">
|
||||||
@if (!user.emailVerified) {
|
{{ user.email }}
|
||||||
<small class="text-warning">
|
@if (!user.emailVerified) {
|
||||||
<ng-icon name="lucideAlertCircle" class="me-1"></ng-icon>
|
<ng-icon
|
||||||
Non vérifié
|
name="lucideAlertTriangle"
|
||||||
</small>
|
class="ms-1 text-warning"
|
||||||
}
|
size="16"
|
||||||
|
title="Email non vérifié"
|
||||||
|
></ng-icon>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@for (role of user.clientRoles; track role) {
|
<span class="badge d-flex align-items-center" [ngClass]="getRoleBadgeClass(user.role)">
|
||||||
<span class="badge me-1 mb-1" [ngClass]="getRoleBadgeClass(role)">
|
<ng-icon [name]="getRoleIcon(user.role)" class="me-1" size="14"></ng-icon>
|
||||||
{{ role }}
|
{{ getRoleLabel(user.role) }}
|
||||||
</span>
|
</span>
|
||||||
}
|
|
||||||
@if (user.clientRoles.length === 0) {
|
|
||||||
<span class="text-muted small">Aucun rôle</span>
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span [class]="getStatusBadgeClass(user)">
|
<span [class]="getStatusBadgeClass(user)">
|
||||||
@ -171,7 +228,7 @@
|
|||||||
<ng-icon name="lucideEye"></ng-icon>
|
<ng-icon name="lucideEye"></ng-icon>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-info btn-sm"
|
class="btn btn-outline-warning btn-sm"
|
||||||
(click)="resetPassword(user)"
|
(click)="resetPassword(user)"
|
||||||
title="Réinitialiser le mot de passe"
|
title="Réinitialiser le mot de passe"
|
||||||
>
|
>
|
||||||
@ -179,11 +236,11 @@
|
|||||||
</button>
|
</button>
|
||||||
@if (user.enabled) {
|
@if (user.enabled) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-warning btn-sm"
|
class="btn btn-outline-secondary btn-sm"
|
||||||
(click)="disableUser(user)"
|
(click)="disableUser(user)"
|
||||||
title="Désactiver l'utilisateur"
|
title="Désactiver l'utilisateur"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucidePause"></ng-icon>
|
<ng-icon name="lucideUserX"></ng-icon>
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else {
|
||||||
<button
|
<button
|
||||||
@ -191,16 +248,18 @@
|
|||||||
(click)="enableUser(user)"
|
(click)="enableUser(user)"
|
||||||
title="Activer l'utilisateur"
|
title="Activer l'utilisateur"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucidePlay"></ng-icon>
|
<ng-icon name="lucideUserCheck"></ng-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (canDeleteUsers) {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-danger btn-sm"
|
||||||
|
(click)="deleteUser(user)"
|
||||||
|
title="Supprimer l'utilisateur"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideTrash2"></ng-icon>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<button
|
|
||||||
class="btn btn-outline-danger btn-sm"
|
|
||||||
(click)="deleteUser(user)"
|
|
||||||
title="Supprimer l'utilisateur"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideTrash2"></ng-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -208,11 +267,17 @@
|
|||||||
@empty {
|
@empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="text-center py-4">
|
<td colspan="6" class="text-center py-4">
|
||||||
<ng-icon name="lucideUsers" class="text-muted fs-1 mb-2"></ng-icon>
|
<div class="text-muted">
|
||||||
<p class="text-muted">Aucun utilisateur trouvé</p>
|
<ng-icon name="lucideUsers" class="fs-1 mb-3 opacity-50"></ng-icon>
|
||||||
<button class="btn btn-primary" (click)="openCreateModal.emit()">
|
<h5 class="mb-2">Aucun utilisateur trouvé</h5>
|
||||||
Créer le premier utilisateur
|
<p class="mb-3">Aucun utilisateur ne correspond à vos critères de recherche.</p>
|
||||||
</button>
|
@if (canCreateUsers) {
|
||||||
|
<button class="btn btn-primary" (click)="openCreateModal.emit()">
|
||||||
|
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
||||||
|
Créer le premier utilisateur
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@ -221,6 +286,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
|
@if (totalPages > 1) {
|
||||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Affichage de {{ getStartIndex() }} à {{ getEndIndex() }} sur {{ totalItems }} utilisateurs
|
Affichage de {{ getStartIndex() }} à {{ getEndIndex() }} sur {{ totalItems }} utilisateurs
|
||||||
@ -237,7 +303,40 @@
|
|||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Résumé des résultats -->
|
||||||
|
@if (displayedUsers.length > 0) {
|
||||||
|
<div class="mt-3 pt-3 border-top">
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>Total :</strong> {{ allUsers.length }} utilisateurs
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>Actifs :</strong> {{ getEnabledUsersCount() }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>Admins :</strong> {{ getUsersCountByRole(UserRole.DCB_ADMIN) }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>Support :</strong> {{ getUsersCountByRole(UserRole.DCB_SUPPORT) }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>Partenaires :</strong> {{ getUsersCountByRole(UserRole.DCB_PARTNER) }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</app-ui-card>
|
</app-ui-card>
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
|
// src/app/modules/users/list/list.ts
|
||||||
|
import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, Input, OnDestroy } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { UsersService } from '../services/users.service';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
import { UserResponse } from '../models/user';
|
import { HubUsersService, HubUserResponse, UserRole } from '../services/users.service';
|
||||||
|
import { RoleManagementService } from '@core/services/role-management.service';
|
||||||
import { UiCard } from '@app/components/ui-card';
|
import { UiCard } from '@app/components/ui-card';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -19,21 +21,26 @@ import { UiCard } from '@app/components/ui-card';
|
|||||||
],
|
],
|
||||||
templateUrl: './list.html',
|
templateUrl: './list.html',
|
||||||
})
|
})
|
||||||
export class UsersList implements OnInit {
|
export class UsersList implements OnInit, OnDestroy {
|
||||||
private usersService = inject(UsersService);
|
private usersService = inject(HubUsersService);
|
||||||
|
private roleService = inject(RoleManagementService);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
readonly UserRole = UserRole;
|
||||||
|
|
||||||
|
@Input() canCreateUsers: boolean = false;
|
||||||
|
@Input() canDeleteUsers: boolean = false;
|
||||||
|
|
||||||
@Output() userSelected = new EventEmitter<string>();
|
@Output() userSelected = new EventEmitter<string>();
|
||||||
@Output() openCreateModal = new EventEmitter<void>();
|
@Output() openCreateModal = new EventEmitter<void>();
|
||||||
@Output() openResetPasswordModal = new EventEmitter<string>();
|
@Output() openResetPasswordModal = new EventEmitter<string>();
|
||||||
@Output() openDeleteUserModal = new EventEmitter<string>();
|
@Output() openDeleteUserModal = new EventEmitter<string>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Données
|
// Données
|
||||||
allUsers: UserResponse[] = [];
|
allUsers: HubUserResponse[] = [];
|
||||||
filteredUsers: UserResponse[] = [];
|
filteredUsers: HubUserResponse[] = [];
|
||||||
displayedUsers: UserResponse[] = [];
|
displayedUsers: HubUserResponse[] = [];
|
||||||
|
|
||||||
// États
|
// États
|
||||||
loading = false;
|
loading = false;
|
||||||
@ -43,6 +50,7 @@ export class UsersList implements OnInit {
|
|||||||
searchTerm = '';
|
searchTerm = '';
|
||||||
statusFilter: 'all' | 'enabled' | 'disabled' = 'all';
|
statusFilter: 'all' | 'enabled' | 'disabled' = 'all';
|
||||||
emailVerifiedFilter: 'all' | 'verified' | 'not-verified' = 'all';
|
emailVerifiedFilter: 'all' | 'verified' | 'not-verified' = 'all';
|
||||||
|
roleFilter: UserRole | 'all' = 'all';
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
@ -51,31 +59,46 @@ export class UsersList implements OnInit {
|
|||||||
totalPages = 0;
|
totalPages = 0;
|
||||||
|
|
||||||
// Tri
|
// Tri
|
||||||
sortField: keyof UserResponse = 'username';
|
sortField: keyof HubUserResponse = 'username';
|
||||||
sortDirection: 'asc' | 'desc' = 'asc';
|
sortDirection: 'asc' | 'desc' = 'asc';
|
||||||
|
|
||||||
|
// Rôles disponibles pour le filtre
|
||||||
|
availableRoles = [
|
||||||
|
{ value: 'all' as const, label: 'Tous les rôles' },
|
||||||
|
{ value: UserRole.DCB_ADMIN, label: 'Administrateurs' },
|
||||||
|
{ value: UserRole.DCB_SUPPORT, label: 'Support' },
|
||||||
|
{ value: UserRole.DCB_PARTNER, label: 'Partenaires' }
|
||||||
|
];
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadUsers();
|
this.loadUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
loadUsers() {
|
loadUsers() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
|
|
||||||
this.usersService.findAllUsers().subscribe({
|
this.usersService.findAllUsers()
|
||||||
next: (response) => {
|
.pipe(takeUntil(this.destroy$))
|
||||||
this.allUsers = response.data;
|
.subscribe({
|
||||||
this.applyFiltersAndPagination();
|
next: (response) => {
|
||||||
this.loading = false;
|
this.allUsers = response.users;
|
||||||
this.cdRef.detectChanges();
|
this.applyFiltersAndPagination();
|
||||||
},
|
this.loading = false;
|
||||||
error: (error) => {
|
this.cdRef.detectChanges();
|
||||||
this.error = 'Erreur lors du chargement des utilisateurs';
|
},
|
||||||
this.loading = false;
|
error: (error) => {
|
||||||
this.cdRef.detectChanges();
|
this.error = 'Erreur lors du chargement des utilisateurs';
|
||||||
console.error('Error loading users:', error);
|
this.loading = false;
|
||||||
}
|
this.cdRef.detectChanges();
|
||||||
});
|
console.error('Error loading users:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recherche et filtres
|
// Recherche et filtres
|
||||||
@ -88,6 +111,7 @@ export class UsersList implements OnInit {
|
|||||||
this.searchTerm = '';
|
this.searchTerm = '';
|
||||||
this.statusFilter = 'all';
|
this.statusFilter = 'all';
|
||||||
this.emailVerifiedFilter = 'all';
|
this.emailVerifiedFilter = 'all';
|
||||||
|
this.roleFilter = 'all';
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.applyFiltersAndPagination();
|
this.applyFiltersAndPagination();
|
||||||
}
|
}
|
||||||
@ -112,7 +136,10 @@ export class UsersList implements OnInit {
|
|||||||
(this.emailVerifiedFilter === 'verified' && user.emailVerified) ||
|
(this.emailVerifiedFilter === 'verified' && user.emailVerified) ||
|
||||||
(this.emailVerifiedFilter === 'not-verified' && !user.emailVerified);
|
(this.emailVerifiedFilter === 'not-verified' && !user.emailVerified);
|
||||||
|
|
||||||
return matchesSearch && matchesStatus && matchesEmailVerified;
|
// Filtre par rôle
|
||||||
|
const matchesRole = this.roleFilter === 'all' || user.role === this.roleFilter;
|
||||||
|
|
||||||
|
return matchesSearch && matchesStatus && matchesEmailVerified && matchesRole;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Appliquer le tri
|
// Appliquer le tri
|
||||||
@ -145,7 +172,7 @@ export class UsersList implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tri
|
// Tri
|
||||||
sort(field: keyof UserResponse) {
|
sort(field: keyof HubUserResponse) {
|
||||||
if (this.sortField === field) {
|
if (this.sortField === field) {
|
||||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||||
} else {
|
} else {
|
||||||
@ -155,7 +182,7 @@ export class UsersList implements OnInit {
|
|||||||
this.applyFiltersAndPagination();
|
this.applyFiltersAndPagination();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortIcon(field: keyof UserResponse): string {
|
getSortIcon(field: keyof HubUserResponse): string {
|
||||||
if (this.sortField !== field) return 'lucideArrowUpDown';
|
if (this.sortField !== field) return 'lucideArrowUpDown';
|
||||||
return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown';
|
return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown';
|
||||||
}
|
}
|
||||||
@ -180,79 +207,125 @@ export class UsersList implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Méthode pour réinitialiser le mot de passe
|
// Méthode pour réinitialiser le mot de passe
|
||||||
resetPassword(user: UserResponse) {
|
resetPassword(user: HubUserResponse) {
|
||||||
this.openResetPasswordModal.emit(user.id);
|
this.openResetPasswordModal.emit(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthode pour ouvrir le modal de suppression
|
// Méthode pour ouvrir le modal de suppression
|
||||||
deleteUser(user: UserResponse) {
|
deleteUser(user: HubUserResponse) {
|
||||||
this.openDeleteUserModal.emit(user.id);
|
if (this.canDeleteUsers) {
|
||||||
|
this.openDeleteUserModal.emit(user.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enableUser(user: HubUserResponse) {
|
||||||
enableUser(user: UserResponse) {
|
this.usersService.enableUser(user.id)
|
||||||
this.usersService.enableUser(user.id).subscribe({
|
.pipe(takeUntil(this.destroy$))
|
||||||
next: () => {
|
.subscribe({
|
||||||
user.enabled = true;
|
next: () => {
|
||||||
this.applyFiltersAndPagination();
|
user.enabled = true;
|
||||||
this.cdRef.detectChanges();
|
this.applyFiltersAndPagination();
|
||||||
},
|
this.cdRef.detectChanges();
|
||||||
error: (error) => {
|
},
|
||||||
console.error('Error enabling user:', error);
|
error: (error) => {
|
||||||
alert('Erreur lors de l\'activation de l\'utilisateur');
|
console.error('Error enabling user:', error);
|
||||||
}
|
this.error = 'Erreur lors de l\'activation de l\'utilisateur';
|
||||||
});
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
disableUser(user: UserResponse) {
|
disableUser(user: HubUserResponse) {
|
||||||
this.usersService.disableUser(user.id).subscribe({
|
this.usersService.disableUser(user.id)
|
||||||
next: () => {
|
.pipe(takeUntil(this.destroy$))
|
||||||
user.enabled = false;
|
.subscribe({
|
||||||
this.applyFiltersAndPagination();
|
next: () => {
|
||||||
this.cdRef.detectChanges();
|
user.enabled = false;
|
||||||
},
|
this.applyFiltersAndPagination();
|
||||||
error: (error) => {
|
this.cdRef.detectChanges();
|
||||||
console.error('Error disabling user:', error);
|
},
|
||||||
alert('Erreur lors de la désactivation de l\'utilisateur');
|
error: (error) =>{
|
||||||
}
|
console.error('Error disabling user:', error);
|
||||||
});
|
this.error = 'Erreur lors de la désactivation de l\'utilisateur';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utilitaires d'affichage
|
// Utilitaires d'affichage
|
||||||
getStatusBadgeClass(user: UserResponse): string {
|
getStatusBadgeClass(user: HubUserResponse): string {
|
||||||
if (!user.enabled) return 'badge bg-danger';
|
if (!user.enabled) return 'badge bg-danger';
|
||||||
if (!user.emailVerified) return 'badge bg-warning';
|
if (!user.emailVerified) return 'badge bg-warning';
|
||||||
return 'badge bg-success';
|
return 'badge bg-success';
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatusText(user: UserResponse): string {
|
getStatusText(user: HubUserResponse): string {
|
||||||
if (!user.enabled) return 'Désactivé';
|
if (!user.enabled) return 'Désactivé';
|
||||||
if (!user.emailVerified) return 'Email non vérifié';
|
if (!user.emailVerified) return 'Email non vérifié';
|
||||||
return 'Actif';
|
return 'Actif';
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleBadgeClass(role: string): string {
|
getRoleBadgeClass(role: UserRole): string {
|
||||||
switch (role) {
|
return this.roleService.getRoleBadgeClass(role);
|
||||||
case 'admin': return 'bg-danger';
|
}
|
||||||
case 'merchant': return 'bg-success';
|
|
||||||
case 'support': return 'bg-info';
|
getRoleLabel(role: UserRole): string {
|
||||||
case 'user': return 'bg-secondary';
|
return this.roleService.getRoleLabel(role);
|
||||||
default: return 'bg-secondary';
|
}
|
||||||
}
|
|
||||||
|
getRoleIcon(role: UserRole): string {
|
||||||
|
return this.roleService.getRoleIcon(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
formatTimestamp(timestamp: number): string {
|
formatTimestamp(timestamp: number): string {
|
||||||
return new Date(timestamp).toLocaleDateString('fr-FR');
|
if (!timestamp) return 'Non disponible';
|
||||||
|
return new Date(timestamp).toLocaleDateString('fr-FR', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserInitials(user: UserResponse): string {
|
getUserInitials(user: HubUserResponse): string {
|
||||||
return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U';
|
return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U';
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserDisplayName(user: UserResponse): string {
|
getUserDisplayName(user: HubUserResponse): string {
|
||||||
if (user.firstName && user.lastName) {
|
if (user.firstName && user.lastName) {
|
||||||
return `${user.firstName} ${user.lastName}`;
|
return `${user.firstName} ${user.lastName}`;
|
||||||
}
|
}
|
||||||
return user.username;
|
return user.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Statistiques
|
||||||
|
/**
|
||||||
|
* Récupère le nombre d'utilisateurs par rôle (méthode publique pour le template)
|
||||||
|
*/
|
||||||
|
getUsersCountByRole(role: UserRole): number {
|
||||||
|
return this.allUsers.filter(user => user.role === role).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEnabledUsersCount(): number {
|
||||||
|
return this.allUsers.filter(user => user.enabled).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisabledUsersCount(): number {
|
||||||
|
return this.allUsers.filter(user => !user.enabled).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification des permissions pour les actions
|
||||||
|
canManageUser(user: HubUserResponse): boolean {
|
||||||
|
// Implémentez votre logique de permission ici
|
||||||
|
// Par exemple, empêcher un utilisateur de se modifier lui-même
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recherche rapide par rôle
|
||||||
|
filterByRole(role: UserRole | 'all') {
|
||||||
|
this.roleFilter = role;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.applyFiltersAndPagination();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
54
src/app/modules/users/models/hub-user.model.ts
Normal file
54
src/app/modules/users/models/hub-user.model.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// src/app/modules/users/models/user.model.ts
|
||||||
|
export enum UserRole {
|
||||||
|
DCB_ADMIN = 'DCB_ADMIN',
|
||||||
|
DCB_SUPPORT = 'DCB_SUPPORT',
|
||||||
|
DCB_PARTNER = 'DCB_PARTNER'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HubUserResponse {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
role: UserRole;
|
||||||
|
enabled: boolean;
|
||||||
|
emailVerified: boolean;
|
||||||
|
createdBy: string;
|
||||||
|
createdByUsername: string;
|
||||||
|
createdTimestamp: number;
|
||||||
|
lastLogin?: number;
|
||||||
|
userType: 'HUB';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateHubUserDto {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
password: string;
|
||||||
|
role: UserRole;
|
||||||
|
enabled?: boolean;
|
||||||
|
emailVerified?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateHubUserDto {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
email?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResetPasswordDto {
|
||||||
|
userId: string;
|
||||||
|
newPassword: string;
|
||||||
|
temporary?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedUserResponse {
|
||||||
|
users: HubUserResponse[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
311
src/app/modules/users/models/user.model.ts
Normal file
311
src/app/modules/users/models/user.model.ts
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
import { IsString, IsEmail, IsBoolean, IsOptional, IsArray, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class User {
|
||||||
|
id?: string;
|
||||||
|
username: string = '';
|
||||||
|
email: string = '';
|
||||||
|
firstName?: string = '';
|
||||||
|
lastName?: string = '';
|
||||||
|
enabled: boolean = true;
|
||||||
|
emailVerified: boolean = false;
|
||||||
|
attributes?: Record<string, any> = {};
|
||||||
|
clientRoles: string[] = [];
|
||||||
|
createdTimestamp?: number;
|
||||||
|
|
||||||
|
constructor(partial?: Partial<User>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserCredentials {
|
||||||
|
type: string = 'password';
|
||||||
|
value: string = '';
|
||||||
|
temporary: boolean = false;
|
||||||
|
|
||||||
|
constructor(type?: string, value?: string, temporary?: boolean) {
|
||||||
|
if (type) this.type = type;
|
||||||
|
if (value) this.value = value;
|
||||||
|
if (temporary !== undefined) this.temporary = temporary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateUserDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(3)
|
||||||
|
username: string = '';
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email: string = '';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
firstName: string = '';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
lastName: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8)
|
||||||
|
password: string = '';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
enabled: boolean = true;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
emailVerified: boolean = false;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
attributes?: Record<string, any> = {};
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
clientRoles: string[] = [];
|
||||||
|
|
||||||
|
constructor(partial?: Partial<CreateUserDto>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateUserDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
firstName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
lastName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
enabled?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
emailVerified?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
attributes?: Record<string, any>;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
clientRoles?: string[];
|
||||||
|
|
||||||
|
constructor(partial?: Partial<UpdateUserDto>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserQueryDto {
|
||||||
|
@IsOptional()
|
||||||
|
page: number = 1;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
limit: number = 10;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
enabled?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
emailVerified?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
firstName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
lastName?: string;
|
||||||
|
|
||||||
|
constructor(partial?: Partial<UserQueryDto>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ResetPasswordDto {
|
||||||
|
@IsString()
|
||||||
|
userId: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8)
|
||||||
|
newPassword: string = '';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
temporary: boolean = false;
|
||||||
|
|
||||||
|
constructor(partial?: Partial<ResetPasswordDto>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserResponse {
|
||||||
|
id: string = '';
|
||||||
|
username: string = '';
|
||||||
|
email: string = '';
|
||||||
|
firstName: string = '';
|
||||||
|
lastName: string = '';
|
||||||
|
enabled: boolean = true;
|
||||||
|
emailVerified: boolean = false;
|
||||||
|
attributes: Record<string, any> = {};
|
||||||
|
clientRoles: string[] = [];
|
||||||
|
createdTimestamp: number = Date.now();
|
||||||
|
|
||||||
|
constructor(user?: any) {
|
||||||
|
if (user) {
|
||||||
|
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 PaginatedUserResponse {
|
||||||
|
users: UserResponse[] = [];
|
||||||
|
total: number = 0;
|
||||||
|
page: number = 1;
|
||||||
|
limit: number = 10;
|
||||||
|
totalPages: number = 0;
|
||||||
|
|
||||||
|
constructor(users: UserResponse[] = [], total: number = 0, page: number = 1, limit: number = 10) {
|
||||||
|
this.users = users;
|
||||||
|
this.total = total;
|
||||||
|
this.page = page;
|
||||||
|
this.limit = limit;
|
||||||
|
this.totalPages = Math.ceil(total / limit) || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AssignRolesDto {
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
roles: string[] = [];
|
||||||
|
|
||||||
|
constructor(partial?: Partial<AssignRolesDto>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
@IsString()
|
||||||
|
username: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
password: string = '';
|
||||||
|
|
||||||
|
constructor(partial?: Partial<LoginDto>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TokenResponse {
|
||||||
|
access_token: string = '';
|
||||||
|
refresh_token?: string = '';
|
||||||
|
expires_in: number = 0;
|
||||||
|
token_type: string = '';
|
||||||
|
scope?: string = '';
|
||||||
|
|
||||||
|
constructor(partial?: Partial<TokenResponse>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiResponse<T> {
|
||||||
|
data: T | null = null;
|
||||||
|
message: string = '';
|
||||||
|
status: string = '';
|
||||||
|
|
||||||
|
constructor(partial?: Partial<ApiResponse<T>>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserRoleMapping {
|
||||||
|
id: string = '';
|
||||||
|
name: string = '';
|
||||||
|
description?: string = '';
|
||||||
|
composite?: boolean = false;
|
||||||
|
clientRole?: boolean = false;
|
||||||
|
containerId?: string = '';
|
||||||
|
|
||||||
|
constructor(partial?: Partial<UserRoleMapping>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserSession {
|
||||||
|
id: string = '';
|
||||||
|
username: string = '';
|
||||||
|
userId: string = '';
|
||||||
|
ipAddress: string = '';
|
||||||
|
start: number = 0;
|
||||||
|
lastAccess: number = 0;
|
||||||
|
clients: Record<string, string> = {};
|
||||||
|
|
||||||
|
constructor(partial?: Partial<UserSession>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types pour les rôles client
|
||||||
|
export type ClientRole =
|
||||||
|
| 'dcb-admin'
|
||||||
|
| 'dcb-partner'
|
||||||
|
| 'dcb-support'
|
||||||
|
| 'dcb-partner-admin'
|
||||||
|
| 'dcb-partner-manager'
|
||||||
|
| 'dcb-partner-support'
|
||||||
|
| 'dcb-partner-user';
|
||||||
@ -1,180 +0,0 @@
|
|||||||
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<User>) {
|
|
||||||
// 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<CreateUserDto>) {
|
|
||||||
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<UpdateUserDto>) {
|
|
||||||
if (partial) {
|
|
||||||
Object.assign(this, partial);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ResetPasswordDto {
|
|
||||||
userId: string;
|
|
||||||
newPassword: string;
|
|
||||||
temporary?: boolean;
|
|
||||||
|
|
||||||
constructor(partial?: Partial<ResetPasswordDto>) {
|
|
||||||
// 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<UserQueryDto>) {
|
|
||||||
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<T> {
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
@ -14,26 +14,36 @@
|
|||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb mb-0">
|
<ol class="breadcrumb mb-0">
|
||||||
<li class="breadcrumb-item">
|
<li class="breadcrumb-item">
|
||||||
<a href="javascript:void(0)" class="text-decoration-none">Profile Utilisateurs</a>
|
<a href="javascript:void(0)" (click)="back.emit()" class="text-decoration-none cursor-pointer">
|
||||||
|
Utilisateurs
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">
|
||||||
|
@if (user) {
|
||||||
|
{{ getUserDisplayName() }}
|
||||||
|
} @else {
|
||||||
|
Profil
|
||||||
|
}
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
|
<!-- Bouton de réinitialisation de mot de passe -->
|
||||||
@if (user && !isEditing) {
|
@if (user && canEditUsers && !isEditing) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-warning"
|
class="btn btn-warning"
|
||||||
(click)="openResetPasswordModal.emit()"
|
(click)="resetPassword()"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
||||||
Réinitialiser MDP
|
Réinitialiser MDP
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Bouton activation/désactivation -->
|
||||||
@if (user.enabled) {
|
@if (user.enabled) {
|
||||||
<button
|
<button
|
||||||
class="btn btn-warning"
|
class="btn btn-outline-warning"
|
||||||
(click)="disableUser()"
|
(click)="disableUser()"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucidePause" class="me-1"></ng-icon>
|
<ng-icon name="lucidePause" class="me-1"></ng-icon>
|
||||||
@ -41,7 +51,7 @@
|
|||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else {
|
||||||
<button
|
<button
|
||||||
class="btn btn-success"
|
class="btn btn-outline-success"
|
||||||
(click)="enableUser()"
|
(click)="enableUser()"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucidePlay" class="me-1"></ng-icon>
|
<ng-icon name="lucidePlay" class="me-1"></ng-icon>
|
||||||
@ -49,6 +59,7 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Bouton modification -->
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
(click)="startEditing()"
|
(click)="startEditing()"
|
||||||
@ -62,18 +73,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Indicateur de permissions -->
|
||||||
|
@if (currentUserRole && !canEditUsers) {
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideShield" class="me-2"></ng-icon>
|
||||||
|
<div>
|
||||||
|
<strong>Permissions limitées :</strong> Vous ne pouvez que consulter ce profil
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Messages d'alerte -->
|
<!-- Messages d'alerte -->
|
||||||
@if (error) {
|
@if (error) {
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
<div class="d-flex align-items-center">
|
||||||
{{ error }}
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
|
<div>{{ error }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (success) {
|
@if (success) {
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success">
|
||||||
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
<div class="d-flex align-items-center">
|
||||||
{{ success }}
|
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||||
|
<div>{{ success }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,12 +126,12 @@
|
|||||||
<!-- Carte profil -->
|
<!-- Carte profil -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header bg-light">
|
||||||
<h5 class="card-title mb-0">Profil Keycloak</h5>
|
<h5 class="card-title mb-0">Profil Utilisateur Hub</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<!-- Avatar -->
|
<!-- Avatar -->
|
||||||
<div class="avatar-lg mx-auto mb-3">
|
<div class="avatar-lg mx-auto mb-3">
|
||||||
<div class="avatar-title bg-primary rounded-circle text-white fs-24">
|
<div class="avatar-title bg-primary bg-opacity-10 rounded-circle text-primary fs-24">
|
||||||
{{ getUserInitials() }}
|
{{ getUserInitials() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -118,70 +149,115 @@
|
|||||||
<div class="d-flex align-items-center mb-2">
|
<div class="d-flex align-items-center mb-2">
|
||||||
<ng-icon name="lucideMail" class="me-2 text-muted"></ng-icon>
|
<ng-icon name="lucideMail" class="me-2 text-muted"></ng-icon>
|
||||||
<small>{{ user.email }}</small>
|
<small>{{ user.email }}</small>
|
||||||
|
@if (!user.emailVerified) {
|
||||||
|
<ng-icon name="lucideAlertTriangle" class="ms-1 text-warning" size="14" title="Email non vérifié"></ng-icon>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
<ng-icon name="lucideCalendar" class="me-2 text-muted"></ng-icon>
|
||||||
<small>Créé le {{ formatTimestamp(user.createdTimestamp) }}</small>
|
<small>Créé le {{ formatTimestamp(user.createdTimestamp) }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
@if (user.lastLogin) {
|
||||||
|
<div class="d-flex align-items-center mt-2">
|
||||||
|
<ng-icon name="lucideLogIn" class="me-2 text-muted"></ng-icon>
|
||||||
|
<small>Dernière connexion : {{ formatTimestamp(user.lastLogin) }}</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Carte rôles client -->
|
<!-- Carte rôle utilisateur -->
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title mb-0">Rôles Client</h5>
|
<h5 class="card-title mb-0">Rôle Utilisateur</h5>
|
||||||
@if (!isEditing) {
|
@if (canManageRoles && !isEditing) {
|
||||||
<button
|
<span class="badge bg-info">Modifiable</span>
|
||||||
class="btn btn-outline-primary btn-sm"
|
|
||||||
(click)="updateUserRoles()"
|
|
||||||
[disabled]="updatingRoles"
|
|
||||||
>
|
|
||||||
@if (updatingRoles) {
|
|
||||||
<div class="spinner-border spinner-border-sm me-1" role="status">
|
|
||||||
<span class="visually-hidden">Chargement...</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
Mettre à jour
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-2">
|
<!-- Rôle actuel -->
|
||||||
@for (role of availableRoles; track role) {
|
<div class="text-center mb-3">
|
||||||
<div class="col-6">
|
<span class="badge d-flex align-items-center justify-content-center" [ngClass]="getRoleBadgeClass(user.role)">
|
||||||
<div class="form-check">
|
<ng-icon [name]="getRoleIcon(user.role)" class="me-2"></ng-icon>
|
||||||
<input
|
{{ getRoleLabel(user.role) }}
|
||||||
class="form-check-input"
|
</span>
|
||||||
type="checkbox"
|
<small class="text-muted d-block mt-1">
|
||||||
[id]="'role-' + role"
|
{{ getRoleDescription(user.role) }}
|
||||||
[checked]="isRoleSelected(role)"
|
</small>
|
||||||
(change)="toggleRole(role)"
|
|
||||||
[disabled]="isEditing"
|
|
||||||
>
|
|
||||||
<label class="form-check-label" [for]="'role-' + role">
|
|
||||||
<span class="badge" [ngClass]="getRoleBadgeClass(role)">
|
|
||||||
{{ role }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rôles actuels -->
|
<!-- Changement de rôle -->
|
||||||
@if (user.clientRoles.length > 0) {
|
@if (canManageRoles && !isEditing) {
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<h6>Rôles assignés :</h6>
|
<label class="form-label fw-semibold">Changer le rôle</label>
|
||||||
@for (role of user.clientRoles; track role) {
|
<select
|
||||||
<span class="badge me-1 mb-1" [ngClass]="getRoleBadgeClass(role)">
|
class="form-select"
|
||||||
{{ role }}
|
[value]="user.role"
|
||||||
</span>
|
(change)="updateUserRole($any($event.target).value)"
|
||||||
}
|
[disabled]="updatingRoles"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Sélectionnez un nouveau rôle</option>
|
||||||
|
@for (role of availableRoles; track role.value) {
|
||||||
|
<option
|
||||||
|
[value]="role.value"
|
||||||
|
[disabled]="!canAssignRole(role.value) || role.value === user.role"
|
||||||
|
>
|
||||||
|
{{ role.label }}
|
||||||
|
@if (!canAssignRole(role.value)) {
|
||||||
|
(Non autorisé)
|
||||||
|
} @else if (role.value === user.role) {
|
||||||
|
(Actuel)
|
||||||
|
}
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<div class="form-text">
|
||||||
|
@if (updatingRoles) {
|
||||||
|
<div class="spinner-border spinner-border-sm me-1" role="status">
|
||||||
|
<span class="visually-hidden">Mise à jour...</span>
|
||||||
|
</div>
|
||||||
|
Mise à jour en cours...
|
||||||
|
} @else {
|
||||||
|
Sélectionnez un nouveau rôle pour cet utilisateur
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else if (!canManageRoles) {
|
||||||
|
<div class="alert alert-info mt-3">
|
||||||
|
<small>
|
||||||
|
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||||
|
Vous n'avez pas la permission de modifier les rôles
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Informations de création -->
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="card-title mb-0">Informations de Création</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-2 small">
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Créé par :</strong>
|
||||||
|
<div class="text-muted">{{ user.createdByUsername || 'Système' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Date de création :</strong>
|
||||||
|
<div class="text-muted">{{ formatTimestamp(user.createdTimestamp) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Type d'utilisateur :</strong>
|
||||||
|
<div class="text-muted">
|
||||||
|
<span class="badge bg-secondary">{{ user.userType }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Colonne de droite - Détails et édition -->
|
<!-- Colonne de droite - Détails et édition -->
|
||||||
@ -190,8 +266,10 @@
|
|||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title mb-0">
|
<h5 class="card-title mb-0">
|
||||||
@if (isEditing) {
|
@if (isEditing) {
|
||||||
|
<ng-icon name="lucideEdit" class="me-2"></ng-icon>
|
||||||
Modification du Profil
|
Modification du Profil
|
||||||
} @else {
|
} @else {
|
||||||
|
<ng-icon name="lucideUser" class="me-2"></ng-icon>
|
||||||
Détails du Compte
|
Détails du Compte
|
||||||
}
|
}
|
||||||
</h5>
|
</h5>
|
||||||
@ -200,10 +278,11 @@
|
|||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-secondary btn-sm"
|
class="btn btn-outline-secondary btn-sm"
|
||||||
(click)="cancelEditing()"
|
(click)="cancelEditing()"
|
||||||
[disabled]="saving"
|
[disabled]="saving"
|
||||||
>
|
>
|
||||||
|
<ng-icon name="lucideX" class="me-1"></ng-icon>
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@ -217,6 +296,7 @@
|
|||||||
<span class="visually-hidden">Chargement...</span>
|
<span class="visually-hidden">Chargement...</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<ng-icon name="lucideCheck" class="me-1"></ng-icon>
|
||||||
Enregistrer
|
Enregistrer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -234,10 +314,11 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
[(ngModel)]="editedUser.firstName"
|
[(ngModel)]="editedUser.firstName"
|
||||||
placeholder="Entrez le prénom"
|
placeholder="Entrez le prénom"
|
||||||
|
[disabled]="saving"
|
||||||
>
|
>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
{{ user.firstName || 'Non défini' }}
|
{{ user.firstName || 'Non renseigné' }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -251,10 +332,11 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
[(ngModel)]="editedUser.lastName"
|
[(ngModel)]="editedUser.lastName"
|
||||||
placeholder="Entrez le nom"
|
placeholder="Entrez le nom"
|
||||||
|
[disabled]="saving"
|
||||||
>
|
>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
{{ user.lastName || 'Non défini' }}
|
{{ user.lastName || 'Non renseigné' }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -262,18 +344,12 @@
|
|||||||
<!-- Nom d'utilisateur -->
|
<!-- Nom d'utilisateur -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Nom d'utilisateur</label>
|
<label class="form-label">Nom d'utilisateur</label>
|
||||||
@if (isEditing) {
|
<div class="form-control-plaintext font-monospace">
|
||||||
<input
|
{{ user.username }}
|
||||||
type="text"
|
</div>
|
||||||
class="form-control"
|
<div class="form-text">
|
||||||
[(ngModel)]="editedUser.username"
|
Le nom d'utilisateur ne peut pas être modifié
|
||||||
placeholder="Nom d'utilisateur"
|
</div>
|
||||||
>
|
|
||||||
} @else {
|
|
||||||
<div class="form-control-plaintext">
|
|
||||||
{{ user.username }}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Email -->
|
<!-- Email -->
|
||||||
@ -285,10 +361,14 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
[(ngModel)]="editedUser.email"
|
[(ngModel)]="editedUser.email"
|
||||||
placeholder="email@exemple.com"
|
placeholder="email@exemple.com"
|
||||||
|
[disabled]="saving"
|
||||||
>
|
>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="form-control-plaintext">
|
<div class="form-control-plaintext">
|
||||||
{{ user.email }}
|
{{ user.email }}
|
||||||
|
@if (!user.emailVerified) {
|
||||||
|
<span class="badge bg-warning ms-2">Non vérifié</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -302,25 +382,23 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="enabledSwitch"
|
id="enabledSwitch"
|
||||||
[(ngModel)]="editedUser.enabled"
|
[(ngModel)]="editedUser.enabled"
|
||||||
|
[disabled]="saving"
|
||||||
>
|
>
|
||||||
<label class="form-check-label" for="enabledSwitch">
|
<label class="form-check-label" for="enabledSwitch">
|
||||||
Compte activé
|
Compte activé
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
L'utilisateur peut se connecter si activé
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
} @else {
|
||||||
<!-- Email vérifié -->
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-check form-switch">
|
<label class="form-label">Statut du compte</label>
|
||||||
<input
|
<div class="form-control-plaintext">
|
||||||
class="form-check-input"
|
<span [class]="getStatusBadgeClass()">
|
||||||
type="checkbox"
|
{{ getStatusText() }}
|
||||||
id="emailVerifiedSwitch"
|
</span>
|
||||||
[(ngModel)]="editedUser.emailVerified"
|
|
||||||
>
|
|
||||||
<label class="form-check-label" for="emailVerifiedSwitch">
|
|
||||||
Email vérifié
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -329,11 +407,14 @@
|
|||||||
@if (!isEditing) {
|
@if (!isEditing) {
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<hr>
|
<hr>
|
||||||
<h6>Informations Système</h6>
|
<h6 class="mb-3">
|
||||||
|
<ng-icon name="lucideSettings" class="me-2"></ng-icon>
|
||||||
|
Informations Système
|
||||||
|
</h6>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">ID Utilisateur</label>
|
<label class="form-label">ID Utilisateur</label>
|
||||||
<div class="form-control-plaintext font-monospace small">
|
<div class="form-control-plaintext font-monospace small text-truncate">
|
||||||
{{ user.id }}
|
{{ user.id }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -343,12 +424,74 @@
|
|||||||
{{ formatTimestamp(user.createdTimestamp) }}
|
{{ formatTimestamp(user.createdTimestamp) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Créé par</label>
|
||||||
|
<div class="form-control-plaintext">
|
||||||
|
{{ user.createdByUsername || 'Système' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Type d'utilisateur</label>
|
||||||
|
<div class="form-control-plaintext">
|
||||||
|
<span class="badge bg-secondary">{{ user.userType }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions supplémentaires -->
|
||||||
|
@if (!isEditing && canEditUsers) {
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="card-title mb-0">Actions de Gestion</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-warning w-100"
|
||||||
|
(click)="resetPassword()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideKey" class="me-1"></ng-icon>
|
||||||
|
Réinitialiser MDP
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
@if (user.enabled) {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary w-100"
|
||||||
|
(click)="disableUser()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideUserX" class="me-1"></ng-icon>
|
||||||
|
Désactiver
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-success w-100"
|
||||||
|
(click)="enableUser()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideUserCheck" class="me-1"></ng-icon>
|
||||||
|
Activer
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-primary w-100"
|
||||||
|
(click)="startEditing()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
import { UsersProfile } from './profile';
|
import { UserProfile } from './profile';
|
||||||
describe('UsersProfile', () => {});
|
describe('UserProfile', () => {});
|
||||||
@ -1,10 +1,13 @@
|
|||||||
import { Component, inject, OnInit, Input, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
|
// src/app/modules/users/profile/profile.ts
|
||||||
|
import { Component, inject, OnInit, Input, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { UsersService } from '../services/users.service';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
import { UserResponse, UpdateUserDto, ClientRole } from '../models/user';
|
import { HubUsersService, HubUserResponse, UserRole, UpdateHubUserDto } from '../services/users.service';
|
||||||
|
import { RoleManagementService } from '@core/services/role-management.service';
|
||||||
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-profile',
|
selector: 'app-user-profile',
|
||||||
@ -21,66 +24,126 @@ import { UserResponse, UpdateUserDto, ClientRole } from '../models/user';
|
|||||||
}
|
}
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class UserProfile implements OnInit {
|
export class UserProfile implements OnInit, OnDestroy {
|
||||||
private usersService = inject(UsersService);
|
private usersService = inject(HubUsersService);
|
||||||
|
private roleService = inject(RoleManagementService);
|
||||||
|
private authService = inject(AuthService);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
@Input() userId!: string;
|
@Input() userId!: string;
|
||||||
@Output() openResetPasswordModal = new EventEmitter<void>();
|
@Output() back = new EventEmitter<void>();
|
||||||
|
@Output() openResetPasswordModal = new EventEmitter<string>();
|
||||||
|
|
||||||
user: UserResponse | null = null;
|
user: HubUserResponse | null = null;
|
||||||
loading = false;
|
loading = false;
|
||||||
saving = false;
|
saving = false;
|
||||||
error = '';
|
error = '';
|
||||||
success = '';
|
success = '';
|
||||||
|
|
||||||
|
// Gestion des permissions
|
||||||
|
currentUserRole: UserRole | null = null;
|
||||||
|
canEditUsers = false;
|
||||||
|
canManageRoles = false;
|
||||||
|
canDeleteUsers = false;
|
||||||
|
|
||||||
// Édition
|
// Édition
|
||||||
isEditing = false;
|
isEditing = false;
|
||||||
editedUser: UpdateUserDto = {};
|
editedUser: UpdateHubUserDto = {};
|
||||||
|
|
||||||
// Gestion des rôles
|
// Gestion des rôles
|
||||||
availableRoles: ClientRole[] = ['admin', 'merchant', 'support', 'user'];
|
availableRoles: { value: UserRole; label: string; description: string }[] = [];
|
||||||
selectedRoles: ClientRole[] = [];
|
|
||||||
updatingRoles = false;
|
updatingRoles = false;
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.userId) {
|
if (this.userId) {
|
||||||
|
this.initializeUserPermissions();
|
||||||
|
this.loadAvailableRoles();
|
||||||
this.loadUserProfile();
|
this.loadUserProfile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les permissions de l'utilisateur courant
|
||||||
|
*/
|
||||||
|
private initializeUserPermissions(): void {
|
||||||
|
this.authService.loadUserProfile()
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (profile) => {
|
||||||
|
this.currentUserRole = profile?.roles?.[0] as UserRole || null;
|
||||||
|
if (this.currentUserRole) {
|
||||||
|
this.canEditUsers = this.roleService.canEditUsers(this.currentUserRole);
|
||||||
|
this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole);
|
||||||
|
this.canDeleteUsers = this.roleService.canDeleteUsers(this.currentUserRole);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading user permissions:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge les rôles disponibles
|
||||||
|
*/
|
||||||
|
private loadAvailableRoles(): void {
|
||||||
|
this.roleService.getAvailableRolesSimple()
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (roles) => {
|
||||||
|
this.availableRoles = roles;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading available roles:', error);
|
||||||
|
// Fallback
|
||||||
|
this.availableRoles = [
|
||||||
|
{ value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' },
|
||||||
|
{ value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' },
|
||||||
|
{ value: UserRole.DCB_PARTNER, label: 'DCB Partner', description: 'Partenaire commercial' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
loadUserProfile() {
|
loadUserProfile() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
|
|
||||||
this.usersService.getUserById(this.userId).subscribe({
|
this.usersService.getUserById(this.userId)
|
||||||
next: (user) => {
|
.pipe(takeUntil(this.destroy$))
|
||||||
this.user = user;
|
.subscribe({
|
||||||
this.selectedRoles = user.clientRoles
|
next: (user) => {
|
||||||
.filter((role): role is ClientRole =>
|
this.user = user;
|
||||||
this.availableRoles.includes(role as ClientRole)
|
this.loading = false;
|
||||||
);
|
this.cdRef.detectChanges();
|
||||||
this.loading = false;
|
},
|
||||||
this.cdRef.detectChanges();
|
error: (error) => {
|
||||||
},
|
this.error = 'Erreur lors du chargement du profil utilisateur';
|
||||||
error: (error) => {
|
this.loading = false;
|
||||||
this.error = 'Erreur lors du chargement du profil utilisateur';
|
this.cdRef.detectChanges();
|
||||||
this.loading = false;
|
console.error('Error loading user profile:', error);
|
||||||
this.cdRef.detectChanges();
|
}
|
||||||
console.error('Error loading user profile:', error);
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startEditing() {
|
startEditing() {
|
||||||
|
if (!this.canEditUsers) {
|
||||||
|
this.error = 'Vous n\'avez pas la permission de modifier les utilisateurs';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.isEditing = true;
|
this.isEditing = true;
|
||||||
this.editedUser = {
|
this.editedUser = {
|
||||||
firstName: this.user?.firstName,
|
firstName: this.user?.firstName,
|
||||||
lastName: this.user?.lastName,
|
lastName: this.user?.lastName,
|
||||||
email: this.user?.email,
|
email: this.user?.email,
|
||||||
username: this.user?.username,
|
enabled: this.user?.enabled
|
||||||
enabled: this.user?.enabled,
|
|
||||||
emailVerified: this.user?.emailVerified
|
|
||||||
};
|
};
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
@ -94,100 +157,121 @@ export class UserProfile implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveProfile() {
|
saveProfile() {
|
||||||
if (!this.user) return;
|
if (!this.user || !this.canEditUsers) return;
|
||||||
|
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
this.success = '';
|
this.success = '';
|
||||||
|
|
||||||
this.usersService.updateUser(this.user.id, this.editedUser).subscribe({
|
this.usersService.updateUser(this.user.id, this.editedUser)
|
||||||
next: (updatedUser) => {
|
.pipe(takeUntil(this.destroy$))
|
||||||
this.user = updatedUser;
|
.subscribe({
|
||||||
this.isEditing = false;
|
next: (updatedUser) => {
|
||||||
this.saving = false;
|
this.user = updatedUser;
|
||||||
this.success = 'Profil mis à jour avec succès';
|
this.isEditing = false;
|
||||||
this.editedUser = {};
|
this.saving = false;
|
||||||
this.cdRef.detectChanges();
|
this.success = 'Profil mis à jour avec succès';
|
||||||
},
|
this.editedUser = {};
|
||||||
error: (error) => {
|
this.cdRef.detectChanges();
|
||||||
this.error = 'Erreur lors de la mise à jour du profil';
|
},
|
||||||
this.saving = false;
|
error: (error) => {
|
||||||
this.cdRef.detectChanges();
|
this.error = this.getErrorMessage(error);
|
||||||
console.error('Error updating user:', error);
|
this.saving = false;
|
||||||
}
|
this.cdRef.detectChanges();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion des rôles
|
// Gestion des rôles
|
||||||
toggleRole(role: ClientRole) {
|
updateUserRole(newRole: UserRole) {
|
||||||
const index = this.selectedRoles.indexOf(role);
|
if (!this.user || !this.canManageRoles) return;
|
||||||
if (index > -1) {
|
|
||||||
this.selectedRoles.splice(index, 1);
|
// Vérifier que l'utilisateur peut attribuer ce rôle
|
||||||
} else {
|
if (!this.roleService.canAssignRole(this.currentUserRole, newRole)) {
|
||||||
this.selectedRoles.push(role);
|
this.error = 'Vous n\'avez pas la permission d\'attribuer ce rôle';
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
this.cdRef.detectChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
isRoleSelected(role: ClientRole): boolean {
|
|
||||||
return this.selectedRoles.includes(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUserRoles() {
|
|
||||||
if (!this.user) return;
|
|
||||||
|
|
||||||
this.updatingRoles = true;
|
this.updatingRoles = true;
|
||||||
this.usersService.assignClientRoles(this.user.id, this.selectedRoles).subscribe({
|
this.error = '';
|
||||||
next: () => {
|
this.success = '';
|
||||||
this.updatingRoles = false;
|
|
||||||
this.success = 'Rôles mis à jour avec succès';
|
this.usersService.updateUserRole(this.user.id, newRole)
|
||||||
if (this.user) {
|
.pipe(takeUntil(this.destroy$))
|
||||||
this.user.clientRoles = [...this.selectedRoles];
|
.subscribe({
|
||||||
|
next: (updatedUser) => {
|
||||||
|
this.user = updatedUser;
|
||||||
|
this.updatingRoles = false;
|
||||||
|
this.success = 'Rôle mis à jour avec succès';
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.updatingRoles = false;
|
||||||
|
this.error = this.getErrorMessage(error);
|
||||||
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
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
|
// Gestion du statut
|
||||||
enableUser() {
|
enableUser() {
|
||||||
if (!this.user) return;
|
if (!this.user || !this.canEditUsers) return;
|
||||||
|
|
||||||
this.usersService.enableUser(this.user.id).subscribe({
|
this.usersService.enableUser(this.user.id)
|
||||||
next: () => {
|
.pipe(takeUntil(this.destroy$))
|
||||||
this.user!.enabled = true;
|
.subscribe({
|
||||||
this.success = 'Utilisateur activé avec succès';
|
next: (updatedUser) => {
|
||||||
this.cdRef.detectChanges();
|
this.user = updatedUser;
|
||||||
},
|
this.success = 'Utilisateur activé avec succès';
|
||||||
error: (error) => {
|
this.cdRef.detectChanges();
|
||||||
this.error = 'Erreur lors de l\'activation de l\'utilisateur';
|
},
|
||||||
this.cdRef.detectChanges();
|
error: (error) => {
|
||||||
console.error('Error enabling user:', error);
|
this.error = this.getErrorMessage(error);
|
||||||
}
|
this.cdRef.detectChanges();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
disableUser() {
|
disableUser() {
|
||||||
if (!this.user) return;
|
if (!this.user || !this.canEditUsers) return;
|
||||||
|
|
||||||
this.usersService.disableUser(this.user.id).subscribe({
|
this.usersService.disableUser(this.user.id)
|
||||||
next: () => {
|
.pipe(takeUntil(this.destroy$))
|
||||||
this.user!.enabled = false;
|
.subscribe({
|
||||||
this.success = 'Utilisateur désactivé avec succès';
|
next: (updatedUser) => {
|
||||||
this.cdRef.detectChanges();
|
this.user = updatedUser;
|
||||||
},
|
this.success = 'Utilisateur désactivé avec succès';
|
||||||
error: (error) => {
|
this.cdRef.detectChanges();
|
||||||
this.error = 'Erreur lors de la désactivation de l\'utilisateur';
|
},
|
||||||
this.cdRef.detectChanges();
|
error: (error) => {
|
||||||
console.error('Error disabling user:', error);
|
this.error = this.getErrorMessage(error);
|
||||||
}
|
this.cdRef.detectChanges();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Réinitialisation du mot de passe
|
||||||
|
resetPassword() {
|
||||||
|
if (this.user) {
|
||||||
|
this.openResetPasswordModal.emit(this.user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion des erreurs
|
||||||
|
private getErrorMessage(error: any): string {
|
||||||
|
if (error.error?.message) {
|
||||||
|
return error.error.message;
|
||||||
|
}
|
||||||
|
if (error.status === 403) {
|
||||||
|
return 'Vous n\'avez pas les permissions nécessaires pour cette action';
|
||||||
|
}
|
||||||
|
if (error.status === 404) {
|
||||||
|
return 'Utilisateur non trouvé';
|
||||||
|
}
|
||||||
|
if (error.status === 400) {
|
||||||
|
return 'Données invalides';
|
||||||
|
}
|
||||||
|
return 'Une erreur est survenue. Veuillez réessayer.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utilitaires d'affichage
|
// Utilitaires d'affichage
|
||||||
@ -206,6 +290,7 @@ export class UserProfile implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatTimestamp(timestamp: number): string {
|
formatTimestamp(timestamp: number): string {
|
||||||
|
if (!timestamp) return 'Non disponible';
|
||||||
return new Date(timestamp).toLocaleDateString('fr-FR', {
|
return new Date(timestamp).toLocaleDateString('fr-FR', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
@ -228,13 +313,31 @@ export class UserProfile implements OnInit {
|
|||||||
return this.user.username;
|
return this.user.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleBadgeClass(role: string): string {
|
getRoleBadgeClass(role: UserRole): string {
|
||||||
switch (role) {
|
return this.roleService.getRoleBadgeClass(role);
|
||||||
case 'admin': return 'bg-danger';
|
}
|
||||||
case 'merchant': return 'bg-success';
|
|
||||||
case 'support': return 'bg-info';
|
getRoleLabel(role: UserRole): string {
|
||||||
case 'user': return 'bg-secondary';
|
return this.roleService.getRoleLabel(role);
|
||||||
default: return 'bg-secondary';
|
}
|
||||||
}
|
|
||||||
|
getRoleIcon(role: UserRole): string {
|
||||||
|
return this.roleService.getRoleIcon(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleDescription(role: UserRole): string {
|
||||||
|
const roleInfo = this.availableRoles.find(r => r.value === role);
|
||||||
|
return roleInfo?.description || 'Description non disponible';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification des permissions pour les actions
|
||||||
|
canAssignRole(targetRole: UserRole): boolean {
|
||||||
|
return this.roleService.canAssignRole(this.currentUserRole, targetRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifie si c'est le profil de l'utilisateur courant
|
||||||
|
isCurrentUserProfile(): boolean {
|
||||||
|
// Implémentez cette logique selon votre système d'authentification
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
78
src/app/modules/users/services/api.service.ts
Normal file
78
src/app/modules/users/services/api.service.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||||
|
import { Observable, throwError } from 'rxjs';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
|
import { environment } from '@environments/environment';
|
||||||
|
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
data?: T;
|
||||||
|
message?: string;
|
||||||
|
success: boolean;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ApiService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private baseUrl = environment.iamApiUrl;
|
||||||
|
|
||||||
|
private getHeaders(): HttpHeaders {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
return new HttpHeaders({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T>(endpoint: string, params?: any): Observable<T> {
|
||||||
|
return this.http.get<T>(`${this.baseUrl}/${endpoint}`, {
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
params: this.createParams(params)
|
||||||
|
}).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
post<T>(endpoint: string, data: any): Observable<T> {
|
||||||
|
return this.http.post<T>(`${this.baseUrl}/${endpoint}`, data, {
|
||||||
|
headers: this.getHeaders()
|
||||||
|
}).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
put<T>(endpoint: string, data: any): Observable<T> {
|
||||||
|
return this.http.put<T>(`${this.baseUrl}/${endpoint}`, data, {
|
||||||
|
headers: this.getHeaders()
|
||||||
|
}).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete<T>(endpoint: string): Observable<T> {
|
||||||
|
return this.http.delete<T>(`${this.baseUrl}/${endpoint}`, {
|
||||||
|
headers: this.getHeaders()
|
||||||
|
}).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createParams(params: any): HttpParams {
|
||||||
|
let httpParams = new HttpParams();
|
||||||
|
if (params) {
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
if (params[key] !== null && params[key] !== undefined) {
|
||||||
|
httpParams = httpParams.set(key, params[key].toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return httpParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: any) {
|
||||||
|
console.error('API Error:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,31 +1,80 @@
|
|||||||
|
// src/app/modules/users/services/users.service.ts
|
||||||
import { Injectable, inject } from '@angular/core';
|
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 { environment } from '@environments/environment';
|
||||||
import { Observable, map, catchError, throwError } from 'rxjs';
|
import { Observable, map, catchError, throwError, of } from 'rxjs';
|
||||||
import { of } from 'rxjs';
|
import { AvailableRolesResponse } from '@core/services/role-management.service';
|
||||||
|
// Interfaces alignées avec le contrôleur NestJS
|
||||||
|
export interface HubUserResponse {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
role: UserRole;
|
||||||
|
enabled: boolean;
|
||||||
|
emailVerified: boolean;
|
||||||
|
createdBy: string;
|
||||||
|
createdByUsername: string;
|
||||||
|
createdTimestamp: number;
|
||||||
|
lastLogin?: number;
|
||||||
|
userType: 'HUB';
|
||||||
|
}
|
||||||
|
|
||||||
import {
|
export interface CreateHubUserDto {
|
||||||
UserResponse,
|
username: string;
|
||||||
CreateUserDto,
|
email: string;
|
||||||
UpdateUserDto,
|
firstName: string;
|
||||||
ResetPasswordDto,
|
lastName: string;
|
||||||
PaginatedUserResponse,
|
password: string;
|
||||||
ClientRole
|
role: UserRole;
|
||||||
} from '../models/user';
|
enabled?: boolean;
|
||||||
|
emailVerified?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateHubUserDto {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
email?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResetPasswordDto {
|
||||||
|
newPassword: string;
|
||||||
|
temporary?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedUserResponse {
|
||||||
|
users: HubUserResponse[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserRole {
|
||||||
|
DCB_ADMIN = 'dcb-admin',
|
||||||
|
DCB_SUPPORT = 'dcb-support',
|
||||||
|
DCB_PARTNER = 'dcb-partner'
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class UsersService {
|
export class HubUsersService {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private apiUrl = `${environment.iamApiUrl}/users`;
|
private apiUrl = `${environment.iamApiUrl}/hub-users`;
|
||||||
|
|
||||||
// === CRUD COMPLET ===
|
// === CRUD COMPLET ===
|
||||||
createUser(createUserDto: CreateUserDto): Observable<UserResponse> {
|
|
||||||
|
/**
|
||||||
|
* Crée un nouvel utilisateur Hub
|
||||||
|
*/
|
||||||
|
createUser(createUserDto: CreateHubUserDto): Observable<HubUserResponse> {
|
||||||
// Validation
|
// Validation
|
||||||
if (!createUserDto.username || createUserDto.username.trim() === '') {
|
if (!createUserDto.username?.trim()) {
|
||||||
return throwError(() => 'Username is required and cannot be empty');
|
return throwError(() => 'Username is required and cannot be empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!createUserDto.email || createUserDto.email.trim() === '') {
|
if (!createUserDto.email?.trim()) {
|
||||||
return throwError(() => 'Email is required and cannot be empty');
|
return throwError(() => 'Email is required and cannot be empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,6 +82,10 @@ export class UsersService {
|
|||||||
return throwError(() => 'Password must be at least 8 characters');
|
return throwError(() => 'Password must be at least 8 characters');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!createUserDto.role) {
|
||||||
|
return throwError(() => 'Role is required');
|
||||||
|
}
|
||||||
|
|
||||||
// Nettoyage des données
|
// Nettoyage des données
|
||||||
const payload = {
|
const payload = {
|
||||||
username: createUserDto.username.trim(),
|
username: createUserDto.username.trim(),
|
||||||
@ -40,129 +93,230 @@ export class UsersService {
|
|||||||
firstName: (createUserDto.firstName || '').trim(),
|
firstName: (createUserDto.firstName || '').trim(),
|
||||||
lastName: (createUserDto.lastName || '').trim(),
|
lastName: (createUserDto.lastName || '').trim(),
|
||||||
password: createUserDto.password,
|
password: createUserDto.password,
|
||||||
|
role: createUserDto.role,
|
||||||
enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true,
|
enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true,
|
||||||
emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : false,
|
emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : false,
|
||||||
clientRoles: createUserDto.clientRoles || []
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.http.post<UserResponse>(`${this.apiUrl}`, payload).pipe(
|
return this.http.post<HubUserResponse>(this.apiUrl, payload).pipe(
|
||||||
catchError(error => throwError(() => error))
|
catchError(error => throwError(() => error))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// READ - Obtenir tous les utilisateurs
|
/**
|
||||||
findAllUsers(): Observable<PaginatedUserResponse> {
|
* Récupère tous les utilisateurs Hub avec pagination
|
||||||
return this.http.get<{
|
*/
|
||||||
users: any[];
|
findAllUsers(page: number = 1, limit: number = 10, filters?: any): Observable<PaginatedUserResponse> {
|
||||||
total: number;
|
let params = new HttpParams()
|
||||||
page: number;
|
.set('page', page.toString())
|
||||||
limit: number;
|
.set('limit', limit.toString());
|
||||||
totalPages: number;
|
|
||||||
}>(`${this.apiUrl}`).pipe(
|
if (filters) {
|
||||||
|
Object.keys(filters).forEach(key => {
|
||||||
|
if (filters[key] !== undefined && filters[key] !== null) {
|
||||||
|
params = params.set(key, filters[key].toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<HubUserResponse[]>(this.apiUrl, { params, observe: 'response' }).pipe(
|
||||||
map(response => {
|
map(response => {
|
||||||
const users = response.users.map(user => new UserResponse(user));
|
const users = response.body || [];
|
||||||
return new PaginatedUserResponse(users, response.total, response.page, response.limit);
|
const total = parseInt(response.headers.get('X-Total-Count') || '0');
|
||||||
|
|
||||||
|
return {
|
||||||
|
users,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit)
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error('Error loading users:', error);
|
console.error('Error loading users:', error);
|
||||||
return of(new PaginatedUserResponse([], 0, 1, 10));
|
return throwError(() => error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// READ - Obtenir un utilisateur par ID
|
/**
|
||||||
getUserById(id: string): Observable<UserResponse> {
|
* Récupère tous les utilisateurs Hub avec pagination
|
||||||
return this.http.get<any>(`${this.apiUrl}/${id}`).pipe(
|
*/
|
||||||
map(response => new UserResponse(response))
|
findAllMerchantUsers(page: number = 1, limit: number = 10, filters?: any): Observable<PaginatedUserResponse> {
|
||||||
|
let params = new HttpParams()
|
||||||
|
.set('page', page.toString())
|
||||||
|
.set('limit', limit.toString());
|
||||||
|
|
||||||
|
if (filters) {
|
||||||
|
Object.keys(filters).forEach(key => {
|
||||||
|
if (filters[key] !== undefined && filters[key] !== null) {
|
||||||
|
params = params.set(key, filters[key].toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<HubUserResponse[]>(`${this.apiUrl}//merchants/all`, { params, observe: 'response' }).pipe(
|
||||||
|
map(response => {
|
||||||
|
const users = response.body || [];
|
||||||
|
const total = parseInt(response.headers.get('X-Total-Count') || '0');
|
||||||
|
|
||||||
|
return {
|
||||||
|
users,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit)
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error loading users:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// READ - Obtenir le profil de l'utilisateur connecté
|
/**
|
||||||
getCurrentUserProfile(): Observable<UserResponse> {
|
* Récupère un utilisateur Hub par ID
|
||||||
return this.http.get<any>(`${this.apiUrl}/profile/me`).pipe(
|
*/
|
||||||
map(response => new UserResponse(response))
|
getUserById(id: string): Observable<HubUserResponse> {
|
||||||
);
|
return this.http.get<HubUserResponse>(`${this.apiUrl}/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UPDATE - Mettre à jour un utilisateur
|
/**
|
||||||
updateUser(id: string, updateUserDto: UpdateUserDto): Observable<UserResponse> {
|
* Met à jour un utilisateur Hub
|
||||||
return this.http.put<any>(`${this.apiUrl}/${id}`, updateUserDto).pipe(
|
*/
|
||||||
map(response => new UserResponse(response))
|
updateUser(id: string, updateUserDto: UpdateHubUserDto): Observable<HubUserResponse> {
|
||||||
);
|
return this.http.put<HubUserResponse>(`${this.apiUrl}/${id}`, updateUserDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UPDATE - Mettre à jour le profil de l'utilisateur connecté
|
/**
|
||||||
updateCurrentUserProfile(updateUserDto: UpdateUserDto): Observable<UserResponse> {
|
* Supprime un utilisateur Hub
|
||||||
return this.http.put<any>(`${this.apiUrl}/profile/me`, updateUserDto).pipe(
|
*/
|
||||||
map(response => new UserResponse(response))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE - Supprimer un utilisateur
|
|
||||||
deleteUser(id: string): Observable<{ message: string }> {
|
deleteUser(id: string): Observable<{ message: string }> {
|
||||||
return this.http.delete<{ message: string }>(`${this.apiUrl}/${id}`);
|
return this.http.delete<{ message: string }>(`${this.apiUrl}/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === GESTION DES MOTS DE PASSE ===
|
// === GESTION DES MOTS DE PASSE ===
|
||||||
resetPassword(resetPasswordDto: ResetPasswordDto): Observable<{ message: string }> {
|
|
||||||
return this.http.put<{ message: string }>(
|
/**
|
||||||
`${this.apiUrl}/${resetPasswordDto.userId}/password`,
|
* Réinitialise le mot de passe d'un utilisateur
|
||||||
resetPasswordDto
|
*/
|
||||||
|
resetPassword(userId: string, newPassword: string, temporary: boolean = true): Observable<{ message: string }> {
|
||||||
|
return this.http.post<{ message: string }>(
|
||||||
|
`${this.apiUrl}/${userId}/reset-password`,
|
||||||
|
{ newPassword, temporary }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un email de réinitialisation de mot de passe
|
||||||
|
*/
|
||||||
|
sendPasswordResetEmail(userId: string): Observable<{ message: string }> {
|
||||||
|
return this.http.post<{ message: string }>(
|
||||||
|
`${this.apiUrl}/${userId}/send-reset-email`,
|
||||||
|
{}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === GESTION DU STATUT ===
|
// === GESTION DU STATUT ===
|
||||||
enableUser(id: string): Observable<{ message: string }> {
|
|
||||||
return this.http.put<{ message: string }>(`${this.apiUrl}/${id}/enable`, {});
|
/**
|
||||||
|
* Active un utilisateur
|
||||||
|
*/
|
||||||
|
enableUser(id: string): Observable<HubUserResponse> {
|
||||||
|
return this.http.put<HubUserResponse>(`${this.apiUrl}/${id}`, { enabled: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
disableUser(id: string): Observable<{ message: string }> {
|
/**
|
||||||
return this.http.put<{ message: string }>(`${this.apiUrl}/${id}/disable`, {});
|
* Désactive un utilisateur
|
||||||
}
|
*/
|
||||||
|
disableUser(id: string): Observable<HubUserResponse> {
|
||||||
// === RECHERCHE ET VÉRIFICATION ===
|
return this.http.put<HubUserResponse>(`${this.apiUrl}/${id}`, { enabled: false });
|
||||||
userExists(username: string): Observable<{ exists: boolean }> {
|
|
||||||
return this.http.get<{ exists: boolean }>(`${this.apiUrl}/check/${username}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
findUserByUsername(username: string): Observable<UserResponse[]> {
|
|
||||||
return this.http.get<any[]>(`${this.apiUrl}/search/username/${username}`).pipe(
|
|
||||||
map(users => users.map(user => new UserResponse(user)))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
findUserByEmail(email: string): Observable<UserResponse[]> {
|
|
||||||
return this.http.get<any[]>(`${this.apiUrl}/search/email/${email}`).pipe(
|
|
||||||
map(users => users.map(user => new UserResponse(user)))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === GESTION DES RÔLES ===
|
// === GESTION DES RÔLES ===
|
||||||
getUserClientRoles(id: string): Observable<{ roles: string[] }> {
|
|
||||||
return this.http.get<{ roles: string[] }>(`${this.apiUrl}/${id}/roles`);
|
/**
|
||||||
|
* Met à jour le rôle d'un utilisateur
|
||||||
|
*/
|
||||||
|
updateUserRole(id: string, role: UserRole): Observable<HubUserResponse> {
|
||||||
|
return this.http.put<HubUserResponse>(`${this.apiUrl}/${id}/role`, { role });
|
||||||
}
|
}
|
||||||
|
|
||||||
assignClientRoles(userId: string, roles: ClientRole[]): Observable<{ message: string }> {
|
/**
|
||||||
return this.http.put<{ message: string }>(`${this.apiUrl}/${userId}/roles`, { roles });
|
* Récupère les rôles Hub disponibles
|
||||||
|
*/
|
||||||
|
getAvailableHubRoles(): Observable<AvailableRolesResponse> {
|
||||||
|
return this.http.get<AvailableRolesResponse>(
|
||||||
|
`${this.apiUrl}/roles/available`
|
||||||
|
).pipe(
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error loading available roles:', error);
|
||||||
|
// Fallback en cas d'erreur
|
||||||
|
return of({
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
value: UserRole.DCB_ADMIN,
|
||||||
|
label: 'DCB Admin',
|
||||||
|
description: 'Full administrative access to the entire system'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: UserRole.DCB_SUPPORT,
|
||||||
|
label: 'DCB Support',
|
||||||
|
description: 'Support access with limited administrative capabilities'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: UserRole.DCB_PARTNER,
|
||||||
|
label: 'DCB Partner',
|
||||||
|
description: 'Merchant partner with access to their own merchant ecosystem'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === SESSIONS ET TOKENS ===
|
/**
|
||||||
getUserSessions(userId: string): Observable<any[]> {
|
* Récupère les utilisateurs par rôle
|
||||||
return this.http.get<any[]>(`${this.apiUrl}/${userId}/sessions`);
|
*/
|
||||||
|
getUsersByRole(role: UserRole): Observable<HubUserResponse[]> {
|
||||||
|
return this.http.get<HubUserResponse[]>(`${this.apiUrl}/role/${role}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
logoutUser(userId: string): Observable<{ message: string }> {
|
|
||||||
return this.http.post<{ message: string }>(`${this.apiUrl}/${userId}/logout`, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// === STATISTIQUES ===
|
// === STATISTIQUES ===
|
||||||
getUserStats(): Observable<{
|
|
||||||
total: number;
|
/**
|
||||||
enabled: number;
|
* Récupère les statistiques des utilisateurs
|
||||||
disabled: number;
|
*/
|
||||||
emailVerified: number;
|
getUsersStats(): Observable<any> {
|
||||||
emailNotVerified: number;
|
return this.http.get<any>(`${this.apiUrl}/stats/overview`);
|
||||||
}> {
|
}
|
||||||
return this.http.get<any>(`${this.apiUrl}/stats`);
|
|
||||||
|
// === UTILITAIRES ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un nom d'utilisateur existe
|
||||||
|
*/
|
||||||
|
userExists(username: string): Observable<{ exists: boolean }> {
|
||||||
|
// Implémentation temporaire - à adapter selon votre API
|
||||||
|
return this.findAllUsers().pipe(
|
||||||
|
map(response => ({
|
||||||
|
exists: response.users.some(user => user.username === username)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche des utilisateurs
|
||||||
|
*/
|
||||||
|
searchUsers(query: string): Observable<HubUserResponse[]> {
|
||||||
|
return this.findAllUsers().pipe(
|
||||||
|
map(response => response.users.filter(user =>
|
||||||
|
user.username.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
user.email.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
user.firstName?.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
user.lastName?.toLowerCase().includes(query.toLowerCase())
|
||||||
|
))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,6 +5,42 @@
|
|||||||
[badge]="{icon:'lucideUsers', text:'Keycloak Users'}"
|
[badge]="{icon:'lucideUsers', text:'Keycloak Users'}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Indicateur de permissions -->
|
||||||
|
@if (currentUserRole) {
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info py-2">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideInfo" class="me-2"></ng-icon>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<small>
|
||||||
|
<strong>Rôle actuel :</strong>
|
||||||
|
<span class="badge" [ngClass]="getRoleBadgeClass(currentUserRole)">
|
||||||
|
{{ roleService.getRoleLabel(currentUserRole) }}
|
||||||
|
</span>
|
||||||
|
@if (!canCreateUsers) {
|
||||||
|
<span class="text-warning ms-2">
|
||||||
|
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||||
|
Permissions limitées
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
@if (canCreateUsers) {
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
(click)="openCreateUserModal()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
||||||
|
Nouvel Utilisateur
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Navigation par onglets avec style bordered -->
|
<!-- Navigation par onglets avec style bordered -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@ -22,6 +58,8 @@
|
|||||||
</a>
|
</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<app-users-list
|
<app-users-list
|
||||||
|
[canCreateUsers]="canCreateUsers"
|
||||||
|
[canDeleteUsers]="canDeleteUsers"
|
||||||
(userSelected)="showTab('profile', $event)"
|
(userSelected)="showTab('profile', $event)"
|
||||||
(openCreateModal)="openCreateUserModal()"
|
(openCreateModal)="openCreateUserModal()"
|
||||||
(openResetPasswordModal)="openResetPasswordModal($event)"
|
(openResetPasswordModal)="openResetPasswordModal($event)"
|
||||||
@ -75,6 +113,19 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Avertissement permissions -->
|
||||||
|
@if (!canManageRoles && assignableRoles.length === 1) {
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<small>
|
||||||
|
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||||
|
<strong>Permissions limitées :</strong> Vous ne pouvez créer que des utilisateurs avec le rôle
|
||||||
|
<span class="badge" [ngClass]="getRoleBadgeClass(assignableRoles[0])">
|
||||||
|
{{ roleService.getRoleLabel(assignableRoles[0]) }}
|
||||||
|
</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<form (ngSubmit)="createUser()" #userForm="ngForm">
|
<form (ngSubmit)="createUser()" #userForm="ngForm">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<!-- Informations de base -->
|
<!-- Informations de base -->
|
||||||
@ -158,6 +209,64 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Sélection du rôle -->
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">
|
||||||
|
Rôle <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
class="form-select"
|
||||||
|
[(ngModel)]="newUser.role"
|
||||||
|
name="role"
|
||||||
|
required
|
||||||
|
[disabled]="creatingUser || !canManageRoles"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Sélectionnez un rôle</option>
|
||||||
|
@for (role of availableRoles; track role.value) {
|
||||||
|
<option
|
||||||
|
[value]="role.value"
|
||||||
|
[disabled]="!canAssignRole(role.value)"
|
||||||
|
>
|
||||||
|
{{ role.label }} - {{ role.description }}
|
||||||
|
@if (!canAssignRole(role.value)) {
|
||||||
|
(Non autorisé)
|
||||||
|
}
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<div class="form-text">
|
||||||
|
@if (canManageRoles) {
|
||||||
|
Sélectionnez le rôle à assigner à cet utilisateur
|
||||||
|
} @else {
|
||||||
|
Vous ne pouvez pas modifier les rôles disponibles
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aperçu du rôle sélectionné -->
|
||||||
|
@if (newUser.role) {
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon
|
||||||
|
[name]="roleService.getRoleIcon(newUser.role)"
|
||||||
|
class="me-2"
|
||||||
|
></ng-icon>
|
||||||
|
<div>
|
||||||
|
<strong>Rôle sélectionné :</strong>
|
||||||
|
<span class="badge ms-2" [ngClass]="getRoleBadgeClass(newUser.role)">
|
||||||
|
{{ roleService.getRoleLabel(newUser.role) }}
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ getRoleDescription(newUser.role) }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Configuration du compte -->
|
<!-- Configuration du compte -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
@ -192,51 +301,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-text">L'utilisateur n'aura pas à vérifier son email</div>
|
<div class="form-text">L'utilisateur n'aura pas à vérifier son email</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rôles client -->
|
|
||||||
<div class="col-12">
|
|
||||||
<label class="form-label">Rôles Client</label>
|
|
||||||
<div class="row g-2">
|
|
||||||
@for (role of availableRoles; track role) {
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
[id]="'role-' + role"
|
|
||||||
[checked]="isRoleSelected(role)"
|
|
||||||
(change)="toggleRole(role)"
|
|
||||||
[disabled]="creatingUser"
|
|
||||||
>
|
|
||||||
<label class="form-check-label" [for]="'role-' + role">
|
|
||||||
<span class="badge"
|
|
||||||
[class]="{
|
|
||||||
'bg-primary': role === 'admin',
|
|
||||||
'bg-success': role === 'merchant',
|
|
||||||
'bg-info': role === 'support'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ role }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="form-text">Sélectionnez les rôles à assigner à cet utilisateur</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Aperçu des rôles sélectionnés -->
|
|
||||||
@if (selectedRoles.length > 0) {
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<strong>Rôles sélectionnés :</strong>
|
|
||||||
@for (role of selectedRoles; track role) {
|
|
||||||
<span class="badge bg-primary ms-1">{{ role }}</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer mt-4">
|
<div class="modal-footer mt-4">
|
||||||
@ -251,7 +315,7 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
[disabled]="!userForm.form.valid || creatingUser"
|
[disabled]="!userForm.form.valid || creatingUser || !canAssignRole(newUser.role)"
|
||||||
>
|
>
|
||||||
@if (creatingUser) {
|
@if (creatingUser) {
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status">
|
<div class="spinner-border spinner-border-sm me-2" role="status">
|
||||||
@ -302,11 +366,24 @@
|
|||||||
|
|
||||||
@if (!resetPasswordSuccess && selectedUserForReset) {
|
@if (!resetPasswordSuccess && selectedUserForReset) {
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<strong>Utilisateur :</strong> {{ selectedUserForReset.username }}
|
<div class="d-flex align-items-center">
|
||||||
@if (selectedUserForReset.firstName || selectedUserForReset.lastName) {
|
<ng-icon
|
||||||
<br>
|
[name]="roleService.getRoleIcon(selectedUserForReset.role)"
|
||||||
<strong>Nom :</strong> {{ selectedUserForReset.firstName }} {{ selectedUserForReset.lastName }}
|
class="me-2"
|
||||||
}
|
></ng-icon>
|
||||||
|
<div>
|
||||||
|
<strong>Utilisateur :</strong> {{ selectedUserForReset.username }}
|
||||||
|
@if (selectedUserForReset.firstName || selectedUserForReset.lastName) {
|
||||||
|
<br>
|
||||||
|
<strong>Nom :</strong> {{ selectedUserForReset.firstName }} {{ selectedUserForReset.lastName }}
|
||||||
|
}
|
||||||
|
<br>
|
||||||
|
<strong>Rôle :</strong>
|
||||||
|
<span class="badge ms-1" [ngClass]="getRoleBadgeClass(selectedUserForReset.role)">
|
||||||
|
{{ roleService.getRoleLabel(selectedUserForReset.role) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form (ngSubmit)="confirmResetPassword()" #resetForm="ngForm">
|
<form (ngSubmit)="confirmResetPassword()" #resetForm="ngForm">
|
||||||
@ -418,8 +495,8 @@
|
|||||||
|
|
||||||
@if (selectedUserForDelete) {
|
@if (selectedUserForDelete) {
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-start">
|
||||||
<ng-icon name="lucideAlertTriangle" class="me-2"></ng-icon>
|
<ng-icon name="lucideAlertTriangle" class="me-2 mt-1"></ng-icon>
|
||||||
<div>
|
<div>
|
||||||
<strong>Utilisateur :</strong> {{ selectedUserForDelete.username }}
|
<strong>Utilisateur :</strong> {{ selectedUserForDelete.username }}
|
||||||
@if (selectedUserForDelete.firstName || selectedUserForDelete.lastName) {
|
@if (selectedUserForDelete.firstName || selectedUserForDelete.lastName) {
|
||||||
@ -428,6 +505,11 @@
|
|||||||
}
|
}
|
||||||
<br>
|
<br>
|
||||||
<strong>Email :</strong> {{ selectedUserForDelete.email }}
|
<strong>Email :</strong> {{ selectedUserForDelete.email }}
|
||||||
|
<br>
|
||||||
|
<strong>Rôle :</strong>
|
||||||
|
<span class="badge ms-1" [ngClass]="getRoleBadgeClass(selectedUserForDelete.role)">
|
||||||
|
{{ roleService.getRoleLabel(selectedUserForDelete.role) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -456,7 +538,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
(click)="confirmDeleteUser()"
|
(click)="confirmDeleteUser()"
|
||||||
[disabled]="deletingUser"
|
[disabled]="deletingUser || !canDeleteUsers"
|
||||||
>
|
>
|
||||||
@if (deletingUser) {
|
@if (deletingUser) {
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status">
|
<div class="spinner-border spinner-border-sm me-2" role="status">
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
import { Component, inject, OnInit, TemplateRef, ViewChild, ChangeDetectorRef } from '@angular/core';
|
// src/app/modules/users/users.ts
|
||||||
|
import { Component, inject, OnInit, TemplateRef, ViewChild, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
import { UsersList } from './list/list';
|
import { UsersList } from './list/list';
|
||||||
import { UserProfile } from './profile/profile';
|
import { UserProfile } from './profile/profile';
|
||||||
import { UsersService } from './services/users.service';
|
import { HubUsersService, CreateHubUserDto, UserRole, HubUserResponse } from './services/users.service';
|
||||||
import { CreateUserDto, ClientRole, UserResponse } from './models/user';
|
import { RoleManagementService } from '@core/services/role-management.service';
|
||||||
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-users',
|
selector: 'app-users',
|
||||||
@ -24,54 +27,143 @@ import { CreateUserDto, ClientRole, UserResponse } from './models/user';
|
|||||||
],
|
],
|
||||||
templateUrl: './users.html',
|
templateUrl: './users.html',
|
||||||
})
|
})
|
||||||
export class Users implements OnInit {
|
export class Users implements OnInit, OnDestroy {
|
||||||
private modalService = inject(NgbModal);
|
private modalService = inject(NgbModal);
|
||||||
private usersService = inject(UsersService);
|
private usersService = inject(HubUsersService);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private authService = inject(AuthService);
|
||||||
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
// Rendre le service accessible au template via des méthodes proxy
|
||||||
|
protected roleService = inject(RoleManagementService);
|
||||||
|
|
||||||
activeTab: 'list' | 'profile' = 'list';
|
activeTab: 'list' | 'profile' = 'list';
|
||||||
selectedUserId: string | null = null;
|
selectedUserId: string | null = null;
|
||||||
|
|
||||||
|
// Gestion des permissions
|
||||||
|
currentUserRole: UserRole | null = null;
|
||||||
|
userPermissions: any = null;
|
||||||
|
canCreateUsers = false;
|
||||||
|
canDeleteUsers = false;
|
||||||
|
canManageRoles = false;
|
||||||
|
|
||||||
// Données pour la création d'utilisateur
|
// Données pour la création d'utilisateur
|
||||||
newUser: CreateUserDto = {
|
newUser: CreateHubUserDto = {
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
role: UserRole.DCB_SUPPORT,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
emailVerified: false,
|
emailVerified: false
|
||||||
clientRoles: ['user']
|
|
||||||
};
|
};
|
||||||
|
|
||||||
availableRoles: ClientRole[] = ['admin', 'merchant', 'support', 'user'];
|
availableRoles: { value: UserRole; label: string; description: string }[] = [];
|
||||||
selectedRoles: ClientRole[] = ['user'];
|
assignableRoles: UserRole[] = [];
|
||||||
|
|
||||||
creatingUser = false;
|
creatingUser = false;
|
||||||
createUserError = '';
|
createUserError = '';
|
||||||
|
|
||||||
// Données pour la réinitialisation de mot de passe
|
// Données pour la réinitialisation de mot de passe
|
||||||
selectedUserForReset: UserResponse | null = null;
|
selectedUserForReset: HubUserResponse | null = null;
|
||||||
newPassword = '';
|
newPassword = '';
|
||||||
temporaryPassword = false;
|
temporaryPassword = false;
|
||||||
resettingPassword = false;
|
resettingPassword = false;
|
||||||
resetPasswordError = '';
|
resetPasswordError = '';
|
||||||
resetPasswordSuccess = '';
|
resetPasswordSuccess = '';
|
||||||
|
|
||||||
selectedUserForDelete: UserResponse | null = null;
|
selectedUserForDelete: HubUserResponse | null = null;
|
||||||
deletingUser = false;
|
deletingUser = false;
|
||||||
deleteUserError = '';
|
deleteUserError = '';
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.activeTab = 'list';
|
this.activeTab = 'list';
|
||||||
this.synchronizeRoles();
|
this.initializeUserPermissions();
|
||||||
|
this.loadAvailableRoles();
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronizeRoles(): void {
|
ngOnDestroy(): void {
|
||||||
// S'assurer que les rôles sont synchronisés
|
this.destroy$.next();
|
||||||
this.selectedRoles = [...this.newUser.clientRoles as ClientRole[]];
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les permissions de l'utilisateur courant
|
||||||
|
*/
|
||||||
|
private initializeUserPermissions(): void {
|
||||||
|
this.authService.getUserProfile()
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (profile) => {
|
||||||
|
// Supposons que le rôle principal est le premier rôle
|
||||||
|
this.currentUserRole = profile?.roles?.[0] as UserRole || null;
|
||||||
|
|
||||||
|
if (this.currentUserRole) {
|
||||||
|
this.roleService.setCurrentUserRole(this.currentUserRole);
|
||||||
|
this.userPermissions = this.roleService.getPermissionsForRole(this.currentUserRole);
|
||||||
|
this.canCreateUsers = this.roleService.canCreateUsers(this.currentUserRole);
|
||||||
|
this.canDeleteUsers = this.roleService.canDeleteUsers(this.currentUserRole);
|
||||||
|
this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole);
|
||||||
|
|
||||||
|
// Rôles que l'utilisateur peut attribuer
|
||||||
|
this.assignableRoles = this.roleService.getAssignableRoles(this.currentUserRole);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading user profile:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge les rôles disponibles
|
||||||
|
*/
|
||||||
|
private loadAvailableRoles(): void {
|
||||||
|
this.roleService.getAvailableRolesSimple()
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (roles) => {
|
||||||
|
this.availableRoles = roles;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading available roles:', error);
|
||||||
|
// Fallback en cas d'erreur
|
||||||
|
this.availableRoles = [
|
||||||
|
{ value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' },
|
||||||
|
{ value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' },
|
||||||
|
{ value: UserRole.DCB_PARTNER, label: 'DCB Partner', description: 'Partenaire commercial' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut attribuer un rôle spécifique
|
||||||
|
*/
|
||||||
|
canAssignRole(targetRole: UserRole): boolean {
|
||||||
|
return this.roleService.canAssignRole(this.currentUserRole, targetRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes proxy pour le template
|
||||||
|
getRoleBadgeClass(role: UserRole): string {
|
||||||
|
return this.roleService.getRoleBadgeClass(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleLabel(role: UserRole): string {
|
||||||
|
return this.roleService.getRoleLabel(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleIcon(role: UserRole): string {
|
||||||
|
return this.roleService.getRoleIcon(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleDescription(role: UserRole): string {
|
||||||
|
const roleInfo = this.availableRoles.find(r => r.value === role);
|
||||||
|
return roleInfo?.description || 'Description non disponible';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
showTab(tab: 'list' | 'profile', userId?: string) {
|
showTab(tab: 'list' | 'profile', userId?: string) {
|
||||||
this.activeTab = tab;
|
this.activeTab = tab;
|
||||||
|
|
||||||
@ -96,93 +188,88 @@ export class Users implements OnInit {
|
|||||||
|
|
||||||
// Méthode pour ouvrir le modal de création d'utilisateur
|
// Méthode pour ouvrir le modal de création d'utilisateur
|
||||||
openCreateUserModal() {
|
openCreateUserModal() {
|
||||||
|
if (!this.canCreateUsers) {
|
||||||
|
console.warn('User does not have permission to create users');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.newUser = {
|
this.newUser = {
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
role: this.assignableRoles[0] || UserRole.DCB_SUPPORT,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
emailVerified: false,
|
emailVerified: false
|
||||||
clientRoles: ['user']
|
|
||||||
};
|
};
|
||||||
this.selectedRoles = ['user'];
|
|
||||||
this.createUserError = '';
|
this.createUserError = '';
|
||||||
this.openModal(this.createUserModal);
|
this.openModal(this.createUserModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthode pour ouvrir le modal de réinitialisation de mot de passe
|
// Méthode pour ouvrir le modal de réinitialisation de mot de passe
|
||||||
openResetPasswordModal(userId: string) {
|
openResetPasswordModal(userId: string) {
|
||||||
// Charger les données de l'utilisateur
|
this.usersService.getUserById(userId)
|
||||||
this.usersService.getUserById(userId).subscribe({
|
.pipe(takeUntil(this.destroy$))
|
||||||
next: (user) => {
|
.subscribe({
|
||||||
this.selectedUserForReset = user;
|
next: (user) => {
|
||||||
this.newPassword = '';
|
this.selectedUserForReset = user;
|
||||||
this.temporaryPassword = false;
|
this.newPassword = '';
|
||||||
this.resetPasswordError = '';
|
this.temporaryPassword = false;
|
||||||
this.openModal(this.resetPasswordModal);
|
this.resetPasswordError = '';
|
||||||
},
|
this.resetPasswordSuccess = '';
|
||||||
error: (error) => {
|
this.openModal(this.resetPasswordModal);
|
||||||
console.error('Error loading user for password reset:', error);
|
},
|
||||||
}
|
error: (error) => {
|
||||||
});
|
console.error('Error loading user for password reset:', error);
|
||||||
}
|
this.resetPasswordError = 'Erreur lors du chargement de l\'utilisateur';
|
||||||
|
}
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Création d'utilisateur
|
||||||
createUser() {
|
createUser() {
|
||||||
|
if (!this.canCreateUsers) {
|
||||||
|
this.createUserError = 'Vous n\'avez pas la permission de créer des utilisateurs';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const validation = this.validateUserForm();
|
const validation = this.validateUserForm();
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
this.createUserError = validation.error!;
|
this.createUserError = validation.error!;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérifier que l'utilisateur peut attribuer ce rôle
|
||||||
|
if (!this.canAssignRole(this.newUser.role)) {
|
||||||
|
this.createUserError = 'Vous n\'avez pas la permission d\'attribuer ce rôle';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.creatingUser = true;
|
this.creatingUser = true;
|
||||||
this.createUserError = '';
|
this.createUserError = '';
|
||||||
|
|
||||||
const payload = {
|
this.usersService.createUser(this.newUser)
|
||||||
username: this.newUser.username.trim(),
|
.pipe(takeUntil(this.destroy$))
|
||||||
email: this.newUser.email.trim(),
|
.subscribe({
|
||||||
firstName: this.newUser.firstName.trim(),
|
next: (createdUser) => {
|
||||||
lastName: this.newUser.lastName.trim(),
|
this.creatingUser = false;
|
||||||
password: this.newUser.password,
|
this.modalService.dismissAll();
|
||||||
enabled: this.newUser.enabled,
|
|
||||||
emailVerified: this.newUser.emailVerified,
|
// Rafraîchir la liste
|
||||||
clientRoles: this.selectedRoles
|
if (this.usersListComponent) {
|
||||||
};
|
this.usersListComponent.loadUsers();
|
||||||
|
}
|
||||||
|
|
||||||
this.usersService.createUser(payload).subscribe({
|
this.showTab('list');
|
||||||
next: (createdUser) => {
|
this.cdRef.detectChanges();
|
||||||
this.creatingUser = false;
|
},
|
||||||
this.modalService.dismissAll();
|
error: (error) => {
|
||||||
|
this.creatingUser = false;
|
||||||
if(this.usersListComponent){
|
this.createUserError = this.getErrorMessage(error);
|
||||||
this.usersListComponent.loadUsers();
|
this.cdRef.detectChanges();
|
||||||
this.usersListComponent.onClearFilters();
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
this.showTab('list');
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
this.creatingUser = false;
|
|
||||||
this.createUserError = this.getErrorMessage(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Réinitialiser le mot de passe
|
// Réinitialiser le mot de passe
|
||||||
@ -196,27 +283,77 @@ export class Users implements OnInit {
|
|||||||
this.resetPasswordError = '';
|
this.resetPasswordError = '';
|
||||||
this.resetPasswordSuccess = '';
|
this.resetPasswordSuccess = '';
|
||||||
|
|
||||||
const resetDto = {
|
this.usersService.resetPassword(
|
||||||
userId: this.selectedUserForReset.id,
|
this.selectedUserForReset.id,
|
||||||
newPassword: this.newPassword,
|
this.newPassword,
|
||||||
temporary: this.temporaryPassword
|
this.temporaryPassword
|
||||||
};
|
)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
this.usersService.resetPassword(resetDto).subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.resettingPassword = false;
|
this.resettingPassword = false;
|
||||||
this.resetPasswordSuccess = 'Mot de passe réinitialisé avec succès !';
|
this.resetPasswordSuccess = 'Mot de passe réinitialisé avec succès !';
|
||||||
this.cdRef.detectChanges(); // Forcer la détection des changements
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.resettingPassword = false;
|
this.resettingPassword = false;
|
||||||
this.resetPasswordError = this.getResetPasswordErrorMessage(error);
|
this.resetPasswordError = this.getResetPasswordErrorMessage(error);
|
||||||
this.cdRef.detectChanges(); // Forcer la détection des changements
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion des erreurs améliorée
|
// Méthode pour ouvrir le modal de suppression
|
||||||
|
openDeleteUserModal(userId: string) {
|
||||||
|
if (!this.canDeleteUsers) {
|
||||||
|
console.warn('User does not have permission to delete users');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.usersService.getUserById(userId)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (user) => {
|
||||||
|
this.selectedUserForDelete = user;
|
||||||
|
this.deleteUserError = '';
|
||||||
|
this.openModal(this.deleteUserModal);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading user for deletion:', error);
|
||||||
|
this.deleteUserError = 'Erreur lors du chargement de l\'utilisateur';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDeleteUser() {
|
||||||
|
if (!this.selectedUserForDelete || !this.canDeleteUsers) return;
|
||||||
|
|
||||||
|
this.deletingUser = true;
|
||||||
|
this.deleteUserError = '';
|
||||||
|
|
||||||
|
this.usersService.deleteUser(this.selectedUserForDelete.id)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.deletingUser = false;
|
||||||
|
this.modalService.dismissAll();
|
||||||
|
|
||||||
|
// Rafraîchir la liste
|
||||||
|
if (this.usersListComponent) {
|
||||||
|
this.usersListComponent.loadUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.deletingUser = false;
|
||||||
|
this.deleteUserError = this.getDeleteErrorMessage(error);
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion des erreurs
|
||||||
private getErrorMessage(error: any): string {
|
private getErrorMessage(error: any): string {
|
||||||
if (error.error?.message) {
|
if (error.error?.message) {
|
||||||
return error.error.message;
|
return error.error.message;
|
||||||
@ -227,6 +364,9 @@ export class Users implements OnInit {
|
|||||||
if (error.status === 409) {
|
if (error.status === 409) {
|
||||||
return 'Un utilisateur avec ce nom ou email existe déjà.';
|
return 'Un utilisateur avec ce nom ou email existe déjà.';
|
||||||
}
|
}
|
||||||
|
if (error.status === 403) {
|
||||||
|
return 'Vous n\'avez pas les permissions nécessaires pour cette action.';
|
||||||
|
}
|
||||||
return 'Erreur lors de la création de l\'utilisateur. Veuillez réessayer.';
|
return 'Erreur lors de la création de l\'utilisateur. Veuillez réessayer.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,59 +380,12 @@ export class Users implements OnInit {
|
|||||||
if (error.status === 400) {
|
if (error.status === 400) {
|
||||||
return 'Le mot de passe ne respecte pas les critères de sécurité.';
|
return 'Le mot de passe ne respecte pas les critères de sécurité.';
|
||||||
}
|
}
|
||||||
|
if (error.status === 403) {
|
||||||
|
return 'Vous n\'avez pas les permissions pour réinitialiser ce mot de passe.';
|
||||||
|
}
|
||||||
return 'Erreur lors de la réinitialisation du mot de passe. Veuillez réessayer.';
|
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 {
|
private getDeleteErrorMessage(error: any): string {
|
||||||
if (error.error?.message) {
|
if (error.error?.message) {
|
||||||
return error.error.message;
|
return error.error.message;
|
||||||
@ -306,11 +399,6 @@ export class Users implements OnInit {
|
|||||||
return 'Erreur lors de la suppression de l\'utilisateur. Veuillez réessayer.';
|
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
|
// Validation du formulaire
|
||||||
private validateUserForm(): { isValid: boolean; error?: string } {
|
private validateUserForm(): { isValid: boolean; error?: string } {
|
||||||
const requiredFields = [
|
const requiredFields = [
|
||||||
@ -326,13 +414,25 @@ export class Users implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validation email
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(this.newUser.email)) {
|
||||||
|
return { isValid: false, error: 'Format d\'email invalide' };
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.newUser.password || this.newUser.password.length < 8) {
|
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: false, error: 'Le mot de passe doit contenir au moins 8 caractères' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.newUser.role) {
|
||||||
|
return { isValid: false, error: 'Le rôle est requis' };
|
||||||
|
}
|
||||||
|
|
||||||
return { isValid: true };
|
return { isValid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewChild(UsersList) usersListComponent!: UsersList;
|
||||||
|
|
||||||
// Références aux templates de modals
|
// Références aux templates de modals
|
||||||
@ViewChild('createUserModal') createUserModal!: TemplateRef<any>;
|
@ViewChild('createUserModal') createUserModal!: TemplateRef<any>;
|
||||||
@ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef<any>;
|
@ViewChild('resetPasswordModal') resetPasswordModal!: TemplateRef<any>;
|
||||||
|
|||||||
26
src/app/utils/truncate.pipe.ts
Normal file
26
src/app/utils/truncate.pipe.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'truncate',
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class TruncatePipe implements PipeTransform {
|
||||||
|
transform(
|
||||||
|
value: string,
|
||||||
|
limit: number = 25,
|
||||||
|
completeWords: boolean = false,
|
||||||
|
ellipsis: string = '...'
|
||||||
|
): string {
|
||||||
|
if (!value) return '';
|
||||||
|
if (value.length <= limit) return value;
|
||||||
|
|
||||||
|
if (completeWords) {
|
||||||
|
// substring(0, limit) instead of substr(0, limit)
|
||||||
|
limit = value.substring(0, limit).lastIndexOf(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.length > limit
|
||||||
|
? value.substring(0, limit) + ellipsis
|
||||||
|
: value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -57,7 +57,7 @@ export const environment = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Configuration Merchants
|
// Configuration Partners
|
||||||
merchants: {
|
merchants: {
|
||||||
onboarding: {
|
onboarding: {
|
||||||
maxFileSize: 10 * 1024 * 1024,
|
maxFileSize: 10 * 1024 * 1024,
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export const environment = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Configuration Merchants
|
// Configuration Partners
|
||||||
merchants: {
|
merchants: {
|
||||||
onboarding: {
|
onboarding: {
|
||||||
maxFileSize: 10 * 1024 * 1024,
|
maxFileSize: 10 * 1024 * 1024,
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export const environment = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Configuration Merchants
|
// Configuration Partners
|
||||||
merchants: {
|
merchants: {
|
||||||
onboarding: {
|
onboarding: {
|
||||||
maxFileSize: 10 * 1024 * 1024,
|
maxFileSize: 10 * 1024 * 1024,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user