import { Component, OnDestroy, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { ActivatedRoute } from '@angular/router'; import { Subscription, firstValueFrom } from 'rxjs'; import { VigenciaService, VigenciaRow, VigenciaClientGroup, PagedResult, UpdateVigenciaRequest } from '../../services/vigencia.service'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { AuthService } from '../../services/auth.service'; import { LinesService, MobileLineDetail } from '../../services/lines.service'; import { PlanAutoFillService } from '../../services/plan-autofill.service'; import { TableExportService } from '../../services/table-export.service'; import { ImportPageTemplateService } from '../../services/import-page-template.service'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; import { computeTotalPages } from '../../utils/pagination.util'; import { VigenciaModalsComponent } from '../../components/page-modals/vigencia-modals/vigencia-modals'; type SortDir = 'asc' | 'desc'; type ToastType = 'success' | 'danger'; type ViewMode = 'lines' | 'groups'; interface LineOptionDto { id: string; item: number; linha: string | null; usuario: string | null; label?: string; } @Component({ selector: 'app-vigencia', standalone: true, imports: [CommonModule, FormsModule, CustomSelectComponent, VigenciaModalsComponent], templateUrl: './vigencia.html', styleUrls: ['./vigencia.scss'], }) export class VigenciaComponent implements OnInit, OnDestroy { readonly vm = this; loading = false; exporting = false; exportingTemplate = false; errorMsg = ''; // Filtros search = ''; client = ''; clients: string[] = []; // Paginação page = 1; pageSize = 10; pageSizeOptions = [10, 20, 50, 100]; total = 0; // Ordenação sortBy = 'cliente'; sortDir: SortDir = 'asc'; // PADRÃO: GROUPS viewMode: ViewMode = 'groups'; // Dados groups: VigenciaClientGroup[] = []; rows: VigenciaRow[] = []; // === KPIs GERAIS (Vindos do Backend) === kpiTotalClientes = 0; kpiTotalLinhas = 0; kpiTotalVencidos = 0; // === ACORDEÃO === expandedGroup: string | null = null; expandedLoading = false; groupRows: VigenciaRow[] = []; // UI detailsOpen = false; selectedRow: VigenciaRow | null = null; editOpen = false; editSaving = false; editModel: VigenciaRow | null = null; editEfetivacao = ''; editTermino = ''; editingId: string | null = null; deleteOpen = false; deleteTarget: VigenciaRow | null = null; createOpen = false; createSaving = false; createModel: any = { selectedClient: '', mobileLineId: '', item: '', conta: '', linha: '', cliente: '', usuario: '', planoContrato: '', total: null }; createEfetivacao = ''; createTermino = ''; lineOptionsCreate: LineOptionDto[] = []; createClientsLoading = false; createLinesLoading = false; clientsFromGeral: string[] = []; planOptions: string[] = []; isSysAdmin = false; toastOpen = false; toastMessage = ''; toastType: ToastType = 'success'; private toastTimer: any = null; private searchTimer: any = null; private readonly subs = new Subscription(); constructor( private vigenciaService: VigenciaService, private authService: AuthService, private linesService: LinesService, private planAutoFill: PlanAutoFillService, private route: ActivatedRoute, private tableExportService: TableExportService, private importPageTemplateService: ImportPageTemplateService ) {} ngOnInit(): void { this.isSysAdmin = this.authService.hasRole('sysadmin'); this.loadClients(); this.loadPlanRules(); this.fetch(1); this.bindOpenFromNotificationQuery(); } ngOnDestroy(): void { if (this.searchTimer) clearTimeout(this.searchTimer); if (this.toastTimer) clearTimeout(this.toastTimer); this.subs.unsubscribe(); } setView(mode: ViewMode): void { if (this.viewMode === mode) return; this.viewMode = mode; this.page = 1; this.expandedGroup = null; this.groupRows = []; this.sortBy = mode === 'groups' ? 'cliente' : 'item'; this.fetch(1); } loadClients(): void { this.vigenciaService.getClients().subscribe({ next: (list) => (this.clients = list ?? []), error: () => (this.clients = []), }); } private async loadPlanRules() { try { await this.planAutoFill.load(); this.planOptions = this.planAutoFill.getPlanOptions(); } catch { this.planOptions = []; } } get totalPages(): number { return computeTotalPages(this.total || 0, this.pageSize || 10); } fetch(goToPage?: number): void { if (goToPage) this.page = goToPage; this.loading = true; this.errorMsg = ''; if(goToPage && goToPage !== this.page) this.expandedGroup = null; if (this.viewMode === 'groups') { this.fetchGroups(); } else { this.fetchLines(); } } private fetchGroups() { this.vigenciaService.getGroups({ search: this.search?.trim(), page: this.page, pageSize: this.pageSize, sortBy: this.sortBy, sortDir: this.sortDir, }).subscribe({ next: (res) => { // ✅ Preenche Lista this.groups = res.data.items || []; this.total = res.data.total || 0; // ✅ Preenche KPIs Globais this.kpiTotalClientes = res.kpis.totalClientes; this.kpiTotalLinhas = res.kpis.totalLinhas; this.kpiTotalVencidos = res.kpis.totalVencidos; this.loading = false; }, error: (err) => this.handleError(err, 'Erro ao carregar clientes.'), }); } private fetchLines() { this.vigenciaService.getVigencia({ search: this.search?.trim(), client: this.client?.trim(), page: this.page, pageSize: this.pageSize, sortBy: this.sortBy, sortDir: this.sortDir, }).subscribe({ next: (res) => { this.rows = res.items || []; this.total = res.total || 0; this.loading = false; }, error: (err) => this.handleError(err, 'Erro ao carregar linhas.'), }); } toggleGroup(g: VigenciaClientGroup): void { if (this.expandedGroup === g.cliente) { this.expandedGroup = null; this.groupRows = []; return; } this.expandedGroup = g.cliente; this.expandedLoading = true; this.groupRows = []; this.vigenciaService.getVigencia({ client: g.cliente, page: 1, pageSize: 200, sortBy: 'item', sortDir: 'asc' }).subscribe({ next: (res) => { this.groupRows = res.items || []; this.expandedLoading = false; }, error: () => { this.showToast('Erro ao carregar detalhes do cliente.', 'danger'); this.expandedLoading = false; } }); } public isVencido(dateValue: any): boolean { if(!dateValue) return false; const d = this.parseAnyDate(dateValue); if(!d) return false; return this.startOfDay(d) < this.startOfDay(new Date()); } public isAtivo(dateValue: any): boolean { if(!dateValue) return true; const d = this.parseAnyDate(dateValue); if(!d) return true; return this.startOfDay(d) >= this.startOfDay(new Date()); } public isAVencer(dateValue: any): boolean { if (!dateValue) return false; const d = this.parseAnyDate(dateValue); if (!d) return false; const today = this.startOfDay(new Date()); const end = this.startOfDay(d); const days = Math.round((end.getTime() - today.getTime()) / (24 * 60 * 60 * 1000)); return days >= 0 && days <= 30; } getRenewalBadge(row: VigenciaRow): string { if (!row.autoRenewYears) return ''; return `Auto +${row.autoRenewYears} ano(s)`; } public parseAnyDate(value: any): Date | null { if (!value) return null; const d = new Date(value); return isNaN(d.getTime()) ? null : d; } public startOfDay(d: Date): Date { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); } onSearchChange() { if (this.searchTimer) clearTimeout(this.searchTimer); this.searchTimer = setTimeout(() => this.fetch(1), 300); } clearFilters() { this.search = ''; if (this.searchTimer) clearTimeout(this.searchTimer); this.fetch(1); } async onExport(): Promise { if (this.exporting) return; this.exporting = true; try { const baseRows = await this.fetchAllRowsForExport(); const rows = await this.fetchDetailedRowsForExport(baseRows); if (!rows.length) { this.showToast('Nenhum registro encontrado para exportar.', 'danger'); return; } const timestamp = this.tableExportService.buildTimestamp(); await this.tableExportService.exportAsXlsx({ fileName: `vigencia_${timestamp}`, sheetName: 'Vigencia', rows, columns: [ { header: 'ID', value: (row) => row.id ?? '' }, { header: 'Item', type: 'number', value: (row) => this.toNullableNumber(row.item) ?? 0 }, { header: 'Linha', value: (row) => row.linha ?? '' }, { header: 'Conta', value: (row) => row.conta ?? '' }, { header: 'Cliente', value: (row) => row.cliente ?? '' }, { header: 'Usuario', value: (row) => row.usuario ?? '' }, { header: 'Plano', value: (row) => row.planoContrato ?? '' }, { header: 'Efetivacao', type: 'date', value: (row) => row.dtEfetivacaoServico ?? '' }, { header: 'Termino Fidelizacao', type: 'date', value: (row) => row.dtTerminoFidelizacao ?? '' }, { header: 'Status', value: (row) => (this.isVencido(row.dtTerminoFidelizacao) ? 'Vencido' : 'Ativo') }, { header: 'Auto Renovacao (anos)', type: 'number', value: (row) => this.toNullableNumber(row.autoRenewYears) ?? 0 }, { header: 'Auto Renovacao Referencia', type: 'date', value: (row) => row.autoRenewReferenceEndDate ?? '' }, { header: 'Auto Renovacao Configurada Em', type: 'datetime', value: (row) => row.autoRenewConfiguredAt ?? '' }, { header: 'Ultima Auto Renovacao', type: 'datetime', value: (row) => row.lastAutoRenewedAt ?? '' }, { header: 'Total', type: 'currency', value: (row) => this.toNullableNumber(row.total) ?? 0 }, { header: 'Criado Em', type: 'datetime', value: (row) => row.createdAt ?? '' }, { header: 'Atualizado Em', type: 'datetime', value: (row) => row.updatedAt ?? '' }, ], }); this.showToast(`Planilha exportada com ${rows.length} registro(s).`, 'success'); } catch { this.showToast('Erro ao exportar planilha.', 'danger'); } finally { this.exporting = false; } } async onExportTemplate(): Promise { if (this.exportingTemplate) return; this.exportingTemplate = true; try { await this.importPageTemplateService.exportVigenciaTemplate(); this.showToast('Modelo da página exportado.', 'success'); } catch { this.showToast('Erro ao exportar o modelo da página.', 'danger'); } finally { this.exportingTemplate = false; } } private async fetchAllRowsForExport(): Promise { const pageSize = 500; let page = 1; let expectedTotal = 0; const all: VigenciaRow[] = []; while (page <= 500) { const response = await firstValueFrom( this.vigenciaService.getVigencia({ search: this.search?.trim(), client: this.client?.trim(), page, pageSize, sortBy: 'item', sortDir: 'asc', }) ); const items = response?.items ?? []; expectedTotal = response?.total ?? 0; all.push(...items); if (items.length === 0) break; if (items.length < pageSize) break; if (expectedTotal > 0 && all.length >= expectedTotal) break; page += 1; } return all; } private async fetchDetailedRowsForExport(rows: VigenciaRow[]): Promise { if (!rows.length) return []; const detailedRows: VigenciaRow[] = []; const chunkSize = 10; for (let i = 0; i < rows.length; i += chunkSize) { const chunk = rows.slice(i, i + chunkSize); const resolved = await Promise.all( chunk.map(async (row) => { try { return await firstValueFrom(this.vigenciaService.getById(row.id)); } catch { return row; } }) ); detailedRows.push(...resolved); } return detailedRows; } scheduleAutoRenew(row: VigenciaRow): void { if (!row?.id) return; const years = 2; this.vigenciaService.configureAutoRenew(row.id, { years }).subscribe({ next: () => { row.autoRenewYears = years; row.autoRenewReferenceEndDate = row.dtTerminoFidelizacao; row.autoRenewConfiguredAt = new Date().toISOString(); this.showToast(`Renovação automática (+${years} ano${years > 1 ? 's' : ''}) programada.`, 'success'); }, error: () => this.showToast('Não foi possível programar a renovação automática.', 'danger') }); } openDetails(r: VigenciaRow) { this.selectedRow = r; this.detailsOpen = true; } closeDetails() { this.detailsOpen = false; } openEdit(r: VigenciaRow) { if (!this.isSysAdmin) return; this.editingId = r.id; this.editModel = { ...r }; this.editEfetivacao = this.toDateInput(r.dtEfetivacaoServico); this.editTermino = this.toDateInput(r.dtTerminoFidelizacao); this.editOpen = true; } closeEdit() { this.editOpen = false; this.editSaving = false; this.editModel = null; this.editEfetivacao = ''; this.editTermino = ''; this.editingId = null; } saveEdit() { if (!this.editModel || !this.editingId) return; this.editSaving = true; const payload: UpdateVigenciaRequest = { item: this.toNullableNumber(this.editModel.item), conta: this.editModel.conta, linha: this.editModel.linha, cliente: this.editModel.cliente, usuario: this.editModel.usuario, planoContrato: this.editModel.planoContrato, dtEfetivacaoServico: this.dateInputToIso(this.editEfetivacao), dtTerminoFidelizacao: this.dateInputToIso(this.editTermino), total: this.toNullableNumber(this.editModel.total) }; this.vigenciaService.update(this.editingId, payload).subscribe({ next: () => { this.editSaving = false; this.closeEdit(); this.fetch(); this.showToast('Registro atualizado!', 'success'); }, error: () => { this.editSaving = false; this.showToast('Erro ao salvar.', 'danger'); } }); } // ========================== // CREATE // ========================== openCreate() { if (!this.isSysAdmin) return; this.resetCreateModel(); this.createOpen = true; this.preloadGeralClients(); } closeCreate() { this.createOpen = false; this.createSaving = false; this.createModel = null; } private resetCreateModel() { this.createModel = { selectedClient: '', mobileLineId: '', item: '', conta: '', linha: '', cliente: '', usuario: '', planoContrato: '', total: null }; this.createEfetivacao = ''; this.createTermino = ''; this.lineOptionsCreate = []; this.createLinesLoading = false; this.createClientsLoading = false; } private preloadGeralClients() { this.createClientsLoading = true; this.linesService.getClients().subscribe({ next: (list) => { this.clientsFromGeral = list ?? []; this.createClientsLoading = false; }, error: () => { this.clientsFromGeral = []; this.createClientsLoading = false; } }); } onCreateClientChange() { const c = (this.createModel.selectedClient ?? '').trim(); this.createModel.mobileLineId = ''; this.createModel.linha = ''; this.createModel.conta = ''; this.createModel.usuario = ''; this.createModel.planoContrato = ''; this.createModel.total = null; this.createModel.cliente = c; this.lineOptionsCreate = []; if (c) this.loadLinesForClient(c); } private loadLinesForClient(cliente: string) { const c = (cliente ?? '').trim(); if (!c) return; this.createLinesLoading = true; 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, usuario: x.usuario ?? null, label: `${x.item ?? ''} • ${x.linha ?? '-'} • ${x.usuario ?? 'SEM USUÁRIO'}` })) .filter(x => !!String(x.linha ?? '').trim()); this.lineOptionsCreate = mapped; this.createLinesLoading = false; }, error: () => { this.lineOptionsCreate = []; this.createLinesLoading = false; this.showToast('Erro ao carregar linhas da GERAL.', 'danger'); } }); } onCreateLineChange() { const id = String(this.createModel.mobileLineId ?? '').trim(); if (!id) return; this.linesService.getById(id).subscribe({ next: (d: MobileLineDetail) => this.applyLineDetailToCreate(d), error: () => this.showToast('Erro ao carregar dados da linha.', 'danger') }); } private applyLineDetailToCreate(d: MobileLineDetail) { this.createModel.linha = d.linha ?? ''; this.createModel.conta = d.conta ?? ''; this.createModel.cliente = d.cliente ?? this.createModel.cliente ?? ''; this.createModel.usuario = d.usuario ?? ''; this.createModel.planoContrato = d.planoContrato ?? ''; this.createEfetivacao = this.toDateInput(d.dtEfetivacaoServico ?? null); this.createTermino = this.toDateInput(d.dtTerminoFidelizacao ?? null); this.ensurePlanOption(this.createModel.planoContrato); if (!String(this.createModel.item ?? '').trim() && d.item) { this.createModel.item = String(d.item); } this.onCreatePlanChange(); } onCreatePlanChange() { this.ensurePlanOption(this.createModel?.planoContrato); this.applyPlanSuggestion(this.createModel); } onEditPlanChange() { if (!this.editModel) return; this.ensurePlanOption(this.editModel?.planoContrato); this.applyPlanSuggestion(this.editModel); } private applyPlanSuggestion(model: any) { const plan = (model?.planoContrato ?? '').toString().trim(); if (!plan) return; const suggestion = this.planAutoFill.suggest(plan); if (!suggestion) return; if (suggestion.valorPlano != null) { model.total = suggestion.valorPlano; } } private ensurePlanOption(plan: any) { const p = (plan ?? '').toString().trim(); if (!p) return; if (!this.planOptions.includes(p)) { this.planOptions = [p, ...this.planOptions]; } } saveCreate() { if (!this.createModel) return; this.applyPlanSuggestion(this.createModel); const payload = { item: this.toNullableNumber(this.createModel.item), conta: this.createModel.conta, linha: this.createModel.linha, cliente: this.createModel.cliente, usuario: this.createModel.usuario, planoContrato: this.createModel.planoContrato, dtEfetivacaoServico: this.dateInputToIso(this.createEfetivacao), dtTerminoFidelizacao: this.dateInputToIso(this.createTermino), total: this.toNullableNumber(this.createModel.total) }; this.createSaving = true; this.vigenciaService.create(payload).subscribe({ next: () => { this.createSaving = false; this.closeCreate(); this.fetch(); this.showToast('Vigência criada com sucesso!', 'success'); }, error: () => { this.createSaving = false; this.showToast('Erro ao criar vigência.', 'danger'); } }); } openDelete(r: VigenciaRow) { if (!this.isSysAdmin) return; this.deleteTarget = r; this.deleteOpen = true; } cancelDelete() { this.deleteOpen = false; this.deleteTarget = null; } async confirmDelete() { if (!this.deleteTarget) return; if (!(await confirmDeletionWithTyping('este registro de vigência'))) return; const id = this.deleteTarget.id; this.vigenciaService.remove(id).subscribe({ next: () => { this.deleteOpen = false; this.deleteTarget = null; this.fetch(); this.showToast('Registro removido.', 'success'); }, error: () => { this.deleteOpen = false; this.deleteTarget = null; this.showToast('Erro ao remover.', 'danger'); } }); } private toDateInput(value: string | null): string { if (!value) return ''; const d = new Date(value); if (isNaN(d.getTime())) return ''; return d.toISOString().slice(0, 10); } private dateInputToIso(value: string): string | null { if (!value) return null; const d = new Date(`${value}T00:00:00`); if (isNaN(d.getTime())) return null; return d.toISOString(); } private toNullableNumber(value: any): number | null { if (value === undefined || value === null || value === '') return null; const n = Number(value); return Number.isNaN(n) ? null : n; } private bindOpenFromNotificationQuery(): void { this.subs.add( this.route.queryParamMap.subscribe((params) => { const lineId = (params.get('lineId') ?? '').trim(); const linha = (params.get('linha') ?? '').trim(); if (!lineId && !linha) return; const openMode = (params.get('open') ?? 'edit').trim().toLowerCase(); if (lineId) { this.openVigenciaLineById(lineId, openMode); } else if (linha) { this.openVigenciaLineByNumber(linha, openMode); } }) ); } private openVigenciaLineById(lineId: string, openMode: string): void { this.vigenciaService.getById(lineId).subscribe({ next: (row) => { if (this.isSysAdmin && openMode !== 'details') { this.openEdit(row); return; } this.openDetails(row); }, error: () => this.showToast('Não foi possível abrir a linha da vigência pela notificação.', 'danger') }); } private openVigenciaLineByNumber(linha: string, openMode: string): void { const onlyDigits = (linha || '').replace(/\D/g, ''); const lookup = onlyDigits || linha; if (!lookup) return; this.vigenciaService.getVigencia({ search: lookup, page: 1, pageSize: 20, sortBy: 'item', sortDir: 'asc' }).subscribe({ next: (res) => { const rows = res?.items ?? []; const match = rows.find(r => (r.linha ?? '').replace(/\D/g, '') === onlyDigits) ?? rows[0]; if (!match) { this.showToast('Linha da notificação não encontrada na vigência.', 'danger'); return; } if (this.isSysAdmin && openMode !== 'details') { this.openEdit(match); return; } this.openDetails(match); }, error: () => this.showToast('Não foi possível localizar a linha da notificação na vigência.', 'danger') }); } handleError(err: HttpErrorResponse, msg: string) { this.loading = false; this.expandedLoading = false; this.errorMsg = (err.error as any)?.message || msg; this.showToast(msg, 'danger'); } showToast(msg: string, type: ToastType) { this.toastMessage = msg; this.toastType = type; this.toastOpen = true; if(this.toastTimer) clearTimeout(this.toastTimer); this.toastTimer = setTimeout(() => this.toastOpen = false, 3000); } hideToast() { this.toastOpen = false; } }