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

View File

@ -459,26 +459,18 @@ $red: #ef4444;
} }
.faq-answer { .faq-answer {
$padding-x: 1.25rem; padding: 0 1.25rem 1.25rem 4rem;
$padding-y: 1.25rem;
$padding-left: 4rem;
padding: 0 $padding-x $padding-y $padding-left;
animation: fadeIn 0.3s ease; animation: fadeIn 0.3s ease;
p { p {
$font-size: 0.9375rem; font-size: 0.9375rem;
$line-height: 1.8;
$border-width: 3px;
font-size: $font-size;
color: $text-secondary; color: $text-secondary;
line-height: $line-height; line-height: 1.8;
margin: 0; margin: 0;
padding: 1rem; padding: 1rem;
background: $bg-card; background: $bg-card;
border-radius: 0.75rem; 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 // Récupérer les utilisateurs de MerchantConfig
return this.merchantConfigService.getMerchantUsers(Number(merchantConfigId)).pipe( return this.merchantConfigService.getMerchantUsers(Number(merchantConfigId)).pipe(
switchMap(merchantConfigUsers => { switchMap(merchantConfigUsers => {
if (merchantConfigUsers.length === 0) { if (merchantConfigUsers.total === 0) {
return of([]); return of([]);
} }
// Récupérer les détails de chaque utilisateur depuis Keycloak // 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( this.getUserById(mcUser.userId).pipe(
catchError(() => of(null)) // Ignorer les utilisateurs non trouvés 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 { export class MerchantUsersList implements OnInit, OnDestroy {
private authService = inject(AuthService); private authService = inject(AuthService);
private merchantUsersService = inject(MerchantUsersService); private merchantUsersService = inject(MerchantUsersService);
private merchantConfigService = inject(MerchantConfigService);
protected roleService = inject(RoleManagementService); protected roleService = inject(RoleManagementService);
private cdRef = inject(ChangeDetectorRef); private cdRef = inject(ChangeDetectorRef);
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();

View File

@ -69,8 +69,8 @@
</ng-template> </ng-template>
</li> </li>
<li [ngbNavItem]="'profile'" [hidden]="activeTab !== 'profile'"> <li [ngbNavItem]="'user-profile'" [hidden]="activeTab !== 'user-profile'">
<a ngbNavLink (click)="showTab('profile')"> <a ngbNavLink (click)="showTab('user-profile')">
<ng-icon name="lucideUser" class="fs-lg me-md-1 d-inline-flex align-middle" /> <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> <span class="d-none d-md-inline-block align-middle">Profil Utilisateur</span>
</a> </a>
@ -121,19 +121,6 @@
</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])">
{{ 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">
<!-- Pour les Hub Admin/Support : afficher la liste déroulante --> <!-- Pour les Hub Admin/Support : afficher la liste déroulante -->
@ -335,11 +322,7 @@
} }
</select> </select>
<div class="form-text"> <div class="form-text">
@if (canManageRoles) {
Sélectionnez le rôle principal à assigner à cet utilisateur Sélectionnez le rôle principal à assigner à cet utilisateur
} @else {
Vous ne pouvez pas modifier les rôles disponibles
}
</div> </div>
</div> </div>
@ -356,24 +339,6 @@
</div> </div>
</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) { @if (newUser.role) {
<div class="col-12"> <div class="col-12">
<div class="alert alert-info"> <div class="alert alert-info">

View File

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

View File

@ -9,307 +9,379 @@
</a> </a>
<div card-body> <div card-body>
@if (!shouldDisplayMerchantList()) {
<!-- Barre d'actions supérieure --> <div class="alert alert-info text-center py-4">
<div class="row mb-3"> <ng-icon name="lucideInfo" class="me-2 fs-4"></ng-icon>
<div class="col-md-6"> <h5 class="mb-2">Accès réservé</h5>
<div class="d-flex align-items-center gap-2"> <p class="mb-0">Cette section est réservée aux administrateurs Hub.</p>
<!-- Statistiques rapides par statut --> </div>
<div class="btn-group btn-group-sm"> } @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">
<!-- 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"
class="btn btn-outline-primary"
>
Tous ({{ getTotalMerchantsCount() }})
</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-end gap-2">
@if (showCreateButton && canCreateMerchants) {
<button
class="btn btn-primary"
(click)="openCreateMerchantModal.emit()"
>
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
Nouveau Marchand
</button>
}
<button <button
type="button" class="btn btn-outline-secondary"
class="btn btn-outline-primary" (click)="refreshData()"
[disabled]="loading"
> >
Tous ({{ getTotalMerchantsCount() }}) <ng-icon name="lucideRefreshCw" class="me-1" [class.spin]="loading"></ng-icon>
Actualiser
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <!-- Barre de recherche et filtres -->
<div class="d-flex justify-content-end gap-2"> <div class="row mb-3">
@if (showCreateButton && canCreateMerchants) { <div class="col-md-4">
<button <div class="input-group">
class="btn btn-primary" <span class="input-group-text">
(click)="openCreateMerchantModal.emit()" <ng-icon name="lucideSearch"></ng-icon>
</span>
<input
type="text"
class="form-control"
placeholder="Rechercher par nom, adresse, téléphone..."
[(ngModel)]="searchTerm"
(keyup.enter)="onSearch()"
[disabled]="loading"
> >
<ng-icon name="lucidePlus" class="me-1"></ng-icon> @if (searchTerm) {
Nouveau Marchand <button
</button> class="btn btn-outline-secondary"
} type="button"
<button (click)="searchTerm = ''; onSearch()"
class="btn btn-outline-secondary" [disabled]="loading"
(click)="refreshData()" >
<ng-icon name="lucideX"></ng-icon>
</button>
}
</div>
</div>
<div class="col-md-3">
<select
class="form-select"
[(ngModel)]="operatorFilter"
(change)="onFilterChange()"
[disabled]="loading" [disabled]="loading"
> >
<ng-icon name="lucideRefreshCw" class="me-1" [class.spin]="loading"></ng-icon> @for (operator of availableOperators; track operator.value) {
Actualiser <option [value]="operator.value">{{ operator.label }}</option>
}
</select>
</div>
<div class="col-md-2">
<button
class="btn btn-outline-secondary w-100"
(click)="onClearFilters()"
[disabled]="loading"
>
<ng-icon name="lucideFilterX" class="me-1"></ng-icon>
Effacer
</button> </button>
</div> </div>
</div> </div>
</div>
<!-- Barre de recherche et filtres avancés --> <!-- États de chargement et erreur -->
<div class="row mb-3"> @if (loading) {
<div class="col-md-4"> <div class="text-center py-4">
<div class="input-group"> <div class="spinner-border text-primary" role="status">
<span class="input-group-text"> <span class="visually-hidden">Chargement...</span>
<ng-icon name="lucideSearch"></ng-icon>
</span>
<input
type="text"
class="form-control"
placeholder="Rechercher par nom, adresse, téléphone..."
[(ngModel)]="searchTerm"
(input)="onSearch()"
[disabled]="loading"
>
</div>
</div>
<div class="col-md-3">
<select class="form-select" [(ngModel)]="operatorFilter" (change)="applyFiltersAndPagination()">
@for (operator of availableOperators; track operator.value) {
<option [value]="operator.value">{{ operator.label }}</option>
}
</select>
</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>
</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">{{ getLoadingText() }}</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>
<button class="btn-close ms-auto" (click)="error = ''"></button>
</div>
</div>
}
<!-- Merchants Table -->
@if (!loading && !error) {
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead class="table-light">
<tr>
<th (click)="sort('name')" class="cursor-pointer">
<div class="d-flex align-items-center">
<span>Marchand</span>
<ng-icon [name]="getSortIcon('name')" class="ms-1 fs-12"></ng-icon>
</div>
</th>
<th>Description</th>
<th>Contact</th>
<th>Configurations</th>
<th>Contacts Tech</th>
<th (click)="sort('createdAt')" class="cursor-pointer">
<div class="d-flex align-items-center">
<span>Créé le</span>
<ng-icon [name]="getSortIcon('createdAt')" class="ms-1 fs-12"></ng-icon>
</div>
</th>
<th width="180">Actions</th>
</tr>
</thead>
<tbody>
@for (merchant of displayedMerchants; track merchant.id) {
<tr>
<td>
<div class="d-flex align-items-center">
@if (merchant.logo) {
<img
[src]="merchant.logo"
alt="Logo {{ merchant.name }}"
class="avatar-sm rounded-circle me-2"
onerror="this.style.display='none'"
>
}
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
<ng-icon name="lucideStore" class="text-primary fs-12"></ng-icon>
</div>
<div>
<strong class="d-block">{{ merchant.name }}</strong>
<small class="text-muted">{{ merchant.adresse }}</small>
</div>
</div>
</td>
<td>
@if (merchant.description) {
<span class="text-muted">{{ merchant.description }}</span>
} @else {
<span class="text-muted fst-italic">Aucune description</span>
}
</td>
<td>
<div class="d-flex flex-column">
<small class="text-muted">
<ng-icon name="lucidePhone" class="me-1" size="12"></ng-icon>
{{ merchant.phone }}
</small>
@if (merchant.technicalContacts && merchant.technicalContacts.length > 0) {
<small class="text-muted">
<ng-icon name="lucideUser" class="me-1" size="12"></ng-icon>
{{ merchant.technicalContacts[0].firstName }} {{ merchant.technicalContacts[0].lastName }}
</small>
}
</div>
</td>
<td>
<div class="d-flex flex-column gap-1">
@if (merchant.configs && merchant.configs.length > 0) {
<div class="d-flex flex-wrap gap-1">
@for (config of merchant.configs.slice(0, 2); track config.id) {
<span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-10">
{{ getConfigTypeLabel(config.name) }}
</span>
}
@if (merchant.configs.length > 2) {
<span class="badge bg-secondary">
+{{ merchant.configs.length - 2 }}
</span>
}
</div>
<small class="text-muted">
{{ merchant.configs.length }} configuration(s)
</small>
} @else {
<span class="badge bg-warning bg-opacity-10 text-warning">
Aucune config
</span>
}
</div>
</td>
<td>
@if (merchant.technicalContacts && merchant.technicalContacts.length > 0) {
<div class="d-flex flex-column">
<span class="badge bg-info bg-opacity-10 text-info">
{{ merchant.technicalContacts.length }} contact(s)
</span>
<small class="text-muted">
{{ merchant.technicalContacts[0].email }}
</small>
</div>
} @else {
<span class="badge bg-warning bg-opacity-10 text-warning">
Aucun contact
</span>
}
</td>
<td>
<small class="text-muted">
{{ formatTimestamp(merchant.createdAt!) }}
</small>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button
class="btn btn-outline-primary btn-sm"
(click)="viewMerchantProfile(merchant)"
title="Voir les détails"
>
<ng-icon name="lucideEye"></ng-icon>
</button>
<button
class="btn btn-outline-warning btn-sm"
(click)="editMerchant(merchant)"
title="Modifier le marchand"
>
<ng-icon name="lucideEdit"></ng-icon>
</button>
@if (showDeleteButton) {
<button
class="btn btn-outline-danger btn-sm"
(click)="deleteMerchant(merchant)"
title="Supprimer le marchand"
>
<ng-icon name="lucideTrash2"></ng-icon>
</button>
}
</div>
</td>
</tr>
}
@empty {
<tr>
<td [attr.colspan]="getColumnCount()" class="text-center py-4">
<div class="text-muted">
<ng-icon name="lucideStore" class="fs-1 mb-3 opacity-50"></ng-icon>
<h5 class="mb-2">{{ getEmptyStateTitle() }}</h5>
<p class="mb-3">{{ 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>
</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 }} marchands
</div> </div>
<nav> <p class="mt-2 text-muted">{{ getLoadingText() }}</p>
<ngb-pagination
[collectionSize]="totalItems"
[page]="currentPage"
[pageSize]="itemsPerPage"
[maxSize]="5"
[rotate]="true"
[boundaryLinks]="true"
(pageChange)="onPageChange($event)"
/>
</nav>
</div> </div>
} }
<!-- Résumé des résultats --> @if (error && !loading) {
@if (displayedMerchants.length > 0) { <div class="alert alert-danger" role="alert">
<div class="mt-3 pt-3 border-top"> <div class="d-flex align-items-center">
<div class="row text-center"> <ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
<div class="col"> <div>{{ error }}</div>
<small class="text-muted"> <button class="btn-close ms-auto" (click)="error = ''"></button>
<strong>Total :</strong> {{ allMerchants.length }} marchands
</small>
</div>
<div class="col">
<small class="text-muted">
<strong>Configurations :</strong> {{ getTotalConfigsCount() }}
</small>
</div>
<div class="col">
<small class="text-muted">
<strong>Contacts :</strong> {{ getTotalContactsCount() }}
</small>
</div>
</div> </div>
</div> </div>
} }
<!-- 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">
<div class="d-flex align-items-center">
<span>Marchand</span>
<ng-icon [name]="getSortIcon('name')" class="ms-1 fs-12"></ng-icon>
</div>
</th>
<th>Description</th>
<th>Contact</th>
<th>Configurations</th>
<th>Contacts Tech</th>
<th (click)="sort('createdAt')" class="cursor-pointer">
<div class="d-flex align-items-center">
<span>Créé le</span>
<ng-icon [name]="getSortIcon('createdAt')" class="ms-1 fs-12"></ng-icon>
</div>
</th>
<th width="180">Actions</th>
</tr>
</thead>
<tbody>
@for (merchant of displayedMerchants; track merchant.id) {
<tr>
<td>
<div class="d-flex align-items-center">
@if (merchant.logo) {
<img
[src]="merchant.logo"
alt="Logo {{ merchant.name }}"
class="avatar-sm rounded-circle me-2"
onerror="this.style.display='none'"
>
}
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
<ng-icon name="lucideStore" class="text-primary fs-12"></ng-icon>
</div>
<div>
<strong class="d-block">{{ merchant.name }}</strong>
<small class="text-muted">{{ merchant.adresse }}</small>
</div>
</div>
</td>
<td>
@if (merchant.description) {
<span class="text-muted">{{ merchant.description }}</span>
} @else {
<span class="text-muted fst-italic">Aucune description</span>
}
</td>
<td>
<div class="d-flex flex-column">
<small class="text-muted">
<ng-icon name="lucidePhone" class="me-1" size="12"></ng-icon>
{{ merchant.phone }}
</small>
@if (merchant.technicalContacts && merchant.technicalContacts.length > 0) {
<small class="text-muted">
<ng-icon name="lucideUser" class="me-1" size="12"></ng-icon>
{{ merchant.technicalContacts[0].firstName }} {{ merchant.technicalContacts[0].lastName }}
</small>
}
</div>
</td>
<td>
<div class="d-flex flex-column gap-1">
@if (merchant.configs && merchant.configs.length > 0) {
<div class="d-flex flex-wrap gap-1">
@for (config of merchant.configs.slice(0, 2); track config.id) {
<span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-10">
{{ getConfigTypeLabel(config.name) }}
</span>
}
@if (merchant.configs.length > 2) {
<span class="badge bg-secondary">
+{{ merchant.configs.length - 2 }}
</span>
}
</div>
<small class="text-muted">
{{ merchant.configs.length }} configuration(s)
</small>
} @else {
<span class="badge bg-warning bg-opacity-10 text-warning">
Aucune config
</span>
}
</div>
</td>
<td>
@if (merchant.technicalContacts && merchant.technicalContacts.length > 0) {
<div class="d-flex flex-column">
<span class="badge bg-info bg-opacity-10 text-info">
{{ merchant.technicalContacts.length }} contact(s)
</span>
<small class="text-muted">
{{ merchant.technicalContacts[0].email }}
</small>
</div>
} @else {
<span class="badge bg-warning bg-opacity-10 text-warning">
Aucun contact
</span>
}
</td>
<td>
<small class="text-muted">
{{ formatTimestamp(merchant.createdAt!) }}
</small>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button
class="btn btn-outline-primary btn-sm"
(click)="viewMerchantProfile(merchant)"
title="Voir les détails"
>
<ng-icon name="lucideEye"></ng-icon>
</button>
<button
class="btn btn-outline-warning btn-sm"
(click)="editMerchant(merchant)"
title="Modifier le marchand"
>
<ng-icon name="lucideEdit"></ng-icon>
</button>
@if (showDeleteButton) {
<button
class="btn btn-outline-danger btn-sm"
(click)="deleteMerchant(merchant)"
title="Supprimer le marchand"
>
<ng-icon name="lucideTrash2"></ng-icon>
</button>
}
</div>
</td>
</tr>
}
@empty {
<tr>
<td [attr.colspan]="getColumnCount()" class="text-center py-4">
<div class="text-muted">
<ng-icon name="lucideStore" class="fs-1 mb-3 opacity-50"></ng-icon>
<h5 class="mb-2">{{ getEmptyStateTitle() }}</h5>
<p class="mb-3">{{ 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>
</td>
</tr>
}
</tbody>
</table>
<!-- 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
@if (searchTerm || operatorFilter !== 'all') {
<span class="ms-2 badge bg-info">
Filtres actifs
</span>
}
</div>
<nav>
<ngb-pagination
[collectionSize]="totalItems"
[page]="currentPage"
[pageSize]="itemsPerPage"
[maxSize]="5"
[rotate]="true"
[boundaryLinks]="true"
(pageChange)="onPageChange($event)"
[disabled]="loading"
/>
</nav>
</div>
}
<!-- Résumé des résultats -->
<div class="mt-3 pt-3 border-top">
<div class="row text-center">
<div class="col">
<small class="text-muted">
<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">
<small class="text-muted">
<strong>Configurations :</strong> {{ getTotalConfigsCount() }}
</small>
</div>
<div class="col">
<small class="text-muted">
<strong>Contacts :</strong> {{ getTotalContactsCount() }}
</small>
</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> </div>
</app-ui-card> </app-ui-card>

View File

@ -3,7 +3,7 @@ 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 { Observable, Subject, map, of } from 'rxjs'; import { Observable, Subject, of } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators'; import { catchError, takeUntil } from 'rxjs/operators';
import { import {
@ -12,6 +12,8 @@ import {
Operator, Operator,
MerchantUtils, MerchantUtils,
UserRole, UserRole,
PaginatedResponse,
SearchMerchantsParams,
} from '@core/models/merchant-config.model'; } from '@core/models/merchant-config.model';
import { MerchantConfigService } from '../merchant-config.service'; import { MerchantConfigService } from '../merchant-config.service';
@ -55,7 +57,6 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
// Données // Données
allMerchants: Merchant[] = []; allMerchants: Merchant[] = [];
filteredMerchants: Merchant[] = [];
displayedMerchants: Merchant[] = []; displayedMerchants: Merchant[] = [];
// États // États
@ -69,6 +70,7 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
// Pagination // Pagination
currentPage = 1; currentPage = 1;
itemsPerPage = 10; itemsPerPage = 10;
itemsPerPageOptions = [5, 10, 20, 50, 100]; // Options pour le sélecteur
totalItems = 0; totalItems = 0;
totalPages = 0; totalPages = 0;
@ -81,49 +83,9 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
// Permissions // Permissions
currentUserRole: any = null; currentUserRole: any = null;
canViewAllMerchants = false; isHubUser = false;
currentMerchantConfigId: string | undefined;
// ==================== CONVERSION IDS ==================== // Getters
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
get showCreateButton(): boolean { get showCreateButton(): boolean {
return this.canCreateMerchants; return this.canCreateMerchants;
} }
@ -133,14 +95,11 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
} }
getColumnCount(): number { getColumnCount(): number {
return 8; return 7;
} }
ngOnInit() { ngOnInit() {
this.initializeAvailableFilters(); this.initializeAvailableFilters();
}
ngAfterViewInit() {
this.loadCurrentUserPermissions(); this.loadCurrentUserPermissions();
} }
@ -155,10 +114,11 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
.subscribe({ .subscribe({
next: (user) => { next: (user) => {
this.currentUserRole = this.extractUserRole(user); this.currentUserRole = this.extractUserRole(user);
this.isHubUser = this.checkIfHubUser();
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole); if (this.isHubUser) {
this.loadMerchants();
this.loadMerchants(); }
}, },
error: (error) => { error: (error) => {
console.error('Error loading current user permissions:', 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 { private extractUserRole(user: any): any {
const userRoles = this.authService.getCurrentUserRoles(); const userRoles = this.authService.getCurrentUserRoles();
return userRoles && userRoles.length > 0 ? userRoles[0] : null; return userRoles && userRoles.length > 0 ? userRoles[0] : null;
} }
private canViewAllMerchantsCheck(role: any): boolean { private checkIfHubUser(): boolean {
if (!role) return false; if (!this.currentUserRole) return false;
const canViewAllRoles = [ const hubRoles = [
UserRole.DCB_ADMIN, UserRole.DCB_ADMIN,
UserRole.DCB_SUPPORT UserRole.DCB_SUPPORT
]; ];
return canViewAllRoles.includes(role); return hubRoles.includes(this.currentUserRole);
} }
private initializeAvailableFilters() { private initializeAvailableFilters() {
@ -202,30 +151,48 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
} }
loadMerchants() { loadMerchants() {
if (!this.isHubUser) {
console.log('⚠️ User is not a Hub user, merchant list not displayed');
return;
}
this.loading = true; this.loading = true;
this.error = ''; this.error = '';
let merchantsObservable: Observable<Merchant[]>; this.merchantConfigService.getMerchants(
this.currentPage,
if (this.canViewAllMerchants) { this.itemsPerPage,
merchantsObservable = this.getAllMerchants(); this.buildSearchParams()
} else { )
merchantsObservable = this.getMyMerchants();
}
merchantsObservable
.pipe( .pipe(
takeUntil(this.destroy$), takeUntil(this.destroy$),
catchError(error => { catchError(error => {
console.error('Error loading merchants:', error); console.error('Error loading merchants:', error);
this.error = 'Erreur lors du chargement des marchands'; 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({ .subscribe({
next: (merchants) => { next: (response) => {
this.allMerchants = merchants || []; console.log('📊 Pagination response:', {
this.applyFiltersAndPagination(); 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.loading = false;
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
@ -233,39 +200,90 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
this.error = 'Erreur lors du chargement des marchands'; this.error = 'Erreur lors du chargement des marchands';
this.loading = false; this.loading = false;
this.allMerchants = []; this.allMerchants = [];
this.filteredMerchants = [];
this.displayedMerchants = []; this.displayedMerchants = [];
this.totalItems = 0;
this.totalPages = 0;
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }
}); });
} }
private getAllMerchants(): Observable<Merchant[]> { private buildSearchParams(): SearchMerchantsParams {
return this.merchantConfigService.getMerchants(1, 1000).pipe( const params: SearchMerchantsParams = {};
map(response => {
return this.convertMerchantsToFrontend(response.items); if (this.searchTerm.trim()) {
}), params.query = this.searchTerm.trim();
catchError(error => { }
console.error('Error getting all merchants:', error);
return of([]); return params;
})
);
} }
private getMyMerchants(): Observable<Merchant[]> { // ==================== RECHERCHE ET FILTRES ====================
const merchantConfigId = Number(this.currentMerchantConfigId);
return this.merchantConfigService.getMerchantUsers(merchantConfigId).pipe( onSearch() {
map(response => { this.currentPage = 1;
return this.convertMerchantsToFrontend(response); this.loadMerchants();
}),
catchError(error => {
console.error('Error getting all merchants:', error);
return of([]);
})
);
} }
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 ==================== // ==================== ACTIONS ====================
@ -281,110 +299,10 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
this.deleteMerchantRequested.emit(merchant); 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 ==================== // ==================== MÉTHODES STATISTIQUES ====================
getTotalMerchantsCount(): number { getTotalMerchantsCount(): number {
return this.allMerchants.length; return this.totalItems;
} }
getTotalConfigsCount(): number { getTotalConfigsCount(): number {
@ -417,23 +335,20 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
// ==================== MÉTHODES POUR LE TEMPLATE ==================== // ==================== MÉTHODES POUR LE TEMPLATE ====================
refreshData() { refreshData() {
this.currentPage = 1;
this.loadMerchants(); this.loadMerchants();
} }
getCardTitle(): string { getCardTitle(): string {
return this.canViewAllMerchants return 'Tous les Marchands';
? 'Tous les Marchands'
: 'Mes Marchands';
} }
getHelperText(): string { getHelperText(): string {
return this.canViewAllMerchants return 'Vue administrative - Gestion de tous les marchands';
? 'Vue administrative - Gestion de tous les marchands'
: 'Vos marchands partenaires';
} }
getHelperIcon(): string { getHelperIcon(): string {
return this.canViewAllMerchants ? 'lucideShield' : 'lucideStore'; return 'lucideShield';
} }
getLoadingText(): string { getLoadingText(): string {
@ -451,4 +366,8 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
getEmptyStateButtonText(): string { getEmptyStateButtonText(): string {
return 'Créer le premier marchand'; return 'Créer le premier marchand';
} }
shouldDisplayMerchantList(): boolean {
return this.isHubUser;
}
} }

View File

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

View File

@ -1,7 +1,7 @@
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams, HttpErrorResponse } from '@angular/common/http'; import { HttpClient, HttpParams, HttpErrorResponse } from '@angular/common/http';
import { environment } from '@environments/environment'; 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 { import {
Merchant, Merchant,
@ -25,16 +25,25 @@ import {
// SERVICE DE CONVERSION // SERVICE DE CONVERSION
import { MerchantDataAdapter } from './merchant-data-adapter.service'; 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' }) @Injectable({ providedIn: 'root' })
export class MerchantConfigService { export class MerchantConfigService {
private http = inject(HttpClient); private http = inject(HttpClient);
private merchantUsersService = inject(MerchantUsersService);
private dataAdapter = inject(MerchantDataAdapter); private dataAdapter = inject(MerchantDataAdapter);
private baseApiUrl = `${environment.configApiUrl}/merchants`; private baseApiUrl = `${environment.configApiUrl}/merchants`;
private readonly REQUEST_TIMEOUT = 30000; private readonly REQUEST_TIMEOUT = 30000;
private readonly MAX_RETRIES = 2; private readonly MAX_RETRIES = 2;
private merchantsCache: Merchant[] = [];
private cachedTake = 0;
private cacheParams: SearchMerchantsParams | undefined;
// ==================== MERCHANT CRUD OPERATIONS ==================== // ==================== MERCHANT CRUD OPERATIONS ====================
createMerchant(createMerchantDto: CreateMerchantDto): Observable<Merchant> { createMerchant(createMerchantDto: CreateMerchantDto): Observable<Merchant> {
@ -54,46 +63,183 @@ export class MerchantConfigService {
} }
getMerchants(page: number = 1, limit: number = 10, params?: SearchMerchantsParams): Observable<PaginatedResponse<Merchant>> { getMerchants(page: number = 1, limit: number = 10, params?: SearchMerchantsParams): Observable<PaginatedResponse<Merchant>> {
let httpParams = new HttpParams() // Vérifier si le cache est valide
.set('page', page.toString()) const paramsChanged = !this.areParamsEqual(params, this.cacheParams);
.set('limit', limit.toString());
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) { if (params?.query) {
httpParams = httpParams.set('query', params.query.trim()); 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, { return this.http.get<ApiMerchant[]>(this.baseApiUrl, {
params: httpParams params: httpParams
}).pipe( }).pipe(
timeout(this.REQUEST_TIMEOUT), timeout(this.REQUEST_TIMEOUT),
retry(this.MAX_RETRIES), map(apiMerchants =>
map(apiMerchants => { apiMerchants.map(merchant =>
const total = apiMerchants.length; this.dataAdapter.convertApiMerchantToFrontend(merchant)
const totalPages = Math.ceil(total / limit); )
)
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedItems = apiMerchants.slice(startIndex, endIndex);
const response: PaginatedResponse<Merchant> = {
items: paginatedItems.map(apiMerchant =>
this.dataAdapter.convertApiMerchantToFrontend(apiMerchant)
),
total: total,
page: page,
limit: limit,
totalPages: totalPages
};
console.log(`✅ Loaded ${response.items.length} merchants`);
return response;
}),
catchError(error => this.handleError('getMerchants', error))
); );
} }
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 = Math.min(startIndex + limit, total);
const paginatedItems = merchants.slice(startIndex, endIndex);
return {
items: paginatedItems,
total: total,
page: page,
limit: limit,
totalPages: totalPages
};
}
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[]> { getAllMerchants(params?: SearchMerchantsParams): Observable<Merchant[]> {
let httpParams = new HttpParams(); let httpParams = new HttpParams();
@ -176,20 +322,149 @@ export class MerchantConfigService {
); );
} }
getMerchantUsers(merchantId: number): Observable<MerchantUser[]> { getMerchantUsers(
//const merchantId = this.convertIdToNumber(merchantId); merchantId: number,
page: number = 1,
return this.http.get<ApiMerchant>(`${this.baseApiUrl}/${merchantId}/users`).pipe( limit: number = 10,
timeout(this.REQUEST_TIMEOUT), params?: SearchUsersParams
map(apiMerchant => { ): Observable<PaginatedResponse<MerchantUser>> {
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>> { getAllMerchantUsers(page: number = 1, limit: number = 10, params?: SearchMerchantsParams): Observable<PaginatedResponse<MerchantUser>> {
let httpParams = new HttpParams(); let httpParams = new HttpParams();

View File

@ -64,9 +64,10 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
badge: any = { icon: 'lucideSettings', text: 'Merchant Management' }; badge: any = { icon: 'lucideSettings', text: 'Merchant Management' };
// État de l'interface // État de l'interface
activeTab: 'list' | 'profile' = 'list'; activeTab: 'list' | 'merchant-profile' = 'list';
selectedMerchantId: number | null = null; selectedMerchantId: number | null = null;
selectedConfigId: number | null = null; selectedConfigId: number | null = null;
selectedUserId: string | null = null;
// Gestion des permissions // Gestion des permissions
currentUserRole: UserRole | null = null; currentUserRole: UserRole | null = null;
@ -145,15 +146,36 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
merchantUsersError = ''; merchantUsersError = '';
currentMerchantPartnerId: string = ''; currentMerchantPartnerId: string = '';
isInitializing = true;
ngOnInit() { ngOnInit() {
this.loadCurrentUserPermissions(); this.loadCurrentUserPermissions();
this.initializeUserType(); this.initializeUserType();
this.initializeMerchantPartnerContext(); 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 * Initialise le type d'utilisateur
*/ */
@ -181,44 +203,21 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
console.log(`Is Hub User: ${this.isHubUser}`); 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) * Charge le marchand de l'utilisateur (pour les merchant users)
*/ */
private loadUserMerchant(): void { private loadUserMerchant(): void {
if (!this.isMerchantUser || !this.currentMerchantConfigId) return; if (!this.isMerchantUser || !this.currentMerchantConfigId) {
this.isInitializing = false;
this.cdRef.detectChanges();
return;
}
this.loadingUserMerchant = true; this.loadingUserMerchant = true;
const merchantPartnerId = Number(this.currentMerchantConfigId);
const merchantConfigId = Number(this.currentMerchantConfigId);
this.merchantConfigService.getMerchantById(merchantPartnerId)
this.merchantConfigService.getMerchantById(merchantConfigId)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (merchant) => { next: (merchant) => {
@ -226,68 +225,28 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
const frontendMerchant = this.convertMerchantToFrontend(merchant); const frontendMerchant = this.convertMerchantToFrontend(merchant);
this.userMerchantId = frontendMerchant.id!; this.userMerchantId = frontendMerchant.id!;
this.userMerchant = frontendMerchant; this.userMerchant = frontendMerchant;
// Afficher automatiquement le profil du marchand // Vérifier que nous sommes toujours sur l'onglet profil
this.showTab('profile', merchantConfigId); if (this.activeTab === 'merchant-profile') {
this.showTab('merchant-profile', merchantPartnerId);
}
} else { } else {
console.warn('No merchant found for current user'); console.warn('No merchant found for current user');
// Si aucun marchand trouvé, revenir à la liste
this.activeTab = 'list';
} }
this.loadingUserMerchant = false; this.loadingUserMerchant = false;
this.isInitializing = false;
this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
console.error('Error loading user merchant:', error); console.error('Error loading user merchant:', error);
this.loadingUserMerchant = false; this.loadingUserMerchant = false;
} this.isInitializing = false;
}); // Revenir à la liste en cas d'erreur
} this.activeTab = 'list';
this.cdRef.detectChanges();
/**
* 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;
} }
}); });
} }
@ -441,7 +400,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
} }
private extractMerchantConfigId(user: any): string { private extractMerchantConfigId(user: any): string {
return user?.merchantConfigId || ''; return user?.merchantPartnerId || '';
} }
// ==================== PERMISSIONS SPÉCIFIQUES MARCHAND ==================== // ==================== PERMISSIONS SPÉCIFIQUES MARCHAND ====================
@ -503,35 +462,50 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
// ==================== GESTION DES ONGLETS ==================== // ==================== GESTION DES ONGLETS ====================
showTab(tab: 'list' | 'profile', merchantId?: number): void { // Méthode pour changer d'onglet
console.log(`Switching to tab: ${tab}`, merchantId ? `for merchant ${merchantId}` : ''); 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; this.activeTab = tab;
if (tab === 'profile') { if (tab === 'merchant-profile' && merchantId) {
// Déterminer l'ID du marchand à afficher
if (this.isMerchantUser) { if (this.isMerchantUser) {
// Pour les merchant users, toujours utiliser leur propre marchand this.userMerchantId = merchantId;
this.selectedMerchantId = this.userMerchantId || null;
} else { } else {
// Pour les Hub users, utiliser l'ID fourni ou null this.selectedMerchantId = merchantId;
this.selectedMerchantId = merchantId || null;
}
// 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);
}
} 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();
} }
} }
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;
}
if (this.isMerchantUser) {
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 ==================== // ==================== ÉVÉNEMENTS DES COMPOSANTS ENFANTS ====================
onMerchantSelected(merchantId: number): void { 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 { 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 ==================== // ==================== GESTION DES ERREURS ====================
private getCreateErrorMessage(error: any): string { private getCreateErrorMessage(error: any): string {
@ -1026,56 +992,33 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
// Propriété pour le message de succès // Propriété pour le message de succès
successMessage: string = ''; successMessage: string = '';
// ==================== GETTERS TEMPLATE ==================== // ==================== GETTERS TEMPLATE ====================
get showListTab(): boolean { // ==================== GETTERS POUR LES ONGLETS ====================
// Pour les merchant users, toujours montrer l'onglet liste (pour les utilisateurs)
// Pour les Hub users, montrer la liste si autorisé get shouldShowMerchantProfileContent(): boolean {
if (this.isMerchantUser) { if (this.isMerchantUser) {
return true; return !!this.userMerchantId;
} else {
return !!this.selectedMerchantId;
} }
return this.canViewMerchantList;
} }
get showProfileTab(): boolean { getMerchantProfileId(): number | null{
// 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é
if (this.isMerchantUser) { if (this.isMerchantUser) {
return !!this.userMerchantId; // Montrer seulement si le marchand est chargé return this.userMerchantId;
} else {
return this.selectedMerchantId;
} }
return !!this.selectedMerchantId;
} }
get pageTitleForUser(): string { get activeMerchantProfileId(): number | null {
// Retourner l'ID du marchand à afficher dans l'onglet profil
if (this.isMerchantUser) { 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 {
if (this.isMerchantUser) {
return this.activeTab === 'list'
? 'Gérez les utilisateurs de votre marchand'
: 'Gérez la configuration technique de votre marchand';
}
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 { get shouldDisplayMerchantList(): boolean {