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, HttpParams } from '@angular/common/http'; import { LinesService } from '../../services/lines.service'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { environment } from '../../../environments/environment'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; type MuregKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataDaMureg' | 'cliente'; interface ApiPagedResult { page?: number; pageSize?: number; total?: number; items?: T[]; } interface ClientGroup { cliente: string; total: number; trocas: number; comIccid: number; semIccid: number; } interface MuregRow { id: string; item: string; linhaAntiga: string; linhaNova: string; iccid: string; dataDaMureg: string; cliente: string; mobileLineId: string; raw: any; } /** ✅ AGORA COM item/usuario/chip (igual Troca de Número) */ interface LineOptionDto { id: string; item: number; linha: string | null; chip: string | null; // => ICCID usuario: string | null; cliente?: string | null; skil?: string | null; label?: string; } interface MuregDetailDto { id: string; item: number; linhaAntiga: string | null; linhaNova: string | null; iccid: string | null; dataDaMureg: string | null; mobileLineId: string; cliente: string | null; usuario: string | null; skil: string | null; linhaAtualNaGeral: string | null; chipNaGeral: string | null; contaNaGeral: string | null; statusNaGeral: string | null; } @Component({ standalone: true, imports: [CommonModule, FormsModule, CustomSelectComponent], templateUrl: './mureg.html', styleUrls: ['./mureg.scss'] }) export class Mureg 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 linesService: LinesService ) {} private readonly apiBase = (() => { const raw = (environment.apiUrl || '').replace(/\/+$/, ''); const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; return `${apiBase}/mureg`; })(); // ====== DATA ====== clientGroups: ClientGroup[] = []; pagedClientGroups: ClientGroup[] = []; expandedGroup: string | null = null; groupRows: MuregRow[] = []; private rowsByClient = new Map(); // KPIs groupLoadedRecords = 0; groupTotalTrocas = 0; groupTotalIccids = 0; // ====== FILTERS & PAGINATION ====== searchTerm = ''; private searchTimer: any = null; page = 1; pageSize = 10; pageSizeOptions = [10, 20, 50, 100]; total = 0; // ====== OPTIONS (GERAL) ====== clientOptions: string[] = []; // create options lineOptionsCreate: LineOptionDto[] = []; createClientsLoading = false; createLinesLoading = false; // edit options lineOptionsEdit: LineOptionDto[] = []; editClientsLoading = false; editLinesLoading = false; // ====== EDIT MODAL ====== editOpen = false; editSaving = false; editModel: any = null; // ====== DETAIL MODAL ====== detailOpen = false; detailLoading = false; detailData: MuregDetailDto | null = null; // ====== DELETE MODAL ====== deleteOpen = false; deleteSaving = false; deleteTarget: MuregRow | null = null; // ====== CREATE MODAL ====== createOpen = false; createSaving = false; createModel: any = { selectedClient: '', mobileLineId: '', item: '', linhaAntiga: '', linhaNova: '', iccid: '', dataDaMureg: '', clienteInfo: '' }; async ngAfterViewInit() { if (!isPlatformBrowser(this.platformId)) return; this.initAnimations(); setTimeout(() => { this.preloadClients(); // ✅ já deixa o select pronto 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: MuregRow) { return row.id; } // ======================================================================= // LOAD LOGIC (lista e grupos) // ======================================================================= 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', 'cliente') .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 MUREG.'); } }); } private buildGroups(all: MuregRow[]) { this.rowsByClient.clear(); const safeClient = (c: any) => (String(c ?? '').trim() || 'SEM CLIENTE'); for (const r of all) { const key = safeClient(r.cliente); r.cliente = key; const arr = this.rowsByClient.get(key) ?? []; arr.push(r); this.rowsByClient.set(key, arr); } const groups: ClientGroup[] = []; let trocasTotal = 0; let iccidsTotal = 0; this.rowsByClient.forEach((arr, cliente) => { 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({ cliente, total, trocas, comIccid, semIccid }); }); groups.sort((a, b) => a.cliente.localeCompare(b.cliente, 'pt-BR', { sensitivity: 'base' })); this.clientGroups = 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.pagedClientGroups = this.clientGroups.slice(start, end); if (this.expandedGroup && !this.pagedClientGroups.some(g => g.cliente === this.expandedGroup)) { this.expandedGroup = null; this.groupRows = []; } } toggleGroup(cliente: string) { if (this.expandedGroup === cliente) { this.expandedGroup = null; this.groupRows = []; return; } this.expandedGroup = cliente; const rows = this.rowsByClient.get(cliente) ?? []; 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: MuregRow): 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): MuregRow { 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 dataDaMureg = pick(x, ['dataDaMureg', 'data_da_mureg', 'DATA DA MUREG']); const cliente = pick(x, ['cliente', 'CLIENTE']); const mobileLineId = String(pick(x, ['mobileLineId', 'MobileLineId', 'mobile_line_id']) ?? ''); 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 ?? ''), dataDaMureg: String(dataDaMureg ?? ''), cliente: String(cliente ?? ''), mobileLineId, raw: x }; } // ======================================================================= // CLIENTS / LINES OPTIONS (GERAL) // ======================================================================= private preloadClients() { if (this.clientOptions.length > 0) return; this.createClientsLoading = true; this.editClientsLoading = true; this.linesService.getClients().subscribe({ next: (list) => { this.clientOptions = (list ?? []).filter(x => !!String(x ?? '').trim()); this.createClientsLoading = false; this.editClientsLoading = false; this.cdr.detectChanges(); }, error: async () => { this.createClientsLoading = false; this.editClientsLoading = false; await this.showToast('Erro ao carregar clientes da GERAL.'); } }); } private loadLinesForClient(cliente: string, target: 'create' | 'edit') { const c = (cliente ?? '').trim(); if (!c) { if (target === 'create') this.lineOptionsCreate = []; else this.lineOptionsEdit = []; return; } if (target === 'create') { this.createLinesLoading = true; this.lineOptionsCreate = []; } else { this.editLinesLoading = true; this.lineOptionsEdit = []; } // ✅ aqui assumimos que o getLinesByClient retorna (id,item,linha,chip,usuario...) this.linesService.getLinesByClient(c).subscribe({ next: (items: any[]) => { const mapped: LineOptionDto[] = (items ?? []) .filter(x => !!String(x?.id ?? '').trim()) .map(x => ({ id: String(x.id), item: Number(x.item ?? 0), linha: x.linha ?? null, chip: x.chip ?? null, usuario: x.usuario ?? null, cliente: x.cliente ?? null, skil: x.skil ?? null, label: `${x.item ?? ''} • ${x.linha ?? '-'} • ${x.usuario ?? 'SEM USUÁRIO'}` })) .filter(x => !!String(x.linha ?? '').trim()); if (target === 'create') { this.lineOptionsCreate = mapped; this.createLinesLoading = false; } else { this.lineOptionsEdit = mapped; this.editLinesLoading = false; } this.cdr.detectChanges(); }, error: async () => { if (target === 'create') this.createLinesLoading = false; else this.editLinesLoading = false; await this.showToast('Erro ao carregar linhas do cliente (GERAL).'); } }); } private applySelectedLineToModel(model: any, selectedId: string, options: LineOptionDto[]) { const id = String(selectedId ?? '').trim(); const opt = options.find(x => x.id === id); if (!opt) return; // ✅ snapshot automático (linha antiga) model.linhaAntiga = opt.linha ?? ''; // ✅ ICCID automático (chip do GERAL) model.iccid = opt.chip ?? ''; // ✅ se item estiver vazio, preenche com item da GERAL (igual Troca) if (!String(model.item ?? '').trim() && opt.item) { model.item = String(opt.item); } model.clienteInfo = `Vínculo (GERAL): ${model.selectedClient} • ${opt.item} • ${opt.linha ?? '-'} • ${opt.usuario ?? 'SEM USUÁRIO'}`; } // ======================================================================= // CREATE MODAL // ======================================================================= onCreate() { this.preloadClients(); this.createOpen = true; this.createSaving = false; this.createModel = { selectedClient: '', mobileLineId: '', item: '', linhaAntiga: '', linhaNova: '', iccid: '', dataDaMureg: '', clienteInfo: '' }; this.lineOptionsCreate = []; } closeCreate() { this.createOpen = false; } onCreateClientChange() { const c = (this.createModel.selectedClient ?? '').trim(); this.createModel.mobileLineId = ''; this.createModel.linhaAntiga = ''; this.createModel.iccid = ''; this.createModel.clienteInfo = c ? `Cliente selecionado: ${c}` : ''; this.loadLinesForClient(c, 'create'); } onCreateLineChange() { this.applySelectedLineToModel(this.createModel, this.createModel.mobileLineId, this.lineOptionsCreate); } saveCreate() { const mobileLineId = String(this.createModel.mobileLineId ?? '').trim(); const linhaNova = String(this.createModel.linhaNova ?? '').trim(); if (!mobileLineId || !linhaNova) { this.showToast('Selecione Cliente + Linha Antiga (GERAL) e preencha Linha Nova.'); return; } this.createSaving = true; const payload: any = { item: this.toIntOrZero(this.createModel.item), mobileLineId, linhaAntiga: (this.createModel.linhaAntiga ?? '') || null, linhaNova: (this.createModel.linhaNova ?? '') || null, iccid: (this.createModel.iccid ?? '') || null, dataDaMureg: this.dateInputToIso(this.createModel.dataDaMureg) }; if (!payload.item || payload.item <= 0) delete payload.item; this.http.post(this.apiBase, payload).subscribe({ next: async () => { this.createSaving = false; await this.showToast('Mureg criada com sucesso!'); this.closeCreate(); this.loadForGroups(); }, error: async (err) => { this.createSaving = false; const msg = this.extractApiMessage(err) ?? 'Erro ao criar Mureg.'; await this.showToast(msg); } }); } // ======================================================================= // EDIT MODAL // ======================================================================= onEditar(r: MuregRow) { this.preloadClients(); this.editOpen = true; this.editSaving = false; this.editModel = null; this.lineOptionsEdit = []; this.http.get(`${this.apiBase}/${r.id}`).subscribe({ next: (d) => { const selectedClient = String(d?.cliente ?? '').trim(); this.editModel = { id: d.id, item: String(d.item ?? ''), dataDaMureg: this.isoToDateInput(d.dataDaMureg), // snapshot atual linhaAntiga: String(d.linhaAntiga ?? d.linhaAtualNaGeral ?? ''), linhaNova: String(d.linhaNova ?? ''), // ✅ se não tiver iccid salvo, usa chip da geral iccid: String(d.iccid ?? d.chipNaGeral ?? ''), mobileLineId: String(d.mobileLineId ?? ''), selectedClient, clienteInfo: selectedClient ? `GERAL: ${selectedClient} • Linha atual: ${d.linhaAtualNaGeral ?? '-'} • Usuário: ${d.usuario ?? '-'} • Chip: ${d.chipNaGeral ?? '-'}` : '' }; if (selectedClient) { this.loadLinesForClient(selectedClient, 'edit'); } this.cdr.detectChanges(); }, error: async () => { this.editOpen = false; await this.showToast('Erro ao abrir detalhes do registro.'); } }); } closeEdit() { this.editOpen = false; this.editModel = null; this.editSaving = false; } onEditClientChange() { const c = (this.editModel?.selectedClient ?? '').trim(); this.editModel.mobileLineId = ''; this.editModel.linhaAntiga = ''; this.editModel.iccid = ''; this.editModel.clienteInfo = c ? `Cliente selecionado: ${c}` : ''; this.loadLinesForClient(c, 'edit'); } onEditLineChange() { if (!this.editModel) return; this.applySelectedLineToModel(this.editModel, this.editModel.mobileLineId, this.lineOptionsEdit); } saveEdit() { if (!this.editModel || !this.editModel.id) return; const mobileLineId = String(this.editModel.mobileLineId ?? '').trim(); if (!mobileLineId) { this.showToast('Selecione Cliente e Linha Antiga (GERAL).'); return; } this.editSaving = true; const payload: any = { item: this.toIntOrNull(this.editModel.item), mobileLineId, linhaAntiga: (this.editModel.linhaAntiga ?? '') || null, linhaNova: (this.editModel.linhaNova ?? '') || null, iccid: (this.editModel.iccid ?? '') || null, dataDaMureg: this.dateInputToIso(this.editModel.dataDaMureg) }; if (payload.item == null) delete payload.item; this.http.put(`${this.apiBase}/${this.editModel.id}`, payload).subscribe({ next: async () => { this.editSaving = false; await this.showToast('Registro atualizado com sucesso!'); const currentGroup = this.expandedGroup; this.closeEdit(); this.loadForGroups(); if (currentGroup) { setTimeout(() => { this.expandedGroup = currentGroup; this.toggleGroup(currentGroup); }, 400); } }, error: async (err) => { this.editSaving = false; const msg = this.extractApiMessage(err) ?? 'Erro ao salvar edição.'; await this.showToast(msg); } }); } // ======================================================================= // DETAIL MODAL // ======================================================================= onView(row: MuregRow) { this.detailOpen = true; this.detailLoading = true; this.detailData = null; this.http.get(`${this.apiBase}/${row.id}`).subscribe({ next: (data) => { this.detailData = data; this.detailLoading = false; }, error: async () => { this.detailLoading = false; await this.showToast('Erro ao carregar detalhes da Mureg.'); } }); } closeDetail() { this.detailOpen = false; this.detailLoading = false; this.detailData = null; } // ======================================================================= // DELETE MODAL // ======================================================================= onDelete(row: MuregRow) { this.deleteTarget = row; this.deleteOpen = true; this.deleteSaving = false; } closeDelete() { this.deleteOpen = false; this.deleteTarget = null; this.deleteSaving = false; } async confirmDelete() { if (!this.deleteTarget?.id) return; if (!(await confirmDeletionWithTyping('esta Mureg'))) return; this.deleteSaving = true; const targetId = this.deleteTarget.id; const currentGroup = this.expandedGroup; this.http.delete(`${this.apiBase}/${targetId}`).subscribe({ next: async () => { this.deleteSaving = false; await this.showToast('Mureg excluída com sucesso!'); this.closeDelete(); this.loadForGroups(); if (currentGroup) { setTimeout(() => { this.expandedGroup = currentGroup; this.toggleGroup(currentGroup); }, 400); } }, error: async (err) => { this.deleteSaving = false; const msg = this.extractApiMessage(err) ?? 'Erro ao excluir Mureg.'; await this.showToast(msg); } }); } // ======================================================================= // Helpers // ======================================================================= private toIntOrZero(val: any): number { const n = parseInt(String(val ?? '').trim(), 10); return Number.isFinite(n) ? n : 0; } private toIntOrNull(val: any): number | null { const s = String(val ?? '').trim(); if (!s) return null; const n = parseInt(s, 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(); } private extractApiMessage(err: any): string | null { try { const m1 = err?.error?.message; if (m1) return String(m1); const m2 = err?.error?.title; if (m2) return String(m2); return null; } catch { return null; } } displayValue(key: MuregKey, v: any): string { if (v === null || v === undefined || String(v).trim() === '') return '-'; if (key === 'dataDaMureg') { 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); } } }