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 { ZardMenuModule } from '@shared/components/menu/menu.module'; import { ZardTooltipModule } from '@shared/components/tooltip/tooltip'; import { SortDir } from '@shared/paging/paging'; import { TpeDevice, TpeStatus } from 'src/app/core/interfaces/tpe'; import { TpeService } from 'src/app/core/services/tpe'; import { TpeForm } from '@shared/forms/tpe-form/tpe-form'; import { Agent } from 'src/app/core/interfaces/agent'; import { AgentService } from 'src/app/core/services/agent'; import { ZardSelectComponent } from '@shared/components/select/select.component'; import { ZardSelectItemComponent } from '@shared/components/select/select-item.component'; import { ZardFormModule } from '@shared/components/form/form.module'; import { forkJoin, Subject } from 'rxjs'; import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; import { toast } from 'ngx-sonner'; import { PointsVenteService } from 'src/app/core/services/points-vente'; @Component({ standalone: true, selector: 'app-tpe-list', templateUrl: './tpe.html', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ CommonModule, DataTable, Paginator, SearchBar, Modal, ZardButtonComponent, TpeForm, ZardMenuModule, ZardTooltipModule, ZardSelectComponent, ZardSelectItemComponent, ZardFormModule, ], }) export class TpePage implements OnInit { rows = signal([]); total = signal(0); loading = signal(false); page = signal(0); perPage = signal(10); search = signal(''); sort = signal({ key: 'id', dir: 'asc' }); selectedStatut = signal(null); modalOpen = signal(false); modalTitle = signal('Nouvel équipement'); editingItem = signal(null); // Agent assignment modal assignModalOpen = signal(false); assigningTpe = signal(null); agents = signal([]); selectedAgentId = signal(''); agentsLoading = signal(false); // Stats statsByStatut = signal>({}); assignmentStats = signal({ total: 0, assignes: 0, disponibles: 0 }); statsLoading = signal(false); // Live search private searchSubject = new Subject(); @ViewChild(TpeForm) formComp?: TpeForm; formatStatut(statut: string): string { const statutMap: Record = { VALIDE: 'Valide', INVALIDE: 'Invalide', EN_PANNE: 'En panne', BLOQUE: 'Bloqué', DISPONIBLE: 'Disponible', AFFECTE: 'Affecté', EN_MAINTENANCE: 'En maintenance', HORS_SERVICE: 'Hors service', VOLE: 'Volé', }; return statutMap[statut] || statut; } cols: TableColumn[] = [ { key: 'numeroSerie', label: 'Numéro de série', sortable: true }, { key: 'pointDeVenteId', label: 'Point de vente', sortable: true, cell:(p) => { let pointVente = signal(''); this.pointVenteService.getById(p.pointDeVenteId).subscribe({ next: (pdv)=>{ if(!pdv || pdv === undefined){ pointVente.set("Aucun point") return; }; pointVente.set(`${pdv.ville}/${pdv.adresse}`) }, error:(err)=>{ pointVente.set('Aucun point') } }) return pointVente(); }, }, { key: 'versionLogicielle', label: 'Version', sortable: true }, { key: 'typeTerminal', label: 'Type', sortable: true }, { key: 'systemeExploitation', label: 'Sytème', sortable: true }, { key: 'statut', label: 'Statut', sortable: true, cell: (d) => this.formatStatut(d.statut) }, { key: 'assigne', label: 'Assigné à', cell: (d) => { if (!d.agentConnecteId) { return 'Non assigné'; } // a rectifier apres avec les données des agents! const agent = {} as any; const code = agent?.code ? `${agent.code}` : ''; const name = agent.nom && agent.prenom ? `
${agent.nom} ${agent.prenom}
` : agent.nom || agent.prenom ? `
${agent.nom || agent.prenom}
` : ''; const phone = agent.phone ? `
${agent.phone}
` : ''; const zone = agent.zone ? `
Zone: ${agent.zone}
` : ''; const parts = [code, name, phone, zone].filter(Boolean); if (parts.length === 0) { return 'Agent assigné'; } return `
${parts.join('')}
`; }, }, ]; allStatuses: TpeStatus[] = ['ACTIF', 'HORS_SERVICE']; constructor( private api: TpeService, private agentService: AgentService, private pointVenteService: PointsVenteService ) { effect(() => { // Only trigger fetch when page, perPage, or sort changes (not search - handled by searchSubject) const searchValue = this.search(); const params = { page: this.page(), size: this.perPage(), 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()) { return this.api.search(query); } else { // If empty, use normal list return this.api.list({ page: this.page(), size: this.perPage(), search: '', sortKey: this.sort().key, sortDir: this.sort().dir as SortDir, }); } }) ) .subscribe({ next: (res) => { if (Array.isArray(res)) { // Search API returns array this.rows.set(res); this.total.set(res.length); } else { // List returns PagedResult this.rows.set(res.content); this.total.set(res.pageable.total); } this.loading.set(false); }, error: (err) => { console.error('Search error:', err); this.rows.set([]); this.total.set(0); this.loading.set(false); }, }); } ngOnInit() { this.loadStats(); // Initial fetch if no search query if (!this.search().trim()) { this.fetch({ page: this.page(), size: this.perPage(), search: '', sortKey: this.sort().key, sortDir: this.sort().dir as SortDir, }); } } loadStats() { this.statsLoading.set(true); forkJoin({ byStatut: this.api.getCountByStatut(), assignes: this.api.getAssignesStats(), }).subscribe({ next: ({ byStatut, assignes }) => { this.statsByStatut.set(byStatut || {}); // Calculate total from statsByStatut const total = Object.values(byStatut || {}).reduce((sum, count) => sum + count, 0); // Calculate disponibles (total - assignes) const disponibles = Math.max(0, total - (assignes || 0)); this.assignmentStats.set({ total: total, assignes: assignes || 0, disponibles: disponibles, }); this.statsLoading.set(false); }, error: (err) => { console.error('Error loading stats:', err); this.statsByStatut.set({}); this.assignmentStats.set({ total: 0, assignes: 0, disponibles: 0 }); this.statsLoading.set(false); }, }); } 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 statut = this.selectedStatut(); if (statut) { // Filter by statut - returns array this.api.getByStatut(statut).subscribe({ next: (res: TpeDevice[]) => { 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.perPage(), search: '', sortKey: this.sort().key, sortDir: this.sort().dir as SortDir, }); } } onStatutFilter(statut: TpeStatus | null) { this.selectedStatut.set(statut); this.page.set(1); this.fetch({ page: this.page(), size: this.perPage(), search: this.search(), sortKey: this.sort().key, sortDir: this.sort().dir as SortDir, }); } onUpdateStatut(row: TpeDevice, newStatut: TpeStatus) { if (!confirm(`Changer le statut de ${row.numeroSerie} vers ${this.formatStatut(newStatut)} ?`)) return; this.api.updateStatut(row.id, newStatut).subscribe({ next: () => { this.fetch({ page: this.page(), size: this.perPage(), search: this.search(), sortKey: this.sort().key, sortDir: this.sort().dir as SortDir, }); this.loadStats(); }, }); } onLiberer(row: TpeDevice) { if (!confirm(`Libérer le TPE ${row.numeroSerie} ?`)) return; this.api.liberer(row.id).subscribe({ next: () => { this.fetch({ page: this.page(), size: this.perPage(), search: this.search(), sortKey: this.sort().key, sortDir: this.sort().dir as SortDir, }); this.loadStats(); }, }); } onAssigner(row: TpeDevice) { this.assigningTpe.set(row); this.selectedAgentId.set(''); this.loadAgents(); this.assignModalOpen.set(true); } loadAgents() { this.agentsLoading.set(true); // Load active agents only this.agentService.getByStatut('ACTIF').subscribe({ next: (agents) => { this.agents.set(agents); this.agentsLoading.set(false); }, error: () => { this.agents.set([]); this.agentsLoading.set(false); }, }); } confirmAssign() { const tpe = this.assigningTpe(); const agentId = this.selectedAgentId(); if (!tpe || !agentId) { alert('Veuillez sélectionner un agent'); return; } this.api.assigner(tpe.id, agentId).subscribe({ next: () => { this.assignModalOpen.set(false); this.assigningTpe.set(null); this.selectedAgentId.set(''); this.fetch({ page: this.page(), size: this.perPage(), search: this.search(), sortKey: this.sort().key, sortDir: this.sort().dir as SortDir, }); this.loadStats(); }, error: () => { alert("Erreur lors de l'assignation du TPE"); }, }); } closeAssignModal() { this.assignModalOpen.set(false); this.assigningTpe.set(null); this.selectedAgentId.set(''); } openCreate() { this.modalTitle.set('Nouvel équipement'); this.editingItem.set(null); queueMicrotask(() => this.modalOpen.set(true)); } openEdit(row: TpeDevice) { this.modalTitle.set("Modifier l'équipement"); this.editingItem.set(row); queueMicrotask(() => this.modalOpen.set(true)); } closeModal() { this.modalOpen.set(false); setTimeout(() => { this.editingItem.set(null); }, 0); } submitChildForm() { this.formComp?.onSubmit(); } onFormSave(payload: Partial) { const current = this.editingItem(); const isCreating = !current?.id; const req$ = current?.id ? this.api.update(current.id, payload) : this.api.create(payload as Omit); req$.subscribe({ next: (result) => { toast.success('Tpe créé avec succès!'); // For update, check if result is valid (update can return undefined on error) if (current?.id && !result) { console.error('Update failed - result is undefined'); // Don't close modal, let user retry return; } // Success - close modal first this.modalOpen.set(false); // Then reset form and clear editing item after a short delay setTimeout(() => { this.editingItem.set(null); this.formComp?.resetForm(); }, 100); // Refresh data this.fetch({ page: this.page(), size: this.perPage(), search: this.search(), sortKey: this.sort().key, sortDir: this.sort().dir as SortDir, }); this.loadStats(); }, error: (err) => { console.error('Error saving TPE:', err); // Don't close modal on error, let user fix and retry // Form stays filled so user can correct and resubmit }, }); } remove(row: TpeDevice) { if (!confirm(`Supprimer l\'équipement IMEI ${row.numeroSerie} ?`)) return; this.api.delete(row.id).subscribe(() => { this.fetch({ page: this.page(), size: this.perPage(), search: this.search(), sortKey: this.sort().key, sortDir: this.sort().dir as SortDir, }); this.loadStats(); }); } }