feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature

This commit is contained in:
diallolatoile 2025-12-17 16:09:31 +00:00
parent d88311deaa
commit 7a06403f85
11 changed files with 927 additions and 762 deletions

View File

@ -63,7 +63,10 @@ export interface MerchantUser {
username?: string;
email?: string;
firstName?: string;
lastName?: string; // Référence au merchant dans MerchantConfig
lastName?: string;
merchantPartnerId: string;
enabled: boolean;
emailVerified: boolean;
createdAt?: string;
updatedAt?: string;
}
@ -212,8 +215,9 @@ export interface MerchantStatsResponse {
// === SEARCH ===
export interface SearchMerchantsParams {
query?: string;
page?: number;
take?: number;
limit?: number;
page?: number;
}
// === TYPES POUR GESTION DES RÔLES ===

View File

@ -459,26 +459,18 @@ $red: #ef4444;
}
.faq-answer {
$padding-x: 1.25rem;
$padding-y: 1.25rem;
$padding-left: 4rem;
padding: 0 $padding-x $padding-y $padding-left;
padding: 0 1.25rem 1.25rem 4rem;
animation: fadeIn 0.3s ease;
p {
$font-size: 0.9375rem;
$line-height: 1.8;
$border-width: 3px;
font-size: $font-size;
font-size: 0.9375rem;
color: $text-secondary;
line-height: $line-height;
line-height: 1.8;
margin: 0;
padding: 1rem;
background: $bg-card;
border-radius: 0.75rem;
border-left: $border-width solid $indigo;
border-left: 3px solid $indigo;
}
}

View File

