dcb-backoffice/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts
2026-01-13 03:49:10 +00:00

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();
}
}