547 lines
15 KiB
TypeScript
547 lines
15 KiB
TypeScript
import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, Input, OnDestroy } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { NgIcon } from '@ng-icons/core';
|
|
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
|
import { Observable, Subject, of } from 'rxjs';
|
|
import { catchError, map, takeUntil, tap } from 'rxjs/operators';
|
|
|
|
import {
|
|
Merchant,
|
|
ConfigType,
|
|
Operator,
|
|
MerchantUtils,
|
|
UserRole,
|
|
PaginatedResponse,
|
|
SearchMerchantsParams,
|
|
} from '@core/models/merchant-config.model';
|
|
|
|
import { MerchantConfigService } from '../merchant-config.service';
|
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
|
import { AuthService } from '@core/services/auth.service';
|
|
import { UiCard } from '@app/components/ui-card';
|
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
import { MinioService } from '@core/services/minio.service';
|
|
|
|
@Component({
|
|
selector: 'app-merchant-config-list',
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
FormsModule,
|
|
NgIcon,
|
|
UiCard,
|
|
NgbPaginationModule
|
|
],
|
|
templateUrl: './merchant-config-list.html',
|
|
})
|
|
export class MerchantConfigsList implements OnInit, OnDestroy {
|
|
private authService = inject(AuthService);
|
|
private merchantConfigService = inject(MerchantConfigService);
|
|
protected roleService = inject(RoleManagementService);
|
|
private cdRef = inject(ChangeDetectorRef);
|
|
private destroy$ = new Subject<void>();
|
|
|
|
private minioService = inject(MinioService);
|
|
|
|
// Cache des URLs de logos
|
|
private logoUrlCache = new Map<string, string>();
|
|
// Ajouter un cache pour les logos non trouvés
|
|
private logoErrorCache = new Set<string>();
|
|
|
|
// Configuration
|
|
readonly ConfigType = ConfigType;
|
|
readonly Operator = Operator;
|
|
readonly MerchantUtils = MerchantUtils;
|
|
|
|
// Inputs
|
|
@Input() canCreateMerchants: boolean = false;
|
|
@Input() canDeleteMerchants: boolean = false;
|
|
|
|
// Outputs
|
|
@Output() merchantSelected = new EventEmitter<number>();
|
|
@Output() openCreateMerchantModal = new EventEmitter<void>();
|
|
@Output() editMerchantRequested = new EventEmitter<Merchant>();
|
|
@Output() deleteMerchantRequested = new EventEmitter<Merchant>();
|
|
|
|
// Données
|
|
allMerchants: Merchant[] = [];
|
|
displayedMerchants: Merchant[] = [];
|
|
|
|
// États
|
|
loading = false;
|
|
error = '';
|
|
|
|
// Recherche et filtres
|
|
searchTerm = '';
|
|
operatorFilter: Operator | 'all' = 'all';
|
|
|
|
// Pagination
|
|
currentPage = 1;
|
|
itemsPerPage = 10;
|
|
itemsPerPageOptions = [5, 10, 20, 50, 100]; // Options pour le sélecteur
|
|
totalItems = 0;
|
|
totalPages = 0;
|
|
|
|
// Tri
|
|
sortField: keyof Merchant = 'name';
|
|
sortDirection: 'asc' | 'desc' = 'asc';
|
|
|
|
// Filtres disponibles
|
|
availableOperators: { value: Operator | 'all'; label: string }[] = [];
|
|
|
|
// Permissions
|
|
currentUserRole: any = null;
|
|
isHubUser = false;
|
|
|
|
// Getters
|
|
get showCreateButton(): boolean {
|
|
return this.canCreateMerchants;
|
|
}
|
|
|
|
get showDeleteButton(): boolean {
|
|
return this.canDeleteMerchants;
|
|
}
|
|
|
|
getColumnCount(): number {
|
|
return 7;
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.initializeAvailableFilters();
|
|
this.loadCurrentUserPermissions();
|
|
}
|
|
|
|
private loadCurrentUserPermissions() {
|
|
this.authService.getUserProfile()
|
|
.pipe(takeUntil(this.destroy$))
|
|
.subscribe({
|
|
next: (user) => {
|
|
this.currentUserRole = this.extractUserRole(user);
|
|
this.isHubUser = this.checkIfHubUser();
|
|
|
|
if (this.isHubUser) {
|
|
this.loadMerchants();
|
|
}
|
|
},
|
|
error: (error) => {
|
|
console.error('Error loading current user permissions:', error);
|
|
this.loadMerchants();
|
|
}
|
|
});
|
|
}
|
|
|
|
private extractUserRole(user: any): any {
|
|
const userRoles = this.authService.getCurrentUserRoles();
|
|
return userRoles && userRoles.length > 0 ? userRoles[0] : null;
|
|
}
|
|
|
|
private checkIfHubUser(): boolean {
|
|
if (!this.currentUserRole) return false;
|
|
|
|
const hubRoles = [
|
|
UserRole.DCB_ADMIN,
|
|
UserRole.DCB_SUPPORT
|
|
];
|
|
|
|
return hubRoles.includes(this.currentUserRole);
|
|
}
|
|
|
|
private initializeAvailableFilters() {
|
|
this.availableOperators = [
|
|
{ value: 'all', label: 'Tous les opérateurs' },
|
|
{ value: Operator.ORANGE_OSN, label: 'Orange' }
|
|
];
|
|
}
|
|
|
|
loadMerchants(): void {
|
|
if (!this.isHubUser) {
|
|
console.log('⚠️ User is not a Hub user, merchant list not displayed');
|
|
return;
|
|
}
|
|
|
|
this.loading = true;
|
|
this.error = '';
|
|
|
|
const params = this.buildSearchParams();
|
|
const skip = (this.currentPage - 1) * this.itemsPerPage;
|
|
|
|
this.merchantConfigService.getAllMerchants(this.currentPage, this.itemsPerPage, params)
|
|
.pipe(
|
|
takeUntil(this.destroy$),
|
|
catchError(error => {
|
|
console.error('❌ Error loading merchants:', error);
|
|
this.error = 'Erreur lors du chargement des marchands';
|
|
return of({
|
|
items: [],
|
|
total: 0,
|
|
page: this.currentPage,
|
|
limit: this.itemsPerPage,
|
|
totalPages: 0
|
|
} as PaginatedResponse<Merchant>);
|
|
})
|
|
)
|
|
.subscribe(response => {
|
|
this.allMerchants = response.items || [];
|
|
this.displayedMerchants = response.items || [];
|
|
this.totalItems = response.total || 0;
|
|
this.totalPages = response.totalPages || Math.ceil((response.total || 0) / this.itemsPerPage);
|
|
|
|
this.loading = false;
|
|
this.cdRef.detectChanges();
|
|
|
|
console.log('📊 Pagination response:', {
|
|
page: response.page,
|
|
total: response.total,
|
|
totalPages: this.totalPages,
|
|
itemsCount: response.items?.length,
|
|
limit: response.limit
|
|
});
|
|
});
|
|
}
|
|
|
|
// ==================== AFFICHAGE DU LOGO ====================
|
|
|
|
|
|
/**
|
|
* Récupère l'URL du logo avec fallback automatique
|
|
*/
|
|
getMerchantLogoUrl(
|
|
merchantId: number | undefined,
|
|
logoFileName: string,
|
|
merchantName: string
|
|
): Observable<string> {
|
|
|
|
const newMerchantId = String(merchantId);
|
|
|
|
const cacheKey = `${merchantId}_${logoFileName}`;
|
|
|
|
// Vérifier si le logo est en cache d'erreur
|
|
if (this.logoErrorCache.has(cacheKey)) {
|
|
const defaultLogo = this.getDefaultLogoUrl(merchantName);
|
|
return of(defaultLogo);
|
|
}
|
|
|
|
// Vérifier le cache normal
|
|
if (this.logoUrlCache.has(cacheKey)) {
|
|
return of(this.logoUrlCache.get(cacheKey)!);
|
|
}
|
|
|
|
// Récupérer l'URL depuis l'API avec la nouvelle structure
|
|
return this.minioService.getMerchantLogoUrl(
|
|
newMerchantId,
|
|
logoFileName,
|
|
{ signed: true, expirySeconds: 3600 }
|
|
).pipe(
|
|
map(response => {
|
|
// Extraire l'URL de la réponse
|
|
const url = response.data.url ;
|
|
|
|
// Mettre en cache avec la clé composite
|
|
this.logoUrlCache.set(cacheKey, url);
|
|
|
|
return url;
|
|
}),
|
|
catchError(error => {
|
|
console.warn(`⚠️ Logo not found for merchant ${merchantId}: ${logoFileName}`, error);
|
|
|
|
// En cas d'erreur, ajouter au cache d'erreur
|
|
this.logoErrorCache.add(cacheKey);
|
|
|
|
// Générer un logo par défaut
|
|
const defaultLogo = this.getDefaultLogoUrl(merchantName);
|
|
|
|
// Mettre le logo par défaut dans le cache normal aussi
|
|
this.logoUrlCache.set(cacheKey, defaultLogo);
|
|
|
|
return of(defaultLogo);
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Génère une URL de logo par défaut basée sur les initiales
|
|
*/
|
|
getDefaultLogoUrl(merchantName: string): string {
|
|
// Créer des initiales significatives
|
|
const initials = this.extractInitials(merchantName);
|
|
|
|
// Palette de couleurs agréables
|
|
const colors = [
|
|
'667eea', // Violet
|
|
'764ba2', // Violet foncé
|
|
'f56565', // Rouge
|
|
'4299e1', // Bleu
|
|
'48bb78', // Vert
|
|
'ed8936', // Orange
|
|
'FF6B6B', // Rouge clair
|
|
'4ECDC4', // Turquoise
|
|
'45B7D1', // Bleu clair
|
|
'96CEB4' // Vert menthe
|
|
];
|
|
|
|
const colorIndex = merchantName.length % colors.length;
|
|
const backgroundColor = colors[colorIndex];
|
|
|
|
// Taille fixe à 80px (l'API génère un carré de cette taille)
|
|
// L'image sera redimensionnée à 40px via CSS
|
|
return `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&background=${backgroundColor}&color=FFFFFF&size=80`;
|
|
}
|
|
|
|
/**
|
|
* Extrait les initiales de manière intelligente
|
|
*/
|
|
private extractInitials(name: string): string {
|
|
if (!name || name.trim() === '') {
|
|
return '??';
|
|
}
|
|
|
|
// Nettoyer le nom
|
|
const cleanedName = name.trim().toUpperCase();
|
|
|
|
// Extraire les mots
|
|
const words = cleanedName.split(/\s+/);
|
|
|
|
// Si un seul mot, prendre les deux premières lettres
|
|
if (words.length === 1) {
|
|
return words[0].substring(0, 2) || '??';
|
|
}
|
|
|
|
// Prendre la première lettre des deux premiers mots
|
|
const initials = words
|
|
.slice(0, 2) // Prendre les 2 premiers mots
|
|
.map(word => word[0] || '')
|
|
.join('');
|
|
|
|
return initials || name.substring(0, 2).toUpperCase() || '??';
|
|
}
|
|
|
|
/**
|
|
* Gère les erreurs de chargement des logos MinIO
|
|
*/
|
|
onLogoError(event: Event, merchantName: string): void {
|
|
const img = event.target as HTMLImageElement;
|
|
|
|
if (!img) return;
|
|
|
|
console.warn('Logo MinIO failed to load, using default for:', merchantName);
|
|
|
|
img.onerror = null;
|
|
img.src = this.getDefaultLogoUrl(merchantName);
|
|
}
|
|
|
|
/**
|
|
* Gère les erreurs de chargement des logos par défaut
|
|
*/
|
|
onDefaultLogoError(event: Event | string): void {
|
|
if (!(event instanceof Event)) {
|
|
console.error('Default logo error (non-event):', event);
|
|
return;
|
|
}
|
|
|
|
const img = event.target as HTMLImageElement | null;
|
|
if (!img) return;
|
|
|
|
console.error('Default logo also failed to load, using fallback SVG');
|
|
|
|
// SVG local
|
|
img.onerror = null; // éviter boucle infinie
|
|
img.src = 'assets/images/default-merchant-logo.svg';
|
|
|
|
// Dernier recours
|
|
img.onerror = (e) => {
|
|
if (!(e instanceof Event)) return;
|
|
const fallbackImg = e.target as HTMLImageElement | null;
|
|
if (!fallbackImg) return;
|
|
|
|
fallbackImg.onerror = null;
|
|
fallbackImg.src = this.generateFallbackDataUrl();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Génère un fallback SVG en data URL
|
|
*/
|
|
private generateFallbackDataUrl(): string {
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
|
|
<rect width="40" height="40" fill="#667eea" rx="20"/>
|
|
<text x="20" y="22" text-anchor="middle" fill="white" font-family="Arial" font-size="14" font-weight="bold">?</text>
|
|
</svg>`;
|
|
|
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
}
|
|
|
|
private buildSearchParams(): SearchMerchantsParams {
|
|
const params: SearchMerchantsParams = {};
|
|
|
|
if (this.searchTerm.trim()) {
|
|
params.query = this.searchTerm.trim();
|
|
}
|
|
|
|
return params;
|
|
}
|
|
|
|
// ==================== RECHERCHE ET FILTRES ====================
|
|
|
|
onSearch() {
|
|
this.currentPage = 1;
|
|
this.loadMerchants();
|
|
}
|
|
|
|
onClearFilters() {
|
|
this.searchTerm = '';
|
|
this.operatorFilter = 'all';
|
|
this.currentPage = 1;
|
|
this.loadMerchants();
|
|
}
|
|
|
|
onFilterChange() {
|
|
this.currentPage = 1;
|
|
this.loadMerchants();
|
|
}
|
|
|
|
filterByOperator(operator: Operator | 'all') {
|
|
this.operatorFilter = operator;
|
|
this.currentPage = 1;
|
|
this.loadMerchants();
|
|
}
|
|
|
|
// ==================== GESTION DU NOMBRE D'ÉLÉMENTS PAR PAGE ====================
|
|
|
|
onItemsPerPageChange() {
|
|
console.log(`📊 Changing items per page to: ${this.itemsPerPage}`);
|
|
this.currentPage = 1; // Retour à la première page
|
|
this.loadMerchants();
|
|
}
|
|
|
|
// ==================== TRI ====================
|
|
|
|
sort(field: keyof Merchant) {
|
|
if (this.sortField === field) {
|
|
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
this.sortField = field;
|
|
this.sortDirection = 'asc';
|
|
}
|
|
this.currentPage = 1;
|
|
this.loadMerchants();
|
|
}
|
|
|
|
getSortIcon(field: string): string {
|
|
if (this.sortField !== field) return 'lucideArrowUpDown';
|
|
return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown';
|
|
}
|
|
|
|
// ==================== PAGINATION ====================
|
|
|
|
onPageChange(page: number) {
|
|
console.log(`📄 Page changed to: ${page}`);
|
|
this.currentPage = page;
|
|
this.loadMerchants();
|
|
}
|
|
|
|
getStartIndex(): number {
|
|
return this.totalItems > 0 ? (this.currentPage - 1) * this.itemsPerPage + 1 : 0;
|
|
}
|
|
|
|
getEndIndex(): number {
|
|
return Math.min(this.currentPage * this.itemsPerPage, this.totalItems);
|
|
}
|
|
|
|
// ==================== ACTIONS ====================
|
|
|
|
viewMerchantProfile(merchant: Merchant) {
|
|
this.merchantSelected.emit(merchant.id!);
|
|
}
|
|
|
|
editMerchant(merchant: Merchant) {
|
|
this.editMerchantRequested.emit(merchant);
|
|
}
|
|
|
|
deleteMerchant(merchant: Merchant) {
|
|
this.deleteMerchantRequested.emit(merchant);
|
|
}
|
|
|
|
// ==================== MÉTHODES STATISTIQUES ====================
|
|
|
|
getTotalMerchantsCount(): number {
|
|
return this.totalItems;
|
|
}
|
|
|
|
getTotalConfigsCount(): number {
|
|
return this.allMerchants.reduce((total, merchant) =>
|
|
total + (merchant.configs ? merchant.configs.length : 0), 0);
|
|
}
|
|
|
|
getTotalContactsCount(): number {
|
|
return this.allMerchants.reduce((total, merchant) =>
|
|
total + (merchant.technicalContacts ? merchant.technicalContacts.length : 0), 0);
|
|
}
|
|
|
|
// ==================== MÉTHODES D'AFFICHAGE ====================
|
|
|
|
getConfigTypeLabel(configName: ConfigType | string): string {
|
|
return MerchantUtils.getConfigTypeName(configName);
|
|
}
|
|
|
|
formatTimestamp(timestamp: string): string {
|
|
if (!timestamp) return 'Non disponible';
|
|
return new Date(timestamp).toLocaleDateString('fr-FR', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
// ==================== MÉTHODES POUR LE TEMPLATE ====================
|
|
|
|
refreshData() {
|
|
this.currentPage = 1;
|
|
this.loadMerchants();
|
|
}
|
|
|
|
getCardTitle(): string {
|
|
return 'Tous les Marchands';
|
|
}
|
|
|
|
getHelperText(): string {
|
|
return 'Vue administrative - Gestion de tous les marchands';
|
|
}
|
|
|
|
getHelperIcon(): string {
|
|
return 'lucideShield';
|
|
}
|
|
|
|
getLoadingText(): string {
|
|
return 'Chargement des marchands...';
|
|
}
|
|
|
|
getEmptyStateTitle(): string {
|
|
return 'Aucun marchand trouvé';
|
|
}
|
|
|
|
getEmptyStateDescription(): string {
|
|
return 'Aucun marchand ne correspond à vos critères de recherche.';
|
|
}
|
|
|
|
getEmptyStateButtonText(): string {
|
|
return 'Créer le premier marchand';
|
|
}
|
|
|
|
shouldDisplayMerchantList(): boolean {
|
|
return this.isHubUser;
|
|
}
|
|
|
|
|
|
|
|
ngOnDestroy(): void {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
|
|
// Nettoyer les caches
|
|
this.logoUrlCache.clear();
|
|
this.logoErrorCache.clear();
|
|
}
|
|
} |