@ -263,12 +263,12 @@ export class MerchantSyncService {
// Récupérer les utilisateurs de MerchantConfig
return this.merchantConfigService.getMerchantUsers(Number(merchantConfigId)).pipe(
switchMap(merchantConfigUsers => {
if (merchantConfigUsers.length === 0) {
if (merchantConfigUsers.total === 0) {
return of([]);
}
// Récupérer les détails de chaque utilisateur depuis Keycloak
const userObservables = merchantConfigUsers.map(mcUser =>
const userObservables = merchantConfigUsers.items.map((mcUser: { userId: string; }) =>
this.getUserById(mcUser.userId).pipe(
catchError(() => of(null)) // Ignorer les utilisateurs non trouvés
)

View File

@ -38,7 +38,6 @@ import { MerchantUser } from '@core/models/merchant-config.model';
export class MerchantUsersList implements OnInit, OnDestroy {
private authService = inject(AuthService);
private merchantUsersService = inject(MerchantUsersService);
private merchantConfigService = inject(MerchantConfigService);
protected roleService = inject(RoleManagementService);
private cdRef = inject(ChangeDetectorRef);
private destroy$ = new Subject<void>();

View File

@ -69,8 +69,8 @@
</ng-template>
</li>
<li [ngbNavItem]="'profile'" [hidden]="activeTab !== 'profile'">
<a ngbNavLink (click)="showTab('profile')">
<li [ngbNavItem]="'user-profile'" [hidden]="activeTab !== 'user-profile'">
<a ngbNavLink (click)="showTab('user-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>
@ -121,19 +121,6 @@
</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])">
{{ getRoleLabel(assignableRoles[0]) }}
</span>
</small>
</div>
}
<form (ngSubmit)="createUser()" #userForm="ngForm">
<div class="row g-3">
<!-- Pour les Hub Admin/Support : afficher la liste déroulante -->
@ -335,11 +322,7 @@
}
</select>
<div class="form-text">
@if (canManageRoles) {
Sélectionnez le rôle principal à assigner à cet utilisateur
} @else {
Vous ne pouvez pas modifier les rôles disponibles
}
</div>
</div>
@ -356,24 +339,6 @@
</div>
</div>
<!-- Avertissement pour les non-DCB_PARTNER -->
@if (!canManageRoles) {
<div class="col-12">
<div class="alert alert-warning">
<div class="d-flex align-items-center">
<ng-icon name="lucideInfo" class="me-2"></ng-icon>
<div>
<small>
<strong>Permissions limitées :</strong>
Vous ne pouvez créer que des utilisateurs avec des rôles spécifiques.
Seul un <strong>DCB Partner</strong> peut attribuer tous les rôles.
</small>
</div>
</div>
</div>
</div>
}
@if (newUser.role) {
<div class="col-12">
<div class="alert alert-info">

View File

@ -20,7 +20,7 @@ import {
} from '@core/models/dcb-bo-hub-user.model';
import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service';
import { AddUserToMerchantDto, Merchant } from '@core/models/merchant-config.model';
import { MerchantSyncService } from './merchant-sync-orchestrator.service';
import { MerchantConfigsList } from '@modules/merchant-config/merchant-config-list/merchant-config-list';
@Component({
selector: 'app-merchant-users',
@ -56,7 +56,7 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
badge: any = { icon: 'lucideBuilding', text: 'Merchant Users' };
// État de l'interface
activeTab: 'list' | 'profile' = 'list';
activeTab: 'list' | 'user-profile' = 'list';
selectedUserId: string | null = null;
// Gestion des permissions
@ -65,7 +65,6 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
userPermissions: any = null;
canCreateUsers = false;
canDeleteUsers = false;
canManageRoles = false;
// Formulaire de création
newUser: {
@ -108,6 +107,7 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
// Références aux composants enfants
@ViewChild(MerchantUsersList) merchantUsersList!: MerchantUsersList;
@ViewChild(MerchantConfigsList) merchantConfigList!: MerchantConfigsList;
// Rôles disponibles
availableRoles: { value: UserRole; label: string; description: string }[] = [];
@ -265,7 +265,7 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
loadingProfiles: { [userId: string]: boolean } = {}; // État de chargement par user
// Méthode pour changer d'onglet
showTab(tab: 'list' | 'profile', userId?: string) {
showTab(tab: 'list' | 'user-profile', userId?: string) {
console.log(`Switching to tab: ${tab}`, userId ? `for user ${userId}` : '');
this.activeTab = tab;
@ -383,7 +383,7 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
// Méthodes de gestion des événements du composant enfant
onUserSelected(userId: string) {
this.showTab('profile', userId);
this.showTab('user-profile', userId);
}
onResetPasswordRequested(event: any) {

View File

@ -9,12 +9,38 @@
</a>
<div card-body>
@if (!shouldDisplayMerchantList()) {
<div class="alert alert-info text-center py-4">
<ng-icon name="lucideInfo" class="me-2 fs-4"></ng-icon>
<h5 class="mb-2">Accès réservé</h5>
<p class="mb-0">Cette section est réservée aux administrateurs Hub.</p>
</div>
} @else {
<!-- 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 par statut -->
<!-- Sélecteur d'éléments par page -->
<div class="d-flex align-items-center me-3">
<label for="itemsPerPage" class="form-label mb-0 me-2 text-muted small">
<ng-icon name="lucideList" class="me-1"></ng-icon>
Affichage :
</label>
<select
id="itemsPerPage"
class="form-select form-select-sm"
style="width: auto;"
[(ngModel)]="itemsPerPage"
(change)="onItemsPerPageChange()"
[disabled]="loading"
>
@for (option of itemsPerPageOptions; track option) {
<option [value]="option">{{ option }} par page</option>
}
</select>
</div>
<!-- Statistiques rapides -->
<div class="btn-group btn-group-sm">
<button
type="button"
@ -49,7 +75,7 @@
</div>
</div>
<!-- Barre de recherche et filtres avancés -->
<!-- Barre de recherche et filtres -->
<div class="row mb-3">
<div class="col-md-4">
<div class="input-group">
@ -61,14 +87,29 @@
class="form-control"
placeholder="Rechercher par nom, adresse, téléphone..."
[(ngModel)]="searchTerm"
(input)="onSearch()"
(keyup.enter)="onSearch()"
[disabled]="loading"
>
@if (searchTerm) {
<button
class="btn btn-outline-secondary"
type="button"
(click)="searchTerm = ''; onSearch()"
[disabled]="loading"
>
<ng-icon name="lucideX"></ng-icon>
</button>
}
</div>
</div>
<div class="col-md-3">
<select class="form-select" [(ngModel)]="operatorFilter" (change)="applyFiltersAndPagination()">
<select
class="form-select"
[(ngModel)]="operatorFilter"
(change)="onFilterChange()"
[disabled]="loading"
>
@for (operator of availableOperators; track operator.value) {
<option [value]="operator.value">{{ operator.label }}</option>
}
@ -76,14 +117,18 @@
</div>
<div class="col-md-2">
<button class="btn btn-outline-secondary w-100" (click)="onClearFilters()" [disabled]="loading">
<ng-icon name="lucideX" class="me-1"></ng-icon>
Effacer les filtres
<button
class="btn btn-outline-secondary w-100"
(click)="onClearFilters()"
[disabled]="loading"
>
<ng-icon name="lucideFilterX" class="me-1"></ng-icon>
Effacer
</button>
</div>
</div>
<!-- Loading State -->
<!-- États de chargement et erreur -->
@if (loading) {
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
@ -93,7 +138,6 @@
</div>
}
<!-- Error State -->
@if (error && !loading) {
<div class="alert alert-danger" role="alert">
<div class="d-flex align-items-center">
@ -104,10 +148,12 @@
</div>
}
<!-- Merchants Table -->
<!-- Table des marchands -->
@if (!loading && !error) {
<div class="table-responsive">
@if (displayedMerchants.length > 0) {
<table class="table table-hover table-striped">
<!-- En-têtes de table -->
<thead class="table-light">
<tr>
<th (click)="sort('name')" class="cursor-pointer">
@ -266,13 +312,19 @@
}
</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 }} marchands
Affichage de {{ getStartIndex() }}
à {{ getEndIndex() }}
sur {{ totalItems }} marchands
@if (searchTerm || operatorFilter !== 'all') {
<span class="ms-2 badge bg-info">
Filtres actifs
</span>
}
</div>
<nav>
<ngb-pagination
@ -283,18 +335,23 @@
[rotate]="true"
[boundaryLinks]="true"
(pageChange)="onPageChange($event)"
[disabled]="loading"
/>
</nav>
</div>
}
<!-- Résumé des résultats -->
@if (displayedMerchants.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> {{ allMerchants.length }} marchands
<strong>Page :</strong> {{ currentPage }}/{{ totalPages }}
</small>
</div>
<div class="col">
<small class="text-muted">
<strong>Résultats :</strong> {{ displayedMerchants.length }} sur {{ totalItems }}
</small>
</div>
<div class="col">
@ -309,6 +366,21 @@
</div>
</div>
</div>
} @else {
<!-- État vide -->
<div class="text-center py-5">
<ng-icon name="lucideStore" class="fs-1 mb-3 opacity-50"></ng-icon>
<h5 class="mb-2">{{ getEmptyStateTitle() }}</h5>
<p class="text-muted mb-4">{{ getEmptyStateDescription() }}</p>
@if (showCreateButton && canCreateMerchants) {
<button class="btn btn-primary" (click)="openCreateMerchantModal.emit()">
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
{{ getEmptyStateButtonText() }}
</button>
}
</div>
}
</div>
}
}
</div>

View File

@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NgIcon } from '@ng-icons/core';
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
import { Observable, Subject, map, of } from 'rxjs';
import { Observable, Subject, of } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators';
import {
@ -12,6 +12,8 @@ import {
Operator,
MerchantUtils,
UserRole,
PaginatedResponse,
SearchMerchantsParams,
} from '@core/models/merchant-config.model';
import { MerchantConfigService } from '../merchant-config.service';
@ -55,7 +57,6 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
// Données
allMerchants: Merchant[] = [];
filteredMerchants: Merchant[] = [];
displayedMerchants: Merchant[] = [];
// États
@ -69,6 +70,7 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
// Pagination
currentPage = 1;
itemsPerPage = 10;
itemsPerPageOptions = [5, 10, 20, 50, 100]; // Options pour le sélecteur
totalItems = 0;
totalPages = 0;
@ -81,49 +83,9 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
// Permissions
currentUserRole: any = null;
canViewAllMerchants = false;
currentMerchantConfigId: string | undefined;
isHubUser = false;
// ==================== CONVERSION IDS ====================
private convertIdToNumber(id: string): number {
const numId = Number(id);
if (isNaN(numId)) {
throw new Error(`ID invalide pour la conversion en number: ${id}`);
}
return numId;
}
private convertIdToString(id: number): string {
return id.toString();
}
private convertMerchantToFrontend(merchant: any): Merchant {
return {
...merchant,
id: merchant.id ? this.convertIdToString(merchant.id) : undefined,
configs: merchant.configs ? merchant.configs.map((config: any) => ({
...config,
id: config.id ? this.convertIdToString(config.id) : undefined,
merchantPartnerId: config.merchantPartnerId ? this.convertIdToString(config.merchantPartnerId) : undefined
})) : [],
technicalContacts: merchant.technicalContacts ? merchant.technicalContacts.map((contact: any) => ({
...contact,
id: contact.id ? this.convertIdToString(contact.id) : undefined,
merchantPartnerId: contact.merchantPartnerId ? this.convertIdToString(contact.merchantPartnerId) : undefined
})) : [],
users: merchant.users ? merchant.users.map((user: any) => ({
...user,
merchantPartnerId: user.merchantPartnerId ? this.convertIdToString(user.merchantPartnerId) : undefined
})) : []
};
}
private convertMerchantsToFrontend(merchants: any[]): Merchant[] {
return merchants.map(merchant => this.convertMerchantToFrontend(merchant));
}
// Getters pour la logique conditionnelle
// Getters
get showCreateButton(): boolean {
return this.canCreateMerchants;
}
@ -133,14 +95,11 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
}
getColumnCount(): number {
return 8;
return 7;
}
ngOnInit() {
this.initializeAvailableFilters();
}
ngAfterViewInit() {
this.loadCurrentUserPermissions();
}
@ -155,10 +114,11 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
.subscribe({
next: (user) => {
this.currentUserRole = this.extractUserRole(user);
this.isHubUser = this.checkIfHubUser();
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
if (this.isHubUser) {
this.loadMerchants();
}
},
error: (error) => {
console.error('Error loading current user permissions:', error);
@ -167,31 +127,20 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
});
}
/**
* Extraire le merchantPartnerId
*/
private extractMerchantConfigId(user: any): string {
if (user?.merchantConfigId) {
return user.merchantConfigId;
}
return '';
}
private extractUserRole(user: any): any {
const userRoles = this.authService.getCurrentUserRoles();
return userRoles && userRoles.length > 0 ? userRoles[0] : null;
}
private canViewAllMerchantsCheck(role: any): boolean {
if (!role) return false;
private checkIfHubUser(): boolean {
if (!this.currentUserRole) return false;
const canViewAllRoles = [
const hubRoles = [
UserRole.DCB_ADMIN,
UserRole.DCB_SUPPORT
];
return canViewAllRoles.includes(role);
return hubRoles.includes(this.currentUserRole);
}
private initializeAvailableFilters() {
@ -202,30 +151,48 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
}
loadMerchants() {
if (!this.isHubUser) {
console.log('⚠️ User is not a Hub user, merchant list not displayed');
return;
}
this.loading = true;
this.error = '';
let merchantsObservable: Observable<Merchant[]>;
if (this.canViewAllMerchants) {
merchantsObservable = this.getAllMerchants();
} else {
merchantsObservable = this.getMyMerchants();
}
merchantsObservable
this.merchantConfigService.getMerchants(
this.currentPage,
this.itemsPerPage,
this.buildSearchParams()
)
.pipe(
takeUntil(this.destroy$),
catchError(error => {
console.error('Error loading merchants:', error);
this.error = 'Erreur lors du chargement des marchands';
return of([] as Merchant[]);
return of({
items: [],
total: 0,
page: this.currentPage,
limit: this.itemsPerPage,
totalPages: 0
} as PaginatedResponse<Merchant>);
})
)
.subscribe({
next: (merchants) => {
this.allMerchants = merchants || [];
this.applyFiltersAndPagination();
next: (response) => {
console.log('📊 Pagination response:', {
page: response.page,
total: response.total,
totalPages: response.totalPages,
itemsCount: response.items?.length,
limit: response.limit
});
this.allMerchants = response.items || [];
this.displayedMerchants = response.items || [];
this.totalItems = response.total || 0;
this.totalPages = response.totalPages || 0;
this.loading = false;
this.cdRef.detectChanges();
},
@ -233,39 +200,90 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
this.error = 'Erreur lors du chargement des marchands';
this.loading = false;
this.allMerchants = [];
this.filteredMerchants = [];
this.displayedMerchants = [];
this.totalItems = 0;
this.totalPages = 0;
this.cdRef.detectChanges();
}
});
}
private getAllMerchants(): Observable<Merchant[]> {
return this.merchantConfigService.getMerchants(1, 1000).pipe(
map(response => {
return this.convertMerchantsToFrontend(response.items);
}),
catchError(error => {
console.error('Error getting all merchants:', error);
return of([]);
})
);
private buildSearchParams(): SearchMerchantsParams {
const params: SearchMerchantsParams = {};
if (this.searchTerm.trim()) {
params.query = this.searchTerm.trim();
}
private getMyMerchants(): Observable<Merchant[]> {
const merchantConfigId = Number(this.currentMerchantConfigId);
return this.merchantConfigService.getMerchantUsers(merchantConfigId).pipe(
map(response => {
return this.convertMerchantsToFrontend(response);
}),
catchError(error => {
console.error('Error getting all merchants:', error);
return of([]);
})
);
return params;
}
// ==================== RECHERCHE ET FILTRES ====================
onSearch() {
this.currentPage = 1;
this.loadMerchants();
}
onClearFilters() {
this.searchTerm = '';
this.operatorFilter = 'all';
this.currentPage = 1;
this.loadMerchants();
}
onFilterChange() {
this.currentPage = 1;
this.loadMerchants();
}
filterByOperator(operator: Operator | 'all') {
this.operatorFilter = operator;
this.currentPage = 1;
this.loadMerchants();
}
// ==================== GESTION DU NOMBRE D'ÉLÉMENTS PAR PAGE ====================
onItemsPerPageChange() {
console.log(`📊 Changing items per page to: ${this.itemsPerPage}`);
this.currentPage = 1; // Retour à la première page
this.loadMerchants();
}
// ==================== TRI ====================
sort(field: keyof Merchant) {
if (this.sortField === field) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortField = field;
this.sortDirection = 'asc';
}
this.currentPage = 1;
this.loadMerchants();
}
getSortIcon(field: string): string {
if (this.sortField !== field) return 'lucideArrowUpDown';
return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown';
}
// ==================== PAGINATION ====================
onPageChange(page: number) {
console.log(`📄 Page changed to: ${page}`);
this.currentPage = page;
this.loadMerchants();
}
getStartIndex(): number {
return this.totalItems > 0 ? (this.currentPage - 1) * this.itemsPerPage + 1 : 0;
}
getEndIndex(): number {
return Math.min(this.currentPage * this.itemsPerPage, this.totalItems);
}
// ==================== ACTIONS ====================
@ -281,110 +299,10 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
this.deleteMerchantRequested.emit(merchant);
}
// ==================== FILTRES ET RECHERCHE ====================
onSearch() {
this.currentPage = 1;
this.applyFiltersAndPagination();
}
onClearFilters() {
this.searchTerm = '';
this.operatorFilter = 'all';
this.currentPage = 1;
this.applyFiltersAndPagination();
}
filterByOperator(operator: Operator | 'all') {
this.operatorFilter = operator;
this.currentPage = 1;
this.applyFiltersAndPagination();
}
applyFiltersAndPagination() {
if (!this.allMerchants) {
this.allMerchants = [];
}
// Appliquer les filtres
this.filteredMerchants = this.allMerchants.filter(merchant => {
const matchesSearch = !this.searchTerm ||
merchant.name.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
merchant.adresse.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
merchant.phone.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
(merchant.description && merchant.description.toLowerCase().includes(this.searchTerm.toLowerCase()));
// Filtrer par opérateur basé sur les configurations
const matchesOperator = this.operatorFilter === 'all' ||
(merchant.configs && merchant.configs.some(config => config.operatorId === this.operatorFilter));
return matchesSearch && matchesOperator;
});
// Appliquer le tri
this.filteredMerchants.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 (aValue instanceof Date && bValue instanceof Date) {
comparison = aValue.getTime() - bValue.getTime();
} else if (typeof aValue === 'number' && typeof bValue === 'number') {
comparison = aValue - bValue;
}
return this.sortDirection === 'asc' ? comparison : -comparison;
});
// Calculer la pagination
this.totalItems = this.filteredMerchants.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.displayedMerchants = this.filteredMerchants.slice(startIndex, endIndex);
}
// ==================== TRI ====================
sort(field: keyof Merchant) {
if (this.sortField === field) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortField = field;
this.sortDirection = 'asc';
}
this.applyFiltersAndPagination();
}
getSortIcon(field: string): 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);
}
// ==================== MÉTHODES STATISTIQUES ====================
getTotalMerchantsCount(): number {
return this.allMerchants.length;
return this.totalItems;
}
getTotalConfigsCount(): number {
@ -417,23 +335,20 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
// ==================== MÉTHODES POUR LE TEMPLATE ====================
refreshData() {
this.currentPage = 1;
this.loadMerchants();
}
getCardTitle(): string {
return this.canViewAllMerchants
? 'Tous les Marchands'
: 'Mes Marchands';
return 'Tous les Marchands';
}
getHelperText(): string {
return this.canViewAllMerchants
? 'Vue administrative - Gestion de tous les marchands'
: 'Vos marchands partenaires';
return 'Vue administrative - Gestion de tous les marchands';
}
getHelperIcon(): string {
return this.canViewAllMerchants ? 'lucideShield' : 'lucideStore';
return 'lucideShield';
}
getLoadingText(): string {
@ -451,4 +366,8 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
getEmptyStateButtonText(): string {
return 'Créer le premier marchand';
}
shouldDisplayMerchantList(): boolean {
return this.isHubUser;
}
}

View File

@ -62,15 +62,11 @@
[destroyOnHide]="false"
class="nav nav-tabs nav-justified nav-bordered nav-bordered-primary mb-3"
>
<li [ngbNavItem]="'list'">
<li [ngbNavItem]="'list'" [hidden]="isMerchantUser">
<a ngbNavLink (click)="showTab('list')">
<ng-icon name="lucideList" class="fs-lg me-md-1 d-inline-flex align-middle" />
<span class="d-none d-md-inline-block align-middle">
@if (isMerchantUser) {
Utilisateurs
} @else {
Marchands
}
</span>
</a>
<ng-template ngbNavContent>
@ -86,8 +82,8 @@
</ng-template>
</li>
<li [ngbNavItem]="'profile'" [hidden]="!showProfileTab">
<a ngbNavLink (click)="showTab('profile')">
<li [ngbNavItem]="'merchant-profile'" [hidden]="!showMerchantProfileTab">
<a ngbNavLink (click)="showTab('merchant-profile')">
<ng-icon name="lucideSettings" class="fs-lg me-md-1 d-inline-flex align-middle" />
<span class="d-none d-md-inline-block align-middle">
@if (isMerchantUser) {
@ -98,11 +94,11 @@
</span>
</a>
<ng-template ngbNavContent>
@if (showProfileTab) {
@if (showMerchantProfileTab) {
<!-- Pour les merchant users, utiliser userMerchantId, pour les autres selectedMerchantId -->
@if (isMerchantUser && userMerchantId) {
<app-merchant-config-view
[merchantId]="userMerchantId"
[merchantId]="userMerchantId!"
(openCreateMerchantModal)="openCreateMerchantModal()"
(editMerchantRequested)="onEditMerchantRequested($event)"
(editConfigRequested)="onEditConfigRequested($event)"
@ -110,7 +106,7 @@
/>
} @else if (!isMerchantUser && selectedMerchantId) {
<app-merchant-config-view
[merchantId]="selectedMerchantId"
[merchantId]="selectedMerchantId!"
(openCreateMerchantModal)="openCreateMerchantModal()"
(editMerchantRequested)="onEditMerchantRequested($event)"
(editConfigRequested)="onEditConfigRequested($event)"

View File

@ -1,7 +1,7 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams, HttpErrorResponse } from '@angular/common/http';
import { environment } from '@environments/environment';
import { Observable, map, catchError, throwError, retry, timeout } from 'rxjs';
import { Observable, map, catchError, throwError, retry, timeout, of, switchMap } from 'rxjs';
import {
Merchant,
@ -25,16 +25,25 @@ import {
// SERVICE DE CONVERSION
import { MerchantDataAdapter } from './merchant-data-adapter.service';
import { SearchUsersParams } from '@core/models/dcb-bo-hub-user.model';
import { MerchantUsersService } from '../hub-users-management/merchant-users.service';
import { User, UserType } from '@core/models/dcb-bo-hub-user.model';
@Injectable({ providedIn: 'root' })
export class MerchantConfigService {
private http = inject(HttpClient);
private merchantUsersService = inject(MerchantUsersService);
private dataAdapter = inject(MerchantDataAdapter);
private baseApiUrl = `${environment.configApiUrl}/merchants`;
private readonly REQUEST_TIMEOUT = 30000;
private readonly MAX_RETRIES = 2;
private merchantsCache: Merchant[] = [];
private cachedTake = 0;
private cacheParams: SearchMerchantsParams | undefined;
// ==================== MERCHANT CRUD OPERATIONS ====================
createMerchant(createMerchantDto: CreateMerchantDto): Observable<Merchant> {
@ -54,44 +63,181 @@ export class MerchantConfigService {
}
getMerchants(page: number = 1, limit: number = 10, params?: SearchMerchantsParams): Observable<PaginatedResponse<Merchant>> {
let httpParams = new HttpParams()
.set('page', page.toString())
.set('limit', limit.toString());
// Vérifier si le cache est valide
const paramsChanged = !this.areParamsEqual(params, this.cacheParams);
if (paramsChanged || this.merchantsCache.length === 0) {
// Nouvelle requête nécessaire
return this.fetchAllMerchants(params).pipe(
map(allMerchants => {
// Mettre en cache
this.merchantsCache = allMerchants;
this.cacheParams = params;
// Appliquer pagination
return this.applyPagination(allMerchants, page, limit);
})
);
} else {
// Vérifier si le cache contient assez d'éléments pour la page demandée
const neededItems = page * limit;
if (neededItems <= this.merchantsCache.length) {
// Cache suffisant
return of(this.applyPagination(this.merchantsCache, page, limit));
} else {
// Besoin de plus d'éléments
const additionalNeeded = neededItems - this.merchantsCache.length;
const newTake = this.calculateOptimalTake(additionalNeeded, page, limit);
console.log(`🔄 Cache insufficient (${this.merchantsCache.length}), fetching ${newTake} more items`);
return this.fetchAdditionalMerchants(newTake, params).pipe(
map(additionalMerchants => {
// Fusionner avec le cache (éviter les doublons)
const merged = this.mergeMerchants(this.merchantsCache, additionalMerchants);
this.merchantsCache = merged;
return this.applyPagination(merged, page, limit);
})
);
}
}
}
// Calculer le take optimal en fonction du total
private calculateOptimalTake(totalCount: number, page: number, limit: number): number {
// Calculer combien d'éléments nous avons besoin pour la pagination
const neededItems = page * limit;
// Ajouter un buffer pour éviter les appels fréquents
const buffer = Math.max(limit * 2, 100);
// Calculer le take nécessaire
let optimalTake = neededItems + buffer;
// Si le total est connu, adapter le take
if (totalCount > 0) {
// Prendre soit ce dont on a besoin, soit le total (le plus petit)
optimalTake = Math.min(optimalTake, totalCount);
}
// Arrondir aux paliers optimaux
return this.roundToOptimalValue(optimalTake);
}
// Arrondir à des valeurs optimales (100, 500, 1000, etc.)
private roundToOptimalValue(value: number): number {
if (value <= 100) return 100;
if (value <= 500) return 500;
if (value <= 1000) return 1000;
if (value <= 2000) return 2000;
if (value <= 5000) return 5000;
if (value <= 10000) return 10000;
// Pour les très grands nombres, arrondir au multiple de 10000 supérieur
return Math.ceil(value / 10000) * 10000;
}
private fetchAllMerchants(params?: SearchMerchantsParams): Observable<Merchant[]> {
// Commencer avec un take raisonnable
const initialTake = 500;
return this.fetchMerchantsWithParams(initialTake, params).pipe(
switchMap(initialBatch => {
// Si nous avons récupéré moins que demandé, c'est probablement tout
if (initialBatch.length < initialTake) {
console.log(`✅ Retrieved all ${initialBatch.length} merchants`);
return of(initialBatch);
}
// Sinon, peut-être qu'il y a plus, essayer un take plus grand
console.log(`⚠️ Initial batch size (${initialBatch.length}) equals take, might be more`);
const largerTake = 2000;
return this.fetchMerchantsWithParams(largerTake, params).pipe(
map(largerBatch => {
console.log(`✅ Retrieved ${largerBatch.length} merchants with larger take`);
return largerBatch;
})
);
})
);
}
private fetchMerchantsWithParams(take: number, params?: SearchMerchantsParams): Observable<Merchant[]> {
let httpParams = new HttpParams().set('take', take.toString());
if (params?.query) {
httpParams = httpParams.set('query', params.query.trim());
}
console.log(`📥 Loading merchants page ${page}, limit ${limit}`, params);
console.log(`📥 Fetching ${take} merchants`);
return this.http.get<ApiMerchant[]>(this.baseApiUrl, {
params: httpParams
}).pipe(
timeout(this.REQUEST_TIMEOUT),
retry(this.MAX_RETRIES),
map(apiMerchants => {
const total = apiMerchants.length;
map(apiMerchants =>
apiMerchants.map(merchant =>
this.dataAdapter.convertApiMerchantToFrontend(merchant)
)
)
);
}
private fetchAdditionalMerchants(take: number, params?: SearchMerchantsParams): Observable<Merchant[]> {
// Prendre à partir de la fin du cache
const skip = this.merchantsCache.length;
let httpParams = new HttpParams()
.set('take', take.toString())
.set('skip', skip.toString()); // Si votre API supporte skip
if (params?.query) {
httpParams = httpParams.set('query', params.query.trim());
}
console.log(`📥 Fetching additional ${take} merchants (skip: ${skip})`);
return this.http.get<ApiMerchant[]>(this.baseApiUrl, {
params: httpParams
}).pipe(
timeout(this.REQUEST_TIMEOUT),
map(apiMerchants =>
apiMerchants.map(merchant =>
this.dataAdapter.convertApiMerchantToFrontend(merchant)
)
)
);
}
private mergeMerchants(existing: Merchant[], newOnes: Merchant[]): Merchant[] {
const existingIds = new Set(existing.map(m => m.id));
const uniqueNewOnes = newOnes.filter(m => !existingIds.has(m.id));
return [...existing, ...uniqueNewOnes];
}
private applyPagination(merchants: Merchant[], page: number, limit: number): PaginatedResponse<Merchant> {
const total = merchants.length;
const totalPages = Math.ceil(total / limit);
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedItems = apiMerchants.slice(startIndex, endIndex);
const endIndex = Math.min(startIndex + limit, total);
const paginatedItems = merchants.slice(startIndex, endIndex);
const response: PaginatedResponse<Merchant> = {
items: paginatedItems.map(apiMerchant =>
this.dataAdapter.convertApiMerchantToFrontend(apiMerchant)
),
return {
items: paginatedItems,
total: total,
page: page,
limit: limit,
totalPages: totalPages
};
}
console.log(`✅ Loaded ${response.items.length} merchants`);
return response;
}),
catchError(error => this.handleError('getMerchants', error))
);
private areParamsEqual(a?: SearchMerchantsParams, b?: SearchMerchantsParams): boolean {
if (!a && !b) return true;
if (!a || !b) return false;
return a.query === b.query;
}
getAllMerchants(params?: SearchMerchantsParams): Observable<Merchant[]> {
@ -176,18 +322,147 @@ export class MerchantConfigService {
);
}
getMerchantUsers(merchantId: number): Observable<MerchantUser[]> {
//const merchantId = this.convertIdToNumber(merchantId);
getMerchantUsers(
merchantId: number,
page: number = 1,
limit: number = 10,
params?: SearchUsersParams
): Observable<PaginatedResponse<MerchantUser>> {
return this.http.get<ApiMerchant>(`${this.baseApiUrl}/${merchantId}/users`).pipe(
timeout(this.REQUEST_TIMEOUT),
map(apiMerchant => {
return (apiMerchant.users || []).map(user =>
this.dataAdapter.convertApiUserToFrontend(user)
return this.tryMerchantConfigApi(merchantId, params).pipe(
switchMap(merchantConfigUsers => {
// Si MerchantConfig retourne des données, les utiliser
if (merchantConfigUsers && merchantConfigUsers.length > 0) {
console.log(`✅ Using ${merchantConfigUsers.length} users from MerchantConfig API`);
return of(this.paginateUsers(merchantConfigUsers, page, limit));
}
// Sinon, fallback à IAM API
console.log('🔄 MerchantConfig API returned no data, falling back to IAM API');
return this.getUsersFromIamApi(merchantId).pipe(
map(iamUsers => this.paginateUsers(iamUsers, page, limit))
);
}),
catchError(error => this.handleError('getMerchantUsers', error, { merchantId }))
catchError(error => {
console.warn('⚠️ MerchantConfig API failed, using IAM API:', error);
return this.getUsersFromIamApi(merchantId).pipe(
map(iamUsers => this.paginateUsers(iamUsers, page, limit))
);
})
);
}
private tryMerchantConfigApi(merchantId: number, params?: SearchUsersParams): Observable<MerchantUser[]> {
const httpParams = this.buildMerchantConfigParams(params);
return this.http.get<any>(
`${this.baseApiUrl}/${merchantId}/users`,
{ params: httpParams }
).pipe(
timeout(this.REQUEST_TIMEOUT),
map(configUsers => {
if (!configUsers) return [];
const items = this.extractItemsFromResponse(configUsers);
return items.map(item => this.convertToMerchantUser(item, merchantId));
}),
catchError(error => {
// Retourner un tableau vide pour déclencher le fallback
return of([]);
})
);
}
private getUsersFromIamApi(merchantId: number ): Observable<MerchantUser[]> {
return this.merchantUsersService.getMyMerchantUsers().pipe(
map(iamUsers => {
if (!iamUsers) return [];
const items = this.extractItemsFromResponse(iamUsers);
return items.map(item => this.convertToMerchantUser(item, merchantId));
}),
catchError(error => {
console.error('❌ IAM API failed:', error);
return of([]);
})
);
}
private paginateUsers(users: MerchantUser[], page: number, limit: number): PaginatedResponse<MerchantUser> {
const total = users.length;
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedItems = users.slice(startIndex, endIndex);
return {
items: paginatedItems,
total: total,
page: page,
limit: limit,
totalPages: Math.ceil(total / limit)
};
}
private buildMerchantConfigParams(params?: SearchUsersParams): HttpParams {
let httpParams = new HttpParams();
if (params?.searchTerm?.trim()) {
httpParams = httpParams.set('search', params.searchTerm.trim());
}
if (params?.role) {
httpParams = httpParams.set('role', params.role);
}
return httpParams;
}
private extractItemsFromResponse(response: any): any[] {
if (Array.isArray(response)) {
return response;
}
if (response && typeof response === 'object') {
if (response.items && Array.isArray(response.items)) {
return response.items;
}
if (response.users && Array.isArray(response.users)) {
return response.users;
}
if (response.data && Array.isArray(response.data)) {
return response.data;
}
// Chercher le premier tableau
const arrayKeys = Object.keys(response).filter(key => Array.isArray(response[key]));
if (arrayKeys.length > 0) {
return response[arrayKeys[0]];
}
}
return [];
}
private convertToMerchantUser(item: any, merchantId: number): MerchantUser {
try {
return this.dataAdapter.convertApiUserToFrontend(item);
} catch (error) {
console.warn('Adapter conversion failed, using manual conversion:', error);
return {
userId: item.userId || item.id || '',
firstName: item.firstName || item.firstname || '',
lastName: item.lastName || item.lastname || '',
email: item.email || '',
role: item.role || '',
merchantPartnerId: item.merchantPartnerId || merchantId.toString(),
enabled: item.enabled !== undefined ? item.enabled : true,
emailVerified: item.emailVerified !== undefined ? item.emailVerified : false,
createdAt: item.createdAt || item.createdDate || new Date().toISOString(),
updatedAt: item.updatedAt || item.updatedDate || new Date().toISOString()
};
}
}
getAllMerchantUsers(page: number = 1, limit: number = 10, params?: SearchMerchantsParams): Observable<PaginatedResponse<MerchantUser>> {

View File

@ -64,9 +64,10 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
badge: any = { icon: 'lucideSettings', text: 'Merchant Management' };
// État de l'interface
activeTab: 'list' | 'profile' = 'list';
activeTab: 'list' | 'merchant-profile' = 'list';
selectedMerchantId: number | null = null;
selectedConfigId: number | null = null;
selectedUserId: string | null = null;
// Gestion des permissions
currentUserRole: UserRole | null = null;
@ -145,15 +146,36 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
merchantUsersError = '';
currentMerchantPartnerId: string = '';
isInitializing = true;
ngOnInit() {
this.loadCurrentUserPermissions();
this.initializeUserType();
this.initializeMerchantPartnerContext();
// Déterminer l'onglet initial en fonction du type d'utilisateur
this.setInitialTab();
}
ngAfterViewInit() {
setTimeout(() => {
this.setInitialTab();
});
}
private setInitialTab(): void {
this.isInitializing = true;
if (this.isMerchantUser) {
// Pour un utilisateur marchand
this.activeTab = 'merchant-profile';
this.loadUserMerchant();
} else {
// Pour un utilisateur Hub
this.activeTab = 'list';
this.isInitializing = false;
this.cdRef.detectChanges();
}
}
/**
* Initialise le type d'utilisateur
*/
@ -181,44 +203,21 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
console.log(`Is Hub User: ${this.isHubUser}`);
}
/**
* Définit l'onglet initial en fonction du type d'utilisateur
*/
private setInitialTab(): void {
if (this.isMerchantUser) {
// Pour un utilisateur marchand, charger directement son marchand ET les utilisateurs
this.activeTab = 'profile'; // On change pour afficher d'abord la liste des utilisateurs
this.loadUserMerchantAndUsers();
} else {
// Pour un utilisateur Hub, afficher la liste des marchands
this.activeTab = 'list';
}
}
/**
* Charge le marchand ET les utilisateurs pour un merchant user
*/
private loadUserMerchantAndUsers(): void {
if (!this.isMerchantUser || !this.currentMerchantConfigId) return;
// Charger le marchand de l'utilisateur
this.loadUserMerchant();
// Charger les utilisateurs du marchand
this.loadMerchantUsers();
}
/**
* Charge le marchand de l'utilisateur (pour les merchant users)
*/
private loadUserMerchant(): void {
if (!this.isMerchantUser || !this.currentMerchantConfigId) return;
if (!this.isMerchantUser || !this.currentMerchantConfigId) {
this.isInitializing = false;
this.cdRef.detectChanges();
return;
}
this.loadingUserMerchant = true;
const merchantPartnerId = Number(this.currentMerchantConfigId);
const merchantConfigId = Number(this.currentMerchantConfigId);
this.merchantConfigService.getMerchantById(merchantConfigId)
this.merchantConfigService.getMerchantById(merchantPartnerId)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (merchant) => {
@ -227,67 +226,27 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
this.userMerchantId = frontendMerchant.id!;
this.userMerchant = frontendMerchant;
// Afficher automatiquement le profil du marchand
this.showTab('profile', merchantConfigId);
// Vérifier que nous sommes toujours sur l'onglet profil
if (this.activeTab === 'merchant-profile') {
this.showTab('merchant-profile', merchantPartnerId);
}
} else {
console.warn('No merchant found for current user');
// Si aucun marchand trouvé, revenir à la liste
this.activeTab = 'list';
}
this.loadingUserMerchant = false;
this.isInitializing = false;
this.cdRef.detectChanges();
},
error: (error) => {
console.error('Error loading user merchant:', error);
this.loadingUserMerchant = false;
}
});
}
/**
* Charge les utilisateurs du marchand (pour les merchant users)
*/
private loadMerchantUsers(): void {
if (!this.isMerchantUser || !this.currentMerchantConfigId) return;
this.loadingMerchantUsers = true;
this.merchantUsersError = '';
this.merchantSyncService.getUsersByMerchant(
this.currentMerchantConfigId
)
.pipe(
takeUntil(this.destroy$),
catchError(error => {
console.error('Error loading merchant users:', error);
this.merchantUsersError = 'Erreur lors du chargement des utilisateurs';
this.loadingMerchantUsers = false;
return of({ users: [] });
})
)
.subscribe({
next: (response: any) => {
// Transformer les données des utilisateurs
this.merchantUsers = response.users.map((userData: any) => {
const keycloakUser = userData.keycloakUser || {};
const merchantConfigUser = userData.merchantConfigUser || {};
return {
id: keycloakUser.id || merchantConfigUser.userId,
username: keycloakUser.username,
email: keycloakUser.email,
firstName: keycloakUser.firstName,
lastName: keycloakUser.lastName,
role: keycloakUser.role || merchantConfigUser.role,
enabled: keycloakUser.enabled,
emailVerified: keycloakUser.emailVerified,
merchantPartnerId: this.currentMerchantPartnerId,
merchantConfigId: this.currentMerchantConfigId,
createdDate: keycloakUser.createdDate || merchantConfigUser.createdDate,
lastModifiedDate: keycloakUser.lastModifiedDate || merchantConfigUser.lastModifiedDate
} as unknown as User;
});
console.log(`Loaded ${this.merchantUsers.length} users for merchant`);
this.loadingMerchantUsers = false;
this.isInitializing = false;
// Revenir à la liste en cas d'erreur
this.activeTab = 'list';
this.cdRef.detectChanges();
}
});
}
@ -441,7 +400,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
}
private extractMerchantConfigId(user: any): string {
return user?.merchantConfigId || '';
return user?.merchantPartnerId || '';
}
// ==================== PERMISSIONS SPÉCIFIQUES MARCHAND ====================
@ -503,35 +462,50 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
// ==================== GESTION DES ONGLETS ====================
showTab(tab: 'list' | 'profile', merchantId?: number): void {
console.log(`Switching to tab: ${tab}`, merchantId ? `for merchant ${merchantId}` : '');
// Méthode pour changer d'onglet
showTab(tab: 'list' | 'merchant-profile', merchantId?: number): void {
// Ne pas permettre de changer d'onglet pendant l'initialisation
if (this.isInitializing && tab !== this.activeTab) {
return;
}
console.log(`🔄 Switching to tab: ${tab}`, merchantId ? `with id: ${merchantId}` : '');
// Pour tous les utilisateurs, permettre de basculer entre les onglets
this.activeTab = tab;
if (tab === 'profile') {
// Déterminer l'ID du marchand à afficher
if (tab === 'merchant-profile' && merchantId) {
if (this.isMerchantUser) {
// Pour les merchant users, toujours utiliser leur propre marchand
this.selectedMerchantId = this.userMerchantId || null;
this.userMerchantId = merchantId;
} else {
// Pour les Hub users, utiliser l'ID fourni ou null
this.selectedMerchantId = merchantId || null;
this.selectedMerchantId = merchantId;
}
}
// Charger le profil si pas déjà chargé et si un ID est disponible
if (this.selectedMerchantId && !this.merchantProfiles[this.selectedMerchantId]) {
this.loadMerchantProfile(this.selectedMerchantId);
this.cdRef.detectChanges();
}
get showMerchantProfileTab(): boolean {
// Ne pas montrer l'onglet pendant l'initialisation pour un merchant user
if (this.isInitializing && this.isMerchantUser) {
return false;
}
} else {
// Pour l'onglet 'list', pas besoin d'ID de marchand sélectionné
this.selectedMerchantId = null;
// Si c'est un merchant user et qu'on va sur l'onglet liste, recharger les utilisateurs
if (this.isMerchantUser) {
this.refreshMerchantUsers();
return !!this.userMerchantId;
} else {
return !!this.selectedMerchantId;
}
}
// Méthode spécifique pour afficher le profil marchand
showMerchantProfile(merchantId: number): void {
console.log(`🏪 Showing merchant profile: ${merchantId}`);
if (this.isMerchantUser) {
this.userMerchantId = merchantId;
} else {
this.selectedMerchantId = merchantId;
}
this.activeTab = 'merchant-profile';
}
@ -585,7 +559,8 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
// ==================== ÉVÉNEMENTS DES COMPOSANTS ENFANTS ====================
onMerchantSelected(merchantId: number): void {
this.showTab('profile', merchantId);
// Pour un Hub user, sélectionner un marchand = voir les détails
this.showMerchantProfile(merchantId);
}
onEditMerchantRequested(merchant: Merchant): void {
@ -949,15 +924,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
}
}
/**
* Rafraîchit la liste des utilisateurs du marchand
*/
refreshMerchantUsers(): void {
if (this.isMerchantUser) {
this.loadMerchantUsers();
}
}
// ==================== GESTION DES ERREURS ====================
private getCreateErrorMessage(error: any): string {
@ -1028,54 +994,31 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
// ==================== GETTERS TEMPLATE ====================
get showListTab(): boolean {
// Pour les merchant users, toujours montrer l'onglet liste (pour les utilisateurs)
// Pour les Hub users, montrer la liste si autorisé
if (this.isMerchantUser) {
return true;
}
return this.canViewMerchantList;
}
// ==================== GETTERS POUR LES ONGLETS ====================
get showProfileTab(): boolean {
// Pour les merchant users, toujours montrer l'onglet profile (pour leur marchand)
// Pour les Hub users, montrer l'onglet profile si un marchand est sélectionné
get shouldShowMerchantProfileContent(): boolean {
if (this.isMerchantUser) {
return !!this.userMerchantId; // Montrer seulement si le marchand est chargé
}
return !!this.userMerchantId;
} else {
return !!this.selectedMerchantId;
}
}
get pageTitleForUser(): string {
getMerchantProfileId(): number | null{
if (this.isMerchantUser) {
return this.activeTab === 'list' ? 'Utilisateurs du Marchand' : 'Mon Marchand';
return this.userMerchantId;
} else {
return this.selectedMerchantId;
}
return this.pageTitle;
}
get pageSubtitleForUser(): string {
get activeMerchantProfileId(): number | null {
// Retourner l'ID du marchand à afficher dans l'onglet profil
if (this.isMerchantUser) {
return this.activeTab === 'list'
? 'Gérez les utilisateurs de votre marchand'
: 'Gérez la configuration technique de votre marchand';
return this.userMerchantId;
} else {
return this.selectedMerchantId;
}
return this.pageSubtitle;
}
get badgeForUser(): any {
if (this.isMerchantUser) {
return this.activeTab === 'list'
? { icon: 'lucideUsers', text: 'Users' }
: { icon: 'lucideStore', text: 'My Merchant' };
}
return this.badge;
}
/**
* Détermine ce qui doit être affiché dans la liste
*/
get shouldDisplayMerchantUsers(): boolean {
return this.isMerchantUser && this.activeTab === 'list';
}
get shouldDisplayMerchantList(): boolean {