import { Component, ElementRef, ViewChild, Inject, PLATFORM_ID, AfterViewInit, ChangeDetectorRef } from '@angular/core'; import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient, HttpClientModule, HttpParams } from '@angular/common/http'; type TrocaKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataTroca' | 'motivo' | 'observacao'; interface TrocaRow { id: string; item: string; linhaAntiga: string; linhaNova: string; iccid: string; dataTroca: string; motivo: string; observacao: string; raw: any; } interface ApiPagedResult { page?: number; pageSize?: number; total?: number; items?: T[]; } interface GroupItem { key: string; // aqui é o MOTIVO total: number; trocas: number; comIccid: number; semIccid: number; } /** ✅ DTO da linha do GERAL (para selects) */ interface LineOptionDto { id: string; item: number; linha: string | null; chip: string | null; cliente: string | null; usuario: string | null; skil: string | null; } @Component({ standalone: true, imports: [CommonModule, FormsModule, HttpClientModule], templateUrl: './troca-numero.html', styleUrls: ['./troca-numero.scss'] }) export class TrocaNumero implements AfterViewInit { toastMessage = ''; loading = false; @ViewChild('successToast', { static: false }) successToast!: ElementRef; constructor( @Inject(PLATFORM_ID) private platformId: object, private http: HttpClient, private cdr: ChangeDetectorRef ) {} private readonly apiBase = 'https://localhost:7205/api/trocanumero'; /** ✅ base do GERAL (para buscar clientes/linhas no modal) */ private readonly linesApiBase = 'https://localhost:7205/api/lines'; // ====== DATA ====== groups: GroupItem[] = []; pagedGroups: GroupItem[] = []; expandedGroup: string | null = null; groupRows: TrocaRow[] = []; private rowsByKey = new Map(); // KPIs groupLoadedRecords = 0; groupTotalTrocas = 0; groupTotalIccids = 0; // ====== FILTERS & PAGINATION ====== searchTerm = ''; private searchTimer: any = null; page = 1; pageSize = 10; total = 0; // ====== EDIT MODAL ====== editOpen = false; editSaving = false; editModel: any = null; // ====== CREATE MODAL ====== createOpen = false; createSaving = false; createModel: any = { item: '', linhaAntiga: '', linhaNova: '', iccid: '', dataTroca: '', motivo: '', observacao: '' }; /** ✅ selects do GERAL no modal */ clientsFromGeral: string[] = []; linesFromClient: LineOptionDto[] = []; selectedCliente: string = ''; selectedLineId: string = ''; loadingClients = false; loadingLines = false; async ngAfterViewInit() { if (!isPlatformBrowser(this.platformId)) return; this.initAnimations(); setTimeout(() => this.refresh()); } private initAnimations() { document.documentElement.classList.add('js-animate'); setTimeout(() => { const items = document.querySelectorAll('[data-animate]'); items.forEach((el) => el.classList.add('is-visible')); }, 100); } refresh() { this.page = 1; this.loadForGroups(); } onSearch() { if (this.searchTimer) clearTimeout(this.searchTimer); this.searchTimer = setTimeout(() => { this.page = 1; this.expandedGroup = null; this.groupRows = []; this.loadForGroups(); }, 300); } clearSearch() { this.searchTerm = ''; this.page = 1; this.expandedGroup = null; this.groupRows = []; this.loadForGroups(); } onPageSizeChange() { this.page = 1; this.applyPagination(); } goToPage(p: number) { this.page = Math.max(1, Math.min(this.totalPages, p)); this.applyPagination(); } get totalPages() { return Math.ceil((this.total || 0) / this.pageSize) || 1; } get pageNumbers() { const total = this.totalPages; const current = this.page; const max = 5; let start = Math.max(1, current - 2); let end = Math.min(total, start + (max - 1)); start = Math.max(1, end - (max - 1)); const pages: number[] = []; for (let i = start; i <= end; i++) pages.push(i); return pages; } get pageStart() { return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; } get pageEnd() { if (this.total === 0) return 0; return Math.min(this.page * this.pageSize, this.total); } trackById(_: number, row: TrocaRow) { return row.id; } // ======================================================================= // LOAD LOGIC (igual MUREG: puxa bastante e agrupa no front) // ======================================================================= private loadForGroups() { this.loading = true; const MAX_FETCH = 5000; let params = new HttpParams() .set('page', '1') .set('pageSize', String(MAX_FETCH)) .set('search', (this.searchTerm ?? '').trim()) .set('sortBy', 'motivo') .set('sortDir', 'asc'); this.http.get | any[]>(this.apiBase, { params }).subscribe({ next: (res: any) => { const items = Array.isArray(res) ? res : (res.items ?? []); const normalized = (items ?? []).map((x: any, idx: number) => this.normalizeRow(x, idx)); this.buildGroups(normalized); this.applyPagination(); this.loading = false; this.cdr.detectChanges(); }, error: async () => { this.loading = false; await this.showToast('Erro ao carregar Troca de Número.'); } }); } private buildGroups(all: TrocaRow[]) { this.rowsByKey.clear(); const safeKey = (v: any) => (String(v ?? '').trim() || 'SEM MOTIVO'); for (const r of all) { const key = safeKey(r.motivo); r.motivo = key; const arr = this.rowsByKey.get(key) ?? []; arr.push(r); this.rowsByKey.set(key, arr); } const groups: GroupItem[] = []; let trocasTotal = 0; let iccidsTotal = 0; this.rowsByKey.forEach((arr, key) => { const total = arr.length; const trocas = arr.filter(x => this.isTroca(x)).length; const comIccid = arr.filter(x => String(x.iccid ?? '').trim() !== '').length; const semIccid = total - comIccid; trocasTotal += trocas; iccidsTotal += comIccid; groups.push({ key, total, trocas, comIccid, semIccid }); }); groups.sort((a, b) => a.key.localeCompare(b.key, 'pt-BR', { sensitivity: 'base' })); this.groups = groups; this.total = groups.length; this.groupLoadedRecords = all.length; this.groupTotalTrocas = trocasTotal; this.groupTotalIccids = iccidsTotal; } private applyPagination() { const start = (this.page - 1) * this.pageSize; const end = start + this.pageSize; this.pagedGroups = this.groups.slice(start, end); if (this.expandedGroup && !this.pagedGroups.some(g => g.key === this.expandedGroup)) { this.expandedGroup = null; this.groupRows = []; } } toggleGroup(key: string) { if (this.expandedGroup === key) { this.expandedGroup = null; this.groupRows = []; return; } this.expandedGroup = key; const rows = this.rowsByKey.get(key) ?? []; this.groupRows = [...rows].sort((a, b) => { const ai = parseInt(String(a.item ?? '0'), 10); const bi = parseInt(String(b.item ?? '0'), 10); if (Number.isFinite(ai) && Number.isFinite(bi) && ai !== bi) return ai - bi; return String(a.linhaNova ?? '').localeCompare(String(b.linhaNova ?? ''), 'pt-BR', { sensitivity: 'base' }); }); } isTroca(r: TrocaRow): boolean { const a = String(r.linhaAntiga ?? '').trim(); const b = String(r.linhaNova ?? '').trim(); if (!a || !b) return false; return a !== b; } private normalizeRow(x: any, idx: number): TrocaRow { const pick = (obj: any, keys: string[]): any => { for (const k of keys) { if (obj && obj[k] !== undefined && obj[k] !== null && String(obj[k]).trim() !== '') return obj[k]; } return ''; }; const item = pick(x, ['item', 'ITEM', 'ITÉM']); const linhaAntiga = pick(x, ['linhaAntiga', 'linha_antiga', 'LINHA ANTIGA']); const linhaNova = pick(x, ['linhaNova', 'linha_nova', 'LINHA NOVA']); const iccid = pick(x, ['iccid', 'ICCID']); const dataTroca = pick(x, ['dataTroca', 'data_troca', 'DATA TROCA', 'DATA DA TROCA']); const motivo = pick(x, ['motivo', 'MOTIVO']); const observacao = pick(x, ['observacao', 'OBSERVAÇÃO', 'OBSERVACAO']); const id = String(pick(x, ['id', 'ID']) || `${idx}-${item}-${linhaNova}-${iccid}`); return { id, item: String(item ?? ''), linhaAntiga: String(linhaAntiga ?? ''), linhaNova: String(linhaNova ?? ''), iccid: String(iccid ?? ''), dataTroca: String(dataTroca ?? ''), motivo: String(motivo ?? ''), observacao: String(observacao ?? ''), raw: x }; } // ======================================================================= // ✅ GERAL -> selects do modal (Clientes / Linhas do cliente) // ======================================================================= private loadClientsFromGeral() { this.loadingClients = true; this.http.get(`${this.linesApiBase}/clients`).subscribe({ next: (res) => { this.clientsFromGeral = (res ?? []).filter(x => !!String(x ?? '').trim()); this.loadingClients = false; this.cdr.detectChanges(); }, error: async () => { this.loadingClients = false; await this.showToast('Erro ao carregar clientes do GERAL.'); } }); } private loadLinesByClient(cliente: string) { const c = String(cliente ?? '').trim(); if (!c) { this.linesFromClient = []; this.selectedLineId = ''; return; } this.loadingLines = true; const params = new HttpParams().set('cliente', c); this.http.get(`${this.linesApiBase}/by-client`, { params }).subscribe({ next: (res) => { this.linesFromClient = (res ?? []); this.loadingLines = false; this.cdr.detectChanges(); }, error: async () => { this.loadingLines = false; await this.showToast('Erro ao carregar linhas do cliente (GERAL).'); } }); } onClienteChange() { // reset quando troca cliente this.selectedLineId = ''; this.linesFromClient = []; // limpa campos auto this.createModel.linhaAntiga = ''; this.createModel.iccid = ''; this.loadLinesByClient(this.selectedCliente); } onLineChange() { const id = String(this.selectedLineId ?? '').trim(); const found = this.linesFromClient.find(x => x.id === id); // preenche automaticamente a partir do GERAL this.createModel.linhaAntiga = found?.linha ?? ''; this.createModel.iccid = found?.chip ?? ''; // Chip do GERAL => ICCID aqui // se quiser, pode setar item automaticamente também: if (found?.item !== undefined && found?.item !== null) { // só seta se estiver vazio (pra não atrapalhar quem quiser digitar) if (!String(this.createModel.item ?? '').trim()) { this.createModel.item = String(found.item); } } } // ====== MODAL EDIÇÃO ====== onEditar(r: TrocaRow) { this.editOpen = true; this.editSaving = false; this.editModel = { id: r.id, item: r.item, linhaAntiga: r.linhaAntiga, linhaNova: r.linhaNova, iccid: r.iccid, motivo: r.motivo, observacao: r.observacao, dataTroca: this.isoToDateInput(r.dataTroca) }; } closeEdit() { this.editOpen = false; this.editModel = null; this.editSaving = false; } saveEdit() { if (!this.editModel || !this.editModel.id) return; this.editSaving = true; const payload = { item: this.toNumberOrNull(this.editModel.item), linhaAntiga: this.editModel.linhaAntiga, linhaNova: this.editModel.linhaNova, iccid: this.editModel.iccid, motivo: this.editModel.motivo, observacao: this.editModel.observacao, dataTroca: this.dateInputToIso(this.editModel.dataTroca) }; this.http.put(`${this.apiBase}/${this.editModel.id}`, payload).subscribe({ next: async () => { this.editSaving = false; await this.showToast('Registro atualizado com sucesso!'); this.closeEdit(); const currentGroup = this.expandedGroup; this.loadForGroups(); if (currentGroup) setTimeout(() => this.expandedGroup = currentGroup, 350); }, error: async () => { this.editSaving = false; await this.showToast('Erro ao salvar edição.'); } }); } // ====== MODAL CRIAÇÃO ====== onCreate() { this.createOpen = true; this.createSaving = false; // reset do form this.createModel = { item: '', linhaAntiga: '', linhaNova: '', iccid: '', dataTroca: '', motivo: '', observacao: '' }; // reset dos selects this.selectedCliente = ''; this.selectedLineId = ''; this.clientsFromGeral = []; this.linesFromClient = []; // carrega clientes do GERAL this.loadClientsFromGeral(); } closeCreate() { this.createOpen = false; } saveCreate() { // ✅ validações do "beber do GERAL" if (!String(this.selectedCliente ?? '').trim()) { this.showToast('Selecione um Cliente do GERAL.'); return; } if (!String(this.selectedLineId ?? '').trim()) { this.showToast('Selecione uma Linha do Cliente (GERAL).'); return; } if (!String(this.createModel.linhaNova ?? '').trim()) { this.showToast('Informe a Linha Nova.'); return; } this.createSaving = true; const payload = { item: this.toNumberOrNull(this.createModel.item), linhaAntiga: this.createModel.linhaAntiga, // auto do GERAL linhaNova: this.createModel.linhaNova, iccid: this.createModel.iccid, // auto do GERAL motivo: this.createModel.motivo, observacao: this.createModel.observacao, dataTroca: this.dateInputToIso(this.createModel.dataTroca) }; this.http.post(this.apiBase, payload).subscribe({ next: async () => { this.createSaving = false; await this.showToast('Troca criada com sucesso!'); this.closeCreate(); this.loadForGroups(); }, error: async () => { this.createSaving = false; await this.showToast('Erro ao criar Troca.'); } }); } // Helpers private toNumberOrNull(v: any): number | null { const n = parseInt(String(v ?? '').trim(), 10); return Number.isFinite(n) ? n : null; } private isoToDateInput(iso: string | null | undefined): string { if (!iso) return ''; const dt = new Date(iso); if (Number.isNaN(dt.getTime())) return ''; return dt.toISOString().slice(0, 10); } private dateInputToIso(val: string | null | undefined): string | null { if (!val) return null; const dt = new Date(val); if (Number.isNaN(dt.getTime())) return null; return dt.toISOString(); } displayValue(key: TrocaKey, v: any): string { if (v === null || v === undefined || String(v).trim() === '') return '-'; if (key === 'dataTroca') { const s = String(v).trim(); const d = new Date(s); if (!Number.isNaN(d.getTime())) return new Intl.DateTimeFormat('pt-BR').format(d); return s; } return String(v); } private async showToast(message: string) { if (!isPlatformBrowser(this.platformId)) return; this.toastMessage = message; this.cdr.detectChanges(); if (!this.successToast?.nativeElement) return; try { const bs = await import('bootstrap'); const toastInstance = bs.Toast.getOrCreateInstance(this.successToast.nativeElement, { autohide: true, delay: 3000 }); toastInstance.show(); } catch (error) { console.error(error); } } }