feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
d88311deaa
commit
7a06403f85
@ -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 ===
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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>();
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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)"
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user