Files
pmu-plateforme-jeux-admin-plr/src/app/dashboard/pages/limits/limits.ts
2025-12-29 13:56:18 +01:00

295 lines
9.3 KiB
TypeScript

import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, ViewChild, effect, signal, untracked, OnInit } from '@angular/core';
import { DataTable, SortState, TableColumn } from '@shared/components/data-table/data-table';
import { Paginator } from '@shared/components/paginator/paginator';
import { SearchBar } from '@shared/components/search-bar/search-bar';
import { Modal } from '@shared/components/modal/modal';
import { ZardButtonComponent } from '@shared/components/button/button.component';
import { SortDir } from '@shared/paging/paging';
import { AgentLimit } from 'src/app/core/interfaces/agent-limit';
import { AgentLimitService } from 'src/app/core/services/agent-limit';
import { LimitForm } from '@shared/forms/limit-form/limit-form';
import { Subject, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
@Component({
standalone: true,
selector: 'app-limits',
templateUrl: './limits.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, DataTable, Paginator, SearchBar, Modal, ZardButtonComponent, LimitForm],
})
export class LimitsPage implements OnInit {
rows = signal<AgentLimit[]>([]);
total = signal(0);
loading = signal(false);
page = signal(1);
size = signal(10);
search = signal('');
sort = signal<SortState>({ key: 'code', dir: 'asc' });
selectedActif = signal<boolean | null>(null);
modalOpen = signal(false);
modalTitle = signal('Nouvelle limite');
editingItem = signal<AgentLimit | null>(null);
// Live search
private searchSubject = new Subject<string>();
@ViewChild(LimitForm) formComp?: LimitForm;
cols: TableColumn<AgentLimit>[] = [
{ key: 'code', label: 'Code', sortable: true },
{ key: 'configCode', label: 'Config', sortable: true },
{ key: 'nom', label: 'Nom', sortable: true },
{
key: 'isDefault',
label: 'Défaut',
cell: (l) =>
l.isDefault
? '<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-primary/10 text-primary text-xs font-medium"><i class="icon-star"></i> Par défaut</span>'
: '<span class="text-muted-foreground">—</span>',
},
{
key: 'actif',
label: 'Actif',
cell: (l) =>
l.actif
? '<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-green-500/10 text-green-600 dark:text-green-400 text-xs font-medium"><i class="icon-check"></i> Actif</span>'
: '<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-gray-500/10 text-gray-600 dark:text-gray-400 text-xs font-medium"><i class="icon-x"></i> Inactif</span>',
},
{
key: 'betMin',
label: 'Min Bet',
cell: (l) => (l.betMin ?? 0).toLocaleString('fr-FR'),
},
{
key: 'betMax',
label: 'Max Bet',
cell: (l) => (l.betMax ?? 0).toLocaleString('fr-FR'),
},
{
key: 'maxBet',
label: 'Max Bet (tx)',
cell: (l) => (l.maxBet ?? 0).toLocaleString('fr-FR'),
},
{
key: 'maxDisburseBet',
label: 'Max Disburse',
cell: (l) => (l.maxDisburseBet ?? 0).toLocaleString('fr-FR'),
},
{
key: 'airtimeMin',
label: 'Airtime Min',
cell: (l) => (l.airtimeMin ?? 0).toLocaleString('fr-FR'),
},
{
key: 'airtimeMax',
label: 'Airtime Max',
cell: (l) => (l.airtimeMax ?? 0).toLocaleString('fr-FR'),
},
];
constructor(private api: AgentLimitService) {
effect(() => {
// Only trigger fetch when page, size, or sort changes (not search - handled by searchSubject)
const searchValue = this.search();
const params = {
page: this.page(),
size: this.size(),
search: searchValue,
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
};
// Only fetch if search is empty (search is handled by searchSubject)
if (!searchValue.trim()) {
untracked(() => this.fetch(params));
}
});
// Setup live search with debounce
this.searchSubject
.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((query) => {
if (query.trim()) {
// Use search API which returns array
return this.api.search(query);
} else {
// If empty, use normal list
return this.api.list({
page: this.page(),
size: this.size(),
search: '',
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
}).pipe(
switchMap((res) => {
// Convert PagedResult to array for consistency
return of(res.content);
})
);
}
})
)
.subscribe({
next: (res) => {
// Search API always returns array
if (Array.isArray(res)) {
this.rows.set(res);
this.total.set(res.length);
}
this.loading.set(false);
},
error: (err) => {
console.error('Search error:', err);
this.rows.set([]);
this.total.set(0);
this.loading.set(false);
},
});
}
ngOnInit() {
// Initial fetch
this.fetch({
page: this.page(),
size: this.size(),
search: this.search(),
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
});
}
private fetch(params: { page: number; size: number; search: string; sortKey: string; sortDir: SortDir }) {
// Don't fetch if there's a search query - it's handled by searchSubject
const searchQuery = params.search.trim();
if (searchQuery) {
return; // Search is handled by searchSubject subscription
}
this.loading.set(true);
const actif = this.selectedActif();
if (actif !== null) {
// Filter by actif status - returns array
this.api.getByActif(actif).subscribe({
next: (res: AgentLimit[]) => {
this.rows.set(res);
this.total.set(res.length);
this.loading.set(false);
},
error: () => {
this.rows.set([]);
this.total.set(0);
this.loading.set(false);
},
});
} else {
// Normal list with pagination
this.api.list(params).subscribe({
next: (res) => {
this.rows.set(res.content);
this.total.set(res.pageable.total);
this.loading.set(false);
},
error: () => {
this.rows.set([]);
this.total.set(0);
this.loading.set(false);
},
});
}
}
onSearch(q: string) {
this.search.set(q);
this.page.set(1);
// Trigger search via subject for live search
if (q.trim()) {
this.loading.set(true);
this.searchSubject.next(q);
} else {
// If empty, fetch normally
this.fetch({
page: this.page(),
size: this.size(),
search: '',
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
});
}
}
onActifFilter(actif: boolean | null) {
this.selectedActif.set(actif);
this.page.set(1);
this.fetch({
page: this.page(),
size: this.size(),
search: this.search(),
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
});
}
openCreate() { this.modalTitle.set('Nouvelle limite'); this.editingItem.set(null); queueMicrotask(() => this.modalOpen.set(true)); }
openEdit(row: AgentLimit) { this.modalTitle.set('Modifier la limite'); this.editingItem.set(row); queueMicrotask(() => this.modalOpen.set(true)); }
closeModal() { this.modalOpen.set(false); }
submitChildForm() { this.formComp?.onSubmit(); }
onFormSave(payload: Partial<AgentLimit>) {
const current = this.editingItem();
const isSettingDefault = payload.isDefault === true;
const wasDefault = current?.isDefault;
// If setting as default and it wasn't default before, show confirmation
if (isSettingDefault && !wasDefault) {
if (!confirm('Définir cette limite comme limite par défaut ?\n\nTous les agents recevront automatiquement cette limite, et l\'ancienne limite par défaut perdra son statut.')) {
return;
}
}
const req$ = current?.id ? this.api.update(current.id, payload) : this.api.create(payload as Omit<AgentLimit, 'id'>);
req$.subscribe({
next: (result) => {
if (!result && current?.id) {
// Update failed
alert('Erreur lors de la sauvegarde de la limite');
return;
}
this.closeModal();
this.fetch({
page: this.page(),
size: this.size(),
search: this.search(),
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
});
if (isSettingDefault && !wasDefault) {
alert('La limite a été définie comme limite par défaut. Tous les agents ont été mis à jour.');
}
},
error: (err) => {
console.error('Error saving limit:', err);
alert('Erreur lors de la sauvegarde de la limite');
},
});
}
remove(row: AgentLimit) {
if (!confirm(`Supprimer la limite ${row.code} ?`)) return;
this.api.delete(row.id).subscribe(() => {
this.fetch({
page: this.page(),
size: this.size(),
search: this.search(),
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
});
});
}
}