From 79d372d67b0d61d103fca81349adf5232324c0a1 Mon Sep 17 00:00:00 2001 From: Leon Date: Thu, 5 Mar 2026 18:30:45 -0300 Subject: [PATCH 1/7] feat: Cliente poder alterar e solicitar alteracoes --- src/app/app.routes.ts | 2 + src/app/app.ts | 1 + src/app/components/header/header.html | 3 + src/app/components/header/header.ts | 1 + src/app/pages/geral/geral.html | 46 ++- src/app/pages/geral/geral.ts | 87 ++++- .../solicitacoes-linhas.html | 138 +++++++ .../solicitacoes-linhas.scss | 368 ++++++++++++++++++ .../solicitacoes-linhas.ts | 189 +++++++++ .../services/solicitacoes-linhas.service.ts | 61 +++ 10 files changed, 881 insertions(+), 15 deletions(-) create mode 100644 src/app/pages/solicitacoes-linhas/solicitacoes-linhas.html create mode 100644 src/app/pages/solicitacoes-linhas/solicitacoes-linhas.scss create mode 100644 src/app/pages/solicitacoes-linhas/solicitacoes-linhas.ts create mode 100644 src/app/services/solicitacoes-linhas.service.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 3c39d3d..0b219bf 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -21,6 +21,7 @@ import { Parcelamentos } from './pages/parcelamentos/parcelamentos'; import { Historico } from './pages/historico/historico'; import { Perfil } from './pages/perfil/perfil'; import { SystemProvisionUserPage } from './pages/system-provision-user/system-provision-user'; +import { SolicitacoesLinhas } from './pages/solicitacoes-linhas/solicitacoes-linhas'; export const routes: Routes = [ { path: '', component: Home }, @@ -38,6 +39,7 @@ export const routes: Routes = [ { path: 'resumo', component: Resumo, canActivate: [authGuard], title: 'Resumo' }, { path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Parcelamentos' }, { path: 'historico', component: Historico, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico' }, + { path: 'solicitacoes', component: SolicitacoesLinhas, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Solicitações' }, { path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' }, { path: 'system/fornecer-usuario', diff --git a/src/app/app.ts b/src/app/app.ts index 11b8f61..0d6c4dc 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -29,6 +29,7 @@ export class AppComponent { // ✅ rotas internas (LOGADO) que devem esconder footer private readonly loggedPrefixes = [ '/geral', + '/solicitacoes-linhas', '/mureg', '/faturamento', '/dadosusuarios', diff --git a/src/app/components/header/header.html b/src/app/components/header/header.html index cc76c2f..4c1289c 100644 --- a/src/app/components/header/header.html +++ b/src/app/components/header/header.html @@ -548,6 +548,9 @@ Histórico + + Solicitações + Dados PF/PJ diff --git a/src/app/components/header/header.ts b/src/app/components/header/header.ts index 0d78ae7..ef45da7 100644 --- a/src/app/components/header/header.ts +++ b/src/app/components/header/header.ts @@ -93,6 +93,7 @@ export class Header implements AfterViewInit, OnDestroy { '/resumo', '/parcelamentos', '/historico', + '/solicitacoes', '/perfil', '/system', ]; diff --git a/src/app/pages/geral/geral.html b/src/app/pages/geral/geral.html index 6ba0955..8e06caa 100644 --- a/src/app/pages/geral/geral.html +++ b/src/app/pages/geral/geral.html @@ -1975,12 +1975,24 @@
- - + + + + + + +
@@ -1994,22 +2006,35 @@
-
- - -
- +
- + + +
+
+
+
+ + +
+
+ + +
@@ -2062,6 +2087,7 @@ +
diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts index 7e78101..bc3f3df 100644 --- a/src/app/pages/geral/geral.ts +++ b/src/app/pages/geral/geral.ts @@ -22,6 +22,7 @@ import { CustomSelectComponent } from '../../components/custom-select/custom-sel import { PlanAutoFillService } from '../../services/plan-autofill.service'; import { AuthService } from '../../services/auth.service'; import { TenantSyncService } from '../../services/tenant-sync.service'; +import { SolicitacoesLinhasService } from '../../services/solicitacoes-linhas.service'; import { firstValueFrom, Subscription, filter } from 'rxjs'; import { environment } from '../../../environments/environment'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; @@ -289,7 +290,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { private planAutoFill: PlanAutoFillService, private authService: AuthService, private router: Router, - private tenantSyncService: TenantSyncService + private tenantSyncService: TenantSyncService, + private solicitacoesLinhasService: SolicitacoesLinhasService ) {} private readonly apiBase = (() => { @@ -347,6 +349,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { financeOpen = false; editOpen = false; editSaving = false; + requestSaving = false; createOpen = false; createSaving = false; @@ -858,6 +861,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.aparelhoReciboFile = null; this.editSaving = false; + this.requestSaving = false; this.createSaving = false; this.editModel = null; @@ -1865,6 +1869,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { async onEditar(r: LineRow) { this.editOpen = true; this.editSaving = false; + this.requestSaving = false; this.editModel = null; this.aparelhoNotaFiscalFile = null; this.aparelhoReciboFile = null; @@ -1942,11 +1947,15 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async saveEdit() { - if (!this.editingId || !this.editModel) return; + if (!this.editingId || !this.editModel || this.requestSaving) return; this.editSaving = true; const editingId = this.editingId; const shouldUploadAttachments = !!(this.aparelhoNotaFiscalFile || this.aparelhoReciboFile); + const franquiaLineAtual = this.toNullableNumber(this.editModel.franquiaLine); + const franquiaLineSolicitada = this.toNullableNumber(this.editModel.franquiaLineSolicitada); + const shouldRequestFranquia = + this.isClientRestricted && !this.nullableNumberEquals(franquiaLineAtual, franquiaLineSolicitada); let payload: UpdateMobileLineRequest; if (this.isClientRestricted) { @@ -1954,15 +1963,21 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { item: this.toInt(this.editModel.item), usuario: (this.editModel.usuario ?? '').toString(), centroDeCustos: (this.editModel.centroDeCustos ?? '').toString(), + setorNome: (this.editModel.setorNome ?? '').toString(), aparelhoId: (this.editModel.aparelhoId ?? null) as string | null, aparelhoNome: (this.editModel.aparelhoNome ?? '').toString(), aparelhoCor: (this.editModel.aparelhoCor ?? '').toString(), - aparelhoImei: (this.editModel.aparelhoImei ?? '').toString() + aparelhoImei: (this.editModel.aparelhoImei ?? '').toString(), + franquiaLine: franquiaLineAtual }; } else { this.calculateFinancials(this.editModel); - const { contaEmpresa: _contaEmpresa, ...editModelPayload } = this.editModel; + const { + contaEmpresa: _contaEmpresa, + franquiaLineSolicitada: _franquiaLineSolicitada, + ...editModelPayload + } = this.editModel; payload = { ...editModelPayload, @@ -2004,12 +2019,48 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return; } + if (shouldRequestFranquia) { + try { + await firstValueFrom(this.solicitacoesLinhasService.create({ + lineId: editingId, + tipoSolicitacao: 'alteracao-franquia', + franquiaLineNova: franquiaLineSolicitada + })); + } catch (err) { + this.editSaving = false; + this.closeAllModals(); + const msg = + (err as HttpErrorResponse)?.error?.message + || 'Registro atualizado, mas falhou ao enviar a solicitação de franquia.'; + await this.showToast(msg); + + if (this.isGroupMode && this.expandedGroup) { + const term = (this.searchTerm ?? '').trim(); + const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined; + this.fetchGroupLines(this.expandedGroup, useTerm); + this.loadGroups(); + this.loadKpis(); + } else { + this.refreshData(); + } + return; + } + } + this.editSaving = false; // fecha e limpa overlay SEMPRE this.closeAllModals(); - await this.showToast(shouldUploadAttachments ? 'Registro e anexos atualizados!' : 'Registro atualizado!'); + if (shouldRequestFranquia) { + await this.showToast( + shouldUploadAttachments + ? 'Registro e anexos atualizados! Solicitação de franquia enviada.' + : 'Registro atualizado! Solicitação de franquia enviada.' + ); + } else { + await this.showToast(shouldUploadAttachments ? 'Registro e anexos atualizados!' : 'Registro atualizado!'); + } if (this.isGroupMode && this.expandedGroup) { const term = (this.searchTerm ?? '').trim(); @@ -2029,6 +2080,26 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { }); } + async requestLineBlock() { + if (!this.editingId || this.requestSaving) return; + + this.requestSaving = true; + try { + await firstValueFrom(this.solicitacoesLinhasService.create({ + lineId: this.editingId, + tipoSolicitacao: 'bloqueio' + })); + + this.requestSaving = false; + this.closeAllModals(); + await this.showToast('Solicitação de bloqueio enviada.'); + } catch (err) { + this.requestSaving = false; + const msg = (err as HttpErrorResponse)?.error?.message || 'Erro ao enviar solicitação de bloqueio.'; + await this.showToast(msg); + } + } + onAparelhoNotaFiscalSelected(event: Event) { const input = event.target as HTMLInputElement | null; this.aparelhoNotaFiscalFile = input?.files && input.files.length > 0 ? input.files[0] : null; @@ -3618,6 +3689,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { valorContratoVivo: d.valorContratoVivo ?? null, franquiaLine: d.franquiaLine ?? null, + franquiaLineSolicitada: d.franquiaLine ?? null, franquiaGestao: d.franquiaGestao ?? null, locacaoAp: d.locacaoAp ?? null, valorContratoLine: d.valorContratoLine ?? null, @@ -3660,6 +3732,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return Number.isNaN(n) ? null : n; } + private nullableNumberEquals(a: number | null, b: number | null): boolean { + if (a === null || b === null) return a === b; + return Math.abs(a - b) < 0.000001; + } + private mergeOption(current: any, list: string[]): string[] { const v = (current ?? '').toString().trim(); if (!v) return list; diff --git a/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.html b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.html new file mode 100644 index 0000000..73098c7 --- /dev/null +++ b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.html @@ -0,0 +1,138 @@ +
+
+
+
+
+
+ Administração +
+ +
+
Solicitações
+ Pedidos de alteração de franquia e bloqueio enviados pelos usuários. +
+ +
+ +
+
+ +
+
+ + + + + +
+ +
+ + Itens por pág: + +
+ +
+
+ +
+ Mostrando {{ pageStart }}-{{ pageEnd }} de {{ total }} +
+
+
+ +
+
+
+ +
+ +
+ {{ errorMsg }} +
+ +
+ Nenhuma solicitação encontrada. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DATACLIENTELINHAUSUARIO LINHATIPOFRANQUIA ANTESFRANQUIA DEPOISDESCRICAO
+
+ {{ formatDate(item.createdAt) }} + {{ formatTime(item.createdAt) }} +
+
{{ item.tenantNome || '-' }}{{ item.linha || '-' }}{{ item.usuarioLinha || '-' }} + {{ tipoLabel(item.tipoSolicitacao) }} + {{ formatFranquia(item.franquiaLineAtual) }}{{ formatFranquia(item.franquiaLineNova) }} + {{ descricao(item) }} +
+
+
+ + +
+
+
diff --git a/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.scss b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.scss new file mode 100644 index 0000000..f4e16fb --- /dev/null +++ b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.scss @@ -0,0 +1,368 @@ +:host { + --brand: #e33dcf; + --text: #111214; + --card-bg: rgba(255, 255, 255, 0.88); + --card-border: 1px solid rgba(227, 61, 207, 0.14); + --stroke: rgba(16, 24, 40, 0.08); + --soft: rgba(17, 18, 20, 0.64); + + display: block; + color: var(--text); +} + +.solicitacoes-page { + min-height: 100vh; + padding: 0 0 18px; + background: + radial-gradient(920px 420px at 15% 8%, rgba(227, 61, 207, 0.1), transparent 60%), + radial-gradient(860px 420px at 85% 20%, rgba(3, 15, 170, 0.07), transparent 60%), + linear-gradient(180deg, #ffffff 0%, #f5f6fb 70%); +} + +.container-geral-responsive { + width: calc(100vw - 2px); + max-width: none; + margin: 16px auto 24px; + position: relative; + z-index: 1; +} + +.geral-card { + border-radius: 22px; + overflow: hidden; + background: var(--card-bg); + border: var(--card-border); + backdrop-filter: blur(8px); + box-shadow: 0 18px 40px rgba(17, 18, 20, 0.1); + display: flex; + flex-direction: column; + min-height: 64vh; +} + +.geral-header { + padding: 16px 22px 14px; + border-bottom: 1px solid var(--stroke); + background: linear-gradient(180deg, rgba(227, 61, 207, 0.05), rgba(255, 255, 255, 0.16)); +} + +.header-row-top { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 12px; + align-items: center; +} + +.title-badge { + justify-self: start; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 7px 12px; + border-radius: 999px; + border: 1px solid rgba(227, 61, 207, 0.2); + background: rgba(255, 255, 255, 0.8); + font-weight: 800; + font-size: 12px; +} + +.header-title { + justify-self: center; + text-align: center; +} + +.title { + font-size: 24px; + font-weight: 900; + letter-spacing: -0.01em; +} + +.subtitle { + color: var(--soft); + font-weight: 600; +} + +.header-actions { + justify-self: end; +} + +.btn-brand { + background: var(--brand); + border-color: var(--brand); + color: #fff; + font-weight: 800; +} + +.controls { + display: grid; + grid-template-columns: minmax(320px, 1.3fr) auto auto; + align-items: center; + gap: 14px; +} + +.search-group { + max-width: 700px; + width: 100%; + border-radius: 13px; + border: 1px solid rgba(16, 24, 40, 0.16); + overflow: hidden; + box-shadow: 0 4px 10px rgba(16, 24, 40, 0.05); + background: #fff; + + .input-group-text, + .form-control, + .btn-clear { + border: none; + } + + .input-group-text { + color: rgba(17, 18, 20, 0.55); + } + + .form-control { + font-size: 13px; + } +} + +.geral-body { + flex: 1; + padding: 0 4px 10px; + overflow: hidden; +} + +.table-wrap { + width: 100%; + min-width: 0; + max-width: 100%; + min-height: clamp(260px, 44vh, 500px); + max-height: 60vh; + border: 1px solid rgba(16, 24, 40, 0.1); + border-radius: 14px; + overflow-x: hidden; + overflow-y: auto; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 251, 255, 0.96) 100%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75); +} + +.table-modern { + width: 100%; + min-width: 0; + table-layout: fixed; +} + +.table-modern col.col-date { width: 9%; } +.table-modern col.col-cliente { width: 14%; } +.table-modern col.col-linha { width: 10%; } +.table-modern col.col-usuario { width: 15%; } +.table-modern col.col-tipo { width: 14%; } +.table-modern col.col-franquia { width: 12%; } +.table-modern col.col-descricao { width: 14%; } + +.table-modern thead th { + position: sticky; + top: 0; + z-index: 2; + background: #f6f8fc; + font-size: 11px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.04em; + color: rgba(17, 18, 20, 0.74); + padding: 10px 12px; + border-bottom: 1px solid #e4e9f3; + white-space: normal; + word-break: break-word; + line-height: 1.2; +} + +.table-modern thead th:nth-child(6), +.table-modern thead th:nth-child(7), +.table-modern tbody td:nth-child(6), +.table-modern tbody td:nth-child(7) { + min-width: 0; + text-align: center; +} + +.table-modern tbody td { + font-size: 13px; + padding: 11px 12px; + border-bottom: 1px solid #ebeff6; + vertical-align: middle; + white-space: normal; + word-break: break-word; + color: #1a1c20; +} + +.table-modern tbody tr:nth-child(even) { + background: rgba(243, 247, 253, 0.56); +} + +.table-modern tbody tr:hover { + background: #eef5ff; +} + +.date-cell { + display: flex; + flex-direction: column; + line-height: 1.15; + gap: 3px; +} + +.date-main { + font-weight: 700; + color: #1a1c20; +} + +.date-sub { + font-size: 11px; + color: rgba(17, 18, 20, 0.55); +} + +.mono-cell { + font-family: "JetBrains Mono", "Consolas", "Monaco", monospace; + font-variant-numeric: tabular-nums; + font-size: 12px; + font-weight: 700; + white-space: normal; + word-break: break-all; +} + +.cell-ellipsis { + overflow: visible; + text-overflow: unset; + white-space: normal; +} + +.type-badge { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + padding: 5px 10px; + font-size: 11px; + font-weight: 800; + line-height: 1; + border: 1px solid transparent; + white-space: nowrap; +} + +.type-badge--franquia { + color: #1e4e8f; + background: rgba(44, 121, 232, 0.14); + border-color: rgba(44, 121, 232, 0.24); +} + +.type-badge--bloqueio { + color: #8f2f2f; + background: rgba(221, 74, 74, 0.14); + border-color: rgba(221, 74, 74, 0.25); +} + +.type-badge--default { + color: #5b6173; + background: rgba(118, 127, 154, 0.14); + border-color: rgba(118, 127, 154, 0.24); +} + +.franquia-cell { + text-align: center; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.message-cell { + white-space: normal; +} + +.message-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + line-height: 1.32; + color: rgba(17, 18, 20, 0.8); + word-break: break-word; +} + +.table-summary { + justify-self: end; + font-size: 12px; + color: rgba(17, 18, 20, 0.64); + font-weight: 600; + + strong { + color: #111214; + font-weight: 800; + } +} + +.empty-group { + padding: 36px 18px; + text-align: center; + color: rgba(17, 18, 20, 0.64); +} + +.geral-footer { + display: none; +} + +.table-wrap::-webkit-scrollbar { + height: 10px; + width: 10px; +} + +.table-wrap::-webkit-scrollbar-track { + background: rgba(17, 18, 20, 0.06); +} + +.table-wrap::-webkit-scrollbar-thumb { + background: rgba(103, 114, 143, 0.45); + border-radius: 999px; +} + +@media (max-width: 1200px) { + .controls { + grid-template-columns: 1fr auto; + } + + .table-summary { + grid-column: 1 / -1; + justify-self: start; + } + + .table-modern col.col-date { width: 10%; } + .table-modern col.col-cliente { width: 13%; } + .table-modern col.col-linha { width: 9%; } + .table-modern col.col-usuario { width: 14%; } + .table-modern col.col-tipo { width: 14%; } + .table-modern col.col-franquia { width: 13%; } + .table-modern col.col-descricao { width: 14%; } + + .table-modern thead th, + .table-modern tbody td { + padding: 9px 8px; + font-size: 11px; + } +} + +@media (max-width: 992px) { + .header-row-top { + grid-template-columns: 1fr; + text-align: center; + } + + .title-badge, + .header-title, + .header-actions { + justify-self: center; + } + + .controls { + grid-template-columns: 1fr; + align-items: start; + } + + .message-text { + -webkit-line-clamp: 3; + } +} diff --git a/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.ts b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.ts new file mode 100644 index 0000000..a52be9f --- /dev/null +++ b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.ts @@ -0,0 +1,189 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { SolicitacaoLinhaDto, SolicitacoesLinhasService } from '../../services/solicitacoes-linhas.service'; + +@Component({ + selector: 'app-solicitacoes-linhas', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent], + templateUrl: './solicitacoes-linhas.html', + styleUrls: ['./solicitacoes-linhas.scss'], +}) +export class SolicitacoesLinhas implements OnInit, OnDestroy { + items: SolicitacaoLinhaDto[] = []; + loading = false; + errorMsg = ''; + + page = 1; + pageSize = 20; + pageSizeOptions = [10, 20, 50, 100]; + total = 0; + + search = ''; + private searchTimer: ReturnType | null = null; + + constructor(private readonly solicitacoesService: SolicitacoesLinhasService) {} + + ngOnInit(): void { + this.fetch(1); + } + + ngOnDestroy(): void { + if (this.searchTimer) { + clearTimeout(this.searchTimer); + this.searchTimer = null; + } + } + + refresh(): void { + this.fetch(); + } + + onSearchChange(): void { + if (this.searchTimer) { + clearTimeout(this.searchTimer); + } + + this.searchTimer = setTimeout(() => { + this.page = 1; + this.fetch(); + }, 300); + } + + clearSearch(): void { + this.search = ''; + this.page = 1; + this.fetch(); + } + + onPageSizeChange(): void { + this.page = 1; + this.fetch(); + } + + goToPage(pageNumber: number): void { + this.page = Math.max(1, Math.min(this.totalPages, pageNumber)); + this.fetch(); + } + + get totalPages(): number { + return Math.ceil((this.total || 0) / this.pageSize) || 1; + } + + get pageNumbers(): number[] { + 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(): number { + return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; + } + + get pageEnd(): number { + if (this.total === 0) return 0; + return Math.min(this.page * this.pageSize, this.total); + } + + private parseDate(value?: string | null): Date | null { + if (!value) return null; + const d = new Date(value); + return Number.isNaN(d.getTime()) ? null : d; + } + + formatDateTime(value?: string | null): string { + const d = this.parseDate(value); + if (!d) return '-'; + return d.toLocaleString('pt-BR'); + } + + formatDate(value?: string | null): string { + const d = this.parseDate(value); + if (!d) return '-'; + return d.toLocaleDateString('pt-BR'); + } + + formatTime(value?: string | null): string { + const d = this.parseDate(value); + if (!d) return '--:--'; + return d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }); + } + + formatFranquiaValor(value?: number | null): string { + if (value === null || value === undefined) return '-'; + return new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 2 }).format(value); + } + + formatFranquia(value?: number | null): string { + const formatted = this.formatFranquiaValor(value); + return formatted === '-' ? '-' : `${formatted} GB`; + } + + tipoLabel(value?: string | null): string { + const v = (value ?? '').toString().trim().toUpperCase(); + if (v === 'ALTERACAO_FRANQUIA') return 'Alteração de franquia'; + if (v === 'BLOQUEIO') return 'Bloqueio'; + return v || '-'; + } + + tipoBadgeClass(value?: string | null): string { + const v = (value ?? '').toString().trim().toUpperCase(); + if (v === 'ALTERACAO_FRANQUIA') return 'type-badge type-badge--franquia'; + if (v === 'BLOQUEIO') return 'type-badge type-badge--bloqueio'; + return 'type-badge type-badge--default'; + } + + trackBySolicitacao(_: number, item: SolicitacaoLinhaDto): string { + return item.id; + } + + descricao(item: SolicitacaoLinhaDto): string { + const tipo = (item.tipoSolicitacao ?? '').toString().trim().toUpperCase(); + const linha = (item.linha ?? '').toString().trim(); + + if (tipo === 'ALTERACAO_FRANQUIA') { + return `Mudanca de franquia de ${this.formatFranquiaValor(item.franquiaLineAtual)} para ${this.formatFranquiaValor(item.franquiaLineNova)}`; + } + + if (tipo === 'BLOQUEIO') { + return `Bloqueio da linha ${linha || '-'}`; + } + + return (item.mensagem ?? '').toString().trim() || '-'; + } + + private fetch(goToPage?: number): void { + if (goToPage) this.page = goToPage; + this.loading = true; + this.errorMsg = ''; + + this.solicitacoesService + .list({ + page: this.page, + pageSize: this.pageSize, + search: this.search?.trim() || undefined, + }) + .subscribe({ + next: (res) => { + this.items = res.items || []; + this.total = res.total || 0; + this.page = res.page || this.page; + this.pageSize = res.pageSize || this.pageSize; + this.loading = false; + }, + error: () => { + this.loading = false; + this.errorMsg = 'Não foi possível carregar as solicitações.'; + }, + }); + } +} diff --git a/src/app/services/solicitacoes-linhas.service.ts b/src/app/services/solicitacoes-linhas.service.ts new file mode 100644 index 0000000..d3982ff --- /dev/null +++ b/src/app/services/solicitacoes-linhas.service.ts @@ -0,0 +1,61 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { environment } from '../../environments/environment'; + +export interface PagedResult { + page: number; + pageSize: number; + total: number; + items: T[]; +} + +export interface SolicitacaoLinhaDto { + id: string; + tenantId: string; + tenantNome?: string | null; + mobileLineId?: string | null; + linha?: string | null; + usuarioLinha?: string | null; + tipoSolicitacao: string; + franquiaLineAtual?: number | null; + franquiaLineNova?: number | null; + solicitanteNome?: string | null; + mensagem: string; + status: string; + createdAt: string; +} + +export interface SolicitacaoLinhaCreatePayload { + lineId: string; + tipoSolicitacao: 'alteracao-franquia' | 'bloqueio'; + franquiaLineNova?: number | null; +} + +@Injectable({ providedIn: 'root' }) +export class SolicitacoesLinhasService { + private readonly baseUrl: string; + + constructor(private readonly http: HttpClient) { + const raw = (environment.apiUrl || '').replace(/\/+$/, ''); + const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.baseUrl = `${apiBase}/solicitacoes-linhas`; + } + + list(params?: { page?: number; pageSize?: number; search?: string }): Observable> { + let httpParams = new HttpParams() + .set('page', String(params?.page ?? 1)) + .set('pageSize', String(params?.pageSize ?? 20)); + + const search = (params?.search ?? '').trim(); + if (search) { + httpParams = httpParams.set('search', search); + } + + return this.http.get>(this.baseUrl, { params: httpParams }); + } + + create(payload: SolicitacaoLinhaCreatePayload): Observable { + return this.http.post(this.baseUrl, payload); + } +} From 8fc8a1303f3c2f66d8bb733f63b5c14b6f56e15f Mon Sep 17 00:00:00 2001 From: Eduardo Lopes Date: Thu, 5 Mar 2026 18:34:20 -0300 Subject: [PATCH 2/7] =?UTF-8?q?Feat:=20Exporta=C3=A7=C3=A3o=20por=20p?= =?UTF-8?q?=C3=A1gina=20e=20Bloquio=20e=20Desbloqueio=20de=20linhas=20em?= =?UTF-8?q?=20lote?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 864 +++++++++++++++++- package.json | 1 + .../chips-controle-recebidos.html | 8 + .../chips-controle-recebidos.ts | 129 ++- .../pages/dados-usuarios/dados-usuarios.html | 5 +- .../pages/dados-usuarios/dados-usuarios.ts | 112 ++- src/app/pages/faturamento/faturamento.html | 8 +- src/app/pages/faturamento/faturamento.ts | 103 ++- src/app/pages/geral/geral.html | 166 +++- src/app/pages/geral/geral.scss | 157 ++++ src/app/pages/geral/geral.ts | 443 ++++++++- src/app/pages/historico/historico.html | 4 + src/app/pages/historico/historico.ts | 106 ++- src/app/pages/mureg/mureg.html | 4 + src/app/pages/mureg/mureg.ts | 167 +++- .../parcelamentos-table.scss | 14 +- .../pages/parcelamentos/parcelamentos.html | 14 + src/app/pages/parcelamentos/parcelamentos.ts | 184 +++- src/app/pages/resumo/resumo.html | 22 +- src/app/pages/resumo/resumo.ts | 131 ++- src/app/pages/troca-numero/troca-numero.html | 5 +- src/app/pages/troca-numero/troca-numero.ts | 99 +- src/app/pages/vigencia/vigencia.html | 4 + src/app/pages/vigencia/vigencia.ts | 108 ++- src/app/services/table-export.service.ts | 462 ++++++++++ 25 files changed, 3188 insertions(+), 132 deletions(-) create mode 100644 src/app/services/table-export.service.ts diff --git a/package-lock.json b/package-lock.json index bcb1605..d41a1fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", "chart.js": "^4.5.1", + "exceljs": "^4.4.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -1354,6 +1355,47 @@ "node": ">=18" } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, "node_modules/@hono/node-server": { "version": "1.19.9", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", @@ -3715,11 +3757,105 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, "node_modules/base64id": { @@ -3765,6 +3901,28 @@ "node": ">=14.0.0" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3778,6 +3936,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", @@ -3849,7 +4024,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -3904,6 +4078,39 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3911,6 +4118,23 @@ "dev": true, "license": "MIT" }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4040,6 +4264,18 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -4214,11 +4450,25 @@ "dev": true, "license": "MIT" }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/connect": { @@ -4357,6 +4607,12 @@ "node": ">=6.6.0" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -4371,6 +4627,31 @@ "node": ">= 0.10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4433,6 +4714,12 @@ "node": ">=4.0" } }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4577,6 +4864,45 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4633,6 +4959,15 @@ "node": ">=0.10.0" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/engine.io": { "version": "6.6.4", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", @@ -4920,6 +5255,26 @@ "node": ">=18.0.0" } }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -4998,6 +5353,19 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5119,6 +5487,12 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -5151,7 +5525,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -5169,6 +5542,35 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5256,7 +5658,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -5310,7 +5711,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-flag": { @@ -5527,6 +5927,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore-walk": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", @@ -5556,6 +5976,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immutable": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", @@ -5578,7 +6004,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -5589,7 +6014,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -5749,6 +6173,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isbinaryfile": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", @@ -5952,6 +6382,48 @@ ], "license": "MIT" }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/karma": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", @@ -6404,6 +6876,63 @@ "node": ">=10" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, "node_modules/listr2": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz", @@ -6483,6 +7012,85 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", @@ -6789,7 +7397,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -6802,7 +7409,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6945,7 +7551,6 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -7156,7 +7761,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7337,7 +7941,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -7446,6 +8049,12 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -7527,7 +8136,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7677,6 +8285,12 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -7764,6 +8378,50 @@ "node": ">= 0.10" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -7953,6 +8611,26 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -8000,6 +8678,18 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -8052,6 +8742,12 @@ "node": ">= 18" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -8520,6 +9216,15 @@ "node": ">=8.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -8597,6 +9302,22 @@ "node": ">=18" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -8628,7 +9349,6 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, "license": "MIT", "engines": { "node": ">=14.14" @@ -8657,6 +9377,15 @@ "node": ">=0.6" } }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -8789,6 +9518,54 @@ "node": ">= 0.8" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -8820,6 +9597,12 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -8830,6 +9613,15 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -9092,7 +9884,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -9117,6 +9908,12 @@ } } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -9175,6 +9972,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/zod": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", diff --git a/package.json b/package.json index 46bb015..b7e9f3b 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", "chart.js": "^4.5.1", + "exceljs": "^4.4.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" diff --git a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html index 513d6e6..5f3da29 100644 --- a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html +++ b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html @@ -35,6 +35,14 @@
+ + @@ -88,7 +92,6 @@ Itens por pág:
-
diff --git a/src/app/pages/dados-usuarios/dados-usuarios.ts b/src/app/pages/dados-usuarios/dados-usuarios.ts index b94a1c9..3526def 100644 --- a/src/app/pages/dados-usuarios/dados-usuarios.ts +++ b/src/app/pages/dados-usuarios/dados-usuarios.ts @@ -2,7 +2,9 @@ import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { TableExportService } from '../../services/table-export.service'; import { DadosUsuariosService, @@ -45,6 +47,7 @@ export class DadosUsuarios implements OnInit { @ViewChild('successToast', { static: false }) successToast!: ElementRef; loading = false; + exporting = false; errorMsg = ''; // Filtros @@ -116,7 +119,8 @@ export class DadosUsuarios implements OnInit { constructor( private service: DadosUsuariosService, private authService: AuthService, - private linesService: LinesService + private linesService: LinesService, + private tableExportService: TableExportService ) {} ngOnInit(): void { @@ -256,6 +260,112 @@ export class DadosUsuarios implements OnInit { } clearFilters() { this.search = ''; 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(); + const fileName = `dados_usuarios_${this.tipoFilter.toLowerCase()}_${timestamp}`; + + await this.tableExportService.exportAsXlsx({ + fileName, + sheetName: 'DadosUsuarios', + rows, + columns: [ + { header: 'ID', value: (row) => row.id ?? '' }, + { header: 'Tipo', value: (row) => this.normalizeTipo(row) }, + { header: 'Cliente', value: (row) => row.cliente ?? '' }, + { + header: this.tipoFilter === 'PJ' ? 'Razao Social' : 'Nome', + value: (row) => (this.normalizeTipo(row) === 'PJ' ? (row.razaoSocial ?? row.cliente ?? '') : (row.nome ?? row.cliente ?? '')), + }, + { header: 'Item', type: 'number', value: (row) => this.toNullableNumber(row.item) ?? 0 }, + { header: 'Linha', value: (row) => row.linha ?? '' }, + { header: 'CPF', value: (row) => row.cpf ?? '' }, + { header: 'CNPJ', value: (row) => row.cnpj ?? '' }, + { header: 'E-mail', value: (row) => row.email ?? '' }, + { header: 'Celular', value: (row) => row.celular ?? '' }, + { header: 'Telefone Fixo', value: (row) => row.telefoneFixo ?? '' }, + { header: 'RG', value: (row) => row.rg ?? '' }, + { header: 'Endereco', value: (row) => row.endereco ?? '' }, + { header: 'Data de Nascimento', type: 'date', value: (row) => row.dataNascimento ?? '' }, + ], + }); + + this.showToast(`Planilha exportada com ${rows.length} registro(s).`, 'success'); + } catch { + this.showToast('Erro ao exportar planilha.', 'danger'); + } finally { + this.exporting = false; + } + } + + private async fetchAllRowsForExport(): Promise { + const pageSize = 500; + let page = 1; + let expectedTotal = 0; + const all: UserDataRow[] = []; + + while (page <= 500) { + const response = await firstValueFrom( + this.service.getRows({ + search: this.search?.trim(), + tipo: this.tipoFilter, + 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.sort((a, b) => { + const byClient = (a.cliente ?? '').localeCompare(b.cliente ?? '', 'pt-BR', { sensitivity: 'base' }); + if (byClient !== 0) return byClient; + return (this.toNullableNumber(a.item) ?? 0) - (this.toNullableNumber(b.item) ?? 0); + }); + } + + private async fetchDetailedRowsForExport(rows: UserDataRow[]): Promise { + if (!rows.length) return []; + + const detailed: UserDataRow[] = []; + 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.service.getById(row.id)); + } catch { + return row; + } + }) + ); + detailed.push(...resolved); + } + + return detailed; + } onPageSizeChange() { this.page = 1; diff --git a/src/app/pages/faturamento/faturamento.html b/src/app/pages/faturamento/faturamento.html index b8dc60b..0fdbf91 100644 --- a/src/app/pages/faturamento/faturamento.html +++ b/src/app/pages/faturamento/faturamento.html @@ -33,7 +33,12 @@ Totais, lucro e comparativo Vivo x Line -
+
+ +
@@ -184,7 +189,6 @@
-
diff --git a/src/app/pages/faturamento/faturamento.ts b/src/app/pages/faturamento/faturamento.ts index 0c46cbb..7046ebc 100644 --- a/src/app/pages/faturamento/faturamento.ts +++ b/src/app/pages/faturamento/faturamento.ts @@ -25,7 +25,9 @@ import { } from '../../services/billing'; import { AuthService } from '../../services/auth.service'; import { LinesService } from '../../services/lines.service'; +import { TableExportService } from '../../services/table-export.service'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; +import { firstValueFrom } from 'rxjs'; interface BillingClientGroup { cliente: string; @@ -54,10 +56,12 @@ export class Faturamento implements AfterViewInit, OnDestroy { private billing: BillingService, private linesService: LinesService, private cdr: ChangeDetectorRef, - private authService: AuthService + private authService: AuthService, + private tableExportService: TableExportService ) {} loading = false; + exporting = false; // filtros searchTerm = ''; @@ -415,6 +419,85 @@ export class Faturamento implements AfterViewInit, OnDestroy { this.loadAllAndApply(forceReloadAll); } + async onExport(): Promise { + if (this.exporting) return; + this.exporting = true; + + try { + const baseRows = this.getRowsForExport(); + const rows = await this.fetchDetailedRowsForExport(baseRows); + if (!rows.length) { + await this.showToast('Nenhum registro encontrado para exportar.'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + const suffix = this.filterTipo === 'ALL' ? 'todos' : this.filterTipo.toLowerCase(); + await this.tableExportService.exportAsXlsx({ + fileName: `faturamento_${suffix}_${timestamp}`, + sheetName: 'Faturamento', + rows, + columns: [ + { header: 'ID', value: (row) => row.id ?? '' }, + { header: 'Tipo', value: (row) => row.tipo ?? '' }, + { header: 'Item', type: 'number', value: (row) => this.toNullableNumber(row.item) ?? 0 }, + { header: 'Cliente', value: (row) => row.cliente ?? '' }, + { header: 'Qtd Linhas', type: 'number', value: (row) => this.toNullableNumber(row.qtdLinhas) ?? 0 }, + { header: 'Franquia Vivo', type: 'number', value: (row) => this.toNullableNumber(row.franquiaVivo) ?? 0 }, + { header: 'Valor Contrato Vivo', type: 'currency', value: (row) => this.toNullableNumber(row.valorContratoVivo) ?? 0 }, + { header: 'Franquia Line', type: 'number', value: (row) => this.toNullableNumber(row.franquiaLine) ?? 0 }, + { header: 'Valor Contrato Line', type: 'currency', value: (row) => this.toNullableNumber(row.valorContratoLine) ?? 0 }, + { header: 'Lucro', type: 'currency', value: (row) => this.toNullableNumber(row.lucro) ?? 0 }, + { header: 'Aparelho', value: (row) => row.aparelho ?? '' }, + { header: 'Forma de Pagamento', value: (row) => row.formaPagamento ?? '' }, + { header: 'Observacao', value: (row) => this.getObservacao(row) }, + { header: 'Criado Em', type: 'datetime', value: (row) => row.createdAt ?? '' }, + { header: 'Atualizado Em', type: 'datetime', value: (row) => row.updatedAt ?? '' }, + ], + }); + + await this.showToast(`Planilha exportada com ${rows.length} registro(s).`); + } catch { + await this.showToast('Erro ao exportar planilha.'); + } finally { + this.exporting = false; + } + } + + private getRowsForExport(): BillingItem[] { + const rows: BillingItem[] = []; + this.rowsByClient.forEach((items) => rows.push(...items)); + + return rows.sort((a, b) => { + const byClient = (a.cliente ?? '').localeCompare(b.cliente ?? '', 'pt-BR', { sensitivity: 'base' }); + if (byClient !== 0) return byClient; + return (this.toNullableNumber(a.item) ?? 0) - (this.toNullableNumber(b.item) ?? 0); + }); + } + + private async fetchDetailedRowsForExport(rows: BillingItem[]): Promise { + if (!rows.length) return []; + + const detailed: BillingItem[] = []; + 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.billing.getById(row.id)); + } catch { + return row; + } + }) + ); + detailed.push(...resolved); + } + + return detailed; + } + private getAllItems(force = false): Promise { const now = Date.now(); @@ -795,4 +878,22 @@ export class Faturamento implements AfterViewInit, OnDestroy { const n = Number(value); return Number.isNaN(n) ? null : n; } + + private async showToast(message: string): Promise { + 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); + } + } } diff --git a/src/app/pages/geral/geral.html b/src/app/pages/geral/geral.html index 6ba0955..f34ddaf 100644 --- a/src/app/pages/geral/geral.html +++ b/src/app/pages/geral/geral.html @@ -31,22 +31,42 @@ Tabela de linhas e dados de telefonia -
- +
+
+ + + +
+ + +
+ +
+ + Selecionadas: {{ batchStatusSelectionCount }} + + + +
@@ -312,6 +352,20 @@ {{ reservaSelectedCount > 0 && reservaSelectedCount === groupLines.length ? 'Limpar seleção' : 'Selecionar todas' }} + + + + + + + + +
{ @@ -303,6 +338,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return `${apiBase}/templates`; })(); loading = false; + exporting = false; isSysAdmin = false; isGestor = false; isClientRestricted = false; @@ -378,6 +414,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { }; reservaTransferLastResult: AssignReservaLinesResultDto | null = null; moveToReservaLastResult: AssignReservaLinesResultDto | null = null; + batchStatusOpen = false; + batchStatusSaving = false; + batchStatusAction: BatchStatusAction = 'BLOCK'; + batchStatusType = ''; + batchStatusUsuario = ''; + batchStatusLastResult: BatchLineStatusUpdateResultDto | null = null; detailData: any = null; financeData: any = null; @@ -609,6 +651,46 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return this.hasGroupLineSelectionTools && !this.isReservaExpandedGroup && !this.isExpandedGroupNamedReserva; } + get blockedStatusOptions(): string[] { + return this.statusOptions.filter((status) => !this.isActiveStatus(status)); + } + + get batchStatusSelectionCount(): number { + return this.reservaSelectedCount; + } + + get canOpenBatchStatusModal(): boolean { + if (this.isClientRestricted) return false; + if (this.loading || this.batchStatusSaving) return false; + return this.batchStatusSelectionCount > 0; + } + + get canSubmitBatchStatusModal(): boolean { + if (this.batchStatusSaving) return false; + if (this.batchStatusSelectionCount <= 0) return false; + if (this.batchStatusAction === 'BLOCK' && !String(this.batchStatusType ?? '').trim()) return false; + return true; + } + + get batchStatusActionLabel(): string { + return this.batchStatusAction === 'BLOCK' ? 'Bloquear' : 'Desbloquear'; + } + + get batchStatusTargetDescription(): string { + return `${this.batchStatusSelectionCount} linha(s) selecionada(s)`; + } + + get batchStatusUserOptions(): string[] { + const users = (this.groupLines ?? []) + .map((x) => (x.usuario ?? '').toString().trim()) + .filter((x) => !!x); + + const current = (this.batchStatusUsuario ?? '').toString().trim(); + if (current) users.push(current); + + return Array.from(new Set(users)).sort((a, b) => a.localeCompare(b, 'pt-BR', { sensitivity: 'base' })); + } + get reservaSelectedCount(): number { return this.reservaSelectedLineIds.length; } @@ -823,7 +905,15 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { // ✅ FIX PRINCIPAL: limpeza forçada de backdrops/scroll lock // ============================================================ private anyModalOpen(): boolean { - return !!(this.detailOpen || this.financeOpen || this.editOpen || this.createOpen || this.reservaTransferOpen || this.moveToReservaOpen); + return !!( + this.detailOpen || + this.financeOpen || + this.editOpen || + this.createOpen || + this.reservaTransferOpen || + this.moveToReservaOpen || + this.batchStatusOpen + ); } private cleanupModalArtifacts() { @@ -851,6 +941,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.createOpen = false; this.reservaTransferOpen = false; this.moveToReservaOpen = false; + this.batchStatusOpen = false; this.detailData = null; this.financeData = null; @@ -869,8 +960,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.batchExcelTemplateDownloading = false; this.reservaTransferSaving = false; this.moveToReservaSaving = false; + this.batchStatusSaving = false; this.reservaTransferLastResult = null; this.moveToReservaLastResult = null; + this.batchStatusLastResult = null; + this.batchStatusUsuario = ''; // Limpa overlays/locks residuais this.cleanupModalArtifacts(); @@ -1800,6 +1894,225 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.refreshData(); } + async onExport(): Promise { + if (this.exporting) return; + this.exporting = true; + + try { + const baseRows = await this.getRowsForExport(); + const rows = await this.getDetailedRowsForExport(baseRows); + if (!rows.length) { + await this.showToast('Nenhum registro encontrado para exportar.'); + return; + } + + const suffix = this.getExportFilterSuffix(); + const timestamp = this.tableExportService.buildTimestamp(); + const fileName = `geral_${suffix}_${timestamp}`; + const templateBuffer = await this.getGeralTemplateBuffer(); + + await this.tableExportService.exportAsXlsx({ + fileName, + sheetName: 'Geral', + templateBuffer, + rows, + columns: [ + { header: 'ID', value: (row) => row.id }, + { header: 'Item', type: 'number', value: (row) => this.toInt(row.item) }, + { header: 'Empresa (Conta)', value: (row) => this.findEmpresaByConta(row.conta) }, + { header: 'Conta', value: (row) => row.conta ?? '' }, + { header: 'Linha', value: (row) => row.linha ?? '' }, + { header: 'Chip', value: (row) => row.chip ?? '' }, + { header: 'Tipo de Chip', value: (row) => row.tipoDeChip ?? '' }, + { header: 'Cliente', value: (row) => row.cliente ?? '' }, + { header: 'Usuario', value: (row) => row.usuario ?? '' }, + { header: 'Centro de Custos', value: (row) => row.centroDeCustos ?? '' }, + { header: 'Setor ID', value: (row) => row.setorId ?? '' }, + { header: 'Setor', value: (row) => row.setorNome ?? '' }, + { header: 'Aparelho ID', value: (row) => row.aparelhoId ?? '' }, + { header: 'Aparelho', value: (row) => row.aparelhoNome ?? '' }, + { header: 'Cor do Aparelho', value: (row) => row.aparelhoCor ?? '' }, + { header: 'IMEI do Aparelho', value: (row) => row.aparelhoImei ?? '' }, + { header: 'NF do Aparelho (anexo)', type: 'boolean', value: (row) => !!row.aparelhoNotaFiscalTemArquivo }, + { header: 'Recibo do Aparelho (anexo)', type: 'boolean', value: (row) => !!row.aparelhoReciboTemArquivo }, + { header: 'Plano Contrato', value: (row) => row.planoContrato ?? '' }, + { header: 'Status', value: (row) => row.status ?? '' }, + { header: 'Tipo (Skil)', value: (row) => row.skil ?? '' }, + { header: 'Modalidade', value: (row) => row.modalidade ?? '' }, + { header: 'Cedente', value: (row) => row.cedente ?? '' }, + { header: 'Solicitante', value: (row) => row.solicitante ?? '' }, + { header: 'Data de Bloqueio', type: 'date', value: (row) => row.dataBloqueio ?? '' }, + { header: 'Data Entrega Operadora', type: 'date', value: (row) => row.dataEntregaOpera ?? '' }, + { header: 'Data Entrega Cliente', type: 'date', value: (row) => row.dataEntregaCliente ?? '' }, + { header: 'Dt. Efetivacao Servico', type: 'date', value: (row) => row.dtEfetivacaoServico ?? '' }, + { header: 'Dt. Termino Fidelizacao', type: 'date', value: (row) => row.dtTerminoFidelizacao ?? '' }, + { header: 'Vencimento da Conta', value: (row) => row.vencConta ?? '' }, + { header: 'Franquia Vivo', type: 'number', value: (row) => this.toNullableNumber(row.franquiaVivo) ?? 0 }, + { header: 'Valor Plano Vivo', type: 'currency', value: (row) => this.toNullableNumber(row.valorPlanoVivo) ?? 0 }, + { header: 'Gestao Voz e Dados', type: 'currency', value: (row) => this.toNullableNumber(row.gestaoVozDados) ?? 0 }, + { header: 'Skeelo', type: 'currency', value: (row) => this.toNullableNumber(row.skeelo) ?? 0 }, + { header: 'Vivo News Plus', type: 'currency', value: (row) => this.toNullableNumber(row.vivoNewsPlus) ?? 0 }, + { header: 'Vivo Travel Mundo', type: 'currency', value: (row) => this.toNullableNumber(row.vivoTravelMundo) ?? 0 }, + { header: 'Vivo Gestao Dispositivo', type: 'currency', value: (row) => this.toNullableNumber(row.vivoGestaoDispositivo) ?? 0 }, + { header: 'Vivo Sync', type: 'currency', value: (row) => this.toNullableNumber(row.vivoSync) ?? 0 }, + { header: 'Valor Contrato Vivo', type: 'currency', value: (row) => this.toNullableNumber(row.valorContratoVivo) ?? 0 }, + { header: 'Franquia Line', type: 'number', value: (row) => this.toNullableNumber(row.franquiaLine) ?? 0 }, + { header: 'Franquia Gestao', type: 'number', value: (row) => this.toNullableNumber(row.franquiaGestao) ?? 0 }, + { header: 'Locacao AP', type: 'currency', value: (row) => this.toNullableNumber(row.locacaoAp) ?? 0 }, + { header: 'Valor Contrato Line', type: 'currency', value: (row) => this.toNullableNumber(row.valorContratoLine) ?? 0 }, + { header: 'Desconto', type: 'currency', value: (row) => this.toNullableNumber(row.desconto) ?? 0 }, + { header: 'Lucro', type: 'currency', value: (row) => this.toNullableNumber(row.lucro) ?? 0 }, + { header: 'Criado Em', type: 'datetime', value: (row) => this.getAnyField(row, ['createdAt', 'CreatedAt']) ?? '' }, + { header: 'Atualizado Em', type: 'datetime', value: (row) => this.getAnyField(row, ['updatedAt', 'UpdatedAt']) ?? '' }, + ], + }); + + await this.showToast(`Planilha exportada com ${rows.length} registro(s).`); + } catch { + await this.showToast('Erro ao exportar a planilha.'); + } finally { + this.exporting = false; + } + } + + private async getRowsForExport(): Promise { + let lines = await this.fetchLinesForGrouping(); + + if (this.selectedClients.length > 0) { + const selected = new Set( + this.selectedClients.map((client) => (client ?? '').toString().trim().toUpperCase()) + ); + lines = lines.filter((line) => selected.has((line.cliente ?? '').toString().trim().toUpperCase())); + } + + const fallbackClient = this.filterSkil === 'RESERVA' ? 'RESERVA' : 'SEM CLIENTE'; + const mapped = lines.map((line) => ({ + id: (line.id ?? '').toString(), + item: String(line.item ?? ''), + linha: line.linha ?? '', + chip: line.chip ?? '', + cliente: ((line.cliente ?? '').toString().trim()) || fallbackClient, + usuario: line.usuario ?? '', + centroDeCustos: line.centroDeCustos ?? '', + setorNome: line.setorNome ?? '', + aparelhoNome: line.aparelhoNome ?? '', + aparelhoCor: line.aparelhoCor ?? '', + status: line.status ?? '', + skil: line.skil ?? '', + contrato: line.vencConta ?? '', + })); + + return mapped.sort((a, b) => { + const byClient = (a.cliente ?? '').localeCompare(b.cliente ?? '', 'pt-BR', { sensitivity: 'base' }); + if (byClient !== 0) return byClient; + + const byItem = this.toInt(a.item) - this.toInt(b.item); + if (byItem !== 0) return byItem; + + return (a.linha ?? '').localeCompare(b.linha ?? '', 'pt-BR', { sensitivity: 'base' }); + }); + } + + private async getDetailedRowsForExport(baseRows: LineRow[]): Promise { + if (!baseRows.length) return []; + + const result: ApiLineDetail[] = []; + const chunkSize = 8; + + for (let i = 0; i < baseRows.length; i += chunkSize) { + const chunk = baseRows.slice(i, i + chunkSize); + const fetched = await Promise.all( + chunk.map(async (row) => { + try { + return await firstValueFrom( + this.http.get(`${this.apiBase}/${row.id}`, { + params: this.withNoCache(new HttpParams()), + }) + ); + } catch { + return this.toDetailFallback(row); + } + }) + ); + + result.push(...fetched); + } + + return result; + } + + private toDetailFallback(row: LineRow): ApiLineDetail { + return { + id: row.id, + item: this.toInt(row.item), + qtdLinhas: null, + conta: row.contrato ?? null, + linha: row.linha ?? null, + chip: row.chip ?? null, + tipoDeChip: null, + cliente: row.cliente ?? null, + usuario: row.usuario ?? null, + centroDeCustos: row.centroDeCustos ?? null, + setorId: null, + setorNome: row.setorNome ?? null, + aparelhoId: null, + aparelhoNome: row.aparelhoNome ?? null, + aparelhoCor: row.aparelhoCor ?? null, + aparelhoImei: null, + aparelhoNotaFiscalTemArquivo: false, + aparelhoReciboTemArquivo: false, + planoContrato: null, + status: row.status ?? null, + skil: row.skil ?? null, + modalidade: null, + dataBloqueio: null, + cedente: null, + solicitante: null, + dataEntregaOpera: null, + dataEntregaCliente: null, + dtEfetivacaoServico: null, + dtTerminoFidelizacao: null, + vencConta: row.contrato ?? null, + franquiaVivo: null, + valorPlanoVivo: null, + gestaoVozDados: null, + skeelo: null, + vivoNewsPlus: null, + vivoTravelMundo: null, + vivoGestaoDispositivo: null, + vivoSync: null, + valorContratoVivo: null, + franquiaLine: null, + franquiaGestao: null, + locacaoAp: null, + valorContratoLine: null, + desconto: null, + lucro: null, + }; + } + + private async getGeralTemplateBuffer(): Promise { + try { + const params = new HttpParams().set('_', `${Date.now()}`); + const blob = await firstValueFrom( + this.http.get(`${this.templatesApiBase}/planilha-geral`, { + params, + responseType: 'blob', + }) + ); + return await blob.arrayBuffer(); + } catch { + return null; + } + } + + private getExportFilterSuffix(): string { + if (this.filterSkil === 'PF') return 'pf'; + if (this.filterSkil === 'PJ') return 'pj'; + if (this.filterSkil === 'RESERVA') return 'reserva'; + return 'todas'; + } + async onImportExcel() { if (!this.isSysAdmin) { await this.showToast('Você não tem permissão para importar planilha.'); @@ -3167,6 +3480,116 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.reservaSelectedLineIds = []; } + async openBatchStatusModal(action: BatchStatusAction) { + if (this.isClientRestricted) { + await this.showToast('Você não tem permissão para bloquear/desbloquear em lote.'); + return; + } + + if (this.batchStatusSelectionCount <= 0) { + await this.showToast('Selecione ao menos uma linha para processar.'); + return; + } + + this.batchStatusAction = action; + this.batchStatusSaving = false; + this.batchStatusLastResult = null; + this.batchStatusUsuario = ''; + + if (action === 'BLOCK') { + const current = (this.batchStatusType ?? '').toString().trim(); + const options = this.blockedStatusOptions; + if (!current || !options.some((x) => x === current)) { + this.batchStatusType = options[0] ?? ''; + } + } else { + this.batchStatusType = ''; + } + + this.batchStatusOpen = true; + this.cdr.detectChanges(); + } + + async submitBatchStatusUpdate() { + if (this.batchStatusSaving) return; + if (!this.canSubmitBatchStatusModal) return; + + const payload = this.buildBatchStatusPayload(); + this.batchStatusSaving = true; + + this.http.post(`${this.apiBase}/batch-status-update`, payload).subscribe({ + next: async (res) => { + this.batchStatusSaving = false; + this.batchStatusLastResult = res; + + const ok = Number(res?.updated ?? 0) || 0; + const failed = Number(res?.failed ?? 0) || 0; + + this.batchStatusOpen = false; + this.clearReservaSelection(); + this.batchStatusUsuario = ''; + + await this.showToast( + failed > 0 + ? `${this.batchStatusActionLabel} em lote concluído com pendências: ${ok} linha(s) processada(s), ${failed} falha(s).` + : `${this.batchStatusActionLabel} em lote concluído: ${ok} linha(s) processada(s).` + ); + + if (this.expandedGroup) { + const term = (this.searchTerm ?? '').trim(); + const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined; + this.fetchGroupLines(this.expandedGroup, useTerm); + } + + this.loadGroups(); + this.loadKpis(); + }, + error: async (err: HttpErrorResponse) => { + this.batchStatusSaving = false; + const msg = (err.error as any)?.message || 'Erro ao processar bloqueio/desbloqueio em lote.'; + await this.showToast(msg); + } + }); + } + + private buildBatchStatusPayload(): BatchLineStatusUpdateRequestDto { + const clients = this.searchResolvedClient + ? [this.searchResolvedClient] + : [...this.selectedClients]; + + const normalizedClients = clients + .map((x) => (x ?? '').toString().trim()) + .filter((x) => !!x); + + const userFilter = (this.batchStatusUsuario ?? '').toString().trim(); + + return { + action: this.batchStatusAction === 'BLOCK' ? 'block' : 'unblock', + blockStatus: this.batchStatusAction === 'BLOCK' ? (this.batchStatusType || null) : null, + applyToAllFiltered: false, + lineIds: [...this.reservaSelectedLineIds], + search: (this.searchTerm ?? '').toString().trim() || null, + skil: this.resolveFilterSkilForApi(), + clients: normalizedClients, + additionalMode: this.resolveAdditionalModeForApi(), + additionalServices: this.selectedAdditionalServices.length > 0 ? this.selectedAdditionalServices.join(',') : null, + usuario: userFilter || null + }; + } + + private resolveFilterSkilForApi(): string | null { + if (this.filterSkil === 'PF') return 'PESSOA FÍSICA'; + if (this.filterSkil === 'PJ') return 'PESSOA JURÍDICA'; + if (this.filterSkil === 'RESERVA') return 'RESERVA'; + return null; + } + + private resolveAdditionalModeForApi(): string | null { + if (this.additionalMode === 'WITH') return 'with'; + if (this.additionalMode === 'WITHOUT') return 'without'; + return null; + } + async openReservaTransferModal() { if (!this.isReservaExpandedGroup) { await this.showToast('Abra um grupo no filtro Reserva para selecionar e atribuir linhas.'); @@ -3588,6 +4011,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return v || '-'; } + private isActiveStatus(status: string | null | undefined): boolean { + const normalized = (status ?? '').toString().trim().toLowerCase(); + if (!normalized) return false; + return normalized.includes('ativo'); + } + private toEditModel(d: ApiLineDetail): any { return { ...d, @@ -3660,6 +4089,16 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return Number.isNaN(n) ? null : n; } + private getAnyField(row: unknown, keys: string[]): unknown { + const source = row as Record; + for (const key of keys) { + if (source && source[key] !== undefined && source[key] !== null && source[key] !== '') { + return source[key]; + } + } + return null; + } + private mergeOption(current: any, list: string[]): string[] { const v = (current ?? '').toString().trim(); if (!v) return list; diff --git a/src/app/pages/historico/historico.html b/src/app/pages/historico/historico.html index e01f1e8..9cc9940 100644 --- a/src/app/pages/historico/historico.html +++ b/src/app/pages/historico/historico.html @@ -33,6 +33,10 @@ +
diff --git a/src/app/pages/historico/historico.ts b/src/app/pages/historico/historico.ts index 5ce35cf..16fe62c 100644 --- a/src/app/pages/historico/historico.ts +++ b/src/app/pages/historico/historico.ts @@ -2,9 +2,11 @@ import { Component, OnInit, ElementRef, ViewChild, ChangeDetectorRef, Inject, PL import { CommonModule, isPlatformBrowser } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { HistoricoService, AuditLogDto, AuditChangeType, HistoricoQuery } from '../../services/historico.service'; +import { TableExportService } from '../../services/table-export.service'; interface SelectOption { value: string; @@ -23,6 +25,7 @@ export class Historico implements OnInit { logs: AuditLogDto[] = []; loading = false; + exporting = false; error = false; errorMsg = ''; toastMessage = ''; @@ -65,7 +68,8 @@ export class Historico implements OnInit { constructor( private historicoService: HistoricoService, private cdr: ChangeDetectorRef, - @Inject(PLATFORM_ID) private platformId: object + @Inject(PLATFORM_ID) private platformId: object, + private tableExportService: TableExportService ) {} ngOnInit(): void { @@ -111,6 +115,47 @@ export class Historico implements OnInit { this.fetch(); } + async onExport(): Promise { + if (this.exporting) return; + this.exporting = true; + + try { + const logs = await this.fetchAllLogsForExport(); + if (!logs.length) { + await this.showToast('Nenhum registro encontrado para exportar.'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `historico_${timestamp}`, + sheetName: 'Historico', + rows: logs, + columns: [ + { header: 'ID', value: (log) => log.id ?? '' }, + { header: 'Data/Hora', type: 'datetime', value: (log) => log.occurredAtUtc ?? '' }, + { header: 'Usuario', value: (log) => this.displayUserName(log) }, + { header: 'E-mail', value: (log) => log.userEmail ?? '' }, + { header: 'Pagina', value: (log) => log.page ?? '' }, + { header: 'Acao', value: (log) => this.formatAction(log.action) }, + { header: 'Entidade', value: (log) => this.displayEntity(log) }, + { header: 'Id Entidade', value: (log) => log.entityId ?? '' }, + { header: 'Metodo HTTP', value: (log) => log.requestMethod ?? '' }, + { header: 'Endpoint', value: (log) => log.requestPath ?? '' }, + { header: 'IP', value: (log) => log.ipAddress ?? '' }, + { header: 'Mudancas', value: (log) => this.formatChangesSummary(log) }, + { header: 'Qtd Mudancas', type: 'number', value: (log) => log.changes?.length ?? 0 }, + ], + }); + + await this.showToast(`Planilha exportada com ${logs.length} registro(s).`); + } catch { + await this.showToast('Erro ao exportar planilha.'); + } finally { + this.exporting = false; + } + } + goToPage(p: number): void { this.page = Math.max(1, Math.min(this.totalPages, p)); this.fetch(); @@ -217,14 +262,9 @@ export class Historico implements OnInit { this.expandedLogId = null; const query: HistoricoQuery = { + ...this.buildBaseQuery(), page: this.page, pageSize: this.pageSize, - pageName: this.filterPageName || undefined, - action: this.filterAction || undefined, - user: this.filterUser?.trim() || undefined, - search: this.filterSearch?.trim() || undefined, - dateFrom: this.toIsoDate(this.dateFrom, false) || undefined, - dateTo: this.toIsoDate(this.dateTo, true) || undefined, }; this.historicoService.list(query).subscribe({ @@ -247,6 +287,58 @@ export class Historico implements OnInit { }); } + private async fetchAllLogsForExport(): Promise { + const pageSize = 500; + let page = 1; + let expectedTotal = 0; + const all: AuditLogDto[] = []; + + while (page <= 500) { + const response = await firstValueFrom( + this.historicoService.list({ + ...this.buildBaseQuery(), + page, + pageSize, + }) + ); + + 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 buildBaseQuery(): Omit { + return { + pageName: this.filterPageName || undefined, + action: this.filterAction || undefined, + user: this.filterUser?.trim() || undefined, + search: this.filterSearch?.trim() || undefined, + dateFrom: this.toIsoDate(this.dateFrom, false) || undefined, + dateTo: this.toIsoDate(this.dateTo, true) || undefined, + }; + } + + private formatChangesSummary(log: AuditLogDto): string { + const changes = log?.changes ?? []; + if (!changes.length) return ''; + return changes + .map((change) => { + const field = change?.field ?? 'campo'; + const oldValue = this.formatChangeValue(change?.oldValue); + const newValue = this.formatChangeValue(change?.newValue); + return `${field}: ${oldValue} -> ${newValue}`; + }) + .join(' | '); + } + private toIsoDate(value: string, endOfDay: boolean): string | null { if (!value) return null; const time = endOfDay ? '23:59:59' : '00:00:00'; diff --git a/src/app/pages/mureg/mureg.html b/src/app/pages/mureg/mureg.html index 58c0764..9a4cfa7 100644 --- a/src/app/pages/mureg/mureg.html +++ b/src/app/pages/mureg/mureg.html @@ -31,6 +31,10 @@
+ diff --git a/src/app/pages/mureg/mureg.ts b/src/app/pages/mureg/mureg.ts index a09f8f8..86fdc7b 100644 --- a/src/app/pages/mureg/mureg.ts +++ b/src/app/pages/mureg/mureg.ts @@ -10,8 +10,10 @@ import { import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient, HttpParams } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; import { LinesService } from '../../services/lines.service'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { TableExportService } from '../../services/table-export.service'; import { environment } from '../../../environments/environment'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; @@ -75,6 +77,17 @@ interface MuregDetailDto { statusNaGeral: string | null; } +type MuregExportRow = MuregRow & { + usuario?: string | null; + skil?: string | null; + linhaAtualNaGeral?: string | null; + chipNaGeral?: string | null; + contaNaGeral?: string | null; + statusNaGeral?: string | null; + createdAt?: string | null; + updatedAt?: string | null; +}; + @Component({ standalone: true, imports: [CommonModule, FormsModule, CustomSelectComponent], @@ -84,6 +97,7 @@ interface MuregDetailDto { export class Mureg implements AfterViewInit { toastMessage = ''; loading = false; + exporting = false; @ViewChild('successToast', { static: false }) successToast!: ElementRef; @@ -91,7 +105,8 @@ export class Mureg implements AfterViewInit { @Inject(PLATFORM_ID) private platformId: object, private http: HttpClient, private cdr: ChangeDetectorRef, - private linesService: LinesService + private linesService: LinesService, + private tableExportService: TableExportService ) {} private readonly apiBase = (() => { @@ -184,6 +199,147 @@ export class Mureg implements AfterViewInit { this.loadForGroups(); } + 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) { + await this.showToast('Nenhum registro encontrado para exportar.'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `mureg_${timestamp}`, + sheetName: 'Mureg', + rows, + columns: [ + { header: 'ID', value: (row) => row.id ?? '' }, + { header: 'Cliente', value: (row) => row.cliente }, + { header: 'Usuario', value: (row) => row.usuario ?? '' }, + { header: 'Skil', value: (row) => row.skil ?? '' }, + { header: 'Item', type: 'number', value: (row) => this.toIntOrZero(row.item) }, + { header: 'Linha Antiga', value: (row) => row.linhaAntiga }, + { header: 'Linha Nova', value: (row) => row.linhaNova }, + { header: 'ICCID', value: (row) => row.iccid }, + { header: 'Data da Mureg', type: 'date', value: (row) => row.dataDaMureg }, + { header: 'Situacao', value: (row) => (this.isTroca(row) ? 'TROCA' : 'SEM TROCA') }, + { header: 'Linha ID (Geral)', value: (row) => row.mobileLineId ?? '' }, + { header: 'Linha Atual na Geral', value: (row) => row.linhaAtualNaGeral ?? '' }, + { header: 'Chip na Geral', value: (row) => row.chipNaGeral ?? '' }, + { header: 'Conta na Geral', value: (row) => row.contaNaGeral ?? '' }, + { header: 'Status na Geral', value: (row) => row.statusNaGeral ?? '' }, + { header: 'Criado Em', type: 'datetime', value: (row) => row.createdAt ?? '' }, + { header: 'Atualizado Em', type: 'datetime', value: (row) => row.updatedAt ?? '' }, + ], + }); + + await this.showToast(`Planilha exportada com ${rows.length} registro(s).`); + } catch { + await this.showToast('Erro ao exportar planilha.'); + } finally { + this.exporting = false; + } + } + + private async fetchAllRowsForExport(): Promise { + const pageSize = 2000; + let page = 1; + let expectedTotal = 0; + const rows: MuregRow[] = []; + + while (page <= 500) { + const params = new HttpParams() + .set('page', String(page)) + .set('pageSize', String(pageSize)) + .set('search', (this.searchTerm ?? '').trim()) + .set('sortBy', 'cliente') + .set('sortDir', 'asc'); + + const response = await firstValueFrom( + this.http.get | any[]>(this.apiBase, { params }) + ); + + const items = Array.isArray(response) ? response : (response.items ?? []); + const normalized = items.map((item: any, idx: number) => this.normalizeRow(item, rows.length + idx)); + rows.push(...normalized); + expectedTotal = Array.isArray(response) ? 0 : Number(response.total ?? 0); + + if (Array.isArray(response)) break; + if (items.length === 0) break; + if (items.length < pageSize) break; + if (expectedTotal > 0 && rows.length >= expectedTotal) break; + page += 1; + } + + return rows.sort((a, b) => { + const byClient = (a.cliente ?? '').localeCompare(b.cliente ?? '', 'pt-BR', { sensitivity: 'base' }); + if (byClient !== 0) return byClient; + + const byItem = this.toIntOrZero(a.item) - this.toIntOrZero(b.item); + if (byItem !== 0) return byItem; + + return (a.linhaNova ?? '').localeCompare(b.linhaNova ?? '', 'pt-BR', { sensitivity: 'base' }); + }); + } + + private async fetchDetailedRowsForExport(rows: MuregRow[]): Promise { + if (!rows.length) return []; + + const result: MuregExportRow[] = []; + const chunkSize = 10; + + for (let i = 0; i < rows.length; i += chunkSize) { + const chunk = rows.slice(i, i + chunkSize); + const detailedChunk = await Promise.all( + chunk.map(async (row) => { + try { + const detail = await firstValueFrom(this.http.get(`${this.apiBase}/${row.id}`)); + const merged: MuregExportRow = { + ...row, + item: detail.item !== undefined && detail.item !== null ? String(detail.item) : row.item, + linhaAntiga: detail.linhaAntiga ?? row.linhaAntiga, + linhaNova: detail.linhaNova ?? row.linhaNova, + iccid: detail.iccid ?? row.iccid, + dataDaMureg: detail.dataDaMureg ?? row.dataDaMureg, + cliente: detail.cliente ?? row.cliente, + mobileLineId: detail.mobileLineId ?? row.mobileLineId, + usuario: detail.usuario ?? null, + skil: detail.skil ?? null, + linhaAtualNaGeral: detail.linhaAtualNaGeral ?? null, + chipNaGeral: detail.chipNaGeral ?? null, + contaNaGeral: detail.contaNaGeral ?? null, + statusNaGeral: detail.statusNaGeral ?? null, + createdAt: this.getRawField(detail, ['createdAt', 'CreatedAt']) ?? this.getRawField(row.raw, ['createdAt', 'CreatedAt']), + updatedAt: this.getRawField(detail, ['updatedAt', 'UpdatedAt']) ?? this.getRawField(row.raw, ['updatedAt', 'UpdatedAt']), + }; + + return merged; + } catch { + return { + ...row, + usuario: this.getRawField(row.raw, ['usuario', 'Usuario']), + skil: this.getRawField(row.raw, ['skil', 'Skil']), + linhaAtualNaGeral: this.getRawField(row.raw, ['linhaAtualNaGeral', 'LinhaAtualNaGeral']), + chipNaGeral: this.getRawField(row.raw, ['chipNaGeral', 'ChipNaGeral']), + contaNaGeral: this.getRawField(row.raw, ['contaNaGeral', 'ContaNaGeral']), + statusNaGeral: this.getRawField(row.raw, ['statusNaGeral', 'StatusNaGeral']), + createdAt: this.getRawField(row.raw, ['createdAt', 'CreatedAt']), + updatedAt: this.getRawField(row.raw, ['updatedAt', 'UpdatedAt']), + }; + } + }) + ); + + result.push(...detailedChunk); + } + + return result; + } + onSearch() { if (this.searchTimer) clearTimeout(this.searchTimer); this.searchTimer = setTimeout(() => { @@ -770,6 +926,15 @@ export class Mureg implements AfterViewInit { } } + private getRawField(source: any, keys: string[]): string | null { + for (const key of keys) { + const value = source?.[key]; + if (value === undefined || value === null || String(value).trim() === '') continue; + return String(value); + } + return null; + } + displayValue(key: MuregKey, v: any): string { if (v === null || v === undefined || String(v).trim() === '') return '-'; diff --git a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss index f1c8de2..cd072b7 100644 --- a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss +++ b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss @@ -429,14 +429,14 @@ display: inline-flex; align-items: center; gap: 8px; +} - span { - color: var(--pg-text-soft, #64748b); - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.05em; - font-weight: 800; - } +.page-size span { + color: var(--pg-text-soft, #64748b); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 800; } .select-glass { diff --git a/src/app/pages/parcelamentos/parcelamentos.html b/src/app/pages/parcelamentos/parcelamentos.html index 1697b20..78ba26f 100644 --- a/src/app/pages/parcelamentos/parcelamentos.html +++ b/src/app/pages/parcelamentos/parcelamentos.html @@ -1,4 +1,14 @@
+
+
+
+ LineGestao + +
+
{{ toastMessage }}
+
+
+
diff --git a/src/app/pages/resumo/resumo.ts b/src/app/pages/resumo/resumo.ts index 3a7706d..a2c04d2 100644 --- a/src/app/pages/resumo/resumo.ts +++ b/src/app/pages/resumo/resumo.ts @@ -31,6 +31,7 @@ import { ReservaPorDdd, ReservaTotal } from '../../services/resumo.service'; +import { TableExportService, type ExportCellType } from '../../services/table-export.service'; import { environment } from '../../../environments/environment'; type ResumoTab = 'planos' | 'clientes' | 'totais' | 'reserva'; @@ -85,6 +86,11 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { loading = false; errorMessage = ''; + toastOpen = false; + toastMessage = ''; + toastType: 'success' | 'danger' = 'success'; + private toastTimer: ReturnType | null = null; + private exportingKeys = new Set(); resumo: ResumoResponse | null = null; activeTab: ResumoTab = 'planos'; @@ -139,7 +145,8 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { private resumoService: ResumoService, private route: ActivatedRoute, private router: Router, - private cdr: ChangeDetectorRef + private cdr: ChangeDetectorRef, + private tableExportService: TableExportService ) { const raw = (environment.apiUrl || '').replace(/\/+$/, ''); this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; @@ -172,6 +179,7 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { ngOnDestroy(): void { Object.values(this.charts).forEach(c => c?.destroy()); + if (this.toastTimer) clearTimeout(this.toastTimer); } setTab(tab: ResumoTab): void { @@ -644,7 +652,7 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { this.updateGroupView(g); } toggleGroupedCompact(g: GroupedTableState) { g.compact = !g.compact; } - exportGroupedCsv(g: GroupedTableState, file: string) { this.exportCsv(g.table, file); } + exportGroupedCsv(g: GroupedTableState, file: string) { void this.exportTableAsXlsx(g.table, file); } isGroupedOpen(g: GroupedTableState, key: string) { return g.open.has(key); } toggleGroupedOpen(g: GroupedTableState, key: string) { if (g.open.has(key)) g.open.delete(key); else g.open.add(key); } openGroupedDetail(g: GroupedTableState, item: GroupItem) { g.detailGroup = item; g.detailOpen = true; } @@ -677,6 +685,10 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { return normalized === 'true' || normalized === '1' || normalized === 'sim'; } + isExporting(key: string): boolean { + return this.exportingKeys.has(key); + } + private initTables() { const hideMoneyColumns = (cols: TableColumn[]) => this.showFinancial ? cols : cols.filter((c) => c.type !== 'money'); @@ -1214,78 +1226,59 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { return Number.isNaN(parsed) ? null : parsed; } - private exportCsv(table: TableState, filename: string) { + private async exportTableAsXlsx(table: TableState, fileKey: string): Promise { if (!isPlatformBrowser(this.platformId)) return; + if (this.exportingKeys.has(fileKey)) return; + const rows = table.data ?? []; - const columns = table.columns ?? []; - const generatedAt = new Date().toLocaleString('pt-BR'); - const escapeHtml = (value: string) => - value - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + if (!rows.length) { + this.showToast('Nenhum registro encontrado para exportar.', 'danger'); + return; + } - const headerHtml = columns - .map((column) => `${escapeHtml(column.label)}`) - .join(''); + this.exportingKeys.add(fileKey); + try { + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `${fileKey}_${timestamp}`, + sheetName: table.label || 'Resumo', + rows, + columns: (table.columns ?? []).map((column) => ({ + header: column.label, + type: this.mapColumnType(column.type), + value: (row: T) => this.getExportColumnValue(column, row), + })), + }); - const bodyHtml = rows - .map((row, index) => { - const cells = columns - .map((column) => { - const value = this.formatCell(column, row); - const toneClass = column.tone ? this.getToneClass(column.value(row)) : ''; - const alignClass = column.align === 'right' ? 'text-right' : column.align === 'center' ? 'text-center' : ''; - const classes = [alignClass, toneClass].filter(Boolean).join(' '); - return `${escapeHtml(String(value))}`; - }) - .join(''); - return `${cells}`; - }) - .join(''); + this.showToast(`Planilha exportada com ${rows.length} registro(s).`, 'success'); + } catch { + this.showToast('Erro ao exportar planilha.', 'danger'); + } finally { + this.exportingKeys.delete(fileKey); + } + } - const html = ` - - - - - - -
${escapeHtml(table.label || 'Resumo')}
-
Exportado em ${escapeHtml(generatedAt)} | Total de linhas: ${rows.length}
- - - ${headerHtml} - - - ${bodyHtml} - -
- -`; + private getExportColumnValue(column: TableColumn, row: T): unknown { + const rawValue = column.value(row); + if (column.type === 'money' || column.type === 'number' || column.type === 'gb') { + const numeric = this.toNumber(rawValue); + if (numeric !== null) return numeric; + } + return this.formatCell(column, row); + } - const blob = new Blob([`\uFEFF${html}`], { type: 'application/vnd.ms-excel;charset=utf-8;' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${filename}.xls`; - a.click(); - URL.revokeObjectURL(url); + private mapColumnType(type: TableColumn['type']): ExportCellType { + if (type === 'money') return 'currency'; + if (type === 'number' || type === 'gb') return 'number'; + return 'text'; + } + + private showToast(message: string, type: 'success' | 'danger'): void { + this.toastMessage = message; + this.toastType = type; + this.toastOpen = true; + if (this.toastTimer) clearTimeout(this.toastTimer); + this.toastTimer = setTimeout(() => (this.toastOpen = false), 3000); } private getReservaPorDddChartData(): Array<{ label: string; totalLinhas: number }> { @@ -1310,7 +1303,7 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { return Array.from(map.entries()).map(([label, totalLinhas]) => ({ label, totalLinhas })); } - exportMacrophonyCsv() { this.exportCsv(this.tableMacrophony, 'macrophony-planos'); } + exportMacrophonyCsv() { void this.exportTableAsXlsx(this.tableMacrophony, 'macrophony-planos'); } findLineTotal(k: string[]): LineTotal | null { const keys = k.map((item) => item.toUpperCase()); const list = this.getEffectiveLineTotais(); diff --git a/src/app/pages/troca-numero/troca-numero.html b/src/app/pages/troca-numero/troca-numero.html index 6e31ffb..0edff36 100644 --- a/src/app/pages/troca-numero/troca-numero.html +++ b/src/app/pages/troca-numero/troca-numero.html @@ -31,6 +31,10 @@
+ @@ -86,7 +90,6 @@ Itens por pág:
-
diff --git a/src/app/pages/troca-numero/troca-numero.ts b/src/app/pages/troca-numero/troca-numero.ts index b27b637..8eda5bc 100644 --- a/src/app/pages/troca-numero/troca-numero.ts +++ b/src/app/pages/troca-numero/troca-numero.ts @@ -10,7 +10,9 @@ import { import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient, HttpParams } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { TableExportService } from '../../services/table-export.service'; import { environment } from '../../../environments/environment'; type TrocaKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataTroca' | 'motivo' | 'observacao'; @@ -63,13 +65,15 @@ interface LineOptionDto { export class TrocaNumero implements AfterViewInit { toastMessage = ''; loading = false; + exporting = false; @ViewChild('successToast', { static: false }) successToast!: ElementRef; constructor( @Inject(PLATFORM_ID) private platformId: object, private http: HttpClient, - private cdr: ChangeDetectorRef + private cdr: ChangeDetectorRef, + private tableExportService: TableExportService ) {} private readonly apiBase = (() => { @@ -151,6 +155,90 @@ export class TrocaNumero implements AfterViewInit { this.loadForGroups(); } + async onExport(): Promise { + if (this.exporting) return; + this.exporting = true; + + try { + const rows = await this.fetchAllRowsForExport(); + if (!rows.length) { + await this.showToast('Nenhum registro encontrado para exportar.'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `troca_numero_${timestamp}`, + sheetName: 'TrocaNumero', + rows, + columns: [ + { header: 'ID', value: (row) => row.id ?? '' }, + { header: 'Motivo', value: (row) => row.motivo }, + { header: 'Cliente', value: (row) => this.getRawField(row, ['cliente', 'Cliente']) ?? '' }, + { header: 'Usuario', value: (row) => this.getRawField(row, ['usuario', 'Usuario']) ?? '' }, + { header: 'Skil', value: (row) => this.getRawField(row, ['skil', 'Skil']) ?? '' }, + { header: 'Item', type: 'number', value: (row) => this.toNumberOrNull(row.item) ?? 0 }, + { header: 'Linha Antiga', value: (row) => row.linhaAntiga }, + { header: 'Linha Nova', value: (row) => row.linhaNova }, + { header: 'ICCID', value: (row) => row.iccid }, + { header: 'Data da Troca', type: 'date', value: (row) => row.dataTroca }, + { header: 'Observacao', value: (row) => row.observacao }, + { header: 'Situacao', value: (row) => (this.isTroca(row) ? 'TROCA' : 'SEM TROCA') }, + { header: 'Linha ID (Geral)', value: (row) => this.getRawField(row, ['mobileLineId', 'MobileLineId']) ?? '' }, + { header: 'Criado Em', type: 'datetime', value: (row) => this.getRawField(row, ['createdAt', 'CreatedAt']) ?? '' }, + { header: 'Atualizado Em', type: 'datetime', value: (row) => this.getRawField(row, ['updatedAt', 'UpdatedAt']) ?? '' }, + ], + }); + + await this.showToast(`Planilha exportada com ${rows.length} registro(s).`); + } catch { + await this.showToast('Erro ao exportar planilha.'); + } finally { + this.exporting = false; + } + } + + private async fetchAllRowsForExport(): Promise { + const pageSize = 2000; + let page = 1; + let expectedTotal = 0; + const rows: TrocaRow[] = []; + + while (page <= 500) { + const params = new HttpParams() + .set('page', String(page)) + .set('pageSize', String(pageSize)) + .set('search', (this.searchTerm ?? '').trim()) + .set('sortBy', 'motivo') + .set('sortDir', 'asc'); + + const response = await firstValueFrom( + this.http.get | any[]>(this.apiBase, { params }) + ); + + const items = Array.isArray(response) ? response : (response.items ?? []); + const normalized = items.map((item: any, idx: number) => this.normalizeRow(item, rows.length + idx)); + rows.push(...normalized); + expectedTotal = Array.isArray(response) ? 0 : Number(response.total ?? 0); + + if (Array.isArray(response)) break; + if (items.length === 0) break; + if (items.length < pageSize) break; + if (expectedTotal > 0 && rows.length >= expectedTotal) break; + page += 1; + } + + return rows.sort((a, b) => { + const byMotivo = (a.motivo ?? '').localeCompare(b.motivo ?? '', 'pt-BR', { sensitivity: 'base' }); + if (byMotivo !== 0) return byMotivo; + + const byItem = (this.toNumberOrNull(a.item) ?? 0) - (this.toNumberOrNull(b.item) ?? 0); + if (byItem !== 0) return byItem; + + return (a.linhaNova ?? '').localeCompare(b.linhaNova ?? '', 'pt-BR', { sensitivity: 'base' }); + }); + } + onSearch() { if (this.searchTimer) clearTimeout(this.searchTimer); this.searchTimer = setTimeout(() => { @@ -542,6 +630,15 @@ export class TrocaNumero implements AfterViewInit { return Number.isFinite(n) ? n : null; } + private getRawField(row: TrocaRow, keys: string[]): string | null { + for (const key of keys) { + const value = row?.raw?.[key]; + if (value === undefined || value === null || String(value).trim() === '') continue; + return String(value); + } + return null; + } + private isoToDateInput(iso: string | null | undefined): string { if (!iso) return ''; const dt = new Date(iso); diff --git a/src/app/pages/vigencia/vigencia.html b/src/app/pages/vigencia/vigencia.html index 1ae2d25..1b92ea9 100644 --- a/src/app/pages/vigencia/vigencia.html +++ b/src/app/pages/vigencia/vigencia.html @@ -24,6 +24,10 @@ Controle de contratos e fidelização
+ diff --git a/src/app/pages/vigencia/vigencia.ts b/src/app/pages/vigencia/vigencia.ts index 31d1148..5b10bbe 100644 --- a/src/app/pages/vigencia/vigencia.ts +++ b/src/app/pages/vigencia/vigencia.ts @@ -3,12 +3,13 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { ActivatedRoute } from '@angular/router'; -import { Subscription } from 'rxjs'; +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 { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; type SortDir = 'asc' | 'desc'; @@ -32,6 +33,7 @@ interface LineOptionDto { }) export class VigenciaComponent implements OnInit, OnDestroy { loading = false; + exporting = false; errorMsg = ''; // Filtros @@ -113,7 +115,8 @@ export class VigenciaComponent implements OnInit, OnDestroy { private authService: AuthService, private linesService: LinesService, private planAutoFill: PlanAutoFillService, - private route: ActivatedRoute + private route: ActivatedRoute, + private tableExportService: TableExportService ) {} ngOnInit(): void { @@ -295,6 +298,107 @@ export class VigenciaComponent implements OnInit, OnDestroy { 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; + } + } + + 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; diff --git a/src/app/services/table-export.service.ts b/src/app/services/table-export.service.ts new file mode 100644 index 0000000..285ed51 --- /dev/null +++ b/src/app/services/table-export.service.ts @@ -0,0 +1,462 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../environments/environment'; + +export type ExportCellType = 'text' | 'number' | 'currency' | 'date' | 'datetime' | 'boolean'; + +export interface TableExportColumn { + header: string; + key?: string; + type?: ExportCellType; + width?: number; + value: (row: T, index: number) => unknown; +} + +export interface TableExportRequest { + fileName: string; + sheetName?: string; + columns: TableExportColumn[]; + rows: T[]; + templateBuffer?: ArrayBuffer | null; +} + +type CellStyleSnapshot = { + font?: Partial; + fill?: import('exceljs').Fill; + border?: Partial; + alignment?: Partial; +}; + +type TemplateStyleSnapshot = { + headerStyles: CellStyleSnapshot[]; + bodyStyle?: CellStyleSnapshot; + bodyAltStyle?: CellStyleSnapshot; + columnWidths: Array; +}; + +@Injectable({ providedIn: 'root' }) +export class TableExportService { + private readonly templatesApiBase = (() => { + const raw = (environment.apiUrl || '').replace(/\/+$/, ''); + const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + return `${apiBase}/templates`; + })(); + private defaultTemplateBufferPromise: Promise | null = null; + private cachedDefaultTemplateStyle?: TemplateStyleSnapshot; + + constructor(private readonly http: HttpClient) {} + + async exportAsXlsx(request: TableExportRequest): Promise { + const ExcelJS = await import('exceljs'); + const templateBuffer = request.templateBuffer ?? (await this.getDefaultTemplateBuffer()); + const templateStyle = await this.resolveTemplateStyle(ExcelJS, templateBuffer); + const workbook = new ExcelJS.Workbook(); + const sheet = workbook.addWorksheet(this.sanitizeSheetName(request.sheetName || 'Dados')); + + const rawColumns = request.columns ?? []; + const columns = rawColumns.filter((column) => !this.shouldExcludeColumnByHeader(column.header)); + const rows = request.rows ?? []; + + if (!columns.length) { + throw new Error('Nenhuma coluna exportavel apos remover ITEM/ID.'); + } + + const headerValues = columns.map((c) => c.header ?? ''); + sheet.addRow(headerValues); + + rows.forEach((row, rowIndex) => { + const values = columns.map((column) => this.normalizeValue(column.value(row, rowIndex), column.type)); + sheet.addRow(values); + }); + + this.applyHeaderStyle(sheet, columns.length, templateStyle); + this.applyBodyStyle(sheet, columns, rows.length, templateStyle); + this.applyColumnWidths(sheet, columns, rows, templateStyle); + this.applyAutoFilter(sheet, columns.length); + sheet.views = [{ state: 'frozen', ySplit: 1 }]; + + const extensionSafeName = this.ensureXlsxExtension(request.fileName); + const buffer = await workbook.xlsx.writeBuffer(); + this.downloadBuffer(buffer, extensionSafeName); + } + + buildTimestamp(date: Date = new Date()): string { + const year = date.getFullYear(); + const month = this.pad2(date.getMonth() + 1); + const day = this.pad2(date.getDate()); + const hour = this.pad2(date.getHours()); + const minute = this.pad2(date.getMinutes()); + return `${year}-${month}-${day}_${hour}-${minute}`; + } + + private applyHeaderStyle( + sheet: import('exceljs').Worksheet, + columnCount: number, + templateStyle?: TemplateStyleSnapshot, + ): void { + const headerRow = sheet.getRow(1); + headerRow.height = 24; + + for (let col = 1; col <= columnCount; col += 1) { + const cell = headerRow.getCell(col); + const templateCell = this.getTemplateStyleByIndex(templateStyle, col - 1); + cell.font = this.cloneStyle(templateCell?.font) || { bold: true, color: { argb: 'FFFFFFFF' }, name: 'Calibri', size: 11 }; + cell.fill = this.cloneStyle(templateCell?.fill) || { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FF0A58CA' }, + }; + cell.alignment = this.cloneStyle(templateCell?.alignment) || { vertical: 'middle', horizontal: 'center', wrapText: true }; + cell.border = this.cloneStyle(templateCell?.border) || this.getDefaultBorder(); + } + } + + private applyBodyStyle( + sheet: import('exceljs').Worksheet, + columns: TableExportColumn[], + rowCount: number, + templateStyle?: TemplateStyleSnapshot, + ): void { + for (let rowIndex = 2; rowIndex <= rowCount + 1; rowIndex += 1) { + const row = sheet.getRow(rowIndex); + const isEven = (rowIndex - 1) % 2 === 0; + const templateRowStyle = isEven + ? (templateStyle?.bodyAltStyle ?? templateStyle?.bodyStyle) + : (templateStyle?.bodyStyle ?? templateStyle?.bodyAltStyle); + + columns.forEach((column, columnIndex) => { + const cell = row.getCell(columnIndex + 1); + cell.font = this.cloneStyle(templateRowStyle?.font) || { name: 'Calibri', size: 11, color: { argb: 'FF1F2937' } }; + cell.border = this.cloneStyle(templateRowStyle?.border) || this.getDefaultBorder(); + cell.alignment = this.cloneStyle(templateRowStyle?.alignment) || this.getAlignment(column.type); + + if (templateRowStyle?.fill) { + const fill = this.cloneStyle(templateRowStyle.fill); + if (fill) cell.fill = fill; + } else if (isEven) { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFF7FAFF' }, + }; + } + + if (column.type === 'number') cell.numFmt = '#,##0.00'; + if (column.type === 'currency') cell.numFmt = '"R$" #,##0.00'; + if (column.type === 'date') cell.numFmt = 'dd/mm/yyyy'; + if (column.type === 'datetime') cell.numFmt = 'dd/mm/yyyy hh:mm'; + }); + } + } + + private applyColumnWidths( + sheet: import('exceljs').Worksheet, + columns: TableExportColumn[], + rows: T[], + templateStyle?: TemplateStyleSnapshot, + ): void { + columns.forEach((column, columnIndex) => { + if (column.width && column.width > 0) { + sheet.getColumn(columnIndex + 1).width = column.width; + return; + } + + const templateWidth = templateStyle?.columnWidths?.[columnIndex]; + if (templateWidth && templateWidth > 0) { + sheet.getColumn(columnIndex + 1).width = templateWidth; + return; + } + + const headerLength = (column.header ?? '').length; + let maxLength = headerLength; + + rows.forEach((row, rowIndex) => { + const value = column.value(row, rowIndex); + const printable = this.toPrintableValue(value, column.type); + if (printable.length > maxLength) maxLength = printable.length; + }); + + const target = Math.max(12, Math.min(maxLength + 3, 48)); + sheet.getColumn(columnIndex + 1).width = target; + }); + } + + private applyAutoFilter(sheet: import('exceljs').Worksheet, columnCount: number): void { + if (columnCount <= 0) return; + sheet.autoFilter = { + from: { row: 1, column: 1 }, + to: { row: 1, column: columnCount }, + }; + } + + private normalizeValue(value: unknown, type?: ExportCellType): string | number | Date | boolean | null { + if (value === null || value === undefined || value === '') return null; + + if (type === 'number' || type === 'currency') { + const numeric = this.toNumber(value); + return numeric ?? String(value); + } + + if (type === 'date' || type === 'datetime') { + const parsedDate = this.toDate(value); + return parsedDate ?? String(value); + } + + if (type === 'boolean') { + if (typeof value === 'boolean') return value; + return this.normalizeBoolean(value); + } + + return String(value); + } + + private toPrintableValue(value: unknown, type?: ExportCellType): string { + if (value === null || value === undefined || value === '') return ''; + + if (type === 'date' || type === 'datetime') { + const parsedDate = this.toDate(value); + if (!parsedDate) return String(value); + const datePart = `${this.pad2(parsedDate.getDate())}/${this.pad2(parsedDate.getMonth() + 1)}/${parsedDate.getFullYear()}`; + if (type === 'date') return datePart; + return `${datePart} ${this.pad2(parsedDate.getHours())}:${this.pad2(parsedDate.getMinutes())}`; + } + + if (type === 'number' || type === 'currency') { + const numeric = this.toNumber(value); + if (numeric === null) return String(value); + if (type === 'currency') { + return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(numeric); + } + return new Intl.NumberFormat('pt-BR').format(numeric); + } + + if (type === 'boolean') { + return this.normalizeBoolean(value) ? 'Sim' : 'Nao'; + } + + return String(value); + } + + private toNumber(value: unknown): number | null { + if (typeof value === 'number') { + return Number.isFinite(value) ? value : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) return null; + const normalized = trimmed + .replace(/[^\d,.-]/g, '') + .replace(/\.(?=\d{3}(\D|$))/g, '') + .replace(',', '.'); + const parsed = Number(normalized); + return Number.isFinite(parsed) ? parsed : null; + } + + return null; + } + + private toDate(value: unknown): Date | null { + if (value instanceof Date) { + return Number.isNaN(value.getTime()) ? null : value; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) return null; + + const brDate = trimmed.match(/^(\d{2})\/(\d{2})\/(\d{4})(?:\s+(\d{2}):(\d{2}))?$/); + if (brDate) { + const day = Number(brDate[1]); + const month = Number(brDate[2]) - 1; + const year = Number(brDate[3]); + const hour = Number(brDate[4] ?? '0'); + const minute = Number(brDate[5] ?? '0'); + const parsedBr = new Date(year, month, day, hour, minute); + return Number.isNaN(parsedBr.getTime()) ? null : parsedBr; + } + + const parsed = new Date(trimmed); + return Number.isNaN(parsed.getTime()) ? null : parsed; + } + + return null; + } + + private normalizeBoolean(value: unknown): boolean { + if (typeof value === 'boolean') return value; + const normalized = String(value ?? '') + .trim() + .toLowerCase(); + return normalized === 'true' || normalized === '1' || normalized === 'sim' || normalized === 'yes'; + } + + private ensureXlsxExtension(fileName: string): string { + const safe = (fileName ?? 'export').trim() || 'export'; + return safe.toLowerCase().endsWith('.xlsx') ? safe : `${safe}.xlsx`; + } + + private sanitizeSheetName(name: string): string { + const safe = (name ?? 'Dados').replace(/[\\/*?:[\]]/g, '').trim(); + return (safe || 'Dados').slice(0, 31); + } + + private shouldExcludeColumnByHeader(header: string | undefined): boolean { + const normalized = this.normalizeHeader(header); + if (!normalized) return false; + + const tokens = normalized.split(/[^a-z0-9]+/).filter(Boolean); + if (!tokens.length) return false; + + return tokens.includes('id') || tokens.includes('item'); + } + + private normalizeHeader(value: string | undefined): string { + return (value ?? '') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .trim(); + } + + private downloadBuffer(buffer: ArrayBuffer, fileName: string): void { + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = fileName; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + URL.revokeObjectURL(url); + } + + private getAlignment(type?: ExportCellType): Partial { + if (type === 'number' || type === 'currency') { + return { vertical: 'middle', horizontal: 'right' }; + } + if (type === 'boolean') { + return { vertical: 'middle', horizontal: 'center' }; + } + return { vertical: 'middle', horizontal: 'left', wrapText: true }; + } + + private getDefaultBorder(): Partial { + return { + top: { style: 'thin', color: { argb: 'FFD6DCE8' } }, + left: { style: 'thin', color: { argb: 'FFD6DCE8' } }, + right: { style: 'thin', color: { argb: 'FFD6DCE8' } }, + bottom: { style: 'thin', color: { argb: 'FFD6DCE8' } }, + }; + } + + private pad2(value: number): string { + return value.toString().padStart(2, '0'); + } + + private async extractTemplateStyle( + excelJsModule: typeof import('exceljs'), + templateBuffer: ArrayBuffer | null, + ): Promise { + if (!templateBuffer) return undefined; + + try { + const workbook = new excelJsModule.Workbook(); + await workbook.xlsx.load(templateBuffer); + const sheet = workbook.getWorksheet(1); + if (!sheet) return undefined; + + const headerRow = sheet.getRow(1); + const headerCount = Math.max(headerRow.actualCellCount, 1); + const headerStyles: CellStyleSnapshot[] = []; + for (let col = 1; col <= headerCount; col += 1) { + headerStyles.push(this.captureCellStyle(headerRow.getCell(col))); + } + + const bodyStyle = this.captureFirstStyledCellRow(sheet.getRow(2)); + const bodyAltStyle = this.captureFirstStyledCellRow(sheet.getRow(3)); + const columnWidths = (sheet.columns ?? []).map((column) => column.width); + + return { headerStyles, bodyStyle, bodyAltStyle, columnWidths }; + } catch { + return undefined; + } + } + + private async resolveTemplateStyle( + excelJsModule: typeof import('exceljs'), + templateBuffer: ArrayBuffer | null, + ): Promise { + if (templateBuffer) { + const style = await this.extractTemplateStyle(excelJsModule, templateBuffer); + if (style) this.cachedDefaultTemplateStyle = style; + return style; + } + + return this.cachedDefaultTemplateStyle; + } + + private async getDefaultTemplateBuffer(): Promise { + if (this.defaultTemplateBufferPromise) { + return this.defaultTemplateBufferPromise; + } + + this.defaultTemplateBufferPromise = this.fetchDefaultTemplateBuffer(); + const buffer = await this.defaultTemplateBufferPromise; + if (!buffer) this.defaultTemplateBufferPromise = null; + return buffer; + } + + private async fetchDefaultTemplateBuffer(): Promise { + try { + const params = new HttpParams().set('_', `${Date.now()}`); + const blob = await firstValueFrom( + this.http.get(`${this.templatesApiBase}/planilha-geral`, { + params, + responseType: 'blob', + }) + ); + return await blob.arrayBuffer(); + } catch { + return null; + } + } + + private captureFirstStyledCellRow(row: import('exceljs').Row): CellStyleSnapshot | undefined { + if (!row) return undefined; + const cellCount = Math.max(row.actualCellCount, 1); + for (let col = 1; col <= cellCount; col += 1) { + const captured = this.captureCellStyle(row.getCell(col)); + if (captured.font || captured.fill || captured.border || captured.alignment) { + return captured; + } + } + return undefined; + } + + private captureCellStyle(cell: import('exceljs').Cell): CellStyleSnapshot { + return { + font: this.cloneStyle(cell.font), + fill: this.cloneStyle(cell.fill), + border: this.cloneStyle(cell.border), + alignment: this.cloneStyle(cell.alignment), + }; + } + + private getTemplateStyleByIndex(style: TemplateStyleSnapshot | undefined, index: number): CellStyleSnapshot | undefined { + if (!style || !style.headerStyles.length) return undefined; + return style.headerStyles[index] ?? style.headerStyles[style.headerStyles.length - 1]; + } + + private cloneStyle(value: T | undefined): T | undefined { + if (!value) return undefined; + try { + return JSON.parse(JSON.stringify(value)) as T; + } catch { + return value; + } + } +} From 56e1fc237946f84996b7e825deffcf1c6ea19437 Mon Sep 17 00:00:00 2001 From: Leon Date: Fri, 6 Mar 2026 12:59:29 -0300 Subject: [PATCH 3/7] feat: tela de dashboard com kpis clicaveis e filtro bloqueados --- src/app/pages/dashboard/dashboard.html | 9 +- src/app/pages/dashboard/dashboard.scss | 10 + src/app/pages/dashboard/dashboard.ts | 43 ++++- src/app/pages/geral/geral.html | 21 +++ src/app/pages/geral/geral.ts | 248 +++++++++++++++++++++++-- 5 files changed, 318 insertions(+), 13 deletions(-) diff --git a/src/app/pages/dashboard/dashboard.html b/src/app/pages/dashboard/dashboard.html index bb9943d..8a75c81 100644 --- a/src/app/pages/dashboard/dashboard.html +++ b/src/app/pages/dashboard/dashboard.html @@ -30,7 +30,14 @@
-
+
diff --git a/src/app/pages/dashboard/dashboard.scss b/src/app/pages/dashboard/dashboard.scss index 80bfabf..9b1c902 100644 --- a/src/app/pages/dashboard/dashboard.scss +++ b/src/app/pages/dashboard/dashboard.scss @@ -178,6 +178,7 @@ display: flex; flex-direction: column; gap: 12px; + cursor: default; transition: all 0.2s ease; box-shadow: var(--shadow-sm); @@ -189,6 +190,15 @@ } } +.hero-card.hero-card-clickable { + cursor: pointer; +} + +.hero-card.hero-card-clickable:focus-visible { + outline: 2px solid rgba(227, 61, 207, 0.7); + outline-offset: 2px; +} + .hero-icon { width: 40px; height: 40px; diff --git a/src/app/pages/dashboard/dashboard.ts b/src/app/pages/dashboard/dashboard.ts index d734fed..bb22e3b 100644 --- a/src/app/pages/dashboard/dashboard.ts +++ b/src/app/pages/dashboard/dashboard.ts @@ -10,7 +10,7 @@ import { import { CommonModule, isPlatformBrowser } from '@angular/common'; import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http'; import { PLATFORM_ID } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import { RouterModule, Router } from '@angular/router'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; @@ -31,6 +31,11 @@ type KpiCard = { hint?: string; }; +type KpiNavigationTarget = { + route: string; + queryParams?: Record; +}; + type SerieMesDto = { mes: string; total: number; @@ -354,11 +359,29 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { private chartResumoReserva?: Chart; private readonly baseApi: string; + private readonly kpiNavigationMap: Record = { + linhas_total: { route: '/geral' }, + linhas_ativas: { route: '/geral' }, + linhas_bloqueadas: { route: '/geral', queryParams: { statusMode: 'blocked' } }, + linhas_reserva: { route: '/geral', queryParams: { skil: 'RESERVA' } }, + franquia_vivo_total: { route: '/geral' }, + franquia_line_total: { route: '/geral' }, + vig_vencidos: { route: '/vigencia' }, + vig_30: { route: '/vigencia' }, + mureg_30: { route: '/mureg' }, + troca_30: { route: '/trocanumero' }, + cadastros_total: { route: '/dadosusuarios' }, + travel_com: { route: '/geral', queryParams: { additionalMode: 'with', additionalServices: 'travel' } }, + adicional_pago: { route: '/geral', queryParams: { additionalMode: 'with' } }, + planos_contratados: { route: '/resumo', queryParams: { tab: 'planos' } }, + usuarios_com_linha: { route: '/dadosusuarios' }, + }; constructor( private http: HttpClient, private resumoService: ResumoService, private authService: AuthService, + private router: Router, @Inject(PLATFORM_ID) private platformId: object ) { const raw = (environment.apiUrl || '').replace(/\/+$/, ''); @@ -1872,6 +1895,24 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { trackByKpiKey = (_: number, item: KpiCard) => item.key; + isKpiClickable(card: KpiCard): boolean { + return !!this.kpiNavigationMap[card.key]; + } + + onKpiClick(card: KpiCard): void { + const target = this.kpiNavigationMap[card.key]; + if (!target) return; + void this.router.navigate([target.route], { + queryParams: target.queryParams + }); + } + + onKpiCardKeydown(event: KeyboardEvent, card: KpiCard): void { + if (event.key !== 'Enter' && event.key !== ' ') return; + event.preventDefault(); + this.onKpiClick(card); + } + private getPalette() { return { brand: '#E33DCF', diff --git a/src/app/pages/geral/geral.html b/src/app/pages/geral/geral.html index 8e06caa..01bb3cf 100644 --- a/src/app/pages/geral/geral.html +++ b/src/app/pages/geral/geral.html @@ -71,6 +71,27 @@ Reservas + + + + +
diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts index bc3f3df..3f3436c 100644 --- a/src/app/pages/geral/geral.ts +++ b/src/app/pages/geral/geral.ts @@ -17,7 +17,7 @@ import { HttpParams, HttpErrorResponse } from '@angular/common/http'; -import { NavigationEnd, Router } from '@angular/router'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { PlanAutoFillService } from '../../services/plan-autofill.service'; import { AuthService } from '../../services/auth.service'; @@ -42,6 +42,7 @@ type CreateMode = 'NEW_CLIENT' | 'NEW_LINE_IN_GROUP'; type CreateEntryMode = 'SINGLE' | 'BATCH'; type AdditionalMode = 'ALL' | 'WITH' | 'WITHOUT'; type AdditionalServiceKey = 'gvd' | 'skeelo' | 'news' | 'travel' | 'sync' | 'dispositivo'; +type BlockedStatusMode = 'ALL' | 'PERDA_ROUBO' | 'BLOQUEIO_120'; interface LineRow { id: string; @@ -290,6 +291,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { private planAutoFill: PlanAutoFillService, private authService: AuthService, private router: Router, + private route: ActivatedRoute, private tenantSyncService: TenantSyncService, private solicitacoesLinhasService: SolicitacoesLinhasService ) {} @@ -317,6 +319,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { searchTerm = ''; filterSkil: 'ALL' | 'PF' | 'PJ' | 'RESERVA' = 'ALL'; + filterStatus: 'ALL' | 'BLOCKED' = 'ALL'; + blockedStatusMode: BlockedStatusMode = 'ALL'; additionalMode: AdditionalMode = 'ALL'; selectedAdditionalServices: AdditionalServiceKey[] = []; readonly additionalServiceOptions: Array<{ key: AdditionalServiceKey; label: string }> = [ @@ -647,6 +651,10 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return this.additionalMode !== 'ALL' || this.selectedAdditionalServices.length > 0; } + get hasClientSideFiltersApplied(): boolean { + return this.hasAdditionalFiltersApplied || this.filterStatus === 'BLOCKED'; + } + get additionalModeLabel(): string { if (this.additionalMode === 'WITH') return 'Com adicionais'; if (this.additionalMode === 'WITHOUT') return 'Sem adicionais'; @@ -735,6 +743,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { if (this.isClientRestricted) { this.filterSkil = 'ALL'; + this.filterStatus = 'ALL'; + this.blockedStatusMode = 'ALL'; this.additionalMode = 'ALL'; this.selectedAdditionalServices = []; this.selectedClients = []; @@ -746,6 +756,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.initAnimations(); setTimeout(() => { + this.applyRouteFilters(this.route.snapshot.queryParams); this.refreshData(); if (!this.isClientRestricted) { this.loadClients(); @@ -766,9 +777,13 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.navigationSub = this.router.events .pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd)) .subscribe((event) => { - const url = (event.urlAfterRedirects || '').toLowerCase(); + const urlAfterRedirects = event.urlAfterRedirects || ''; + const url = urlAfterRedirects.toLowerCase(); if (!url.includes('/geral')) return; + const parsed = this.router.parseUrl(urlAfterRedirects); + this.applyRouteFilters(parsed.queryParams ?? {}); + this.searchResolvedClient = null; if (!this.isClientRestricted) { this.loadClients(); @@ -785,6 +800,137 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { }, 100); } + private applyRouteFilters(query: Record): void { + const skil = this.parseQuerySkilFilter(query['skil']); + if (skil && (!this.isClientRestricted || skil === 'ALL')) { + this.filterSkil = skil; + } + + const status = this.parseQueryStatusFilter(query['statusMode'] ?? query['statusFilter']); + if (status) { + this.filterStatus = status; + } + if (this.filterStatus !== 'BLOCKED') { + this.blockedStatusMode = 'ALL'; + } + + const blockedMode = this.parseQueryBlockedStatusMode(query['blockedMode'] ?? query['blockedType'] ?? query['statusSubtype']); + if (blockedMode) { + this.blockedStatusMode = blockedMode; + this.filterStatus = 'BLOCKED'; + } + + if (!this.isClientRestricted) { + const additionalMode = this.parseQueryAdditionalMode(query['additionalMode']); + if (additionalMode) { + this.additionalMode = additionalMode; + } + + const additionalServices = this.parseQueryAdditionalServices(query['additionalServices']); + if (additionalServices) { + this.selectedAdditionalServices = additionalServices; + } + } + + this.expandedGroup = null; + this.groupLines = []; + this.selectedClients = []; + this.clientSearchTerm = ''; + this.page = 1; + } + + private parseQuerySkilFilter(value: unknown): 'ALL' | 'PF' | 'PJ' | 'RESERVA' | null { + const token = this.normalizeFilterToken(value); + if (!token) return null; + if (token === 'ALL' || token === 'TODOS') return 'ALL'; + if (token === 'PF' || token === 'PESSOAFISICA') return 'PF'; + if (token === 'PJ' || token === 'PESSOAJURIDICA') return 'PJ'; + if (token === 'RESERVA' || token === 'RESERVAS') return 'RESERVA'; + return null; + } + + private parseQueryStatusFilter(value: unknown): 'ALL' | 'BLOCKED' | null { + const token = this.normalizeFilterToken(value); + if (!token) return null; + if (token === 'ALL' || token === 'TODOS') return 'ALL'; + if ( + token === 'BLOCKED' || + token === 'BLOQUEADAS' || + token === 'BLOQUEADOS' || + token === 'BLOQUEADA' || + token === 'BLOQUEADO' || + token === 'BLOQUEIO' + ) { + return 'BLOCKED'; + } + return null; + } + + private parseQueryBlockedStatusMode(value: unknown): BlockedStatusMode | null { + const token = this.normalizeFilterToken(value); + if (!token) return null; + if (token === 'ALL' || token === 'TODOS') return 'ALL'; + if ( + token === 'PERDAROUBO' || + token === 'PERDAEROUBO' || + token === 'PERDA' || + token === 'ROUBO' + ) { + return 'PERDA_ROUBO'; + } + if ( + token === '120' || + token === '120DIAS' || + token === 'BLOQUEIO120' || + token === 'BLOQUEIO120DIAS' + ) { + return 'BLOQUEIO_120'; + } + return null; + } + + private parseQueryAdditionalMode(value: unknown): AdditionalMode | null { + const token = this.normalizeFilterToken(value); + if (!token) return null; + if (token === 'ALL' || token === 'TODOS') return 'ALL'; + if (token === 'WITH' || token === 'COM') return 'WITH'; + if (token === 'WITHOUT' || token === 'SEM') return 'WITHOUT'; + return null; + } + + private parseQueryAdditionalServices(value: unknown): AdditionalServiceKey[] | null { + if (value === undefined || value === null) return null; + const asString = Array.isArray(value) ? value.join(',') : String(value ?? ''); + const chunks = asString + .split(',') + .map((part) => this.mapAdditionalServiceToken(part)) + .filter((part): part is AdditionalServiceKey => !!part); + + const unique = Array.from(new Set(chunks)); + return unique; + } + + private mapAdditionalServiceToken(value: unknown): AdditionalServiceKey | null { + const token = this.normalizeFilterToken(value); + if (!token) return null; + if (token === 'GVD' || token === 'GESTAOVOZDADOS' || token === 'GESTAOVOZEDADOS') return 'gvd'; + if (token === 'SKEELO') return 'skeelo'; + if (token === 'NEWS' || token === 'VIVONEWS' || token === 'VIVONEWSPLUS') return 'news'; + if (token === 'TRAVEL' || token === 'TRAVELMUNDO' || token === 'VIVOTRAVELMUNDO') return 'travel'; + if (token === 'SYNC' || token === 'VIVOSYNC') return 'sync'; + if (token === 'DISPOSITIVO' || token === 'GESTAODISPOSITIVO' || token === 'VIVOGESTAODISPOSITIVO') return 'dispositivo'; + return null; + } + + private normalizeFilterToken(value: unknown): string { + return String(value ?? '') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^A-Za-z0-9]/g, '') + .toUpperCase() + .trim(); + } + private async loadPlanRules() { try { await this.planAutoFill.load(); @@ -895,7 +1041,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { refreshData(opts?: { keepCurrentPage?: boolean }) { const keepCurrentPage = !!opts?.keepCurrentPage; this.keepPageOnNextGroupsLoad = keepCurrentPage; - if (!keepCurrentPage && this.filterSkil === 'RESERVA') { + if (!keepCurrentPage && (this.filterSkil === 'RESERVA' || this.filterStatus === 'BLOCKED')) { this.page = 1; } this.searchResolvedClient = null; @@ -921,7 +1067,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { const s = (term ?? '').trim(); if (!s) return Promise.resolve(null); - const pageSize = this.hasAdditionalFiltersApplied ? '500' : '1'; + const pageSize = this.hasClientSideFiltersApplied ? '500' : '1'; let params = new HttpParams().set('page', '1').set('pageSize', pageSize).set('search', s); params = this.applyBaseFilters(params); @@ -932,7 +1078,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return new Promise((resolve) => { this.http.get>(this.apiBase, { params: this.withNoCache(params) }).subscribe({ next: (res) => { - const source = this.hasAdditionalFiltersApplied + const source = this.hasClientSideFiltersApplied ? this.applyAdditionalFiltersClientSide(res.items ?? []) : (res.items ?? []); const first = source[0]; @@ -984,7 +1130,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { const requestVersion = ++this.groupsRequestVersion; this.loading = true; - if (this.hasAdditionalFiltersApplied) { + if (this.hasClientSideFiltersApplied) { return this.loadOnlyThisClientGroupFromLines(clientName, requestVersion); } @@ -1051,7 +1197,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.loadingClientsList = true; this.clientsList = []; - if (this.hasAdditionalFiltersApplied) { + if (this.hasClientSideFiltersApplied) { void this.loadClientsFromLines(requestVersion); return; } @@ -1147,6 +1293,47 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.refreshData(); } + toggleBlockedFilter() { + if (this.filterStatus === 'BLOCKED') { + this.filterStatus = 'ALL'; + this.blockedStatusMode = 'ALL'; + } else { + this.filterStatus = 'BLOCKED'; + } + this.expandedGroup = null; + this.groupLines = []; + this.searchResolvedClient = null; + this.selectedClients = []; + this.clientSearchTerm = ''; + this.page = 1; + + if (!this.isClientRestricted) { + this.loadClients(); + } + + this.refreshData(); + } + + setBlockedStatusMode(mode: Exclude) { + if (this.filterStatus !== 'BLOCKED') { + this.filterStatus = 'BLOCKED'; + } + + this.blockedStatusMode = this.blockedStatusMode === mode ? 'ALL' : mode; + this.expandedGroup = null; + this.groupLines = []; + this.searchResolvedClient = null; + this.selectedClients = []; + this.clientSearchTerm = ''; + this.page = 1; + + if (!this.isClientRestricted) { + this.loadClients(); + } + + this.refreshData(); + } + setAdditionalMode(mode: AdditionalMode) { if (this.isClientRestricted) return; if (this.additionalMode === mode) return; @@ -1199,6 +1386,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { if (this.filterSkil === 'PF') next = next.set('skil', 'PESSOA FÍSICA'); else if (this.filterSkil === 'PJ') next = next.set('skil', 'PESSOA JURÍDICA'); else if (this.filterSkil === 'RESERVA') next = next.set('skil', 'RESERVA'); + if (this.filterStatus === 'BLOCKED') { + next = next.set('statusMode', 'blocked'); + if (this.blockedStatusMode === 'PERDA_ROUBO') next = next.set('statusSubtype', 'perda_roubo'); + else if (this.blockedStatusMode === 'BLOQUEIO_120') next = next.set('statusSubtype', '120_dias'); + } if (this.additionalMode === 'WITH') next = next.set('additionalMode', 'with'); else if (this.additionalMode === 'WITHOUT') next = next.set('additionalMode', 'without'); @@ -1236,7 +1428,41 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { (this.getAdditionalValue(line, 'dispositivo') > 0); } + private resolveBlockedStatusMode(status: unknown): Exclude | null { + const normalized = this.normalizeFilterToken(status); + if (!normalized) return null; + + const hasBlockedToken = + normalized.includes('BLOQUE') || + normalized.includes('PERDA') || + normalized.includes('ROUBO') || + normalized.includes('FURTO'); + if (!hasBlockedToken) return null; + + if (normalized.includes('120')) return 'BLOQUEIO_120'; + if (normalized.includes('PERDA') || normalized.includes('ROUBO') || normalized.includes('FURTO')) { + return 'PERDA_ROUBO'; + } + + return 'PERDA_ROUBO'; + } + + private isBlockedStatus(status: unknown): boolean { + return this.resolveBlockedStatusMode(status) !== null; + } + + private matchesBlockedStatusMode(status: unknown): boolean { + const mode = this.resolveBlockedStatusMode(status); + if (!mode) return false; + if (this.blockedStatusMode === 'ALL') return true; + return mode === this.blockedStatusMode; + } + private matchesAdditionalFilters(line: ApiLineList): boolean { + if (this.filterStatus === 'BLOCKED' && !this.matchesBlockedStatusMode(line?.status ?? '')) { + return false; + } + const selected = this.selectedAdditionalServices; const hasSelected = selected.length > 0; @@ -1296,7 +1522,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } private async fetchAllGroupsForKpis(): Promise { - if (this.hasAdditionalFiltersApplied) { + if (this.hasClientSideFiltersApplied) { const lines = await this.fetchLinesForGrouping(); let groups = this.buildGroupsFromLines(lines); @@ -1411,11 +1637,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { const keepCurrentPage = this.keepPageOnNextGroupsLoad; this.keepPageOnNextGroupsLoad = false; - if (!keepCurrentPage && this.filterSkil === 'RESERVA' && !hasSelection && !hasResolved) { + if (!keepCurrentPage && (this.filterSkil === 'RESERVA' || this.filterStatus === 'BLOCKED') && !hasSelection && !hasResolved) { this.page = 1; } - if (this.hasAdditionalFiltersApplied) { + if (this.hasClientSideFiltersApplied) { void this.loadGroupsFromLines(hasSelection, hasResolved, requestVersion); return; } @@ -1573,7 +1799,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { const status = ((row?.status ?? '').toString().trim()).toLowerCase(); if (status.includes('ativo')) group.ativos += 1; - if (status.includes('bloque') || status.includes('perda') || status.includes('roubo')) { + if (this.isBlockedStatus(row?.status ?? '')) { group.bloqueados += 1; } } From 4b7c74195e5d11adf754b36598311348efcfc95c Mon Sep 17 00:00:00 2001 From: Eduardo Lopes Date: Fri, 6 Mar 2026 13:09:34 -0300 Subject: [PATCH 4/7] =?UTF-8?q?Feat:=20Novas=20Implementa=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 1064 ++++++++--------- src/app/app.routes.ts | 7 +- src/app/app.ts | 1 + src/app/components/header/header.html | 7 +- src/app/components/header/header.ts | 18 +- .../guards/sysadmin-or-financeiro.guard.ts | 27 + src/app/guards/sysadmin-or-gestor.guard.ts | 5 +- src/app/pages/dashboard/dashboard.ts | 3 +- src/app/pages/geral/geral.html | 10 +- src/app/pages/geral/geral.ts | 28 +- .../historico-linhas/historico-linhas.html | 278 +++++ .../historico-linhas/historico-linhas.scss | 648 ++++++++++ .../historico-linhas/historico-linhas.ts | 598 +++++++++ src/app/pages/mureg/mureg.html | 10 +- src/app/pages/mureg/mureg.ts | 55 +- .../parcelamentos-table.html | 3 +- .../parcelamentos-table.ts | 3 +- .../pages/parcelamentos/parcelamentos.html | 5 +- src/app/pages/parcelamentos/parcelamentos.ts | 28 + src/app/pages/troca-numero/troca-numero.html | 4 +- src/app/pages/troca-numero/troca-numero.ts | 33 + src/app/services/historico.service.ts | 28 + src/app/services/users.service.ts | 2 +- 23 files changed, 2258 insertions(+), 607 deletions(-) create mode 100644 src/app/guards/sysadmin-or-financeiro.guard.ts create mode 100644 src/app/pages/historico-linhas/historico-linhas.html create mode 100644 src/app/pages/historico-linhas/historico-linhas.scss create mode 100644 src/app/pages/historico-linhas/historico-linhas.ts diff --git a/package-lock.json b/package-lock.json index d41a1fd..c495cf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -610,13 +610,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -625,9 +625,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -640,7 +640,6 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -684,14 +683,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -714,13 +713,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -751,29 +750,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -826,27 +825,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -856,33 +855,33 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -890,9 +889,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -1396,10 +1395,23 @@ "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", "license": "MIT" }, + "node_modules/@gar/promise-retry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.2.tgz", + "integrity": "sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "^0.13.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", "dev": true, "license": "MIT", "engines": { @@ -1760,29 +1772,6 @@ } } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -2432,9 +2421,9 @@ } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -2455,18 +2444,18 @@ } }, "node_modules/@npmcli/git": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.1.tgz", - "integrity": "sha512-+XTFxK2jJF/EJJ5SoAzXk3qwIDfvFc5/g+bD274LZ7uY7LE8sTfG6Z8rOanPl2ZEvZWqNvmEdtXC25cE54VcoA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", "dev": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^6.0.0" }, @@ -2485,19 +2474,19 @@ } }, "node_modules/@npmcli/git/node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -2515,13 +2504,13 @@ } }, "node_modules/@npmcli/git/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" @@ -2558,9 +2547,9 @@ } }, "node_modules/@npmcli/package-json": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.4.tgz", - "integrity": "sha512-0wInJG3j/K40OJt/33ax47WfWMzZTm6OQxB9cDhTt5huCP2a9g2GnlsxmfN+PulItNPIpPrZ+kfwwUil7eHcZQ==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2570,41 +2559,64 @@ "json-parse-even-better-errors": "^5.0.0", "proc-log": "^6.0.0", "semver": "^7.5.3", - "validate-npm-package-license": "^3.0.4" + "spdx-expression-parse": "^4.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@npmcli/package-json/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@npmcli/package-json/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", - "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.2", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@npmcli/package-json/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2634,23 +2646,23 @@ } }, "node_modules/@npmcli/promise-spawn/node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" @@ -2670,9 +2682,9 @@ } }, "node_modules/@npmcli/run-script": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.3.tgz", - "integrity": "sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", "dev": true, "license": "ISC", "dependencies": { @@ -2680,23 +2692,12 @@ "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", - "proc-log": "^6.0.0", - "which": "^6.0.0" + "proc-log": "^6.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/run-script/node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/@npmcli/run-script/node_modules/proc-log": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", @@ -2707,35 +2708,19 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/run-script/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" @@ -2745,25 +2730,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -2782,9 +2767,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -2803,9 +2788,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -2824,9 +2809,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], @@ -2845,9 +2830,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], @@ -2866,9 +2851,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], @@ -2887,9 +2872,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], @@ -2908,9 +2893,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], @@ -2929,9 +2914,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], @@ -2950,9 +2935,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], @@ -2971,9 +2956,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -2992,9 +2977,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -3013,9 +2998,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -3033,20 +3018,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/@parcel/watcher/node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -3512,17 +3483,40 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@tufjs/models/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3556,16 +3550,16 @@ "license": "MIT" }, "node_modules/@types/jasmine": { - "version": "5.1.13", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.13.tgz", - "integrity": "sha512-MYCcDkruFc92LeYZux5BC0dmqo2jk+M5UIZ4/oFnAPCXN9mCcQhLyj7F3/Za7rocVyt5YRr1MmqJqFlvQ9LVcg==", + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.15.tgz", + "integrity": "sha512-ZAC8KjmV2MJxbNTrwXFN+HKeajpXQZp6KpPiR6Aa4XvaEnjP6qh23lL/Rqb7AYzlp3h/rcwDrQ7Gg7q28cQTQg==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", - "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "dev": true, "license": "MIT", "peer": true, @@ -3689,9 +3683,9 @@ } }, "node_modules/ansi-escapes": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", - "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "license": "MIT", "dependencies": { @@ -3954,9 +3948,9 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "dev": true, "license": "MIT", "dependencies": { @@ -3966,7 +3960,7 @@ "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", + "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" }, @@ -4044,9 +4038,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -4065,11 +4059,11 @@ "license": "MIT", "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -4168,28 +4162,51 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/cacache/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/cacache/node_modules/glob": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", - "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.2", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -4197,16 +4214,16 @@ } }, "node_modules/cacache/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4244,9 +4261,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001757", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", - "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", "dev": true, "funding": [ { @@ -4614,9 +4631,9 @@ "license": "MIT" }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "dev": true, "license": "MIT", "dependencies": { @@ -4625,6 +4642,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/crc-32": { @@ -4911,9 +4932,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.262", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", - "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", "dev": true, "license": "ISC" }, @@ -4934,31 +4955,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -4969,9 +4965,9 @@ } }, "node_modules/engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", "dev": true, "license": "MIT", "dependencies": { @@ -4981,9 +4977,9 @@ "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" + "ws": "~8.18.3" }, "engines": { "node": ">=10.2.0" @@ -5013,24 +5009,6 @@ "node": ">= 0.6" } }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/engine.io/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -5328,13 +5306,13 @@ } }, "node_modules/express-rate-limit": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", - "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", + "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "10.0.1" + "ip-address": "10.1.0" }, "engines": { "node": ">= 16" @@ -5422,9 +5400,9 @@ } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", "dependencies": { @@ -5436,13 +5414,17 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", "dev": true, "license": "ISC" }, @@ -5602,9 +5584,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", "dev": true, "license": "MIT", "engines": { @@ -5657,7 +5639,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -5766,9 +5748,9 @@ } }, "node_modules/hono": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", - "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", + "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", "dev": true, "license": "MIT", "peer": true, @@ -5790,9 +5772,9 @@ } }, "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -5807,9 +5789,9 @@ "license": "MIT" }, "node_modules/htmlparser2": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", - "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -5822,14 +5804,14 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" + "domutils": "^3.2.2", + "entities": "^7.0.1" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5911,9 +5893,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { @@ -5960,17 +5942,40 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/ignore-walk/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/ignore-walk/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5983,9 +5988,9 @@ "license": "MIT" }, "node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "dev": true, "license": "MIT" }, @@ -6027,9 +6032,9 @@ } }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "dev": true, "license": "MIT", "engines": { @@ -6289,9 +6294,9 @@ "peer": true }, "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.0.tgz", + "integrity": "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==", "dev": true, "license": "MIT", "funding": { @@ -6939,7 +6944,6 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -6953,9 +6957,9 @@ } }, "node_modules/listr2/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, "license": "MIT" }, @@ -7246,12 +7250,13 @@ } }, "node_modules/make-fetch-happen": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", - "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.4.tgz", + "integrity": "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==", "dev": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", @@ -7261,7 +7266,6 @@ "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "ssri": "^13.0.0" }, "engines": { @@ -7311,35 +7315,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -7394,9 +7369,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -7415,11 +7390,11 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -7438,9 +7413,9 @@ } }, "node_modules/minipass-fetch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.1.tgz", - "integrity": "sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7452,7 +7427,7 @@ "node": "^20.17.0 || >=22.9.0" }, "optionalDependencies": { - "encoding": "^0.1.13" + "iconv-lite": "^0.7.2" } }, "node_modules/minipass-flush": { @@ -7577,9 +7552,9 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", - "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.8.tgz", + "integrity": "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==", "dev": true, "license": "MIT", "optional": true, @@ -7699,13 +7674,13 @@ } }, "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/node-gyp/node_modules/proc-log": { @@ -7719,13 +7694,13 @@ } }, "node_modules/node-gyp/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" @@ -7735,9 +7710,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT" }, @@ -7819,9 +7794,9 @@ } }, "node_modules/npm-packlist": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.3.tgz", - "integrity": "sha512-zPukTwJMOu5X5uvm0fztwS5Zxyvmk38H/LfidkOMt3gbZVCyro2cD/ETzwzVPcWZA3JOyPznfUN/nkyFiyUbxg==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", "dev": true, "license": "ISC", "dependencies": { @@ -7987,9 +7962,9 @@ } }, "node_modules/ordered-binary": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.0.tgz", - "integrity": "sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", "dev": true, "license": "MIT", "optional": true @@ -8159,9 +8134,9 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -8169,16 +8144,16 @@ "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -8240,9 +8215,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -8305,6 +8280,16 @@ "node": ">=10" } }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -8337,9 +8322,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8509,9 +8494,9 @@ } }, "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, "license": "MIT", "engines": { @@ -8704,32 +8689,36 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", "dependencies": { @@ -8740,6 +8729,10 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setimmediate": { @@ -8914,16 +8907,16 @@ } }, "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", - "debug": "~4.3.2", + "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" @@ -8933,66 +8926,30 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "debug": "~4.4.1", + "ws": "~8.18.3" } }, "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", "dev": true, "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "debug": "~4.4.1" }, "engines": { "node": ">=10.0.0" } }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/socket.io/node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -9007,24 +8964,6 @@ "node": ">= 0.6" } }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/socket.io/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -9129,17 +9068,6 @@ "node": ">=0.10.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, "node_modules/spdx-exceptions": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", @@ -9148,9 +9076,9 @@ "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9159,16 +9087,16 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.22", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, "node_modules/ssri": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", - "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", "dev": true, "license": "ISC", "dependencies": { @@ -9244,13 +9172,13 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -9286,9 +9214,9 @@ } }, "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", + "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -9567,9 +9495,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -9622,17 +9550,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/validate-npm-package-name": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", @@ -9659,7 +9576,6 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -9887,9 +9803,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", "engines": { diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 3c39d3d..7a30473 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -9,6 +9,7 @@ import { Faturamento } from './pages/faturamento/faturamento'; import { authGuard } from './guards/auth.guard'; import { sysadminOrGestorGuard } from './guards/sysadmin-or-gestor.guard'; +import { sysadminOrFinanceiroGuard } from './guards/sysadmin-or-financeiro.guard'; import { sysadminOnlyGuard } from './guards/sysadmin-only.guard'; import { DadosUsuarios } from './pages/dados-usuarios/dados-usuarios'; import { VigenciaComponent } from './pages/vigencia/vigencia'; @@ -19,6 +20,7 @@ import { ChipsControleRecebidos } from './pages/chips-controle-recebidos/chips-c import { Resumo } from './pages/resumo/resumo'; import { Parcelamentos } from './pages/parcelamentos/parcelamentos'; import { Historico } from './pages/historico/historico'; +import { HistoricoLinhas } from './pages/historico-linhas/historico-linhas'; import { Perfil } from './pages/perfil/perfil'; import { SystemProvisionUserPage } from './pages/system-provision-user/system-provision-user'; @@ -29,15 +31,16 @@ export const routes: Routes = [ { path: 'geral', component: Geral, canActivate: [authGuard], title: 'Geral' }, { path: 'mureg', component: Mureg, canActivate: [authGuard], title: 'Mureg' }, - { path: 'faturamento', component: Faturamento, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Faturamento' }, + { path: 'faturamento', component: Faturamento, canActivate: [authGuard, sysadminOrFinanceiroGuard], title: 'Faturamento' }, { path: 'dadosusuarios', component: DadosUsuarios, canActivate: [authGuard], title: 'Dados dos Usuários' }, { path: 'vigencia', component: VigenciaComponent, canActivate: [authGuard], title: 'Vigência' }, { path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard], title: 'Troca de Número' }, { path: 'notificacoes', component: Notificacoes, canActivate: [authGuard], title: 'Notificações' }, { path: 'chips-controle-recebidos', component: ChipsControleRecebidos, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Chips Controle Recebidos' }, { path: 'resumo', component: Resumo, canActivate: [authGuard], title: 'Resumo' }, - { path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Parcelamentos' }, + { path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, sysadminOrFinanceiroGuard], title: 'Parcelamentos' }, { path: 'historico', component: Historico, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico' }, + { path: 'historico-linhas', component: HistoricoLinhas, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico de Linhas' }, { path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' }, { path: 'system/fornecer-usuario', diff --git a/src/app/app.ts b/src/app/app.ts index 11b8f61..3534422 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -40,6 +40,7 @@ export class AppComponent { '/resumo', '/parcelamentos', '/historico', + '/historico-linhas', '/perfil', '/system', ]; diff --git a/src/app/components/header/header.html b/src/app/components/header/header.html index cc76c2f..44206a8 100644 --- a/src/app/components/header/header.html +++ b/src/app/components/header/header.html @@ -539,15 +539,18 @@ Mureg - + Faturamento - + Parcelamentos Histórico + + Histórico de Linhas + Dados PF/PJ diff --git a/src/app/components/header/header.ts b/src/app/components/header/header.ts index 0d78ae7..b0608bf 100644 --- a/src/app/components/header/header.ts +++ b/src/app/components/header/header.ts @@ -34,7 +34,10 @@ export class Header implements AfterViewInit, OnDestroy { isLoggedHeader = false; isHome = false; isSysAdmin = false; + isGestor = false; + isFinanceiro = false; canViewAll = false; + canViewFinancialPages = false; clientTenantDisplayName = ''; private clientTenantNameTenantId: string | null = null; private readonly baseApi: string; @@ -60,7 +63,7 @@ export class Header implements AfterViewInit, OnDestroy { readonly permissionOptions = [ { value: 'sysadmin', label: 'SysAdmin' }, { value: 'gestor', label: 'Gestor' }, - { value: 'cliente', label: 'Cliente' }, + { value: 'financeiro', label: 'Financeiro' }, ]; manageUsersLoading = false; @@ -93,6 +96,7 @@ export class Header implements AfterViewInit, OnDestroy { '/resumo', '/parcelamentos', '/historico', + '/historico-linhas', '/perfil', '/system', ]; @@ -213,15 +217,22 @@ export class Header implements AfterViewInit, OnDestroy { private syncPermissions() { if (!isPlatformBrowser(this.platformId)) { this.isSysAdmin = false; + this.isGestor = false; + this.isFinanceiro = false; this.canViewAll = false; + this.canViewFinancialPages = false; this.clientTenantDisplayName = ''; this.clientTenantNameTenantId = null; return; } const isSysAdmin = this.authService.hasRole('sysadmin'); const isGestor = this.authService.hasRole('gestor'); + const isFinanceiro = this.authService.hasRole('financeiro'); this.isSysAdmin = isSysAdmin; - this.canViewAll = isSysAdmin || isGestor; + this.isGestor = isGestor; + this.isFinanceiro = isFinanceiro; + this.canViewAll = isSysAdmin || isGestor || isFinanceiro; + this.canViewFinancialPages = isSysAdmin || isFinanceiro; if (!this.isClientHeader) { this.clientTenantDisplayName = ''; @@ -497,7 +508,10 @@ export class Header implements AfterViewInit, OnDestroy { this.optionsOpen = false; this.notificationsOpen = false; this.isSysAdmin = false; + this.isGestor = false; + this.isFinanceiro = false; this.canViewAll = false; + this.canViewFinancialPages = false; this.router.navigate(['/']); } diff --git a/src/app/guards/sysadmin-or-financeiro.guard.ts b/src/app/guards/sysadmin-or-financeiro.guard.ts new file mode 100644 index 0000000..8ce5e8b --- /dev/null +++ b/src/app/guards/sysadmin-or-financeiro.guard.ts @@ -0,0 +1,27 @@ +import { inject, PLATFORM_ID } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { isPlatformBrowser } from '@angular/common'; +import { AuthService } from '../services/auth.service'; + +export const sysadminOrFinanceiroGuard: CanActivateFn = () => { + const router = inject(Router); + const platformId = inject(PLATFORM_ID); + const authService = inject(AuthService); + + if (!isPlatformBrowser(platformId)) { + // Em SSR não há storage do usuário para validar sessão/perfil. + return true; + } + + const token = authService.token; + if (!token) { + return router.parseUrl('/login'); + } + + const hasAccess = authService.hasRole('sysadmin') || authService.hasRole('financeiro'); + if (!hasAccess) { + return router.parseUrl('/dashboard'); + } + + return true; +}; diff --git a/src/app/guards/sysadmin-or-gestor.guard.ts b/src/app/guards/sysadmin-or-gestor.guard.ts index 252bae1..61b19a5 100644 --- a/src/app/guards/sysadmin-or-gestor.guard.ts +++ b/src/app/guards/sysadmin-or-gestor.guard.ts @@ -18,7 +18,10 @@ export const sysadminOrGestorGuard: CanActivateFn = () => { return router.parseUrl('/login'); } - const hasAccess = authService.hasRole('sysadmin') || authService.hasRole('gestor'); + const hasAccess = + authService.hasRole('sysadmin') || + authService.hasRole('gestor') || + authService.hasRole('financeiro'); if (!hasAccess) { return router.parseUrl('/dashboard'); } diff --git a/src/app/pages/dashboard/dashboard.ts b/src/app/pages/dashboard/dashboard.ts index d734fed..943d530 100644 --- a/src/app/pages/dashboard/dashboard.ts +++ b/src/app/pages/dashboard/dashboard.ts @@ -370,7 +370,8 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { const isSysAdmin = this.authService.hasRole('sysadmin'); const isGestor = this.authService.hasRole('gestor'); - this.isCliente = !(isSysAdmin || isGestor); + const isFinanceiro = this.authService.hasRole('financeiro'); + this.isCliente = !(isSysAdmin || isGestor || isFinanceiro); if (this.isCliente) { this.loadClientDashboardData(); diff --git a/src/app/pages/geral/geral.html b/src/app/pages/geral/geral.html index f34ddaf..4953a8f 100644 --- a/src/app/pages/geral/geral.html +++ b/src/app/pages/geral/geral.html @@ -67,7 +67,7 @@
-
+
Selecionadas: {{ batchStatusSelectionCount }} @@ -387,7 +387,7 @@ Enviar p/ Reserva ({{ reservaSelectedCount }}) -
@@ -452,7 +452,7 @@
- + @@ -571,7 +571,7 @@
- + diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts index b10e06c..3c6c305 100644 --- a/src/app/pages/geral/geral.ts +++ b/src/app/pages/geral/geral.ts @@ -341,6 +341,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { exporting = false; isSysAdmin = false; isGestor = false; + isFinanceiro = false; isClientRestricted = false; rows: LineRow[] = []; @@ -644,7 +645,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } get hasGroupLineSelectionTools(): boolean { - return !this.isClientRestricted && !!(this.expandedGroup ?? '').trim(); + return this.canManageLines && !!(this.expandedGroup ?? '').trim(); + } + + get canManageLines(): boolean { + return this.isSysAdmin || this.isGestor; } get canMoveSelectedLinesToReserva(): boolean { @@ -660,7 +665,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } get canOpenBatchStatusModal(): boolean { - if (this.isClientRestricted) return false; + if (!this.canManageLines) return false; if (this.loading || this.batchStatusSaving) return false; return this.batchStatusSelectionCount > 0; } @@ -810,7 +815,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { if (!isPlatformBrowser(this.platformId)) return; this.isSysAdmin = this.authService.hasRole('sysadmin'); this.isGestor = this.authService.hasRole('gestor'); - this.isClientRestricted = !(this.isSysAdmin || this.isGestor); + this.isFinanceiro = this.authService.hasRole('financeiro'); + this.isClientRestricted = !(this.isSysAdmin || this.isGestor || this.isFinanceiro); if (this.isClientRestricted) { this.filterSkil = 'ALL'; @@ -2176,6 +2182,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async onEditar(r: LineRow) { + if (this.isFinanceiro) { + await this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + this.editOpen = true; this.editSaving = false; this.editModel = null; @@ -2255,6 +2266,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async saveEdit() { + if (this.isFinanceiro) { + await this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + if (!this.editingId || !this.editModel) return; this.editSaving = true; @@ -2485,7 +2501,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async onCadastrarLinha() { - if (this.isClientRestricted) { + if (!this.canManageLines) { await this.showToast('Você não tem permissão para cadastrar novos clientes.'); return; } @@ -2498,7 +2514,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async onAddLineToGroup(clientName: string) { - if (this.isClientRestricted) { + if (!this.canManageLines) { await this.showToast('Você não tem permissão para adicionar linhas.'); return; } @@ -3481,7 +3497,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async openBatchStatusModal(action: BatchStatusAction) { - if (this.isClientRestricted) { + if (!this.canManageLines) { await this.showToast('Você não tem permissão para bloquear/desbloquear em lote.'); return; } diff --git a/src/app/pages/historico-linhas/historico-linhas.html b/src/app/pages/historico-linhas/historico-linhas.html new file mode 100644 index 0000000..eb67ad6 --- /dev/null +++ b/src/app/pages/historico-linhas/historico-linhas.html @@ -0,0 +1,278 @@ +
+ +
+ +
+ + + + + +
+
+
+
+
+ Linha +
+ +
+
Histórico de Linhas
+ Timeline completa das alterações feitas em uma linha específica. +
+ +
+ + +
+
+ +
+
+
+ + Filtros +
+
+ + +
+
+ +
+
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+
+ Eventos (filtro) + {{ total }} +
+
+ Status (página) + {{ statusCountInPage }} +
+
+ Trocas de Número (página) + {{ trocaCountInPage }} +
+
+ Mureg (página) + {{ muregCountInPage }} +
+
+
+ +
+
+
+ Informe a linha no filtro para carregar o histórico detalhado. +
+ +
+ +
+ + + +
+ Nenhuma alteração encontrada para a linha informada. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Data/HoraUsuárioOrigemAçãoResumo da alteraçãoDetalhes
{{ formatDateTime(log.occurredAtUtc) }} +
+ {{ displayUserName(log) }} + {{ log.userEmail || '-' }} +
+
+ {{ log.page || '-' }} + + {{ formatAction(log.action) }} + + +
{{ summary.title }}
+
{{ summary.description }}
+
+ {{ formatChangeValue(summary.before) }} + + {{ formatChangeValue(summary.after) }} +
+
+ DDD: {{ formatChangeValue(summary.beforeDdd) }} {{ formatChangeValue(summary.afterDdd) }} +
+
+
+ +
+
+
+
+ Mudanças de campos +
+ +
+
+
+ {{ change.field }} + + {{ changeTypeLabel(change.changeType) }} + +
+
+ {{ formatChangeValue(change.oldValue) }} + + {{ formatChangeValue(change.newValue) }} +
+
+
+
+ +
Sem mudanças detalhadas nesse evento.
+
+
+
+
+
+
+ + +
+
+
diff --git a/src/app/pages/historico-linhas/historico-linhas.scss b/src/app/pages/historico-linhas/historico-linhas.scss new file mode 100644 index 0000000..1fe3352 --- /dev/null +++ b/src/app/pages/historico-linhas/historico-linhas.scss @@ -0,0 +1,648 @@ +:host { + --brand: #e33dcf; + --brand-soft: rgba(227, 61, 207, 0.12); + --blue: #030faa; + --text: #111214; + --muted: rgba(17, 18, 20, 0.64); + --surface: rgba(255, 255, 255, 0.9); + --surface-strong: #ffffff; + --line: rgba(15, 23, 42, 0.11); + --radius-xl: 22px; + --radius-lg: 16px; + --shadow-card: 0 20px 44px rgba(17, 18, 20, 0.1); + + display: block; + font-family: 'Inter', sans-serif; + color: var(--text); + box-sizing: border-box; +} + +.historico-linhas-page { + min-height: 100vh; + padding: 0 12px; + display: flex; + align-items: flex-start; + justify-content: center; + position: relative; + overflow-y: auto; + background: + radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.14), transparent 60%), + radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.08), transparent 60%), + linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%); + + &::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background: rgba(255, 255, 255, 0.25); + } +} + +.page-blob { + position: fixed; + pointer-events: none; + border-radius: 999px; + filter: blur(34px); + opacity: 0.55; + z-index: 0; + background: radial-gradient(circle at 30% 30%, rgba(227, 61, 207, 0.55), rgba(227, 61, 207, 0.06)); + animation: floaty 10s ease-in-out infinite; + + &.blob-1 { width: 420px; height: 420px; top: -140px; left: -140px; } + &.blob-2 { width: 520px; height: 520px; top: -220px; right: -240px; animation-duration: 12s; } + &.blob-3 { width: 360px; height: 360px; bottom: -180px; left: 25%; animation-duration: 14s; } + &.blob-4 { width: 520px; height: 520px; bottom: -260px; right: -260px; animation-duration: 16s; opacity: 0.45; } +} + +@keyframes floaty { + 0% { transform: translate(0, 0) scale(1); } + 50% { transform: translate(18px, 10px) scale(1.03); } + 100% { transform: translate(0, 0) scale(1); } +} + +.container-geral-responsive { + width: 98% !important; + max-width: 1500px !important; + position: relative; + z-index: 1; + margin-top: 40px; + margin-bottom: 200px; +} + +.geral-card { + border-radius: var(--radius-xl); + overflow: hidden; + background: var(--surface); + border: 1px solid rgba(227, 61, 207, 0.16); + backdrop-filter: blur(12px); + box-shadow: var(--shadow-card); + position: relative; + display: flex; + flex-direction: column; + min-height: 80vh; + + &::before { + content: ''; + position: absolute; + inset: 1px; + border-radius: calc(var(--radius-xl) - 1px); + pointer-events: none; + border: 1px solid rgba(255, 255, 255, 0.65); + opacity: 0.75; + } +} + +.geral-header { + padding: 16px 24px; + border-bottom: 1px solid rgba(17, 18, 20, 0.06); + background: linear-gradient(180deg, rgba(227, 61, 207, 0.06), rgba(255, 255, 255, 0.2)); + flex-shrink: 0; +} + +.header-row-top { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 12px; + + @media (max-width: 900px) { + grid-template-columns: 1fr; + text-align: center; + gap: 14px; + + .title-badge { justify-self: center; } + .header-actions { justify-self: center; } + } +} + +.title-badge { + justify-self: start; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 6px 12px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.82); + border: 1px solid rgba(227, 61, 207, 0.22); + color: var(--text); + font-size: 13px; + font-weight: 800; + + i { color: var(--brand); } +} + +.header-title { + justify-self: center; + text-align: center; +} + +.title { + font-size: 26px; + font-weight: 950; + letter-spacing: -0.3px; + color: var(--text); + margin-top: 10px; +} + +.subtitle { + color: var(--muted); + font-weight: 700; +} + +.header-actions { + justify-self: end; +} + +.btn-brand { + background-color: var(--brand); + border-color: var(--brand); + color: #fff; + font-weight: 900; + border-radius: 12px; + transition: transform 0.2s, box-shadow 0.2s; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(227, 61, 207, 0.25); + filter: brightness(1.05); + } +} + +.btn-glass { + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(17, 18, 20, 0.16); + color: rgba(17, 18, 20, 0.85); + border-radius: 12px; + font-weight: 700; +} + +.filters-card { + background: rgba(255, 255, 255, 0.92); + border: 1px solid rgba(17, 18, 20, 0.08); + border-radius: 16px; + padding: 16px; + display: grid; + gap: 14px; + box-shadow: 0 14px 28px rgba(17, 18, 20, 0.08); +} + +.filters-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.filters-title { + display: inline-flex; + align-items: center; + gap: 8px; + font-weight: 900; + font-size: 14px; + color: rgba(17, 18, 20, 0.82); +} + +.filters-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.filters-grid { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 12px; +} + +.filter-field { + display: grid; + gap: 6px; + grid-column: span 2; + min-width: 0; + + label { + font-size: 11px; + font-weight: 800; + color: rgba(17, 18, 20, 0.6); + text-transform: uppercase; + letter-spacing: 0.05em; + } + + input { + width: 100%; + height: 40px; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.15); + padding: 0 12px; + font-size: 14px; + background: #fff; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + } + + input:focus { + outline: none; + border-color: var(--brand); + box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.12); + } +} + +.line-field { + grid-column: span 4; +} + +.period-field { + grid-column: span 3; +} + +.btn-primary, +.btn-ghost { + height: 38px; + border-radius: 10px; + border: none; + font-weight: 700; + font-size: 12px; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0 14px; +} + +.btn-primary { + background: linear-gradient(135deg, var(--brand), #bc30ac); + color: #fff; + box-shadow: 0 8px 16px rgba(227, 61, 207, 0.24); +} + +.btn-ghost { + background: rgba(15, 23, 42, 0.06); + color: rgba(15, 23, 42, 0.85); +} + +.kpi-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.kpi-card { + background: var(--surface-strong); + border: 1px solid rgba(17, 18, 20, 0.08); + border-radius: 14px; + padding: 12px 14px; + display: grid; + gap: 6px; + box-shadow: 0 8px 16px rgba(17, 18, 20, 0.06); +} + +.kpi-label { + font-size: 12px; + font-weight: 700; + color: rgba(17, 18, 20, 0.62); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.kpi-value { + font-size: 22px; + line-height: 1; + font-weight: 900; + color: var(--blue); +} + +.geral-body { + padding: 18px 24px; + flex: 1; +} + +.table-wrap { + width: 100%; + background: rgba(255, 255, 255, 0.84); + border: 1px solid rgba(15, 23, 42, 0.1); + border-radius: 14px; + overflow: hidden; +} + +.table-modern { + margin: 0; + min-width: 980px; + + thead th { + background: linear-gradient(180deg, rgba(3, 15, 170, 0.92), rgba(3, 15, 170, 0.82)); + color: #fff; + font-size: 12px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.05em; + border: none; + padding: 12px; + white-space: nowrap; + } + + tbody td { + border-top: 1px solid rgba(15, 23, 42, 0.08); + vertical-align: middle; + padding: 12px; + background: rgba(255, 255, 255, 0.92); + } + + tbody tr.table-row-item:hover td { + background: rgba(227, 61, 207, 0.05); + } + + tbody tr.table-row-item.expanded td { + background: rgba(227, 61, 207, 0.08); + } +} + +.user-cell { + display: grid; + line-height: 1.2; +} + +.user-name { + font-weight: 800; +} + +.user-email { + color: rgba(17, 18, 20, 0.55); +} + +.origin-pill { + display: inline-flex; + align-items: center; + border-radius: 999px; + padding: 4px 10px; + background: rgba(3, 15, 170, 0.1); + border: 1px solid rgba(3, 15, 170, 0.2); + color: rgba(3, 15, 170, 0.88); + font-size: 12px; + font-weight: 700; +} + +.badge-action { + display: inline-flex; + align-items: center; + border-radius: 999px; + padding: 5px 10px; + font-size: 12px; + font-weight: 800; + border: 1px solid transparent; + + &.action-create { + color: #157347; + background: rgba(25, 135, 84, 0.12); + border-color: rgba(25, 135, 84, 0.24); + } + + &.action-update { + color: #0a58ca; + background: rgba(13, 110, 253, 0.12); + border-color: rgba(13, 110, 253, 0.24); + } + + &.action-delete { + color: #b02a37; + background: rgba(220, 53, 69, 0.12); + border-color: rgba(220, 53, 69, 0.24); + } + + &.action-default { + color: #495057; + background: rgba(108, 117, 125, 0.12); + border-color: rgba(108, 117, 125, 0.24); + } +} + +.summary-col { + min-width: 360px; +} + +.summary-title { + font-size: 13px; + font-weight: 900; + margin-bottom: 2px; +} + +.summary-description { + font-size: 12px; + color: rgba(17, 18, 20, 0.66); +} + +.summary-diff { + margin-top: 6px; + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12px; + border-radius: 10px; + background: rgba(15, 23, 42, 0.05); + padding: 4px 8px; + + .old { + color: #b02a37; + font-weight: 700; + } + + .new { + color: #157347; + font-weight: 700; + } +} + +.summary-ddd { + margin-top: 5px; + font-size: 11px; + color: rgba(17, 18, 20, 0.62); + font-weight: 700; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.tone-mureg { color: #005f73; } +.tone-troca { color: #6f42c1; } +.tone-status { color: #0a58ca; } +.tone-linha { color: #0d6efd; } +.tone-chip { color: #198754; } +.tone-generic { color: #495057; } + +.actions-col { + width: 84px; + text-align: center; +} + +.expand-btn { + width: 34px; + height: 34px; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.15); + background: #fff; + color: rgba(15, 23, 42, 0.85); +} + +.details-row td { + background: rgba(255, 255, 255, 0.94); +} + +.details-panel { + display: grid; + grid-template-columns: 1fr; + gap: 12px; +} + +.details-section { + border: 1px solid rgba(15, 23, 42, 0.1); + border-radius: 12px; + background: #fff; + padding: 10px 12px; +} + +.section-title { + display: inline-flex; + align-items: center; + gap: 7px; + font-weight: 800; + color: rgba(17, 18, 20, 0.84); + margin-bottom: 8px; +} + +.changes-list { + display: grid; + gap: 8px; +} + +.change-item { + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 10px; + padding: 8px; + background: rgba(248, 249, 250, 0.85); +} + +.change-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 4px; +} + +.change-field { + font-size: 12px; + font-weight: 800; + color: #111; +} + +.change-type { + border-radius: 999px; + padding: 2px 8px; + font-size: 11px; + font-weight: 800; + border: 1px solid transparent; + + &.change-added { + background: rgba(25, 135, 84, 0.12); + color: #157347; + border-color: rgba(25, 135, 84, 0.24); + } + + &.change-removed { + background: rgba(220, 53, 69, 0.12); + color: #b02a37; + border-color: rgba(220, 53, 69, 0.24); + } + + &.change-modified { + background: rgba(13, 110, 253, 0.12); + color: #0a58ca; + border-color: rgba(13, 110, 253, 0.24); + } +} + +.change-values { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12px; + + .old { + color: #b02a37; + font-weight: 700; + } + + .new { + color: #157347; + font-weight: 700; + } +} + +.empty-state { + font-size: 13px; + color: rgba(17, 18, 20, 0.62); + font-weight: 600; +} + +.empty-group { + text-align: center; + padding: 28px; + color: rgba(17, 18, 20, 0.65); + font-weight: 700; +} + +.empty-group.helper { + background: rgba(3, 15, 170, 0.05); + border-bottom: 1px solid rgba(3, 15, 170, 0.12); +} + +.geral-footer { + display: none; +} + +.footer-meta { + display: flex; + align-items: center; + gap: 14px; + flex-wrap: wrap; +} + +.pagination-modern .page-link { + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.15); + color: rgba(15, 23, 42, 0.85); + margin: 0 2px; +} + +.pagination-modern .page-item.active .page-link { + background: var(--brand); + border-color: var(--brand); + color: #fff; +} + +@media (max-width: 1200px) { + .filters-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); } + .filter-field { grid-column: span 2; } + .line-field { grid-column: span 3; } + .period-field { grid-column: span 3; } + .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .details-panel { grid-template-columns: 1fr; } +} + +@media (max-width: 768px) { + .geral-header, + .geral-body, + .geral-footer { + padding-left: 14px; + padding-right: 14px; + } + + .filters-grid { grid-template-columns: repeat(1, minmax(0, 1fr)); } + .filter-field, + .line-field { + grid-column: span 1; + } + + .kpi-grid { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } +} diff --git a/src/app/pages/historico-linhas/historico-linhas.ts b/src/app/pages/historico-linhas/historico-linhas.ts new file mode 100644 index 0000000..c9f4bf6 --- /dev/null +++ b/src/app/pages/historico-linhas/historico-linhas.ts @@ -0,0 +1,598 @@ +import { Component, OnInit, ElementRef, ViewChild, ChangeDetectorRef, Inject, PLATFORM_ID } from '@angular/core'; +import { CommonModule, isPlatformBrowser } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { HttpErrorResponse } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; + +import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { + HistoricoService, + AuditLogDto, + AuditChangeType, + AuditFieldChangeDto, + LineHistoricoQuery +} from '../../services/historico.service'; +import { TableExportService } from '../../services/table-export.service'; + +interface SelectOption { + value: string; + label: string; +} + +type EventTone = 'mureg' | 'troca' | 'status' | 'linha' | 'chip' | 'generic'; + +interface EventSummary { + title: string; + description: string; + before?: string | null; + after?: string | null; + beforeDdd?: string | null; + afterDdd?: string | null; + tone: EventTone; +} + +@Component({ + selector: 'app-historico-linhas', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent], + templateUrl: './historico-linhas.html', + styleUrls: ['./historico-linhas.scss'], +}) +export class HistoricoLinhas implements OnInit { + @ViewChild('successToast', { static: false }) successToast!: ElementRef; + + logs: AuditLogDto[] = []; + loading = false; + exporting = false; + error = false; + errorMsg = ''; + toastMessage = ''; + + expandedLogId: string | null = null; + + page = 1; + pageSize = 10; + pageSizeOptions = [10, 20, 50, 100]; + total = 0; + + filterLine = ''; + filterPageName = ''; + filterAction = ''; + filterUser = ''; + dateFrom = ''; + dateTo = ''; + + readonly pageOptions: SelectOption[] = [ + { value: '', label: 'Todas as origens' }, + { value: 'Geral', label: 'Geral' }, + { value: 'Mureg', label: 'Mureg' }, + { value: 'Troca de número', label: 'Troca de número' }, + { value: 'Vigência', label: 'Vigência' }, + { value: 'Parcelamentos', label: 'Parcelamentos' }, + ]; + + readonly actionOptions: SelectOption[] = [ + { value: '', label: 'Todas as ações' }, + { value: 'CREATE', label: 'Criação' }, + { value: 'UPDATE', label: 'Atualização' }, + { value: 'DELETE', label: 'Exclusão' }, + ]; + + private readonly summaryCache = new Map(); + private readonly idFieldExceptions = new Set(['iccid']); + + constructor( + private readonly historicoService: HistoricoService, + private readonly cdr: ChangeDetectorRef, + @Inject(PLATFORM_ID) private readonly platformId: object, + private readonly tableExportService: TableExportService + ) {} + + ngOnInit(): void { + // Tela inicia aguardando o usuário informar a linha. + } + + applyFilters(): void { + this.page = 1; + this.fetch(); + } + + refresh(): void { + this.fetch(); + } + + clearFilters(): void { + this.filterLine = ''; + this.filterPageName = ''; + this.filterAction = ''; + this.filterUser = ''; + this.dateFrom = ''; + this.dateTo = ''; + this.page = 1; + this.logs = []; + this.total = 0; + this.error = false; + this.errorMsg = ''; + this.summaryCache.clear(); + } + + onPageSizeChange(): void { + this.page = 1; + this.fetch(); + } + + goToPage(target: number): void { + this.page = Math.max(1, Math.min(this.totalPages, target)); + this.fetch(); + } + + toggleDetails(log: AuditLogDto, event?: Event): void { + if (event) event.stopPropagation(); + this.expandedLogId = this.expandedLogId === log.id ? null : log.id; + } + + async onExport(): Promise { + if (this.exporting) return; + + const lineTerm = this.normalizedLineTerm; + if (!lineTerm) { + await this.showToast('Informe a linha para exportar.'); + return; + } + + this.exporting = true; + try { + const allLogs = await this.fetchAllLogsForExport(); + if (!allLogs.length) { + await this.showToast('Nenhum evento encontrado para exportar.'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `historico_linhas_${timestamp}`, + sheetName: 'HistoricoLinhas', + rows: allLogs, + columns: [ + { header: 'Data/Hora', type: 'datetime', value: (log) => log.occurredAtUtc ?? '' }, + { header: 'Usuario', value: (log) => this.displayUserName(log) }, + { header: 'E-mail', value: (log) => log.userEmail ?? '' }, + { header: 'Origem', value: (log) => log.page ?? '' }, + { header: 'Acao', value: (log) => this.formatAction(log.action) }, + { header: 'Evento', value: (log) => this.summaryFor(log).title }, + { header: 'Resumo', value: (log) => this.summaryFor(log).description }, + { header: 'Valor Anterior', value: (log) => this.summaryFor(log).before ?? '' }, + { header: 'Valor Novo', value: (log) => this.summaryFor(log).after ?? '' }, + { header: 'DDD Anterior', value: (log) => this.summaryFor(log).beforeDdd ?? '' }, + { header: 'DDD Novo', value: (log) => this.summaryFor(log).afterDdd ?? '' }, + { header: 'Mudancas', value: (log) => this.formatChangesSummary(log) }, + ], + }); + + await this.showToast(`Planilha exportada com ${allLogs.length} evento(s).`); + } catch { + await this.showToast('Erro ao exportar histórico de linhas.'); + } finally { + this.exporting = false; + } + } + + formatDateTime(value?: string | null): string { + if (!value) return '-'; + const dt = new Date(value); + if (Number.isNaN(dt.getTime())) return '-'; + return dt.toLocaleString('pt-BR'); + } + + displayUserName(log: AuditLogDto): string { + const name = (log.userName || '').trim(); + return name ? name : 'SISTEMA'; + } + + formatAction(action?: string | null): string { + const value = (action || '').toUpperCase(); + if (!value) return '-'; + if (value === 'CREATE') return 'Criação'; + if (value === 'UPDATE') return 'Atualização'; + if (value === 'DELETE') return 'Exclusão'; + return 'Outro'; + } + + actionClass(action?: string | null): string { + const value = (action || '').toUpperCase(); + if (value === 'CREATE') return 'action-create'; + if (value === 'UPDATE') return 'action-update'; + if (value === 'DELETE') return 'action-delete'; + return 'action-default'; + } + + changeTypeLabel(type?: AuditChangeType | string | null): string { + if (!type) return 'Alterado'; + if (type === 'added') return 'Adicionado'; + if (type === 'removed') return 'Removido'; + return 'Alterado'; + } + + changeTypeClass(type?: AuditChangeType | string | null): string { + if (type === 'added') return 'change-added'; + if (type === 'removed') return 'change-removed'; + return 'change-modified'; + } + + formatChangeValue(value?: string | null): string { + if (value === undefined || value === null || value === '') return '-'; + return String(value); + } + + summaryFor(log: AuditLogDto): EventSummary { + const cached = this.summaryCache.get(log.id); + if (cached) return cached; + const summary = this.buildEventSummary(log); + this.summaryCache.set(log.id, summary); + return summary; + } + + toneClass(tone: EventTone): string { + return `tone-${tone}`; + } + + trackByLog(_: number, log: AuditLogDto): string { + return log.id; + } + + trackByField(_: number, change: AuditFieldChangeDto): string { + return `${change.field}-${change.oldValue ?? ''}-${change.newValue ?? ''}`; + } + + visibleChanges(log: AuditLogDto): AuditFieldChangeDto[] { + return this.publicChanges(log); + } + + get normalizedLineTerm(): string { + return (this.filterLine || '').trim(); + } + + get hasLineFilter(): boolean { + return !!this.normalizedLineTerm; + } + + get totalPages(): number { + return Math.ceil((this.total || 0) / this.pageSize) || 1; + } + + get pageNumbers(): number[] { + 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 += 1) pages.push(i); + return pages; + } + + get pageStart(): number { + return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; + } + + get pageEnd(): number { + if (this.total === 0) return 0; + return Math.min(this.page * this.pageSize, this.total); + } + + get statusCountInPage(): number { + return this.logs.filter((log) => this.summaryFor(log).tone === 'status').length; + } + + get trocaCountInPage(): number { + return this.logs.filter((log) => this.summaryFor(log).tone === 'troca').length; + } + + get muregCountInPage(): number { + return this.logs.filter((log) => this.summaryFor(log).tone === 'mureg').length; + } + + private fetch(): void { + const lineTerm = this.normalizedLineTerm; + if (!lineTerm) { + this.logs = []; + this.total = 0; + this.error = true; + this.errorMsg = 'Informe a linha para consultar o histórico.'; + this.loading = false; + this.summaryCache.clear(); + return; + } + + this.loading = true; + this.error = false; + this.errorMsg = ''; + this.expandedLogId = null; + + const query: LineHistoricoQuery = { + ...this.buildBaseQuery(), + line: lineTerm, + page: this.page, + pageSize: this.pageSize, + }; + + this.historicoService.listByLine(query).subscribe({ + next: (res) => { + this.logs = res.items || []; + this.total = res.total || 0; + this.page = res.page || this.page; + this.pageSize = res.pageSize || this.pageSize; + this.loading = false; + this.rebuildSummaryCache(); + }, + error: (err: HttpErrorResponse) => { + this.loading = false; + this.error = true; + this.logs = []; + this.total = 0; + this.summaryCache.clear(); + if (err?.status === 400) { + this.errorMsg = err?.error?.message || 'Informe uma linha válida.'; + return; + } + if (err?.status === 403) { + this.errorMsg = 'Acesso restrito.'; + return; + } + this.errorMsg = 'Erro ao carregar histórico da linha. Tente novamente.'; + } + }); + } + + private async fetchAllLogsForExport(): Promise { + const lineTerm = this.normalizedLineTerm; + if (!lineTerm) return []; + + const pageSize = 500; + let page = 1; + let expectedTotal = 0; + const all: AuditLogDto[] = []; + + while (page <= 500) { + const response = await firstValueFrom( + this.historicoService.listByLine({ + ...this.buildBaseQuery(), + line: lineTerm, + page, + pageSize, + }) + ); + + 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 buildBaseQuery(): Omit { + return { + pageName: this.filterPageName || undefined, + action: this.filterAction || undefined, + user: this.filterUser?.trim() || undefined, + dateFrom: this.toIsoDate(this.dateFrom, false) || undefined, + dateTo: this.toIsoDate(this.dateTo, true) || undefined, + }; + } + + private rebuildSummaryCache(): void { + this.summaryCache.clear(); + this.logs.forEach((log) => { + this.summaryCache.set(log.id, this.buildEventSummary(log)); + }); + } + + private buildEventSummary(log: AuditLogDto): EventSummary { + const page = (log.page || '').toLowerCase(); + const entity = (log.entityName || '').toLowerCase(); + + const linhaChange = this.findChange(log, 'linha'); + const statusChange = this.findChange(log, 'status'); + const chipChange = this.findChange(log, 'chip', 'iccid'); + const linhaAntiga = this.findChange(log, 'linhaantiga'); + const linhaNova = this.findChange(log, 'linhanova'); + + const muregLike = entity === 'muregline' || page.includes('mureg'); + if (muregLike) { + const before = this.firstFilled(linhaAntiga?.newValue, linhaAntiga?.oldValue, linhaChange?.oldValue); + const after = this.firstFilled(linhaNova?.newValue, linhaNova?.oldValue, linhaChange?.newValue); + return { + title: 'Troca de Mureg', + description: 'Linha alterada no fluxo de Mureg.', + before, + after, + beforeDdd: this.extractDdd(before), + afterDdd: this.extractDdd(after), + tone: 'mureg', + }; + } + + const trocaLike = entity === 'trocanumeroline' || page.includes('troca'); + if (trocaLike) { + const before = this.firstFilled(linhaAntiga?.newValue, linhaAntiga?.oldValue, linhaChange?.oldValue); + const after = this.firstFilled(linhaNova?.newValue, linhaNova?.oldValue, linhaChange?.newValue); + return { + title: 'Troca de Número', + description: 'Linha antiga substituída por uma nova.', + before, + after, + beforeDdd: this.extractDdd(before), + afterDdd: this.extractDdd(after), + tone: 'troca', + }; + } + + if (statusChange) { + const oldStatus = this.firstFilled(statusChange.oldValue); + const newStatus = this.firstFilled(statusChange.newValue); + const wasBlocked = this.isBlockedStatus(oldStatus); + const isBlocked = this.isBlockedStatus(newStatus); + let description = 'Status da linha atualizado.'; + if (!wasBlocked && isBlocked) description = 'Linha foi bloqueada.'; + if (wasBlocked && !isBlocked) description = 'Linha foi desbloqueada.'; + return { + title: 'Status da Linha', + description, + before: oldStatus, + after: newStatus, + tone: 'status', + }; + } + + if (linhaChange) { + return { + title: 'Alteração da Linha', + description: 'Número da linha foi atualizado.', + before: this.firstFilled(linhaChange.oldValue), + after: this.firstFilled(linhaChange.newValue), + beforeDdd: this.extractDdd(linhaChange.oldValue), + afterDdd: this.extractDdd(linhaChange.newValue), + tone: 'linha', + }; + } + + if (chipChange) { + return { + title: 'Alteração de Chip', + description: 'ICCID/chip atualizado na linha.', + before: this.firstFilled(chipChange.oldValue), + after: this.firstFilled(chipChange.newValue), + tone: 'chip', + }; + } + + const first = this.publicChanges(log)[0]; + if (first) { + return { + title: 'Outras alterações', + description: `Campo ${first.field} foi atualizado.`, + before: this.firstFilled(first.oldValue), + after: this.firstFilled(first.newValue), + tone: 'generic', + }; + } + + return { + title: 'Sem detalhes', + description: 'Não há mudanças detalhadas registradas para este evento.', + tone: 'generic', + }; + } + + private findChange(log: AuditLogDto, ...fields: string[]): AuditFieldChangeDto | null { + if (!fields.length) return null; + const normalizedTargets = new Set(fields.map((field) => this.normalizeField(field))); + return (log.changes || []).find((change) => normalizedTargets.has(this.normalizeField(change.field))) || null; + } + + private normalizeField(value?: string | null): string { + return (value ?? '') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-zA-Z0-9]/g, '') + .toLowerCase() + .trim(); + } + + private firstFilled(...values: Array): string | null { + for (const value of values) { + const normalized = (value ?? '').toString().trim(); + if (normalized) return normalized; + } + return null; + } + + private formatChangesSummary(log: AuditLogDto): string { + const changes = this.publicChanges(log); + if (!changes.length) return ''; + return changes + .map((change) => { + const field = change?.field ?? 'campo'; + const oldValue = this.formatChangeValue(change?.oldValue); + const newValue = this.formatChangeValue(change?.newValue); + return `${field}: ${oldValue} -> ${newValue}`; + }) + .join(' | '); + } + + private publicChanges(log: AuditLogDto): AuditFieldChangeDto[] { + return (log?.changes ?? []).filter((change) => !this.isHiddenIdField(change?.field)); + } + + private isHiddenIdField(field?: string | null): boolean { + const normalized = this.normalizeField(field); + if (!normalized) return false; + if (this.idFieldExceptions.has(normalized)) return false; + if (normalized === 'id') return true; + return normalized.endsWith('id'); + } + + private isBlockedStatus(status?: string | null): boolean { + const normalized = (status ?? '').toLowerCase().trim(); + if (!normalized) return false; + return ( + normalized.includes('bloque') || + normalized.includes('perda') || + normalized.includes('roubo') || + normalized.includes('suspens') + ); + } + + private extractDdd(value?: string | null): string | null { + const digits = this.digitsOnly(value); + if (!digits) return null; + + if (digits.startsWith('55') && digits.length >= 12) { + return digits.slice(2, 4); + } + if (digits.length >= 10) { + return digits.slice(0, 2); + } + if (digits.length >= 2) { + return digits.slice(0, 2); + } + return null; + } + + private digitsOnly(value?: string | null): string { + return (value ?? '').replace(/\D/g, ''); + } + + private toIsoDate(value: string, endOfDay: boolean): string | null { + if (!value) return null; + const time = endOfDay ? '23:59:59' : '00:00:00'; + const date = new Date(`${value}T${time}`); + if (isNaN(date.getTime())) return null; + return date.toISOString(); + } + + private async showToast(message: string): Promise { + 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); + } + } +} diff --git a/src/app/pages/mureg/mureg.html b/src/app/pages/mureg/mureg.html index 9a4cfa7..c742410 100644 --- a/src/app/pages/mureg/mureg.html +++ b/src/app/pages/mureg/mureg.html @@ -35,7 +35,7 @@ Exportar Exportando... -
@@ -177,10 +177,10 @@ - -
@@ -391,8 +391,8 @@
- - + +
diff --git a/src/app/pages/mureg/mureg.ts b/src/app/pages/mureg/mureg.ts index 86fdc7b..8ba13ea 100644 --- a/src/app/pages/mureg/mureg.ts +++ b/src/app/pages/mureg/mureg.ts @@ -11,6 +11,7 @@ import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient, HttpParams } from '@angular/common/http'; import { firstValueFrom } from 'rxjs'; +import { AuthService } from '../../services/auth.service'; import { LinesService } from '../../services/lines.service'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { TableExportService } from '../../services/table-export.service'; @@ -105,6 +106,7 @@ export class Mureg implements AfterViewInit { @Inject(PLATFORM_ID) private platformId: object, private http: HttpClient, private cdr: ChangeDetectorRef, + private authService: AuthService, private linesService: LinesService, private tableExportService: TableExportService ) {} @@ -177,9 +179,20 @@ export class Mureg implements AfterViewInit { clienteInfo: '' }; + isSysAdmin = false; + isGestor = false; + isFinanceiro = false; + + get canManageRecords(): boolean { + return this.isSysAdmin || this.isGestor; + } + async ngAfterViewInit() { if (!isPlatformBrowser(this.platformId)) return; this.initAnimations(); + this.isSysAdmin = this.authService.hasRole('sysadmin'); + this.isGestor = this.authService.hasRole('gestor'); + this.isFinanceiro = this.authService.hasRole('financeiro'); setTimeout(() => { this.preloadClients(); // ✅ já deixa o select pronto this.refresh(); @@ -624,6 +637,11 @@ export class Mureg implements AfterViewInit { // CREATE MODAL // ======================================================================= onCreate() { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + this.preloadClients(); this.createOpen = true; @@ -636,7 +654,7 @@ export class Mureg implements AfterViewInit { linhaAntiga: '', linhaNova: '', iccid: '', - dataDaMureg: '', + dataDaMureg: this.nowDateInput(), clienteInfo: '' }; @@ -663,6 +681,11 @@ export class Mureg implements AfterViewInit { } saveCreate() { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + const mobileLineId = String(this.createModel.mobileLineId ?? '').trim(); const linhaNova = String(this.createModel.linhaNova ?? '').trim(); @@ -679,7 +702,7 @@ export class Mureg implements AfterViewInit { linhaAntiga: (this.createModel.linhaAntiga ?? '') || null, linhaNova: (this.createModel.linhaNova ?? '') || null, iccid: (this.createModel.iccid ?? '') || null, - dataDaMureg: this.dateInputToIso(this.createModel.dataDaMureg) + dataDaMureg: new Date().toISOString() }; if (!payload.item || payload.item <= 0) delete payload.item; @@ -703,6 +726,11 @@ export class Mureg implements AfterViewInit { // EDIT MODAL // ======================================================================= onEditar(r: MuregRow) { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + this.preloadClients(); this.editOpen = true; @@ -770,6 +798,11 @@ export class Mureg implements AfterViewInit { } saveEdit() { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + if (!this.editModel || !this.editModel.id) return; const mobileLineId = String(this.editModel.mobileLineId ?? '').trim(); @@ -844,6 +877,11 @@ export class Mureg implements AfterViewInit { // DELETE MODAL // ======================================================================= onDelete(row: MuregRow) { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + this.deleteTarget = row; this.deleteOpen = true; this.deleteSaving = false; @@ -856,6 +894,11 @@ export class Mureg implements AfterViewInit { } async confirmDelete() { + if (!this.canManageRecords) { + await this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + if (!this.deleteTarget?.id) return; if (!(await confirmDeletionWithTyping('esta Mureg'))) return; @@ -914,6 +957,14 @@ export class Mureg implements AfterViewInit { return dt.toISOString(); } + private nowDateInput(): string { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + private extractApiMessage(err: any): string | null { try { const m1 = err?.error?.message; diff --git a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html index fb8c03c..3b3c311 100644 --- a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html +++ b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html @@ -100,6 +100,7 @@ diff --git a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts index 45a2629..17861bd 100644 --- a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts +++ b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts @@ -26,7 +26,8 @@ export class ParcelamentosTableComponent { @Input() items: ParcelamentoViewItem[] = []; @Input() loading = false; @Input() errorMessage = ''; - @Input() isSysAdmin = false; + @Input() canEdit = false; + @Input() canDelete = false; @Input() segment: ParcelamentoSegment = 'todos'; @Input() segmentCounts: Record = { diff --git a/src/app/pages/parcelamentos/parcelamentos.html b/src/app/pages/parcelamentos/parcelamentos.html index 78ba26f..538debb 100644 --- a/src/app/pages/parcelamentos/parcelamentos.html +++ b/src/app/pages/parcelamentos/parcelamentos.html @@ -29,7 +29,7 @@ Exportar Exportando... - @@ -79,7 +79,8 @@ [total]="total" [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" - [isSysAdmin]="isSysAdmin" + [canEdit]="canManageRecords" + [canDelete]="isSysAdmin" (segmentChange)="setSegment($event)" (detail)="openDetails($event)" (edit)="openEdit($event)" diff --git a/src/app/pages/parcelamentos/parcelamentos.ts b/src/app/pages/parcelamentos/parcelamentos.ts index ef6ce4d..d60e17a 100644 --- a/src/app/pages/parcelamentos/parcelamentos.ts +++ b/src/app/pages/parcelamentos/parcelamentos.ts @@ -95,6 +95,12 @@ export class Parcelamentos implements OnInit, OnDestroy { activeChips: FilterChip[] = []; isSysAdmin = false; + isGestor = false; + isFinanceiro = false; + + get canManageRecords(): boolean { + return this.isSysAdmin || this.isGestor; + } detailOpen = false; detailLoading = false; @@ -168,6 +174,8 @@ export class Parcelamentos implements OnInit, OnDestroy { private syncPermissions(): void { this.isSysAdmin = this.authService.hasRole('sysadmin'); + this.isGestor = this.authService.hasRole('gestor'); + this.isFinanceiro = this.authService.hasRole('financeiro'); } get totalPages(): number { @@ -409,6 +417,11 @@ export class Parcelamentos implements OnInit, OnDestroy { } openCreateModal(): void { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger'); + return; + } + this.createModel = this.buildCreateModel(); this.createError = ''; this.createOpen = true; @@ -421,6 +434,11 @@ export class Parcelamentos implements OnInit, OnDestroy { } saveNewParcelamento(model: ParcelamentoCreateModel): void { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger'); + return; + } + if (this.createSaving) return; this.createSaving = true; this.createError = ''; @@ -439,6 +457,11 @@ export class Parcelamentos implements OnInit, OnDestroy { } openEdit(item: ParcelamentoListItem): void { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger'); + return; + } + const id = this.getItemId(item); if (!id) return; this.editOpen = true; @@ -474,6 +497,11 @@ export class Parcelamentos implements OnInit, OnDestroy { } saveEditParcelamento(model: ParcelamentoCreateModel): void { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger'); + return; + } + if (this.editSaving || !this.editModel || !this.editId) return; this.editSaving = true; this.editError = ''; diff --git a/src/app/pages/troca-numero/troca-numero.html b/src/app/pages/troca-numero/troca-numero.html index 0edff36..0461aa6 100644 --- a/src/app/pages/troca-numero/troca-numero.html +++ b/src/app/pages/troca-numero/troca-numero.html @@ -35,7 +35,7 @@ Exportar Exportando... - @@ -156,7 +156,7 @@ {{ r.observacao || '-' }}
-
diff --git a/src/app/pages/troca-numero/troca-numero.ts b/src/app/pages/troca-numero/troca-numero.ts index 8eda5bc..673a9ce 100644 --- a/src/app/pages/troca-numero/troca-numero.ts +++ b/src/app/pages/troca-numero/troca-numero.ts @@ -11,6 +11,7 @@ import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient, HttpParams } from '@angular/common/http'; import { firstValueFrom } from 'rxjs'; +import { AuthService } from '../../services/auth.service'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { TableExportService } from '../../services/table-export.service'; import { environment } from '../../../environments/environment'; @@ -73,6 +74,7 @@ export class TrocaNumero implements AfterViewInit { @Inject(PLATFORM_ID) private platformId: object, private http: HttpClient, private cdr: ChangeDetectorRef, + private authService: AuthService, private tableExportService: TableExportService ) {} @@ -136,9 +138,20 @@ export class TrocaNumero implements AfterViewInit { loadingClients = false; loadingLines = false; + isSysAdmin = false; + isGestor = false; + isFinanceiro = false; + + get canManageRecords(): boolean { + return this.isSysAdmin || this.isGestor; + } + async ngAfterViewInit() { if (!isPlatformBrowser(this.platformId)) return; this.initAnimations(); + this.isSysAdmin = this.authService.hasRole('sysadmin'); + this.isGestor = this.authService.hasRole('gestor'); + this.isFinanceiro = this.authService.hasRole('financeiro'); setTimeout(() => this.refresh()); } @@ -502,6 +515,11 @@ export class TrocaNumero implements AfterViewInit { // ====== MODAL EDIÇÃO ====== onEditar(r: TrocaRow) { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + this.editOpen = true; this.editSaving = false; @@ -524,6 +542,11 @@ export class TrocaNumero implements AfterViewInit { } saveEdit() { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + if (!this.editModel || !this.editModel.id) return; this.editSaving = true; @@ -555,6 +578,11 @@ export class TrocaNumero implements AfterViewInit { // ====== MODAL CRIAÇÃO ====== onCreate() { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + this.createOpen = true; this.createSaving = false; @@ -584,6 +612,11 @@ export class TrocaNumero implements AfterViewInit { } saveCreate() { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + // ✅ validações do "beber do GERAL" if (!String(this.selectedCliente ?? '').trim()) { this.showToast('Selecione um Cliente do GERAL.'); diff --git a/src/app/services/historico.service.ts b/src/app/services/historico.service.ts index 2fb0c5c..f6ab9b3 100644 --- a/src/app/services/historico.service.ts +++ b/src/app/services/historico.service.ts @@ -50,6 +50,18 @@ export interface HistoricoQuery { pageSize?: number; } +export interface LineHistoricoQuery { + line: string; + pageName?: string; + action?: AuditAction | string; + user?: string; + search?: string; + dateFrom?: string; + dateTo?: string; + page?: number; + pageSize?: number; +} + @Injectable({ providedIn: 'root' }) export class HistoricoService { private readonly baseApi: string; @@ -74,4 +86,20 @@ export class HistoricoService { return this.http.get>(`${this.baseApi}/historico`, { params: httpParams }); } + + listByLine(params: LineHistoricoQuery): Observable> { + let httpParams = new HttpParams(); + if (params.line) httpParams = httpParams.set('line', params.line); + if (params.pageName) httpParams = httpParams.set('pageName', params.pageName); + if (params.action) httpParams = httpParams.set('action', params.action); + if (params.user) httpParams = httpParams.set('user', params.user); + if (params.search) httpParams = httpParams.set('search', params.search); + if (params.dateFrom) httpParams = httpParams.set('dateFrom', params.dateFrom); + if (params.dateTo) httpParams = httpParams.set('dateTo', params.dateTo); + + httpParams = httpParams.set('page', String(params.page || 1)); + httpParams = httpParams.set('pageSize', String(params.pageSize || 10)); + + return this.http.get>(`${this.baseApi}/historico/linhas`, { params: httpParams }); + } } diff --git a/src/app/services/users.service.ts b/src/app/services/users.service.ts index 115f3ce..4d7bb8b 100644 --- a/src/app/services/users.service.ts +++ b/src/app/services/users.service.ts @@ -4,7 +4,7 @@ import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; -export type UserPermission = 'sysadmin' | 'gestor' | 'cliente'; +export type UserPermission = 'sysadmin' | 'gestor' | 'financeiro' | 'cliente'; export type UserDto = { id: string; From 43bf61112230a37f6b2ec727aa1a3d7f0d044045 Mon Sep 17 00:00:00 2001 From: Eduardo Lopes Date: Mon, 9 Mar 2026 14:44:22 -0300 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20novas=20implementa=C3=A7=C3=B5es,?= =?UTF-8?q?=20ajustes=20p=C3=A1ginas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- angular.json | 4 +- src/app/app.routes.ts | 2 + src/app/app.ts | 1 + src/app/components/header/header.html | 37 +- src/app/components/header/header.scss | 60 +- src/app/components/header/header.ts | 3 + .../geral-modals/geral-modals.html | 11 +- src/app/pages/geral/geral.scss | 418 +++++++++++ src/app/pages/geral/geral.ts | 703 +++++++++++++++++- .../pages/mve-auditoria/mve-auditoria.html | 226 ++++++ .../pages/mve-auditoria/mve-auditoria.scss | 660 ++++++++++++++++ src/app/pages/mve-auditoria/mve-auditoria.ts | 358 +++++++++ src/app/pages/notificacoes/notificacoes.html | 2 +- src/app/pages/notificacoes/notificacoes.scss | 7 + .../pages/parcelamentos/parcelamentos.html | 2 +- .../pages/parcelamentos/parcelamentos.scss | 13 + src/app/pages/resumo/resumo.html | 4 +- src/app/pages/resumo/resumo.scss | 13 + src/app/services/auth.service.ts | 9 +- src/app/services/mve-audit.service.ts | 189 +++++ 20 files changed, 2655 insertions(+), 67 deletions(-) create mode 100644 src/app/pages/mve-auditoria/mve-auditoria.html create mode 100644 src/app/pages/mve-auditoria/mve-auditoria.scss create mode 100644 src/app/pages/mve-auditoria/mve-auditoria.ts create mode 100644 src/app/services/mve-audit.service.ts diff --git a/angular.json b/angular.json index 6e8f7df..8e66459 100644 --- a/angular.json +++ b/angular.json @@ -51,8 +51,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "20kB", - "maximumError": "40kB" + "maximumWarning": "35kB", + "maximumError": "60kB" } ], "outputHashing": "all" diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 6bb72d7..f0b8b83 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -24,6 +24,7 @@ import { HistoricoLinhas } from './pages/historico-linhas/historico-linhas'; import { Perfil } from './pages/perfil/perfil'; import { SystemProvisionUserPage } from './pages/system-provision-user/system-provision-user'; import { SolicitacoesLinhas } from './pages/solicitacoes-linhas/solicitacoes-linhas'; +import { MveAuditoriaPage } from './pages/mve-auditoria/mve-auditoria'; export const routes: Routes = [ { path: '', component: Home }, @@ -43,6 +44,7 @@ export const routes: Routes = [ { path: 'historico', component: Historico, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico' }, { path: 'historico-linhas', component: HistoricoLinhas, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico de Linhas' }, { path: 'solicitacoes', component: SolicitacoesLinhas, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Solicitações' }, + { path: 'auditoria-mve', component: MveAuditoriaPage, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Auditoria MVE' }, { path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' }, { path: 'system/fornecer-usuario', diff --git a/src/app/app.ts b/src/app/app.ts index 5a733b1..da02a75 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -29,6 +29,7 @@ export class AppComponent { // ✅ rotas internas (LOGADO) que devem esconder footer private readonly loggedPrefixes = [ '/geral', + '/auditoria-mve', '/solicitacoes-linhas', '/mureg', '/faturamento', diff --git a/src/app/components/header/header.html b/src/app/components/header/header.html index 8df4721..00114a2 100644 --- a/src/app/components/header/header.html +++ b/src/app/components/header/header.html @@ -216,22 +216,24 @@
- - Line Gestão -
-
Line
-
Gestão
-
-
- - -
+
Dashboard @@ -536,6 +538,9 @@ Geral + + Auditoria MVE + Mureg diff --git a/src/app/components/header/header.scss b/src/app/components/header/header.scss index 3ad00d1..7bf2839 100644 --- a/src/app/components/header/header.scss +++ b/src/app/components/header/header.scss @@ -196,6 +196,27 @@ $logo-secondary-grey: #757575; display: inline-flex; align-items: center; gap: 6px; color: $text-main; text-decoration: none; font-weight: 600; font-size: 14px; transition: color 0.2s; &:hover { color: $primary; } } +.public-header-layout { + display: flex; + align-items: center; + gap: 24px; + width: 100%; + min-width: 0; +} + +.public-header-layout > .logo-area { + min-width: 0; +} + +.public-header-layout > .nav-links { + flex: 1 1 auto; +} + +.public-header-actions { + margin-left: auto; + flex: 0 0 auto; +} + .header-actions { display: flex; align-items: center; margin-left: auto; justify-content: flex-end; flex: 0 0 auto; } .btn-login-header { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 99px; @@ -919,7 +940,17 @@ $logo-secondary-grey: #757575; .side-wordmark__movel { display: none; } -.side-menu-body { padding: 16px; display: flex; flex-direction: column; gap: 4px; } +.side-menu-body { + flex: 1 1 auto; + min-height: 0; + padding: 16px; + display: flex; + flex-direction: column; + gap: 4px; + overflow-y: auto; + overflow-x: hidden; + overscroll-behavior: contain; +} .side-item { padding: 10px 12px; border-radius: 8px; color: $text-main; text-decoration: none; font-size: 14px; font-weight: 500; display: flex; align-items: center; gap: 10px; &:hover { background: $bg-light; } @@ -1072,28 +1103,35 @@ $logo-secondary-grey: #757575; --scale: 0.21; } + .public-header-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + } + /* Header público (Home/Login/Register): mantém logo visível e CTA fixo à direita */ - .header-inner > .logo-area { + .public-header-layout > .logo-area { flex: 1 1 auto; min-width: 0; } - .header-inner > .logo-area .lg-wordmark { + .public-header-layout > .logo-area .lg-wordmark { display: block; white-space: nowrap; } - .header-inner > .logo-area .lg-wordmark { + .public-header-layout > .logo-area .lg-wordmark { --scale: 0.19; } - .header-inner > .header-actions { - margin-left: auto; + .public-header-layout > .header-actions { + margin-left: 0; flex: 0 0 auto; justify-content: flex-end; } - .header-inner > .header-actions .btn-login-header { + .public-header-layout > .header-actions .btn-login-header { padding: 7px 10px; gap: 4px; font-size: 12px; @@ -1404,20 +1442,20 @@ $logo-secondary-grey: #757575; } @media (max-width: 420px) { - .header-inner > .logo-area { + .public-header-layout > .logo-area { gap: 5px; } - .header-inner > .logo-area .logo-symbol { + .public-header-layout > .logo-area .logo-symbol { width: 32px; height: 32px; } - .header-inner > .logo-area .lg-wordmark { + .public-header-layout > .logo-area .lg-wordmark { --scale: 0.18; } - .header-inner > .header-actions .btn-login-header { + .public-header-layout > .header-actions .btn-login-header { padding: 6px 8px; font-size: 11px; gap: 3px; diff --git a/src/app/components/header/header.ts b/src/app/components/header/header.ts index d18f6fe..b8f0eda 100644 --- a/src/app/components/header/header.ts +++ b/src/app/components/header/header.ts @@ -39,6 +39,7 @@ export class Header implements AfterViewInit, OnDestroy { isFinanceiro = false; canViewAll = false; canViewFinancialPages = false; + canViewMveAudit = false; clientTenantDisplayName = ''; private clientTenantNameTenantId: string | null = null; private readonly baseApi: string; @@ -100,6 +101,7 @@ export class Header implements AfterViewInit, OnDestroy { '/historico', '/historico-linhas', '/solicitacoes', + '/auditoria-mve', '/perfil', '/system', ]; @@ -235,6 +237,7 @@ export class Header implements AfterViewInit, OnDestroy { this.isFinanceiro = isFinanceiro; this.canViewAll = isSysAdmin || isGestor || isFinanceiro; this.canViewFinancialPages = isSysAdmin || isFinanceiro; + this.canViewMveAudit = isSysAdmin || isGestor; if (!this.isClientHeader) { this.clientTenantDisplayName = ''; diff --git a/src/app/components/page-modals/geral-modals/geral-modals.html b/src/app/components/page-modals/geral-modals/geral-modals.html index afbae88..8ff7fb8 100644 --- a/src/app/components/page-modals/geral-modals/geral-modals.html +++ b/src/app/components/page-modals/geral-modals/geral-modals.html @@ -125,6 +125,7 @@ [disabled]="!createModel.contaEmpresa" [placeholder]="createModel.contaEmpresa ? 'Selecione a conta' : 'Selecione a empresa primeiro'" [(ngModel)]="createModel.conta" + (ngModelChange)="onContaChange(false)" >
@@ -736,7 +737,7 @@ [disabled]="!activeLine.contaEmpresa" [placeholder]="activeLine.contaEmpresa ? 'Selecione a conta' : 'Selecione a empresa primeiro'" [(ngModel)]="activeLine.conta" - (ngModelChange)="onBatchLineDetailsChange()" + (ngModelChange)="onBatchContaChange(activeLine)" >
@@ -1389,6 +1390,10 @@ Plano Contratado {{ detailData.planoContrato || '-' }} +
+ Empresa (Conta) + {{ detailData.contaEmpresa || '-' }} +
Conta {{ detailData.conta || '-' }} @@ -1616,8 +1621,8 @@
-
-
+
+
diff --git a/src/app/pages/geral/geral.scss b/src/app/pages/geral/geral.scss index 38116a6..88e3e57 100644 --- a/src/app/pages/geral/geral.scss +++ b/src/app/pages/geral/geral.scss @@ -866,3 +866,421 @@ div.box-body { padding: 16px; &.compact { padding: 12px 16px; } &.compact-paddin .summary-pill { font-size: 0.72rem; } .batch-validation-banner { align-items: flex-start; } } + +.modal-mve-audit { + width: min(1320px, calc(100vw - 40px)); + max-width: min(1320px, calc(100vw - 40px)); + max-height: calc(100vh - 48px); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.mve-audit-body { + display: grid; + gap: 16px; + overflow: auto; + position: relative; +} + +.mve-intro-card, +.mve-upload-card { + background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(247,249,252,0.98)); + border: 1px solid rgba(17,18,20,0.08); + border-radius: 18px; + padding: 18px; + box-shadow: 0 16px 32px rgba(17,18,20,0.05); +} + +.mve-intro-card { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: flex-start; +} + +.mve-intro-title { + font-size: 1rem; + font-weight: 900; + color: var(--text); +} + +.mve-intro-text { + margin-top: 6px; + color: rgba(17,18,20,0.68); + max-width: 760px; + line-height: 1.45; +} + +.mve-intro-meta, +.mve-summary-notes { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.meta-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 12px; + border-radius: 999px; + background: rgba(3,15,170,0.06); + color: rgba(17,18,20,0.76); + font-size: 0.76rem; + font-weight: 800; + border: 1px solid rgba(3,15,170,0.08); + + &.accent { + background: rgba(12,132,78,0.08); + border-color: rgba(12,132,78,0.16); + color: #0c6c43; + } +} + +.mve-section-title { + font-size: 0.94rem; + font-weight: 900; + color: var(--text); + margin-bottom: 12px; +} + +.mve-upload-zone { + display: grid; + gap: 8px; + justify-items: center; + text-align: center; + border: 1.5px dashed rgba(3,15,170,0.22); + border-radius: 18px; + padding: 28px 18px; + background: + radial-gradient(circle at top right, rgba(3,15,170,0.08), transparent 36%), + linear-gradient(180deg, rgba(255,255,255,0.96), rgba(243,247,255,0.92)); + cursor: pointer; + transition: border-color 0.18s ease, transform 0.18s ease; + + &:hover { + border-color: rgba(3,15,170,0.38); + transform: translateY(-1px); + } + + input { + display: none; + } +} + +.upload-icon { + width: 54px; + height: 54px; + border-radius: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(3,15,170,0.08); + color: var(--blue); + font-size: 1.4rem; +} + +.upload-title { + font-size: 0.96rem; + font-weight: 900; + color: var(--text); +} + +.upload-subtitle, +.processing-text, +.mve-upload-meta { + color: rgba(17,18,20,0.62); + font-size: 0.82rem; +} + +.mve-upload-actions, +.mve-actions-row, +.confirm-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; +} + +.mve-upload-actions { + margin-top: 14px; +} + +.mve-processing { + display: grid; + gap: 10px; + margin-top: 14px; +} + +.progress-track { + width: 100%; + height: 8px; + border-radius: 999px; + background: rgba(17,18,20,0.08); + overflow: hidden; +} + +.progress-indeterminate { + display: block; + width: 34%; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, #030faa, #2b55e3); + animation: mve-progress 1.25s linear infinite; +} + +@keyframes mve-progress { + from { transform: translateX(-120%); } + to { transform: translateX(320%); } +} + +.mve-summary-grid { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 12px; +} + +.mve-summary-card { + background: #fff; + border: 1px solid rgba(17,18,20,0.07); + border-radius: 18px; + padding: 16px 14px; + display: grid; + gap: 6px; + box-shadow: 0 12px 24px rgba(17,18,20,0.05); + + strong { + font-size: 1.36rem; + color: var(--text); + } + + &.is-positive strong { color: #198754; } + &.is-danger strong { color: #b42318; } + &.is-warning strong { color: #b54708; } +} + +.summary-label { + font-size: 0.76rem; + letter-spacing: 0.03em; + text-transform: uppercase; + color: rgba(17,18,20,0.58); + font-weight: 900; +} + +.mve-result-toolbar { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 14px; + align-items: center; +} + +.mve-filter-tabs { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.mve-toolbar-right { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; +} + +.mve-search-group { + min-width: min(420px, 100%); +} + +.mve-sync-banner { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: center; + padding: 14px 16px; + border-radius: 16px; + background: rgba(12,132,78,0.08); + border: 1px solid rgba(12,132,78,0.16); + color: #0c6c43; + font-weight: 700; +} + +.mve-empty { + display: grid; + place-items: center; + gap: 8px; + min-height: 180px; + border-radius: 18px; + border: 1px dashed rgba(17,18,20,0.14); + background: rgba(255,255,255,0.72); + color: rgba(17,18,20,0.62); + + i { + font-size: 1.5rem; + color: #198754; + } +} + +.mve-table-wrap { + max-height: min(48vh, 560px); + border-radius: 18px; + background: #fff; +} + +.mve-table { + min-width: 980px; + + thead th { + position: sticky; + top: 0; + z-index: 1; + background: rgba(248,249,250,0.97); + font-size: 0.73rem; + text-transform: uppercase; + letter-spacing: 0.04em; + color: rgba(17,18,20,0.64); + font-weight: 900; + } +} + +.mve-status-pair, +.mve-cell-stack { + display: flex; + flex-direction: column; + gap: 6px; +} + +.mve-status-pair { + .bi-arrow-right { + font-size: 0.8rem; + } +} + +.mve-issue-tag, +.mve-severity { + display: inline-flex; + align-items: center; + align-self: flex-start; + padding: 6px 10px; + border-radius: 999px; + font-size: 0.74rem; + font-weight: 900; + border: 1px solid transparent; + + &.status { background: rgba(180,35,24,0.09); color: #b42318; border-color: rgba(180,35,24,0.16); } + &.data { background: rgba(181,71,8,0.09); color: #b54708; border-color: rgba(181,71,8,0.16); } + &.system { background: rgba(3,15,170,0.08); color: var(--blue); border-color: rgba(3,15,170,0.15); } + &.report { background: rgba(10,91,168,0.08); color: #0a5ba8; border-color: rgba(10,91,168,0.15); } + &.duplicate { background: rgba(113,46,170,0.08); color: #712eaa; border-color: rgba(113,46,170,0.14); } + &.warning { background: rgba(245,158,11,0.12); color: #9a6700; border-color: rgba(245,158,11,0.16); } + &.neutral { background: rgba(17,18,20,0.06); color: rgba(17,18,20,0.72); border-color: rgba(17,18,20,0.1); } +} + +.mve-severity { + &.critical { background: rgba(180,35,24,0.08); color: #b42318; border-color: rgba(180,35,24,0.15); } + &.medium { background: rgba(181,71,8,0.08); color: #b54708; border-color: rgba(181,71,8,0.14); } + &.warning { background: rgba(245,158,11,0.12); color: #9a6700; border-color: rgba(245,158,11,0.18); } + &.neutral { background: rgba(17,18,20,0.06); color: rgba(17,18,20,0.65); border-color: rgba(17,18,20,0.1); } +} + +.mve-differences-text { + color: rgba(17,18,20,0.78); + line-height: 1.38; + min-width: 260px; +} + +.mve-footer { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: center; +} + +.mve-actions-row { + justify-content: flex-end; +} + +.mve-confirm-overlay { + position: sticky; + bottom: 0; + display: flex; + justify-content: flex-end; + padding-top: 8px; +} + +.mve-confirm-card { + width: min(420px, 100%); + background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(245,248,255,0.98)); + border: 1px solid rgba(3,15,170,0.12); + border-radius: 18px; + padding: 18px; + box-shadow: 0 18px 36px rgba(17,18,20,0.12); + display: grid; + gap: 10px; +} + +.confirm-title { + font-size: 1rem; + font-weight: 900; + color: var(--text); +} + +.confirm-text, +.confirm-footnote { + color: rgba(17,18,20,0.68); + line-height: 1.4; +} + +.confirm-summary { + display: grid; + gap: 6px; + padding: 12px 14px; + border-radius: 14px; + background: rgba(3,15,170,0.05); + border: 1px solid rgba(3,15,170,0.08); + font-weight: 700; +} + +@media (max-width: 1200px) { + .mve-summary-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} + +@media (max-width: 768px) { + .modal-mve-audit { + width: calc(100vw - 20px); + max-width: calc(100vw - 20px); + max-height: calc(100vh - 20px); + } + + .mve-intro-card, + .mve-result-toolbar, + .mve-footer, + .mve-sync-banner { + flex-direction: column; + align-items: stretch; + } + + .mve-summary-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .mve-toolbar-right, + .mve-actions-row, + .confirm-actions { + width: 100%; + } + + .mve-toolbar-right > *, + .mve-actions-row .btn, + .confirm-actions .btn, + .mve-upload-actions .btn { + flex: 1 1 100%; + } + + .mve-search-group { + min-width: 100%; + } +} diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts index 0bd8044..069e76e 100644 --- a/src/app/pages/geral/geral.ts +++ b/src/app/pages/geral/geral.ts @@ -25,12 +25,19 @@ import { AuthService } from '../../services/auth.service'; import { TenantSyncService } from '../../services/tenant-sync.service'; import { TableExportService } from '../../services/table-export.service'; import { SolicitacoesLinhasService } from '../../services/solicitacoes-linhas.service'; +import { + MveAuditService, + type ApplyMveAuditResult, + type MveAuditIssue, + type MveAuditRun, +} from '../../services/mve-audit.service'; import { firstValueFrom, Subscription, filter } from 'rxjs'; import { environment } from '../../../environments/environment'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; import { buildPageNumbers, clampPage, + computePageEnd, computePageStart, computeTotalPages } from '../../utils/pagination.util'; @@ -98,6 +105,14 @@ interface ApiLineList { vivoGestaoDispositivo?: number | null; } +interface SmartSearchTargetResolution { + client: string; + skilFilter: 'ALL' | 'PF' | 'PJ' | 'RESERVA'; + statusFilter: 'ALL' | 'BLOCKED'; + blockedStatusMode: BlockedStatusMode; + requiresFilterAdjustment: boolean; +} + interface ApiLineDetail { id: string; item: number; @@ -306,6 +321,25 @@ interface BatchLineStatusUpdateResultDto { items: BatchLineStatusUpdateItemResultDto[]; } +type MveAuditFilterMode = + | 'ALL' + | 'STATUS' + | 'DATA' + | 'ONLY_IN_SYSTEM' + | 'ONLY_IN_REPORT' + | 'DUPLICATES' + | 'INVALID' + | 'UNKNOWN'; + +type MveAuditApplyMode = 'ALL_SYNCABLE' | 'FILTERED_SYNCABLE'; + +interface MveApplySelectionSummary { + totalIssues: number; + totalStatusIssues: number; + totalDataIssues: number; + totalAffectedLines: number; +} + @Component({ standalone: true, @@ -337,7 +371,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { private route: ActivatedRoute, private tenantSyncService: TenantSyncService, private solicitacoesLinhasService: SolicitacoesLinhasService, - private tableExportService: TableExportService + private tableExportService: TableExportService, + private mveAuditService: MveAuditService ) {} private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'lines'); @@ -429,6 +464,20 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { batchStatusType = ''; batchStatusUsuario = ''; batchStatusLastResult: BatchLineStatusUpdateResultDto | null = null; + mveAuditOpen = false; + mveAuditProcessing = false; + mveAuditApplying = false; + mveAuditFile: File | null = null; + mveAuditResult: MveAuditRun | null = null; + mveAuditError = ''; + mveAuditFilter: MveAuditFilterMode = 'ALL'; + mveAuditSearchTerm = ''; + mveAuditPage = 1; + mveAuditPageSize = 10; + mveAuditPageSizeOptions = [10, 25, 50, 100]; + mveAuditApplyConfirmOpen = false; + mveAuditApplyMode: MveAuditApplyMode = 'ALL_SYNCABLE'; + mveAuditApplyLastResult: ApplyMveAuditResult | null = null; detailData: any = null; financeData: any = null; @@ -755,6 +804,112 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { .map((key) => this.additionalServiceOptions.find((x) => x.key === key)?.label ?? key); } + get hasMveAuditResult(): boolean { + return !!this.mveAuditResult; + } + + get filteredMveAuditIssues(): MveAuditIssue[] { + const source = this.mveAuditResult?.issues ?? []; + const search = this.normalizeMveSearchTerm(this.mveAuditSearchTerm); + + return source.filter((issue) => { + if (!this.matchesMveIssueFilter(issue)) { + return false; + } + + if (!search) { + return true; + } + + const haystack = [ + issue.numeroLinha, + issue.issueType, + issue.situation, + issue.systemStatus, + issue.reportStatus, + issue.systemPlan, + issue.reportPlan, + issue.actionSuggestion, + issue.notes, + ...(issue.differences ?? []).flatMap((diff) => [diff.label, diff.systemValue, diff.reportValue]), + ] + .map((value) => this.normalizeMveSearchTerm(value)) + .join(' '); + + return haystack.includes(search); + }); + } + + get mveAuditTotalPages(): number { + return computeTotalPages(this.filteredMveAuditIssues.length, this.mveAuditPageSize); + } + + get mveAuditPageNumbers(): number[] { + return buildPageNumbers(this.mveAuditPage, this.mveAuditTotalPages); + } + + get pagedMveAuditIssues(): MveAuditIssue[] { + const start = computePageStart(this.filteredMveAuditIssues.length, this.mveAuditPage, this.mveAuditPageSize); + if (start <= 0) { + return this.filteredMveAuditIssues.slice(0, this.mveAuditPageSize); + } + + const offset = start - 1; + return this.filteredMveAuditIssues.slice(offset, offset + this.mveAuditPageSize); + } + + get mveAuditPageStart(): number { + return computePageStart(this.filteredMveAuditIssues.length, this.mveAuditPage, this.mveAuditPageSize); + } + + get mveAuditPageEnd(): number { + return computePageEnd(this.filteredMveAuditIssues.length, this.mveAuditPage, this.mveAuditPageSize); + } + + get allSyncableMveIssues(): MveAuditIssue[] { + return (this.mveAuditResult?.issues ?? []).filter((issue) => issue.syncable && !issue.applied); + } + + get filteredSyncableMveIssues(): MveAuditIssue[] { + return this.filteredMveAuditIssues.filter((issue) => issue.syncable && !issue.applied); + } + + get selectedMveApplyIssues(): MveAuditIssue[] { + return this.mveAuditApplyMode === 'FILTERED_SYNCABLE' + ? this.filteredSyncableMveIssues + : this.allSyncableMveIssues; + } + + get mveApplySelectionSummary(): MveApplySelectionSummary { + const selected = this.selectedMveApplyIssues; + const affectedLines = new Set(); + let totalStatusIssues = 0; + let totalDataIssues = 0; + + for (const issue of selected) { + if (issue.mobileLineId) affectedLines.add(issue.mobileLineId); + else if (issue.numeroLinha) affectedLines.add(issue.numeroLinha); + + if (this.issueHasStatusDifference(issue)) totalStatusIssues++; + if (this.issueHasDataDifference(issue)) totalDataIssues++; + } + + return { + totalIssues: selected.length, + totalStatusIssues, + totalDataIssues, + totalAffectedLines: affectedLines.size, + }; + } + + get canSubmitMveAudit(): boolean { + return !!this.mveAuditFile && !this.mveAuditProcessing && !this.mveAuditApplying; + } + + get canOpenMveApplyConfirm(): boolean { + return !this.mveAuditApplying && this.mveApplySelectionSummary.totalIssues > 0; + } + // ✅ fecha dropdown ao clicar fora @HostListener('document:click', ['$event']) onDocumentClick(ev: MouseEvent) { @@ -1048,6 +1203,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.syncContaEmpresaSelection(this.createModel); this.syncContaEmpresaSelection(this.editModel); + this.syncContaEmpresaSelection(this.detailData); + this.syncContaEmpresaSelection(this.financeData); this.cdr.detectChanges(); }, error: () => { @@ -1056,6 +1213,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.syncContaEmpresaSelection(this.createModel); this.syncContaEmpresaSelection(this.editModel); + this.syncContaEmpresaSelection(this.detailData); + this.syncContaEmpresaSelection(this.financeData); } }); } @@ -1071,7 +1230,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.createOpen || this.reservaTransferOpen || this.moveToReservaOpen || - this.batchStatusOpen + this.batchStatusOpen || + this.mveAuditOpen ); } @@ -1101,6 +1261,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.reservaTransferOpen = false; this.moveToReservaOpen = false; this.batchStatusOpen = false; + this.mveAuditOpen = false; + this.mveAuditApplyConfirmOpen = false; this.detailData = null; this.financeData = null; @@ -1125,6 +1287,16 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.moveToReservaLastResult = null; this.batchStatusLastResult = null; this.batchStatusUsuario = ''; + this.mveAuditFile = null; + this.mveAuditProcessing = false; + this.mveAuditApplying = false; + this.mveAuditResult = null; + this.mveAuditError = ''; + this.mveAuditFilter = 'ALL'; + this.mveAuditSearchTerm = ''; + this.mveAuditPage = 1; + this.mveAuditApplyMode = 'ALL_SYNCABLE'; + this.mveAuditApplyLastResult = null; // Limpa overlays/locks residuais this.cleanupModalArtifacts(); @@ -1158,7 +1330,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { const t = (term ?? '').trim(); if (!t) return false; - const digits = t.replace(/\D/g, ''); + const digits = this.normalizeDigits(t); if (!digits) return false; if (digits.length >= 17) return true; // ICCID @@ -1167,31 +1339,143 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return false; } - private resolveSearchToClient(term: string): Promise { + private normalizeDigits(value: unknown): string { + return String(value ?? '').replace(/\D/g, ''); + } + + private resolveSkilFilterFromLine(skil: unknown): 'ALL' | 'PF' | 'PJ' | 'RESERVA' { + const parsed = this.parseQuerySkilFilter(skil); + return parsed ?? 'ALL'; + } + + private findBestSpecificSearchMatch(items: ApiLineList[], term: string): ApiLineList | null { + if (!Array.isArray(items) || items.length === 0) return null; + + const digits = this.normalizeDigits(term); + if (!digits) return null; + + const isIccidSearch = digits.length >= 17; + const exactMatches = items.filter((item) => { + const lineDigits = this.normalizeDigits(item?.linha); + const chipDigits = this.normalizeDigits(item?.chip); + return lineDigits === digits || chipDigits === digits; + }); + if (exactMatches.length > 0) return exactMatches[0]; + + const compatibleMatches = items.filter((item) => { + const lineDigits = this.normalizeDigits(item?.linha); + const chipDigits = this.normalizeDigits(item?.chip); + + if (isIccidSearch) { + return !!chipDigits && ( + chipDigits.endsWith(digits) || + digits.endsWith(chipDigits) || + chipDigits.includes(digits) + ); + } + + return !!lineDigits && ( + lineDigits.endsWith(digits) || + digits.endsWith(lineDigits) || + lineDigits.includes(digits) + ); + }); + + return compatibleMatches[0] ?? items[0] ?? null; + } + + private async findSpecificSearchMatch( + term: string, + options?: { + ignoreCurrentFilters?: boolean; + skilFilter?: 'ALL' | 'PF' | 'PJ' | 'RESERVA'; + } + ): Promise { const s = (term ?? '').trim(); - if (!s) return Promise.resolve(null); + if (!s) return null; - const pageSize = this.hasClientSideFiltersApplied ? '500' : '1'; - let params = new HttpParams().set('page', '1').set('pageSize', pageSize).set('search', s); - params = this.applyBaseFilters(params); + let params = new HttpParams() + .set('page', '1') + .set('pageSize', options?.ignoreCurrentFilters ? '200' : '500') + .set('search', s); - if (this.selectedClients.length > 0) { + if (!options?.ignoreCurrentFilters) { + params = this.applyBaseFilters(params); this.selectedClients.forEach((c) => (params = params.append('client', c))); + } else if (options?.skilFilter === 'PF') { + params = params.set('skil', 'PESSOA FÍSICA'); + } else if (options?.skilFilter === 'PJ') { + params = params.set('skil', 'PESSOA JURÍDICA'); + } else if (options?.skilFilter === 'RESERVA') { + params = params.set('skil', 'RESERVA'); } - return new Promise((resolve) => { - this.http.get>(this.apiBase, { params: this.withNoCache(params) }).subscribe({ - next: (res) => { - const source = this.hasClientSideFiltersApplied - ? this.applyAdditionalFiltersClientSide(res.items ?? []) - : (res.items ?? []); - const first = source[0]; - const client = (first?.cliente ?? '').trim(); - resolve(client || null); - }, - error: () => resolve(null) - }); + try { + const response = await firstValueFrom( + this.http.get>(this.apiBase, { params: this.withNoCache(params) }) + ); + + let items = response?.items ?? []; + if (!options?.ignoreCurrentFilters && this.hasClientSideFiltersApplied) { + items = this.applyAdditionalFiltersClientSide(items); + } + + return this.findBestSpecificSearchMatch(items, s); + } catch { + return null; + } + } + + private buildSmartSearchTarget( + line: ApiLineList, + requiresFilterAdjustment: boolean + ): SmartSearchTargetResolution | null { + if (!line) return null; + + const skilFilter = this.resolveSkilFilterFromLine(line?.skil); + const blockedStatusMode = this.resolveBlockedStatusMode(line?.status ?? '') ?? 'ALL'; + const client = ((line?.cliente ?? '').toString().trim()) || (skilFilter === 'RESERVA' ? 'RESERVA' : 'SEM CLIENTE'); + + return { + client, + skilFilter, + statusFilter: blockedStatusMode === 'ALL' ? 'ALL' : 'BLOCKED', + blockedStatusMode, + requiresFilterAdjustment + }; + } + + private async resolveSmartSearchTarget(term: string): Promise { + const currentContextMatch = await this.findSpecificSearchMatch(term); + if (currentContextMatch) { + return this.buildSmartSearchTarget(currentContextMatch, false); + } + + const globalMatch = await this.findSpecificSearchMatch(term, { ignoreCurrentFilters: true }); + if (globalMatch) { + return this.buildSmartSearchTarget(globalMatch, true); + } + + const reservaMatch = await this.findSpecificSearchMatch(term, { + ignoreCurrentFilters: true, + skilFilter: 'RESERVA' }); + if (reservaMatch) { + return this.buildSmartSearchTarget(reservaMatch, true); + } + + return null; + } + + private applySmartSearchFilters(target: SmartSearchTargetResolution): void { + this.filterSkil = target.skilFilter; + this.filterStatus = target.statusFilter; + this.blockedStatusMode = target.statusFilter === 'BLOCKED' ? target.blockedStatusMode : 'ALL'; + + this.selectedClients = []; + this.clientSearchTerm = ''; + this.additionalMode = 'ALL'; + this.selectedAdditionalServices = []; } onSearch() { @@ -1215,19 +1499,26 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } if (this.isSpecificSearchTerm(term)) { - const client = await this.resolveSearchToClient(term); + const target = await this.resolveSmartSearchTarget(term); if (requestVersion !== this.searchRequestVersion) return; - if (client) { - this.searchResolvedClient = client; + if (target) { + if (target.requiresFilterAdjustment) { + this.applySmartSearchFilters(target); + if (!this.isClientRestricted) { + this.loadClients(); + } + } + + this.searchResolvedClient = target.client; this.loadKpis(); - await this.loadOnlyThisClientGroup(client); + await this.loadOnlyThisClientGroup(target.client); if (requestVersion !== this.searchRequestVersion) return; - this.expandedGroup = client; - this.fetchGroupLines(client, term); + this.expandedGroup = target.client; + this.fetchGroupLines(target.client, term); return; } } @@ -2449,6 +2740,233 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { }); } + async openMveAuditModal() { + if (!this.canManageLines) { + await this.showToast('Você não tem permissão para auditar linhas com o relatório MVE.'); + return; + } + + this.mveAuditOpen = true; + this.mveAuditFile = null; + this.mveAuditResult = null; + this.mveAuditError = ''; + this.mveAuditFilter = 'ALL'; + this.mveAuditSearchTerm = ''; + this.mveAuditPage = 1; + this.mveAuditApplyMode = 'ALL_SYNCABLE'; + this.mveAuditApplyConfirmOpen = false; + this.mveAuditApplyLastResult = null; + this.cdr.detectChanges(); + } + + onMveAuditFileSelected(event: Event) { + const file = (event.target as HTMLInputElement | null)?.files?.[0] ?? null; + this.mveAuditError = ''; + this.mveAuditApplyLastResult = null; + + if (!file) { + this.mveAuditFile = null; + return; + } + + if (!file.name.toLowerCase().endsWith('.csv')) { + this.mveAuditFile = null; + this.mveAuditError = 'Selecione um arquivo CSV exportado do MVE da Vivo.'; + return; + } + + if (file.size <= 0) { + this.mveAuditFile = null; + this.mveAuditError = 'O arquivo selecionado está vazio.'; + return; + } + + if (file.size > 20 * 1024 * 1024) { + this.mveAuditFile = null; + this.mveAuditError = 'O arquivo do MVE excede o limite de 20 MB.'; + return; + } + + this.mveAuditFile = file; + } + + clearMveAuditFile() { + this.mveAuditFile = null; + this.mveAuditError = ''; + } + + async submitMveAudit() { + if (!this.canSubmitMveAudit || !this.mveAuditFile) { + return; + } + + this.mveAuditProcessing = true; + this.mveAuditError = ''; + this.mveAuditResult = null; + this.mveAuditApplyLastResult = null; + + try { + this.mveAuditResult = await firstValueFrom(this.mveAuditService.preview(this.mveAuditFile)); + this.mveAuditFilter = 'ALL'; + this.mveAuditSearchTerm = ''; + this.mveAuditPage = 1; + await this.showToast('Auditoria MVE processada com sucesso.'); + } catch (err) { + this.mveAuditError = this.extractHttpMessage(err, 'Não foi possível processar o relatório MVE.'); + } finally { + this.mveAuditProcessing = false; + this.cdr.detectChanges(); + } + } + + setMveAuditFilter(filter: MveAuditFilterMode) { + this.mveAuditFilter = filter; + this.mveAuditPage = 1; + } + + onMveAuditSearchChange() { + this.mveAuditPage = 1; + } + + onMveAuditPageSizeChange() { + this.mveAuditPage = 1; + } + + goToMveAuditPage(page: number) { + this.mveAuditPage = clampPage(page, this.mveAuditTotalPages); + } + + openMveApplyConfirm(mode: MveAuditApplyMode) { + if (!this.hasMveAuditResult) return; + this.mveAuditApplyMode = mode; + if (!this.canOpenMveApplyConfirm) return; + this.mveAuditApplyConfirmOpen = true; + } + + closeMveApplyConfirm() { + this.mveAuditApplyConfirmOpen = false; + } + + async confirmMveApply() { + if (!this.mveAuditResult || !this.canOpenMveApplyConfirm) { + return; + } + + const selectedIssues = this.selectedMveApplyIssues; + this.mveAuditApplying = true; + + try { + const result = await firstValueFrom( + this.mveAuditService.apply( + this.mveAuditResult.id, + this.mveAuditApplyMode === 'FILTERED_SYNCABLE' ? selectedIssues.map((issue) => issue.id) : undefined + ) + ); + + this.mveAuditApplyLastResult = result; + this.mveAuditResult = await firstValueFrom(this.mveAuditService.getById(this.mveAuditResult.id)); + this.mveAuditApplyConfirmOpen = false; + + const label = + result.updatedLines > 0 + ? `${result.updatedLines} linha(s) atualizada(s) com base no MVE.` + : 'Nenhuma linha precisou ser alterada com a sincronização MVE.'; + await this.showToast(label); + this.refreshData({ keepCurrentPage: true }); + } catch (err) { + await this.showToast(this.extractHttpMessage(err, 'Não foi possível aplicar a sincronização MVE.')); + } finally { + this.mveAuditApplying = false; + this.cdr.detectChanges(); + } + } + + async exportMveAuditIssues() { + if (!this.hasMveAuditResult || this.filteredMveAuditIssues.length === 0) { + await this.showToast('Não há inconsistências filtradas para exportar.'); + return; + } + + const headers = [ + 'Numero da linha', + 'Item sistema', + 'Situacao', + 'Tipo', + 'Status sistema', + 'Status relatorio', + 'Plano sistema', + 'Plano relatorio', + 'Diferencas', + 'Acao sugerida', + 'Observacoes', + ]; + + const rows = this.filteredMveAuditIssues.map((issue) => + [ + issue.numeroLinha || '', + issue.systemItem != null ? String(issue.systemItem) : '', + issue.situation || '', + issue.issueType || '', + issue.systemStatus || '', + issue.reportStatus || '', + issue.systemPlan || '', + issue.reportPlan || '', + this.describeMveDifferences(issue), + issue.actionSuggestion || '', + issue.notes || '', + ] + .map((value) => this.escapeCsvValue(value)) + .join(';') + ); + + const content = `${headers.join(';')}\n${rows.join('\n')}`; + const blob = new Blob([`\uFEFF${content}`], { type: 'text/csv;charset=utf-8;' }); + const timestamp = this.tableExportService.buildTimestamp(); + this.downloadBlob(blob, `mve_auditoria_${timestamp}.csv`); + await this.showToast(`CSV exportado com ${rows.length} inconsistência(s).`); + } + + describeMveDifferences(issue: MveAuditIssue): string { + const differences = issue.differences ?? []; + if (!differences.length) { + return issue.notes ?? '-'; + } + + return differences + .map((diff) => `${diff.label}: ${diff.systemValue ?? '-'} -> ${diff.reportValue ?? '-'}`) + .join(' | '); + } + + getMveIssueTagClass(issue: MveAuditIssue): string { + switch (issue.issueType) { + case 'STATUS_DIVERGENCE': + case 'STATUS_AND_DATA_DIVERGENCE': + return 'status'; + case 'DATA_DIVERGENCE': + return 'data'; + case 'ONLY_IN_SYSTEM': + return 'system'; + case 'ONLY_IN_REPORT': + return 'report'; + case 'DUPLICATE_REPORT': + case 'DUPLICATE_SYSTEM': + return 'duplicate'; + case 'INVALID_ROW': + case 'UNKNOWN_STATUS': + return 'warning'; + default: + return 'neutral'; + } + } + + getMveSeverityClass(severity: string | null | undefined): string { + const normalized = (severity ?? '').toString().trim().toUpperCase(); + if (normalized === 'HIGH') return 'critical'; + if (normalized === 'MEDIUM') return 'medium'; + if (normalized === 'WARNING') return 'warning'; + return 'neutral'; + } + private getById(id: string, cb: (d: any) => void) { this.http.get(`${this.apiBase}/${id}`).subscribe({ next: cb, @@ -2461,7 +2979,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.detailData = null; this.cdr.detectChanges(); this.getById(r.id, (d) => { - this.detailData = d; + this.detailData = { + ...d, + contaEmpresa: this.findEmpresaByConta(d?.conta) + }; + this.syncContaEmpresaSelection(this.detailData); this.cdr.detectChanges(); }); } @@ -2590,6 +3112,13 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { franquiaLine: franquiaLineAtual }; } else { + const contaEmpresaValidationMessage = this.validateContaEmpresaBinding(this.editModel); + if (contaEmpresaValidationMessage) { + this.editSaving = false; + await this.showToast(contaEmpresaValidationMessage); + return; + } + this.calculateFinancials(this.editModel); const { @@ -3676,6 +4205,16 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { if (!hasMatch) model.conta = ''; } + onContaChange(isEdit: boolean) { + const model = isEdit ? this.editModel : this.createModel; + this.syncContaEmpresaSelection(model); + } + + onBatchContaChange(row: any) { + this.syncContaEmpresaSelection(row); + this.onBatchLineDetailsChange(); + } + onDocTypeChange() { this.createModel.docNumber = ''; this.createModel.skil = this.createModel.docType === 'PF' ? 'PESSOA FÍSICA' : 'PESSOA JURÍDICA'; @@ -4360,6 +4899,73 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } } + private matchesMveIssueFilter(issue: MveAuditIssue): boolean { + switch (this.mveAuditFilter) { + case 'STATUS': + return this.issueHasStatusDifference(issue); + case 'DATA': + return this.issueHasDataDifference(issue); + case 'ONLY_IN_SYSTEM': + return issue.issueType === 'ONLY_IN_SYSTEM'; + case 'ONLY_IN_REPORT': + return issue.issueType === 'ONLY_IN_REPORT'; + case 'DUPLICATES': + return issue.issueType === 'DUPLICATE_REPORT' || issue.issueType === 'DUPLICATE_SYSTEM'; + case 'INVALID': + return issue.issueType === 'INVALID_ROW'; + case 'UNKNOWN': + return issue.issueType === 'UNKNOWN_STATUS'; + default: + return true; + } + } + + private issueHasStatusDifference(issue: MveAuditIssue): boolean { + return (issue.differences ?? []).some((difference) => difference.fieldKey === 'status'); + } + + private issueHasDataDifference(issue: MveAuditIssue): boolean { + return (issue.differences ?? []).some( + (difference) => difference.fieldKey !== 'status' && difference.syncable + ); + } + + private normalizeMveSearchTerm(value: unknown): string { + return (value ?? '') + .toString() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .trim(); + } + + private escapeCsvValue(value: unknown): string { + const text = (value ?? '').toString().replace(/"/g, '""'); + return `"${text}"`; + } + + private downloadBlob(blob: Blob, fileName: string) { + if (!isPlatformBrowser(this.platformId)) return; + + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); + } + + private extractHttpMessage(error: unknown, fallbackMessage: string): string { + const httpError = error as HttpErrorResponse | null; + return ( + (httpError?.error as { message?: string } | null)?.message || + httpError?.message || + fallbackMessage + ); + } + formatMoney(v: any): string { if (v == null || Number.isNaN(v)) return '-'; return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(v); @@ -4563,9 +5169,46 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { private syncContaEmpresaSelection(model: any) { if (!model) return; + const contaAtual = (model.conta ?? '').toString().trim(); const empresaAtual = (model.contaEmpresa ?? '').toString().trim(); - if (empresaAtual) return; + if (!contaAtual) { + if (!empresaAtual) { + model.contaEmpresa = ''; + } + return; + } - model.contaEmpresa = this.findEmpresaByConta(model.conta); + const empresaPorConta = this.findEmpresaByConta(contaAtual); + if (empresaPorConta) { + model.contaEmpresa = empresaPorConta; + return; + } + + if (!empresaAtual) { + model.contaEmpresa = ''; + } + } + + private validateContaEmpresaBinding(model: any): string | null { + if (!model) return 'Dados da linha inválidos.'; + + this.syncContaEmpresaSelection(model); + + const conta = (model.conta ?? '').toString().trim(); + const contaEmpresa = (model.contaEmpresa ?? '').toString().trim(); + + if (!contaEmpresa) return 'Selecione a Empresa (Conta).'; + if (!conta) return 'Selecione uma Conta.'; + + const empresaPorConta = this.findEmpresaByConta(conta); + if (!empresaPorConta) { + return 'A conta informada não está vinculada a nenhuma Empresa (Conta) cadastrada.'; + } + + if (empresaPorConta.localeCompare(contaEmpresa, 'pt-BR', { sensitivity: 'base' }) !== 0) { + model.contaEmpresa = empresaPorConta; + } + + return null; } } diff --git a/src/app/pages/mve-auditoria/mve-auditoria.html b/src/app/pages/mve-auditoria/mve-auditoria.html new file mode 100644 index 0000000..69711c1 --- /dev/null +++ b/src/app/pages/mve-auditoria/mve-auditoria.html @@ -0,0 +1,226 @@ +
+ +
+ +
+ + + + + +
+
+ + +
+
+
+ No sistema + {{ audit.summary.totalSystemLines }} +
+
+ No relatorio + {{ audit.summary.totalReportLines }} +
+
+ Sem diferenca + {{ audit.summary.totalConciliated }} +
+
+ Com diferenca + {{ audit.summary.totalStatusDivergences }} +
+
+ Prontas para atualizar + {{ syncableStatusIssues.length }} +
+
+ +
+ Só no sistema: {{ audit.summary.totalOnlyInSystem }} + Só no relatório: {{ audit.summary.totalOnlyInReport }} +
+ +
+
+ + + +
+ +
+
+ + +
+ + +
+
+ +
+ +
Nenhuma diferenca de status encontrada para o filtro atual.
+
+ +
+ + + + + + + + + + + + + + + + + + + +
NúmeroStatus no sistemaStatus no relatorioSituaçãoAção
+ {{ issue.numeroLinha || '-' }} + + {{ statusLabel(issue.systemStatus) }} + + {{ statusLabel(issue.reportStatus) }} + +
Status diferente.
+
+ Pode atualizar + Atualizada + Sem ação +
+
+ + +
+ + +
+ +
Nenhuma conferencia carregada ainda.
+ Envie o relatorio da Vivo para ver as diferencas de status e atualizar o sistema. +
+
+
+
+
diff --git a/src/app/pages/mve-auditoria/mve-auditoria.scss b/src/app/pages/mve-auditoria/mve-auditoria.scss new file mode 100644 index 0000000..7b8eafb --- /dev/null +++ b/src/app/pages/mve-auditoria/mve-auditoria.scss @@ -0,0 +1,660 @@ +:host { + --brand: #e33dcf; + --brand-deep: #972688; + --brand-soft: rgba(227, 61, 207, 0.1); + --ink: #17161d; + --muted: rgba(23, 22, 29, 0.68); + --surface: rgba(255, 255, 255, 0.84); + --surface-strong: rgba(255, 255, 255, 0.94); + --line: rgba(23, 22, 29, 0.08); + --shadow: 0 24px 60px rgba(24, 17, 33, 0.12); + --success: #198754; + --danger: #dc3545; + --warning: #ffb200; + + display: block; + color: var(--ink); + font-family: 'Inter', sans-serif; +} + +.mve-page { + min-height: 100vh; + position: relative; + overflow: hidden; + padding: 0 14px 120px; + background: + radial-gradient(720px 380px at 12% 8%, rgba(227, 61, 207, 0.16), transparent 60%), + radial-gradient(620px 360px at 88% 12%, rgba(3, 15, 170, 0.08), transparent 58%), + linear-gradient(180deg, #fff 0%, #f6f4f8 100%); +} + +.page-blob { + position: fixed; + border-radius: 999px; + filter: blur(48px); + pointer-events: none; + opacity: 0.42; + z-index: 0; + background: radial-gradient(circle at 30% 30%, rgba(227, 61, 207, 0.6), rgba(227, 61, 207, 0.04)); + + &.blob-1 { width: 420px; height: 420px; top: -180px; left: -120px; } + &.blob-2 { width: 520px; height: 520px; top: -220px; right: -220px; } + &.blob-3 { width: 360px; height: 360px; bottom: -180px; left: 20%; } + &.blob-4 { width: 420px; height: 420px; bottom: -200px; right: 12%; } +} + +.page-shell { + position: relative; + z-index: 1; + width: min(1480px, 98vw); + margin: 38px auto 0; +} + +.page-card { + background: var(--surface); + border: 1px solid rgba(255, 255, 255, 0.72); + border-radius: 28px; + box-shadow: var(--shadow); + backdrop-filter: blur(12px); + overflow: hidden; +} + +.page-header { + padding: 22px 24px 18px; + border-bottom: 1px solid var(--line); + background: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(255,255,255,0.72)); + display: grid; + justify-items: center; +} + +.page-header > * { + width: min(1120px, 100%); +} + +.header-top { + display: grid; + grid-template-columns: 1fr; + gap: 16px; + align-items: center; + justify-items: center; + text-align: center; +} + +.title-badge { + justify-self: center; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 14px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.92); + border: 1px solid rgba(227, 61, 207, 0.22); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55); + font-size: 13px; + font-weight: 800; + + i { + color: var(--brand); + } +} + +.header-title { + text-align: center; +} + +.title { + font-size: 28px; + font-weight: 950; + letter-spacing: -0.04em; + color: var(--ink); +} + +.subtitle { + color: var(--muted); + font-weight: 700; +} + +.header-actions { + justify-self: center; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 10px; +} + +.btn { + border-radius: 14px; + font-weight: 800; +} + +.btn-glass { + background: rgba(255, 255, 255, 0.92); + border: 1px solid rgba(23, 22, 29, 0.08); + color: var(--ink); +} + +.btn-brand { + background: linear-gradient(135deg, var(--brand), var(--brand-deep)); + border: 0; + color: #fff; + box-shadow: 0 12px 24px rgba(151, 38, 136, 0.24); +} + +.intro-card, +.upload-card, +.secondary-notes, +.toolbar, +.summary-grid, +.table-wrap, +.status-empty, +.empty-state { + margin-top: 18px; +} + +.intro-card, +.upload-card, +.toolbar, +.table-wrap, +.status-empty, +.empty-state { + background: var(--surface-strong); + border: 1px solid var(--line); + border-radius: 20px; + box-shadow: 0 18px 34px rgba(24, 17, 33, 0.08); +} + +.intro-card { + padding: 18px 20px; + display: grid; + gap: 14px; + text-align: center; +} + +.intro-title, +.section-title { + font-size: 14px; + font-weight: 900; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--brand-deep); +} + +.intro-text, +.section-subtitle { + color: var(--muted); + line-height: 1.55; +} + +.intro-meta, +.secondary-notes { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: center; +} + +.meta-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 12px; + border-radius: 999px; + background: rgba(24, 17, 33, 0.05); + border: 1px solid rgba(24, 17, 33, 0.08); + font-size: 12px; + font-weight: 700; + color: rgba(24, 17, 33, 0.78); +} + +.upload-card { + padding: 20px; + text-align: center; +} + +.section-head { + display: flex; + justify-content: center; + gap: 12px; + align-items: flex-start; +} + +.upload-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 16px; + margin-top: 16px; + align-items: center; +} + +.upload-zone { + display: grid; + place-items: center; + gap: 8px; + min-height: 180px; + padding: 20px; + border-radius: 22px; + border: 1px dashed rgba(227, 61, 207, 0.35); + background: + linear-gradient(135deg, rgba(227, 61, 207, 0.06), rgba(255, 255, 255, 0.85)), + #fff; + text-align: center; + cursor: pointer; + + input { + display: none; + } +} + +.upload-icon { + width: 62px; + height: 62px; + display: grid; + place-items: center; + border-radius: 20px; + background: rgba(227, 61, 207, 0.12); + color: var(--brand-deep); + font-size: 28px; +} + +.upload-title { + font-size: 18px; + font-weight: 900; + color: var(--ink); +} + +.upload-subtitle { + max-width: 540px; + color: var(--muted); + line-height: 1.45; +} + +.upload-actions { + display: grid; + gap: 10px; + justify-content: center; +} + +.apply-banner { + margin-top: 14px; + padding: 12px 14px; + border-radius: 16px; + background: rgba(25, 135, 84, 0.1); + border: 1px solid rgba(25, 135, 84, 0.18); + color: #11653d; + font-weight: 700; +} + +.page-body { + padding: 0 24px 24px; +} + +.summary-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 14px; +} + +.summary-card { + background: rgba(255, 255, 255, 0.94); + border: 1px solid var(--line); + border-radius: 20px; + padding: 18px; + display: grid; + gap: 10px; + box-shadow: 0 16px 30px rgba(24, 17, 33, 0.08); + + strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.05em; + } + + &.is-positive strong { + color: var(--success); + } + + &.is-danger strong { + color: var(--danger); + } + + &.is-brand strong { + color: var(--brand-deep); + } +} + +.summary-label { + font-size: 12px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); +} + +.toolbar { + padding: 14px 16px; + display: flex; + justify-content: space-between; + gap: 12px; + align-items: center; + flex-wrap: wrap; +} + +.view-tabs { + display: inline-flex; + gap: 8px; + flex-wrap: wrap; +} + +.filter-tab { + border: 1px solid rgba(24, 17, 33, 0.08); + background: rgba(24, 17, 33, 0.04); + color: var(--ink); + border-radius: 999px; + padding: 8px 14px; + font-size: 12px; + font-weight: 800; + + &.active { + background: rgba(227, 61, 207, 0.14); + border-color: rgba(227, 61, 207, 0.24); + color: var(--brand-deep); + } +} + +.toolbar-right { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.search-group { + min-width: min(360px, 86vw); +} + +.page-size-select { + min-width: 140px; +} + +.table-wrap { + overflow: hidden; + max-width: 1120px; + margin-left: auto; + margin-right: auto; +} + +.table-modern { + margin: 0; + width: 100%; + table-layout: fixed; + + thead th { + background: #faf7fc; + border-bottom: 1px solid var(--line); + color: rgba(24, 17, 33, 0.72); + font-size: 12px; + font-weight: 900; + text-transform: uppercase; + letter-spacing: 0.06em; + padding: 14px 16px; + text-align: center; + } + + tbody td { + padding: 16px; + border-top: 1px solid rgba(24, 17, 33, 0.06); + vertical-align: middle; + text-align: center; + } +} + +.line-number-chip { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 120px; + padding: 8px 14px; + border-radius: 999px; + background: rgba(3, 15, 170, 0.08); + border: 1px solid rgba(3, 15, 170, 0.18); + color: #030faa; + font-size: 0.83rem; + font-weight: 950; + letter-spacing: 0.02em; + box-shadow: + 0 8px 18px rgba(3, 15, 170, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.7); +} + +.status-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 14px; + border-radius: 999px; + font-size: 12px; + font-weight: 900; + border: 1px solid transparent; + + &.is-active { + background: rgba(25, 135, 84, 0.12); + color: #11653d; + border-color: rgba(25, 135, 84, 0.18); + } + + &.is-blocked { + background: rgba(220, 53, 69, 0.12); + color: #9f1d2d; + border-color: rgba(220, 53, 69, 0.18); + } + + &.is-neutral { + background: rgba(24, 17, 33, 0.06); + color: rgba(24, 17, 33, 0.72); + border-color: rgba(24, 17, 33, 0.08); + } +} + +.status-diff-copy { + font-weight: 700; + color: rgba(24, 17, 33, 0.82); + text-align: center; +} + +.sync-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 7px 12px; + border-radius: 999px; + font-size: 12px; + font-weight: 900; + + &.ready { + background: rgba(255, 178, 0, 0.16); + color: #8c6200; + } + + &.applied { + background: rgba(25, 135, 84, 0.14); + color: #11653d; + } + + &.muted { + background: rgba(24, 17, 33, 0.06); + color: rgba(24, 17, 33, 0.62); + } +} + +.page-footer { + margin-top: 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.status-empty, +.empty-state { + padding: 42px 20px; + text-align: center; + color: var(--muted); + display: grid; + gap: 8px; + place-items: center; + + i { + font-size: 34px; + color: var(--brand-deep); + } +} + +.empty-state small { + max-width: 560px; + line-height: 1.5; +} + +@media (max-width: 1100px) { + .header-top { + grid-template-columns: 1fr; + text-align: center; + } + + .title-badge, + .header-actions { + justify-self: center; + } + + .summary-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .upload-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 720px) { + .page-shell { + width: calc(100vw - 16px); + margin-top: 18px; + } + + .page-header, + .page-body { + padding-left: 14px; + padding-right: 14px; + } + + .summary-grid { + grid-template-columns: 1fr; + } + + .page-card { + border-radius: 20px; + } + + .page-header > *, + .page-body > * { + width: 100%; + } + + .toolbar-right, + .header-actions { + width: 100%; + } + + .header-actions .btn, + .upload-actions .btn { + width: 100%; + justify-content: center; + } + + .upload-zone { + min-height: 150px; + padding: 18px 14px; + } + + .upload-title { + font-size: 16px; + } + + .intro-meta, + .secondary-notes, + .view-tabs, + .toolbar { + justify-content: center; + } + + .view-tabs { + width: 100%; + } + + .filter-tab { + flex: 1 1 120px; + justify-content: center; + text-align: center; + } + + .search-group, + .page-size-select { + width: 100%; + min-width: 0; + } + + .table-wrap { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + border-radius: 18px; + } + + .table-modern { + min-width: 720px; + } + + .line-number-chip { + min-width: 108px; + font-size: 0.78rem; + padding: 7px 12px; + } + + .page-footer { + justify-content: center; + text-align: center; + } +} + +@media (max-width: 420px) { + .mve-page { + padding-left: 8px; + padding-right: 8px; + } + + .page-header, + .page-body { + padding-left: 12px; + padding-right: 12px; + } + + .title { + font-size: 24px; + } + + .subtitle { + font-size: 0.88rem; + } + + .title-badge, + .meta-pill, + .filter-tab, + .status-pill, + .sync-badge { + font-size: 11px; + } + + .page-footer .pagination { + justify-content: center; + flex-wrap: wrap; + } +} diff --git a/src/app/pages/mve-auditoria/mve-auditoria.ts b/src/app/pages/mve-auditoria/mve-auditoria.ts new file mode 100644 index 0000000..4a8837e --- /dev/null +++ b/src/app/pages/mve-auditoria/mve-auditoria.ts @@ -0,0 +1,358 @@ +import { Component, ChangeDetectorRef, ElementRef, Inject, OnInit, PLATFORM_ID, ViewChild } from '@angular/core'; +import { CommonModule, isPlatformBrowser } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { HttpErrorResponse } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; + +import { + MveAuditService, + type ApplyMveAuditResult, + type MveAuditIssue, + type MveAuditRun, +} from '../../services/mve-audit.service'; +import { confirmActionModal } from '../../utils/destructive-confirmation'; +import { + buildPageNumbers, + clampPage, + computePageEnd, + computePageStart, + computeTotalPages, +} from '../../utils/pagination.util'; + +type MveStatusViewMode = 'PENDING' | 'APPLIED' | 'ALL'; + +@Component({ + selector: 'app-mve-auditoria', + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './mve-auditoria.html', + styleUrls: ['./mve-auditoria.scss'], +}) +export class MveAuditoriaPage implements OnInit { + @ViewChild('feedbackToast', { static: false }) feedbackToast?: ElementRef; + + loadingLatest = false; + processing = false; + syncing = false; + selectedFile: File | null = null; + auditResult: MveAuditRun | null = null; + applyResult: ApplyMveAuditResult | null = null; + errorMessage = ''; + toastMessage = ''; + + searchTerm = ''; + viewMode: MveStatusViewMode = 'PENDING'; + page = 1; + pageSize = 20; + readonly pageSizeOptions = [10, 20, 50, 100]; + + constructor( + private readonly mveAuditService: MveAuditService, + private readonly cdr: ChangeDetectorRef, + @Inject(PLATFORM_ID) private readonly platformId: object + ) {} + + ngOnInit(): void { + this.errorMessage = ''; + const cachedRun = this.mveAuditService.getCachedRun(); + if (cachedRun) { + this.auditResult = cachedRun; + return; + } + + void this.restoreCachedAudit(); + } + + get hasAuditResult(): boolean { + return !!this.auditResult; + } + + get syncableStatusIssues(): MveAuditIssue[] { + return this.statusIssues.filter((issue) => issue.syncable && !issue.applied); + } + + get statusIssues(): MveAuditIssue[] { + const issues = this.auditResult?.issues ?? []; + return issues + .filter((issue) => this.issueHasStatusDifference(issue)) + .sort((left, right) => Number(left.applied) - Number(right.applied)); + } + + get filteredStatusIssues(): MveAuditIssue[] { + const query = this.normalizeSearch(this.searchTerm); + return this.statusIssues.filter((issue) => { + if (this.viewMode === 'PENDING' && issue.applied) return false; + if (this.viewMode === 'APPLIED' && !issue.applied) return false; + if (!query) return true; + + const haystack = [ + issue.numeroLinha, + issue.systemStatus, + issue.reportStatus, + issue.situation, + issue.notes, + ] + .map((value) => this.normalizeSearch(value)) + .join(' '); + + return haystack.includes(query); + }); + } + + get pagedStatusIssues(): MveAuditIssue[] { + const offset = (this.page - 1) * this.pageSize; + return this.filteredStatusIssues.slice(offset, offset + this.pageSize); + } + + get totalPages(): number { + return computeTotalPages(this.filteredStatusIssues.length, this.pageSize); + } + + get pageNumbers(): number[] { + return buildPageNumbers(this.page, this.totalPages); + } + + get pageStart(): number { + return computePageStart(this.filteredStatusIssues.length, this.page, this.pageSize); + } + + get pageEnd(): number { + return computePageEnd(this.filteredStatusIssues.length, this.page, this.pageSize); + } + + get ignoredIssuesCount(): number { + if (!this.auditResult) return 0; + const summary = this.auditResult.summary; + return ( + summary.totalOnlyInSystem + + summary.totalOnlyInReport + + summary.totalDuplicateReportLines + + summary.totalDuplicateSystemLines + + summary.totalInvalidRows + + summary.totalUnknownStatuses + ); + } + + async loadLatestAudit(): Promise { + this.loadingLatest = true; + this.errorMessage = ''; + + try { + this.auditResult = await firstValueFrom(this.mveAuditService.getLatest()); + this.applyResult = null; + this.page = 1; + } catch (error) { + const httpError = error as HttpErrorResponse | null; + if (httpError?.status !== 404) { + this.errorMessage = this.extractHttpMessage(error, 'Nao foi possivel carregar a ultima conferencia.'); + } + this.auditResult = null; + } finally { + this.loadingLatest = false; + } + } + + onFileSelected(event: Event): void { + const input = event.target as HTMLInputElement | null; + const file = input?.files?.[0] ?? null; + + this.errorMessage = ''; + this.applyResult = null; + + if (!file) { + this.selectedFile = null; + return; + } + + if (!file.name.toLowerCase().endsWith('.csv')) { + this.selectedFile = null; + this.errorMessage = 'Selecione o relatorio exportado pela Vivo.'; + return; + } + + if (file.size <= 0) { + this.selectedFile = null; + this.errorMessage = 'O arquivo selecionado está vazio.'; + return; + } + + if (file.size > 20 * 1024 * 1024) { + this.selectedFile = null; + this.errorMessage = 'O arquivo excede o limite de 20 MB.'; + return; + } + + this.selectedFile = file; + } + + clearSelectedFile(): void { + this.selectedFile = null; + this.errorMessage = ''; + } + + async processAudit(): Promise { + if (!this.selectedFile || this.processing || this.syncing) { + return; + } + + this.processing = true; + this.errorMessage = ''; + this.applyResult = null; + + try { + this.auditResult = await firstValueFrom(this.mveAuditService.preview(this.selectedFile)); + this.page = 1; + this.viewMode = 'PENDING'; + this.searchTerm = ''; + await this.showToast('Relatorio conferido com sucesso.'); + } catch (error) { + this.errorMessage = this.extractHttpMessage(error, 'Nao foi possivel conferir o relatorio.'); + } finally { + this.processing = false; + } + } + + async syncStatuses(): Promise { + if (!this.auditResult || this.syncableStatusIssues.length === 0 || this.syncing) { + return; + } + + const confirmed = await confirmActionModal({ + title: 'Atualizar status no sistema', + message: `${this.syncableStatusIssues.length} linha(s) terao o status atualizado de acordo com o relatorio da Vivo.`, + confirmLabel: 'Atualizar agora', + cancelLabel: 'Cancelar', + tone: 'warning', + }); + + if (!confirmed) { + return; + } + + this.syncing = true; + this.errorMessage = ''; + + try { + this.applyResult = await firstValueFrom(this.mveAuditService.apply(this.auditResult.id)); + this.auditResult = await firstValueFrom(this.mveAuditService.getById(this.auditResult.id)); + this.viewMode = 'ALL'; + this.page = 1; + await this.showToast('Status atualizados com sucesso.'); + } catch (error) { + this.errorMessage = this.extractHttpMessage(error, 'Nao foi possivel atualizar os status.'); + } finally { + this.syncing = false; + } + } + + onSearchChange(): void { + this.page = 1; + } + + onPageSizeChange(): void { + this.page = 1; + } + + setViewMode(mode: MveStatusViewMode): void { + this.viewMode = mode; + this.page = 1; + } + + goToPage(page: number): void { + this.page = clampPage(page, this.totalPages); + } + + trackByIssue(_: number, issue: MveAuditIssue): string { + return issue.id; + } + + formatDateTime(value?: string | null): string { + if (!value) return '-'; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return '-'; + return new Intl.DateTimeFormat('pt-BR', { + dateStyle: 'short', + timeStyle: 'short', + }).format(date); + } + + statusClass(status: string | null | undefined): string { + const normalized = (status ?? '').toLowerCase(); + if (normalized.includes('bloq') || normalized.includes('perda') || normalized.includes('roubo')) return 'is-blocked'; + if (normalized.includes('ativo')) return 'is-active'; + return 'is-neutral'; + } + + statusLabel(status: string | null | undefined): string { + const value = (status ?? '').trim(); + return value || '-'; + } + + private issueHasStatusDifference(issue: MveAuditIssue): boolean { + return (issue.differences ?? []).some((difference) => difference.fieldKey === 'status'); + } + + private async restoreCachedAudit(): Promise { + const cachedRunId = this.mveAuditService.getCachedRunId(); + if (!cachedRunId) { + return; + } + + this.loadingLatest = true; + + try { + const restoredRun = await firstValueFrom(this.mveAuditService.restoreCachedRun()); + if (!restoredRun) { + return; + } + + this.auditResult = restoredRun; + this.applyResult = null; + this.page = 1; + } finally { + this.loadingLatest = false; + } + } + + private normalizeSearch(value: unknown): string { + return (value ?? '') + .toString() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .trim(); + } + + private extractHttpMessage(error: unknown, fallbackMessage: string): string { + const httpError = error as HttpErrorResponse | null; + if (httpError?.status === 0) { + return 'A API do LineGestao nao respondeu em http://localhost:5298. Inicie o backend e tente novamente.'; + } + + return ( + (httpError?.error as { message?: string } | null)?.message || + httpError?.message || + fallbackMessage + ); + } + + private async showToast(message: string): Promise { + if (!isPlatformBrowser(this.platformId)) return; + + this.toastMessage = message; + this.cdr.detectChanges(); + + if (!this.feedbackToast?.nativeElement) return; + + try { + const bootstrap = await import('bootstrap'); + const instance = bootstrap.Toast.getOrCreateInstance(this.feedbackToast.nativeElement, { + autohide: true, + delay: 3200, + }); + instance.show(); + } catch { + // ignora falha de feedback visual + } + } +} diff --git a/src/app/pages/notificacoes/notificacoes.html b/src/app/pages/notificacoes/notificacoes.html index a9517bc..fa6e752 100644 --- a/src/app/pages/notificacoes/notificacoes.html +++ b/src/app/pages/notificacoes/notificacoes.html @@ -69,7 +69,7 @@ - diff --git a/src/app/pages/parcelamentos/parcelamentos.scss b/src/app/pages/parcelamentos/parcelamentos.scss index 8a34e91..d606fec 100644 --- a/src/app/pages/parcelamentos/parcelamentos.scss +++ b/src/app/pages/parcelamentos/parcelamentos.scss @@ -208,6 +208,13 @@ box-shadow: 0 6px 14px rgba(15, 23, 42, 0.08); } +.btn-export-glass { + color: var(--pg-primary); + background: rgba(255, 255, 255, 0.68); + border-color: rgba(31, 79, 214, 0.24); + font-weight: 900; +} + .btn-danger { color: #fff; background: linear-gradient(145deg, #cf3131, #a91f1f); @@ -226,6 +233,12 @@ color: var(--pg-primary-strong); } +.btn-export-glass:hover { + background: #fff; + border-color: rgba(227, 61, 207, 0.3); + color: #e33dcf; +} + .lg-modal-card { width: min(1180px, 98vw); max-height: 92vh; diff --git a/src/app/pages/resumo/resumo.html b/src/app/pages/resumo/resumo.html index bd879df..24b7125 100644 --- a/src/app/pages/resumo/resumo.html +++ b/src/app/pages/resumo/resumo.html @@ -128,7 +128,7 @@ {{ macrophonyCompact ? 'Expandir' : 'Compactar' }} - @@ -447,7 +447,7 @@ {{ group.compact ? 'Expandir' : 'Compactar' }} - diff --git a/src/app/pages/resumo/resumo.scss b/src/app/pages/resumo/resumo.scss index 80a7539..2f3f3f6 100644 --- a/src/app/pages/resumo/resumo.scss +++ b/src/app/pages/resumo/resumo.scss @@ -190,6 +190,19 @@ font-size: 12px; } +.btn-export-glass { + color: var(--blue); + border-color: rgba(3, 15, 170, 0.22); + background: rgba(255, 255, 255, 0.72); + font-weight: 800; +} + +.btn-export-glass:hover:not(:disabled) { + border-color: var(--brand); + color: var(--brand); + background: #fff; +} + .btn-icon { width: 32px; height: 32px; diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index 89a9b5a..bd292e3 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { environment } from '../../environments/environment'; import { BehaviorSubject } from 'rxjs'; import { tap } from 'rxjs/operators'; +import { MveAuditService } from './mve-audit.service'; export interface RegisterPayload { name: string; @@ -43,7 +44,10 @@ export class AuthService { private readonly tokenExpiresAtKey = 'tokenExpiresAt'; private readonly rememberMeHours = 6; - constructor(private http: HttpClient) { + constructor( + private http: HttpClient, + private readonly mveAuditService: MveAuditService + ) { this.syncUserProfileFromToken(); } @@ -65,10 +69,12 @@ export class AuthService { logout() { if (typeof window === 'undefined') { + this.mveAuditService.clearCache(); this.userProfileSubject.next(null); return; } + this.mveAuditService.clearCache(); this.clearTokenStorage(localStorage); this.clearTokenStorage(sessionStorage); this.userProfileSubject.next(null); @@ -76,6 +82,7 @@ export class AuthService { setToken(token: string, rememberMe = false) { if (typeof window === 'undefined') return; + this.mveAuditService.clearCache(); this.clearTokenStorage(localStorage); this.clearTokenStorage(sessionStorage); diff --git a/src/app/services/mve-audit.service.ts b/src/app/services/mve-audit.service.ts new file mode 100644 index 0000000..3bedad1 --- /dev/null +++ b/src/app/services/mve-audit.service.ts @@ -0,0 +1,189 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { catchError, tap } from 'rxjs/operators'; +import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; + +export const MVE_AUDIT_CURRENT_RUN_ID_STORAGE_KEY = 'linegestao.mveAudit.currentRunId'; + +export interface MveAuditSummary { + totalSystemLines: number; + totalReportLines: number; + totalConciliated: number; + totalStatusDivergences: number; + totalDataDivergences: number; + totalOnlyInSystem: number; + totalOnlyInReport: number; + totalDuplicateReportLines: number; + totalDuplicateSystemLines: number; + totalInvalidRows: number; + totalUnknownStatuses: number; + totalSyncableIssues: number; + appliedIssuesCount: number; + appliedLinesCount: number; + appliedFieldsCount: number; +} + +export interface MveAuditSnapshot { + numeroLinha?: string | null; + statusLinha?: string | null; + statusConta?: string | null; + planoLinha?: string | null; + dataAtivacao?: string | null; + terminoContrato?: string | null; + chip?: string | null; + conta?: string | null; + cnpj?: string | null; + modeloAparelho?: string | null; + fabricante?: string | null; + servicosAtivos?: string[]; +} + +export interface MveAuditDifference { + fieldKey: string; + label: string; + systemValue?: string | null; + reportValue?: string | null; + syncable: boolean; +} + +export interface MveAuditIssue { + id: string; + sourceRowNumber?: number | null; + numeroLinha: string; + mobileLineId?: string | null; + systemItem?: number | null; + issueType: string; + situation: string; + severity: string; + syncable: boolean; + applied: boolean; + actionSuggestion?: string | null; + notes?: string | null; + systemStatus?: string | null; + reportStatus?: string | null; + systemPlan?: string | null; + reportPlan?: string | null; + systemSnapshot?: MveAuditSnapshot | null; + reportSnapshot?: MveAuditSnapshot | null; + differences: MveAuditDifference[]; +} + +export interface MveAuditRun { + id: string; + fileName?: string | null; + fileEncoding?: string | null; + status: string; + importedAtUtc: string; + appliedAtUtc?: string | null; + appliedByUserName?: string | null; + appliedByUserEmail?: string | null; + summary: MveAuditSummary; + issues: MveAuditIssue[]; +} + +export interface ApplyMveAuditResult { + auditRunId: string; + requestedIssues: number; + appliedIssues: number; + updatedLines: number; + updatedFields: number; + skippedIssues: number; +} + +@Injectable({ providedIn: 'root' }) +export class MveAuditService { + private readonly baseUrl: string; + private currentRun: MveAuditRun | null = null; + + constructor(private readonly http: HttpClient) { + const apiBase = buildApiBaseUrl(environment.apiUrl); + this.baseUrl = `${apiBase}/mve-audit`; + } + + preview(file: File): Observable { + const form = new FormData(); + form.append('file', file); + return this.http + .post(`${this.baseUrl}/preview`, form) + .pipe(tap((run) => this.cacheRun(run))); + } + + getById(id: string): Observable { + return this.http + .get(`${this.baseUrl}/${id}`) + .pipe(tap((run) => this.cacheRun(run))); + } + + getLatest(): Observable { + return this.http + .get(`${this.baseUrl}/latest`) + .pipe(tap((run) => this.cacheRun(run))); + } + + apply(runId: string, issueIds?: string[]): Observable { + return this.http.post(`${this.baseUrl}/${runId}/apply`, { + issueIds: issueIds && issueIds.length > 0 ? issueIds : null, + }); + } + + getCachedRun(): MveAuditRun | null { + return this.currentRun; + } + + getCachedRunId(): string | null { + if (this.currentRun?.id) { + return this.currentRun.id; + } + + if (typeof window === 'undefined') { + return null; + } + + return sessionStorage.getItem(MVE_AUDIT_CURRENT_RUN_ID_STORAGE_KEY); + } + + restoreCachedRun(): Observable { + if (this.currentRun) { + return of(this.currentRun); + } + + const runId = this.getCachedRunId(); + if (!runId) { + return of(null); + } + + return this.getById(runId).pipe( + catchError(() => { + this.clearCache(); + return of(null); + }) + ); + } + + clearCache(): void { + this.currentRun = null; + + if (typeof window === 'undefined') { + return; + } + + sessionStorage.removeItem(MVE_AUDIT_CURRENT_RUN_ID_STORAGE_KEY); + } + + private cacheRun(run: MveAuditRun | null | undefined): void { + if (!run?.id) { + this.clearCache(); + return; + } + + this.currentRun = run; + + if (typeof window === 'undefined') { + return; + } + + sessionStorage.setItem(MVE_AUDIT_CURRENT_RUN_ID_STORAGE_KEY, run.id); + } +} From 3437b2223eb1603dbff29395b906bbfe7793589a Mon Sep 17 00:00:00 2001 From: Leon Date: Mon, 9 Mar 2026 15:14:29 -0300 Subject: [PATCH 6/7] feat: apliquei os filtros por operadora --- package-lock.json | 3 + src/app/pages/dashboard/dashboard.html | 256 +++- src/app/pages/dashboard/dashboard.scss | 296 ++++ src/app/pages/dashboard/dashboard.ts | 1489 ++++++++++++++++++- src/app/pages/geral/geral.html | 360 +++-- src/app/pages/geral/geral.scss | 68 +- src/app/pages/geral/geral.spec.ts | 32 + src/app/pages/geral/geral.ts | 170 ++- src/app/utils/account-operator.util.spec.ts | 80 + src/app/utils/account-operator.util.ts | 176 +++ 10 files changed, 2703 insertions(+), 227 deletions(-) create mode 100644 src/app/utils/account-operator.util.spec.ts create mode 100644 src/app/utils/account-operator.util.ts diff --git a/package-lock.json b/package-lock.json index c495cf0..c562289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -640,6 +640,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -6944,6 +6945,7 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -9576,6 +9578,7 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/src/app/pages/dashboard/dashboard.html b/src/app/pages/dashboard/dashboard.html index 8a75c81..89eb1e0 100644 --- a/src/app/pages/dashboard/dashboard.html +++ b/src/app/pages/dashboard/dashboard.html @@ -29,6 +29,24 @@
+
+
+ + Filtro de Operadora +
+
+ +
+
+
{{ k.title }} {{ k.value }} - {{ k.hint }}
@@ -66,7 +83,12 @@
-
+
@@ -107,7 +129,12 @@
-
+
@@ -143,7 +170,12 @@

Status de vencimento atual

-
+
@@ -156,7 +188,12 @@

Linhas com e sem serviço ativo

-
+
@@ -170,7 +207,12 @@

Distribuição da base por faixa de franquia

-
+
@@ -182,7 +224,12 @@

Quantidade de linhas por serviço adicional ativo

-
+
@@ -194,13 +241,65 @@

Quantidade de linhas e-SIM e SIMCARD

-
+
+
+
+

Comparativo VIVO

+

Comparação entre contas da operadora VIVO: MACROPHONY x LINE MÓVEL.

+
+ +
+
+
+
+

Linhas por Empresa

+

Volume total de linhas VIVO por empresa.

+
+
+
+ +
+
+ +
+
+
+

Adicionais por Empresa

+

Comparação de linhas com e sem adicionais pagos.

+
+
+
+ +
+
+
+ +
+ Não há linhas VIVO vinculadas às empresas MACROPHONY ou LINE MÓVEL para o filtro atual. +
+
+

Página Resumo

Indicadores do Resumo focados em quantidade e distribuição de linhas.

@@ -247,22 +346,50 @@
Top Clientes (Qtd. Linhas)
-
+
+ +
Top Planos (Qtd. Linhas)
-
+
+ +
PF vs PJ (Qtd. Linhas)
-
+
+ +
Reserva por DDD
-
+
+ +
@@ -301,7 +428,12 @@

Histórico mensal de mudanças de plano/aparelho

-
+
@@ -313,7 +445,12 @@

Histórico mensal de trocas realizadas

-
+
@@ -326,7 +463,12 @@

Contratos a encerrar por mês

-
+
@@ -352,7 +494,12 @@
-
+
@@ -373,7 +520,12 @@

Quantidade de linhas por faixa de franquia contratada

-
+
@@ -388,7 +540,12 @@

Planos com maior volume na sua operação

-
+
@@ -401,7 +558,12 @@

Apenas usuários de fato (sem bloqueados/aguardando)

-
+
@@ -414,7 +576,12 @@

Distribuição entre e-SIM, SIMCARD e outros

-
+
@@ -441,5 +608,54 @@
+ + diff --git a/src/app/pages/dashboard/dashboard.scss b/src/app/pages/dashboard/dashboard.scss index 9b1c902..e1076ec 100644 --- a/src/app/pages/dashboard/dashboard.scss +++ b/src/app/pages/dashboard/dashboard.scss @@ -98,6 +98,72 @@ @media(max-width: 768px) { flex-direction: column; align-items: flex-start; gap: 16px; } } +.operadora-filter-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin: -8px 0 22px; + padding: 14px 16px; + border-radius: 14px; + background: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(15, 23, 42, 0.08); + box-shadow: var(--shadow-sm); + + @media (max-width: 840px) { + flex-direction: column; + align-items: stretch; + } +} + +.operadora-filter-label { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--text-muted); + font-weight: 700; + letter-spacing: 0.02em; + + i { + color: var(--brand); + } +} + +.filter-tabs { + display: inline-flex; + flex-wrap: wrap; + gap: 8px; +} + +.filter-tab { + border: 1px solid rgba(15, 23, 42, 0.15); + background: #fff; + color: var(--text-muted); + border-radius: 999px; + padding: 6px 12px; + font-size: 12px; + font-weight: 700; + cursor: pointer; + transition: all 0.2s ease; + + &:hover:not(:disabled) { + border-color: rgba(227, 61, 207, 0.45); + color: var(--brand); + } + + &.active { + background: var(--brand-soft); + border-color: rgba(227, 61, 207, 0.45); + color: var(--brand); + } + + &:disabled { + opacity: 0.55; + cursor: not-allowed; + } +} + .badge-pill { display: inline-flex; align-items: center; @@ -392,6 +458,22 @@ &.compact-half { height: 200px; } } +.chart-click-target { + cursor: zoom-in; + border-radius: 12px; + transition: transform 0.2s ease, box-shadow 0.2s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 10px 18px -16px rgba(17, 18, 20, 0.65); + } + + &:focus-visible { + outline: 2px solid rgba(3, 15, 170, 0.26); + outline-offset: 2px; + } +} + .card-adicionais .card-body-adicionais { padding: 14px 16px 12px; display: grid; @@ -630,7 +712,221 @@ @media(max-width: 1080px) { grid-template-columns: 1fr; } } +.vivo-comparison-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + + @media (max-width: 980px) { + grid-template-columns: 1fr; + } +} + +.vivo-comparison-empty { + margin-top: 10px; + padding: 12px 14px; + border-radius: 10px; + background: rgba(148, 163, 184, 0.12); + border: 1px solid rgba(148, 163, 184, 0.28); + color: var(--text-muted); + font-size: 12px; + font-weight: 600; +} + +.chart-modal-overlay { + position: fixed; + inset: 0; + z-index: 1200; + background: rgba(10, 14, 35, 0.58); + backdrop-filter: blur(5px); + display: flex; + align-items: center; + justify-content: center; + padding: 28px 20px; +} + +.chart-modal-card { + width: min(1120px, 96vw); + max-height: min(86vh, 860px); + background: #fff; + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 18px; + box-shadow: 0 30px 70px -26px rgba(2, 8, 23, 0.65); + display: flex; + flex-direction: column; + overflow: hidden; + animation: modalChartIn 0.22s ease; +} + +.chart-modal-header { + padding: 14px 18px; + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; +} + +.chart-modal-title-wrap { + h3 { + margin: 0; + font-size: 16px; + font-weight: 800; + color: var(--text-main); + } + + p { + margin: 4px 0 0; + font-size: 12px; + color: var(--text-muted); + font-weight: 600; + } +} + +.chart-modal-close { + width: 34px; + height: 34px; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.12); + background: #fff; + color: rgba(17, 18, 20, 0.7); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + border-color: rgba(227, 61, 207, 0.35); + color: var(--brand); + background: var(--brand-soft); + } +} + +.chart-modal-body { + position: relative; + height: min(72vh, 680px); + min-height: 360px; + padding: 14px 16px 16px; +} + +.chart-modal-content { + display: grid; + grid-template-columns: minmax(0, 1.25fr) minmax(300px, 0.75fr); + gap: 14px; + height: 100%; +} + +.chart-modal-visual { + position: relative; + min-height: 0; +} + +.chart-modal-info { + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 12px; + background: #f8fafc; + overflow: auto; + display: flex; + flex-direction: column; +} + +.chart-modal-info-head, +.chart-modal-info-row { + display: grid; + grid-template-columns: minmax(120px, 1.2fr) repeat(var(--dataset-cols, 1), minmax(78px, 1fr)); + gap: 10px; + padding: 10px 12px; + align-items: center; +} + +.chart-modal-info-head { + position: sticky; + top: 0; + z-index: 1; + background: #eef2ff; + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + font-size: 11px; + font-weight: 800; + color: #334155; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.chart-modal-info-row { + border-bottom: 1px solid rgba(15, 23, 42, 0.06); + font-size: 12px; + color: #0f172a; + font-weight: 600; + background: #fff; + + &:nth-child(odd) { + background: #fdfdff; + } +} + +.chart-modal-info .col-label { + text-align: left; + word-break: break-word; +} + +.chart-modal-info .col-value { + text-align: right; + font-variant-numeric: tabular-nums; +} + +.chart-modal-info-total { + display: flex; + flex-wrap: wrap; + gap: 10px 16px; + border-top: 1px solid rgba(15, 23, 42, 0.08); + padding: 10px 12px 12px; + background: #f1f5f9; + font-size: 11px; + font-weight: 700; + color: #334155; +} + +@keyframes modalChartIn { + from { + opacity: 0; + transform: translateY(8px) scale(0.985); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + /* Utils */ .text-brand { color: var(--brand); } .text-brand-dark { color: #b832a8; } .full-width { width: 100%; } + +@media (max-width: 760px) { + .chart-modal-overlay { + padding: 16px 10px; + } + + .chart-modal-card { + width: 100%; + border-radius: 14px; + } + + .chart-modal-body { + min-height: 300px; + height: min(72vh, 620px); + padding: 10px 10px 12px; + } + + .chart-modal-content { + grid-template-columns: 1fr; + grid-template-rows: minmax(200px, 1fr) minmax(140px, auto); + } + + .chart-modal-info-head, + .chart-modal-info-row { + grid-template-columns: minmax(120px, 1.2fr) repeat(var(--dataset-cols, 1), minmax(72px, 1fr)); + } +} diff --git a/src/app/pages/dashboard/dashboard.ts b/src/app/pages/dashboard/dashboard.ts index 31ce174..b9a878e 100644 --- a/src/app/pages/dashboard/dashboard.ts +++ b/src/app/pages/dashboard/dashboard.ts @@ -6,6 +6,7 @@ import { ViewChild, ElementRef, Inject, + HostListener, } from '@angular/core'; import { CommonModule, isPlatformBrowser } from '@angular/common'; import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http'; @@ -22,6 +23,13 @@ import { } from '../../services/resumo.service'; import { AuthService } from '../../services/auth.service'; import { buildApiBaseUrl } from '../../utils/api-base.util'; +import { + type AccountCompanyOption, + DEFAULT_ACCOUNT_COMPANIES, + mergeAccountCompaniesWithDefaults, + type OperadoraFiltro, + resolveOperadoraContext, +} from '../../utils/account-operator.util'; // --- Interfaces (Mantidas intactas para não quebrar contrato) --- type KpiCard = { @@ -162,12 +170,18 @@ type DashboardGeralInsightsDto = { }; type DashboardLineListItemDto = { + id?: string | null; + conta?: string | null; + contaEmpresa?: string | null; + empresaConta?: string | null; linha?: string | null; cliente?: string | null; usuario?: string | null; skil?: string | null; planoContrato?: string | null; status?: string | null; + vencConta?: string | null; + franquiaVivo?: number | null; franquiaLine?: number | null; gestaoVozDados?: number | null; skeelo?: number | null; @@ -212,6 +226,61 @@ type ClientDashboardOverview = { outrosStatus: number; }; +type DashboardHistoryEvent = { + mobileLineId: string; + linhaAntiga: string; + linhaNova: string; + date: Date | null; +}; + +type DashboardVivoComparison = { + macrophonyLinhas: number; + lineMovelLinhas: number; + macrophonyComAdicionais: number; + macrophonySemAdicionais: number; + lineMovelComAdicionais: number; + lineMovelSemAdicionais: number; +}; + +type DashboardChartModalKey = + | 'status' + | 'adicionaisComparativo' + | 'vigenciaBuckets' + | 'travel' + | 'linhasFranquia' + | 'adicionaisPagos' + | 'tipoChip' + | 'vivoEmpresasLinhas' + | 'vivoEmpresasAdicionais' + | 'resumoTopClientes' + | 'resumoTopPlanos' + | 'resumoPfPj' + | 'resumoReservaDdd' + | 'mureg12' + | 'troca12' + | 'vigenciaMesAno'; + +type DashboardChartModalMeta = { + title: string; + subtitle?: string; +}; + +type DashboardChartModalInfoCell = { + dataset: string; + valueText: string; + numericValue: number | null; +}; + +type DashboardChartModalInfoRow = { + label: string; + cells: DashboardChartModalInfoCell[]; +}; + +type DashboardChartModalDatasetTotal = { + dataset: string; + totalText: string; +}; + @Component({ selector: 'app-dashboard', standalone: true, @@ -235,10 +304,21 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { @ViewChild('chartResumoTopPlanos') chartResumoTopPlanos?: ElementRef; @ViewChild('chartResumoTopClientes') chartResumoTopClientes?: ElementRef; @ViewChild('chartResumoReservaDdd') chartResumoReservaDdd?: ElementRef; + @ViewChild('chartVivoEmpresasLinhas') chartVivoEmpresasLinhas?: ElementRef; + @ViewChild('chartVivoEmpresasAdicionais') chartVivoEmpresasAdicionais?: ElementRef; + @ViewChild('chartExpandedCanvas') chartExpandedCanvas?: ElementRef; loading = true; errorMsg: string | null = null; isCliente = false; + operadoraFilter: OperadoraFiltro = 'TODOS'; + operadoraFilterLoading = false; + readonly operadoraFilters: Array<{ label: string; value: OperadoraFiltro }> = [ + { label: 'TODOS', value: 'TODOS' }, + { label: 'VIVO', value: 'VIVO' }, + { label: 'CLARO', value: 'CLARO' }, + { label: 'TIM', value: 'TIM' }, + ]; kpis: KpiCard[] = []; @@ -334,6 +414,20 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { pjLinhas: null, totalLinhas: null, }; + vivoComparison: DashboardVivoComparison = { + macrophonyLinhas: 0, + lineMovelLinhas: 0, + macrophonyComAdicionais: 0, + macrophonySemAdicionais: 0, + lineMovelComAdicionais: 0, + lineMovelSemAdicionais: 0, + }; + chartModalOpen = false; + chartModalTitle = ''; + chartModalSubtitle = ''; + chartModalInfoRows: DashboardChartModalInfoRow[] = []; + chartModalDatasetHeaders: string[] = []; + chartModalDatasetTotals: DashboardChartModalDatasetTotal[] = []; private viewReady = false; private dataReady = false; @@ -358,6 +452,44 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { private chartResumoPlanos?: Chart; private chartResumoClientes?: Chart; private chartResumoReserva?: Chart; + private chartVivoComparacaoLinhas?: Chart; + private chartVivoComparacaoAdicionais?: Chart; + private chartExpanded?: Chart; + private chartModalKey: DashboardChartModalKey | null = null; + private bodyOverflowBeforeChartModal: string | null = null; + private readonly chartModalMeta: Record = { + status: { title: 'Status da Base', subtitle: 'Distribuição atual das linhas' }, + adicionaisComparativo: { title: 'Serviços Adicionais', subtitle: 'Comparativo entre linhas com e sem adicionais' }, + vigenciaBuckets: { title: 'Vigência (Buckets)', subtitle: 'Status de vencimento atual' }, + travel: { title: 'Vivo Travel', subtitle: 'Linhas com e sem serviço ativo' }, + linhasFranquia: { title: 'Linhas por Franquia', subtitle: 'Distribuição por faixa de franquia' }, + adicionaisPagos: { title: 'Adicionais Pagos (Serviços)', subtitle: 'Quantidade por serviço adicional ativo' }, + tipoChip: { title: 'Tipo de Chip', subtitle: 'Distribuição entre e-SIM, SIMCARD e outros' }, + vivoEmpresasLinhas: { title: 'Comparativo VIVO: Linhas por Empresa' }, + vivoEmpresasAdicionais: { title: 'Comparativo VIVO: Adicionais por Empresa' }, + resumoTopClientes: { title: 'Resumo: Top Clientes' }, + resumoTopPlanos: { title: 'Resumo: Top Planos' }, + resumoPfPj: { title: 'Resumo: PF vs PJ' }, + resumoReservaDdd: { title: 'Resumo: Reserva por DDD' }, + mureg12: { title: 'MUREG (12 Meses)' }, + troca12: { title: 'Troca de Número (12 Meses)' }, + vigenciaMesAno: { title: 'Vigência (Próx. 12 Meses)' }, + }; + + private dashboardApiCache: DashboardDto | null = null; + private insightsApiCache: DashboardGeralInsightsDto | null = null; + private resumoApiCache: ResumoResponse | null = null; + private readonly fallbackAccountCompanies: AccountCompanyOption[] = DEFAULT_ACCOUNT_COMPANIES.map((group) => ({ + empresa: group.empresa, + contas: [...group.contas], + })); + private accountCompanies: AccountCompanyOption[] = [...this.fallbackAccountCompanies]; + private operatorLinesCache: DashboardLineListItemDto[] = []; + private operatorMuregCache: DashboardHistoryEvent[] = []; + private operatorTrocaCache: DashboardHistoryEvent[] = []; + private filteredLinesCache: DashboardLineListItemDto[] = []; + private operatorDatasetReady = false; + private lineFranquiaCacheById = new Map(); private readonly baseApi: string; private readonly kpiNavigationMap: Record = { @@ -401,9 +533,13 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { return; } + void this.loadAccountCompaniesForFilters(); this.loadDashboard(); this.loadInsights(); this.loadResumoExecutive(); + void this.preloadOperatorDatasets().catch(() => { + this.operatorDatasetReady = false; + }); } ngAfterViewInit(): void { @@ -413,10 +549,69 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { } ngOnDestroy(): void { + this.closeChartModal(); this.destroyCharts(); this.destroyResumoCharts(); } + get isOperadoraFiltroAtivo(): boolean { + return !this.isCliente && this.operadoraFilter !== 'TODOS'; + } + + get showVivoComparison(): boolean { + return this.isOperadoraFiltroAtivo && this.operadoraFilter === 'VIVO'; + } + + onOperadoraFilterChange(filter: OperadoraFiltro): void { + if (this.operadoraFilter === filter || this.isCliente) return; + this.operadoraFilter = filter; + void this.applyOperadoraFilter(); + } + + @HostListener('document:keydown.escape', ['$event']) + onEscapeKey(event: Event): void { + if (!this.chartModalOpen) return; + if (event instanceof KeyboardEvent) { + event.preventDefault(); + } + this.closeChartModal(); + } + + onChartTargetKeydown(event: Event, key: DashboardChartModalKey): void { + if (!(event instanceof KeyboardEvent)) return; + if (event.key !== 'Enter' && event.key !== ' ') return; + event.preventDefault(); + this.openChartModal(key); + } + + openChartModal(key: DashboardChartModalKey): void { + if (!isPlatformBrowser(this.platformId)) return; + + const sourceChart = this.getChartInstanceByModalKey(key); + if (!sourceChart) return; + + this.chartModalKey = key; + this.chartModalTitle = this.chartModalMeta[key]?.title ?? 'Gráfico'; + this.chartModalSubtitle = this.chartModalMeta[key]?.subtitle ?? ''; + this.updateChartModalInfo(sourceChart); + this.chartModalOpen = true; + this.lockBodyScrollForChartModal(); + + requestAnimationFrame(() => this.renderExpandedChart(sourceChart)); + } + + closeChartModal(): void { + this.chartModalOpen = false; + this.chartModalKey = null; + this.chartModalTitle = ''; + this.chartModalSubtitle = ''; + this.chartModalInfoRows = []; + this.chartModalDatasetHeaders = []; + this.chartModalDatasetTotals = []; + this.destroyExpandedChart(); + this.restoreBodyScrollForChartModal(); + } + private async loadDashboard() { this.loading = true; this.errorMsg = null; @@ -424,13 +619,17 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { try { const dto = await this.fetchDashboardReal(); - this.applyDto(dto); + this.dashboardApiCache = dto; + if (this.operadoraFilter === 'TODOS') { + this.applyDto(dto); + } this.dataReady = true; this.loading = false; this.tryBuildCharts(); } catch (error) { + this.dashboardApiCache = null; this.loading = false; this.dashboardRaw = null; this.kpis = []; @@ -674,6 +873,14 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { this.resumoClientesValues = []; this.resumoReservaLabels = []; this.resumoReservaValues = []; + this.vivoComparison = { + macrophonyLinhas: 0, + lineMovelLinhas: 0, + macrophonyComAdicionais: 0, + macrophonySemAdicionais: 0, + lineMovelComAdicionais: 0, + lineMovelSemAdicionais: 0, + }; this.rebuildPrimaryKpis(); this.destroyCharts(); this.destroyResumoCharts(); @@ -700,15 +907,21 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { const url = `${this.baseApi}/dashboard/geral/insights`; this.http.get(url).subscribe({ next: (dto) => { - this.applyInsights(dto || null); + this.insightsApiCache = dto || null; + if (this.operadoraFilter === 'TODOS') { + this.applyInsights(dto || null); + } this.insightsLoading = false; this.tryBuildCharts(); }, error: () => { + this.insightsApiCache = null; this.insightsLoading = false; this.insightsError = 'Falha nos insights.'; - this.clearInsightsData(); - void this.loadFallbackFromLinesIfNeeded(true); + if (this.operadoraFilter === 'TODOS') { + this.clearInsightsData(); + void this.loadFallbackFromLinesIfNeeded(true); + } }, }); } @@ -721,18 +934,28 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { this.resumoService.getResumo().subscribe({ next: (dto) => { - this.resumo = dto ? this.normalizeResumo(dto) : null; + this.resumoApiCache = dto ? this.normalizeResumo(dto) : null; + if (this.operadoraFilter === 'TODOS') { + this.resumo = this.resumoApiCache; + } this.resumoLoading = false; this.resumoReady = true; - this.buildResumoDerived(); + if (this.operadoraFilter === 'TODOS') { + this.buildResumoDerived(); + } this.tryBuildResumoCharts(); }, error: () => { + this.resumoApiCache = null; this.resumoLoading = false; this.resumoError = 'Falha ao carregar dados do resumo.'; - this.resumo = null; + if (this.operadoraFilter === 'TODOS') { + this.resumo = null; + } this.resumoReady = false; - this.clearResumoDerived(); + if (this.operadoraFilter === 'TODOS') { + this.clearResumoDerived(); + } }, }); } @@ -742,14 +965,832 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { void this.loadClientDashboardData(); return; } + if (this.isOperadoraFiltroAtivo) { + this.applyOperadoraDerivedState(this.filteredLinesCache); + this.tryBuildCharts(); + this.tryBuildResumoCharts(); + return; + } this.buildResumoDerived(); this.tryBuildResumoCharts(); } - private async fetchDashboardReal(): Promise { + private async fetchDashboardReal(operadora: OperadoraFiltro = 'TODOS'): Promise { if (!isPlatformBrowser(this.platformId)) throw new Error('SSR não suportado'); const url = `${this.baseApi}/relatorios/dashboard`; - return await firstValueFrom(this.http.get(url)); + let params = new HttpParams(); + if (operadora !== 'TODOS') { + params = params.set('operadora', operadora); + } + + return await firstValueFrom(this.http.get(url, { params })); + } + + private async fetchInsightsReal(operadora: OperadoraFiltro = 'TODOS'): Promise { + if (!isPlatformBrowser(this.platformId)) throw new Error('SSR não suportado'); + const url = `${this.baseApi}/dashboard/geral/insights`; + let params = new HttpParams(); + if (operadora !== 'TODOS') { + params = params.set('operadora', operadora); + } + + return await firstValueFrom(this.http.get(url, { params })); + } + + private async applyOperadoraFilter(): Promise { + if (this.isCliente) return; + + if (this.operadoraFilter === 'TODOS') { + this.operadoraFilterLoading = false; + this.restoreDashboardFromApiCaches(); + return; + } + + this.operadoraFilterLoading = true; + this.loading = true; + this.resumoLoading = true; + this.errorMsg = null; + this.resumoError = null; + let filtered: DashboardLineListItemDto[] = []; + + try { + await this.preloadOperatorDatasets(); + filtered = this.resolveLinesByOperadora(this.operadoraFilter); + this.applyOperadoraDerivedState(filtered); + const [filteredDashboard, filteredInsights] = await Promise.all([ + this.fetchDashboardReal(this.operadoraFilter), + this.fetchInsightsReal(this.operadoraFilter), + ]); + this.applyDto(filteredDashboard); + this.applyInsights(filteredInsights); + this.insightsError = null; + this.loading = false; + this.resumoLoading = false; + this.dataReady = true; + this.resumoReady = true; + this.tryBuildCharts(); + this.tryBuildResumoCharts(); + } catch { + if (filtered.length > 0) { + this.applyOperadoraDerivedState(filtered, { updateCoreMetrics: true }); + this.dataReady = true; + this.resumoReady = true; + } else { + this.dataReady = false; + this.resumoReady = false; + } + this.loading = false; + this.resumoLoading = false; + this.errorMsg = 'Falha ao aplicar filtro de operadora.'; + } finally { + this.operadoraFilterLoading = false; + } + } + + private restoreDashboardFromApiCaches(): void { + this.loading = false; + this.resumoLoading = false; + this.errorMsg = null; + this.resumoError = null; + + if (this.dashboardApiCache) { + this.applyDto(this.dashboardApiCache); + } else { + void this.loadDashboard(); + } + + if (this.insightsApiCache) { + this.applyInsights(this.insightsApiCache); + } else { + this.loadInsights(); + } + + if (this.resumoApiCache) { + this.resumo = this.normalizeResumo(this.resumoApiCache); + this.resumoReady = true; + this.buildResumoDerived(); + this.tryBuildResumoCharts(); + } else { + this.loadResumoExecutive(); + } + + this.dataReady = true; + this.tryBuildCharts(); + } + + private async loadAccountCompaniesForFilters(): Promise { + try { + const data = await firstValueFrom( + this.http.get(`${this.baseApi}/lines/account-companies`) + ); + const normalized = this.normalizeAccountCompanies(data); + const source = normalized.length > 0 ? normalized : this.fallbackAccountCompanies; + this.accountCompanies = mergeAccountCompaniesWithDefaults(source); + } catch { + this.accountCompanies = mergeAccountCompaniesWithDefaults(this.fallbackAccountCompanies); + } + + if (this.isOperadoraFiltroAtivo && this.operatorDatasetReady) { + const filtered = this.resolveLinesByOperadora(this.operadoraFilter); + this.applyOperadoraDerivedState(filtered); + this.tryBuildCharts(); + this.tryBuildResumoCharts(); + } + } + + private normalizeAccountCompanies( + data: AccountCompanyOption[] | null | undefined + ): AccountCompanyOption[] { + if (!Array.isArray(data)) return []; + + const result: AccountCompanyOption[] = []; + data.forEach((item) => { + const empresa = String(item?.empresa ?? '').trim(); + if (!empresa) return; + + const contas = Array.isArray(item?.contas) + ? Array.from(new Set(item.contas.map((x) => String(x ?? '').trim()).filter(Boolean))) + : []; + + result.push({ empresa, contas }); + }); + + return result; + } + + private async preloadOperatorDatasets(): Promise { + if (this.operatorDatasetReady) return; + + const [lines, muregs, trocas] = await Promise.all([ + this.fetchAllOperatorLines(), + this.fetchAllHistoryEvents('mureg', ['dataDaMureg', 'data_da_mureg', 'DataDaMureg']), + this.fetchAllHistoryEvents('trocanumero', ['dataTroca', 'data_troca', 'DataTroca', 'dataDaTroca']), + ]); + + this.operatorLinesCache = lines; + this.operatorMuregCache = muregs; + this.operatorTrocaCache = trocas; + this.operatorDatasetReady = true; + } + + private async fetchAllOperatorLines(): Promise { + const [allLines, reservaLines] = await Promise.all([ + this.fetchAllDashboardLines(false), + this.fetchAllDashboardLines(true), + ]); + + const merged = [...allLines, ...reservaLines]; + const dedup = new Map(); + + merged.forEach((line, idx) => { + const key = this.getLineDedupKey(line, idx); + if (!key) return; + if (!dedup.has(key)) dedup.set(key, line); + }); + + return Array.from(dedup.values()); + } + + private getLineDedupKey(line: DashboardLineListItemDto, index: number): string { + const lineId = String(this.readNode(line as any, 'id', 'Id') ?? '').trim(); + if (lineId) return `id:${lineId}`; + + const linha = this.normalizeLineDigits(this.readNode(line as any, 'linha', 'Linha')); + const conta = this.readNode(line as any, 'conta', 'Conta'); + const normalizedConta = String(conta ?? '').trim(); + if (linha || normalizedConta) return `line:${linha}|conta:${normalizedConta}`; + + return `idx:${index}`; + } + + private async ensureFranquiaCacheForLines(lines: DashboardLineListItemDto[]): Promise { + if (!Array.isArray(lines) || lines.length === 0) return; + + const idsToLoad: string[] = []; + + lines.forEach((line) => { + const id = String(this.readNode(line as any, 'id', 'Id') ?? '').trim(); + if (!id || this.lineFranquiaCacheById.has(id)) return; + + const franquiaVivoFromList = this.toNumberOrNull( + this.readLineRawField(line, 'franquiaVivo', 'FranquiaVivo') + ); + const franquiaLineFromList = this.toNumberOrNull( + this.readLineRawField(line, 'franquiaLine', 'FranquiaLine') + ); + + if (franquiaVivoFromList !== null || franquiaLineFromList !== null) { + this.lineFranquiaCacheById.set(id, { + franquiaVivo: franquiaVivoFromList, + franquiaLine: franquiaLineFromList, + }); + return; + } + + idsToLoad.push(id); + }); + + if (!idsToLoad.length) return; + + const concurrency = Math.min(20, idsToLoad.length); + let cursor = 0; + + const workers = Array.from({ length: concurrency }, async () => { + while (true) { + const index = cursor++; + if (index >= idsToLoad.length) break; + const id = idsToLoad[index]; + await this.loadFranquiaFromLineDetail(id); + } + }); + + await Promise.all(workers); + } + + private async loadFranquiaFromLineDetail(id: string): Promise { + try { + const detail = await firstValueFrom(this.http.get(`${this.baseApi}/lines/${id}`)); + + const franquiaVivo = this.toNumberOrNull( + this.readUnknownNumericField(detail, 'franquiaVivo', 'FranquiaVivo') + ); + const franquiaLine = this.toNumberOrNull( + this.readUnknownNumericField(detail, 'franquiaLine', 'FranquiaLine') + ); + + this.lineFranquiaCacheById.set(id, { + franquiaVivo, + franquiaLine, + }); + } catch { + this.lineFranquiaCacheById.set(id, { + franquiaVivo: null, + franquiaLine: null, + }); + } + } + + private async fetchAllHistoryEvents( + endpoint: string, + dateKeys: string[] + ): Promise { + const pageSize = 2000; + let page = 1; + const events: DashboardHistoryEvent[] = []; + + while (page <= 500) { + const params = new HttpParams() + .set('page', String(page)) + .set('pageSize', String(pageSize)); + + const response = await firstValueFrom(this.http.get(`${this.baseApi}/${endpoint}`, { params })); + const itemsRaw = this.readNode(response, 'items', 'Items'); + const items = Array.isArray(response) + ? response + : (Array.isArray(itemsRaw) ? itemsRaw : []); + + events.push(...items.map((row: any) => this.mapHistoryEventRow(row, dateKeys))); + + if (Array.isArray(response)) break; + + const total = this.toNumberOrNull(this.readNode(response, 'total', 'Total')); + if (!items.length) break; + if (total !== null && events.length >= total) break; + if (items.length < pageSize) break; + page += 1; + } + + return events; + } + + private mapHistoryEventRow(row: any, dateKeys: string[]): DashboardHistoryEvent { + return { + mobileLineId: String(this.readNode(row, 'mobileLineId', 'MobileLineId', 'mobile_line_id') ?? '').trim(), + linhaAntiga: String(this.readNode(row, 'linhaAntiga', 'LinhaAntiga', 'linha_antiga') ?? ''), + linhaNova: String(this.readNode(row, 'linhaNova', 'LinhaNova', 'linha_nova') ?? ''), + date: this.parseDateValue(this.readNode(row, ...dateKeys)), + }; + } + + private resolveLinesByOperadora(filter: OperadoraFiltro): DashboardLineListItemDto[] { + if (filter === 'TODOS') return [...this.operatorLinesCache]; + + return this.operatorLinesCache.filter((line) => { + const conta = this.readNode(line as any, 'conta', 'Conta'); + const empresaConta = this.readNode( + line as any, + 'contaEmpresa', + 'ContaEmpresa', + 'empresaConta', + 'EmpresaConta', + 'empresa_conta', + 'Empresa_Conta', + 'empresa (conta)', + 'EMPRESA (CONTA)', + 'empresa', + 'Empresa' + ); + const operadora = resolveOperadoraContext({ + conta, + empresaConta, + accountCompanies: this.accountCompanies, + }).operadora; + return operadora === filter; + }); + } + + private applyOperadoraDerivedState( + lines: DashboardLineListItemDto[], + options?: { updateCoreMetrics?: boolean } + ): void { + this.filteredLinesCache = [...lines]; + const shouldUpdateCoreMetrics = options?.updateCoreMetrics === true; + + const now = new Date(); + const clientesSet = new Set(); + const usuariosSet = new Set(); + const planoMap = new Map(); + const clienteMap = new Map(); + const franquiaBandMap = new Map(); + const reservaDddMap = new Map(); + + const additionalCounts = { + gvd: 0, + skeelo: 0, + news: 0, + travel: 0, + sync: 0, + dispositivo: 0, + }; + + let totalLinhas = 0; + let ativas = 0; + let bloqueadas = 0; + let reservas = 0; + let perdaRoubo = 0; + let bloq120 = 0; + let outras = 0; + let franquiaVivoTotalGb = 0; + let franquiaLineTotalGb = 0; + let pfLinhas = 0; + let pjLinhas = 0; + let eSim = 0; + let simCard = 0; + let outrosChip = 0; + let comAdicionais = 0; + let semAdicionais = 0; + let travelCom = 0; + let vigenciaTotal = 0; + + const vigenciaBuckets: VigenciaBucketsDto = { + vencidos: 0, + aVencer0a30: 0, + aVencer31a60: 0, + aVencer61a90: 0, + acima90: 0, + }; + + const vigenciaFuturaMap = new Map(); + const vigenciaAxis = this.buildMonthAxis(0, 12); + vigenciaAxis.keys.forEach((key) => vigenciaFuturaMap.set(key, 0)); + + const lineIds = new Set(); + const lineDigits = new Set(); + + for (const line of lines) { + totalLinhas += 1; + + const lineId = String(this.readNode(line as any, 'id', 'Id') ?? '').trim(); + if (lineId) lineIds.add(lineId); + + const linhaDigits = this.normalizeLineDigits(this.readNode(line as any, 'linha', 'Linha')); + if (linhaDigits) lineDigits.add(linhaDigits); + + const cliente = this.readLineString(line, 'cliente', 'Cliente').trim(); + const usuario = this.readLineString(line, 'usuario', 'Usuario').trim(); + const statusRaw = this.readLineString(line, 'status', 'Status'); + const status = this.normalizeSeriesKey(statusRaw); + const skil = this.normalizeSeriesKey(this.readLineString(line, 'skil', 'Skil')); + const plano = this.readLineString(line, 'planoContrato', 'PlanoContrato').trim(); + const franquiaVivo = this.readLineNumber(line, 'franquiaVivo', 'FranquiaVivo'); + const franquiaLine = this.readLineNumber(line, 'franquiaLine', 'FranquiaLine'); + const tipoChip = this.normalizeChipType(this.readLineString(line, 'tipoDeChip', 'TipoDeChip')); + const isReserva = this.isReservaLine(line); + + if (cliente && !this.normalizeSeriesKey(cliente).includes('RESERVA')) { + clientesSet.add(cliente); + clienteMap.set(cliente, (clienteMap.get(cliente) ?? 0) + 1); + } + if (usuario && this.normalizeSeriesKey(usuario) !== 'RESERVA') { + usuariosSet.add(usuario); + } + + if (isReserva) { + reservas += 1; + const ddd = this.extractDddFromLine(this.readLineString(line, 'linha', 'Linha')) ?? '-'; + reservaDddMap.set(ddd, (reservaDddMap.get(ddd) ?? 0) + 1); + } else if (status.includes('ATIV')) { + ativas += 1; + } else if (status.includes('BLOQUE') || status.includes('PERDA') || status.includes('ROUBO') || status.includes('SUSPEN') || status.includes('CANCEL')) { + bloqueadas += 1; + if (status.includes('PERDA') || status.includes('ROUBO')) { + perdaRoubo += 1; + } else if (status.includes('120')) { + bloq120 += 1; + } else { + outras += 1; + } + } else { + outras += 1; + } + + if (!isReserva) { + const planoKey = plano || 'Sem plano'; + planoMap.set(planoKey, (planoMap.get(planoKey) ?? 0) + 1); + } + + if (skil.includes('FISICA') || skil === 'PF') pfLinhas += 1; + if (skil.includes('JURIDICA') || skil === 'PJ') pjLinhas += 1; + + if (franquiaVivo > 0) franquiaVivoTotalGb += franquiaVivo; + if (franquiaLine > 0) franquiaLineTotalGb += franquiaLine; + const faixa = this.resolveFranquiaLineBand(franquiaLine); + franquiaBandMap.set(faixa, (franquiaBandMap.get(faixa) ?? 0) + 1); + + if (tipoChip === 'ESIM') eSim += 1; + else if (tipoChip === 'SIMCARD') simCard += 1; + else outrosChip += 1; + + const hasPaidAdditional = this.hasAnyPaidAdditional(line); + if (hasPaidAdditional) comAdicionais += 1; + else semAdicionais += 1; + + if (this.readLineNumber(line, 'gestaoVozDados', 'GestaoVozDados') > 0) additionalCounts.gvd += 1; + if (this.readLineNumber(line, 'skeelo', 'Skeelo') > 0) additionalCounts.skeelo += 1; + if (this.readLineNumber(line, 'vivoNewsPlus', 'VivoNewsPlus') > 0) additionalCounts.news += 1; + if (this.readLineNumber(line, 'vivoTravelMundo', 'VivoTravelMundo') > 0) { + additionalCounts.travel += 1; + travelCom += 1; + } + if (this.readLineNumber(line, 'vivoSync', 'VivoSync') > 0) additionalCounts.sync += 1; + if (this.readLineNumber(line, 'vivoGestaoDispositivo', 'VivoGestaoDispositivo') > 0) additionalCounts.dispositivo += 1; + + const vencContaRaw = this.readNode(line as any, 'vencConta', 'VencConta'); + const vencConta = this.parseDateValue(vencContaRaw); + if (vencConta) { + vigenciaTotal += 1; + const diff = Math.floor((this.startOfDay(vencConta).getTime() - this.startOfDay(now).getTime()) / 86400000); + if (diff < 0) vigenciaBuckets.vencidos += 1; + else if (diff <= 30) vigenciaBuckets.aVencer0a30 += 1; + else if (diff <= 60) vigenciaBuckets.aVencer31a60 += 1; + else if (diff <= 90) vigenciaBuckets.aVencer61a90 += 1; + else vigenciaBuckets.acima90 += 1; + + const key = `${vencConta.getFullYear()}-${this.pad2(vencConta.getMonth() + 1)}`; + if (vigenciaFuturaMap.has(key)) { + vigenciaFuturaMap.set(key, (vigenciaFuturaMap.get(key) ?? 0) + 1); + } + } + } + + const muregFiltered = this.filterHistoryEventsByLines(this.operatorMuregCache, lineIds, lineDigits); + const trocaFiltered = this.filterHistoryEventsByLines(this.operatorTrocaCache, lineIds, lineDigits); + + const muregSeries = this.buildHistorySeries(muregFiltered, 11, 12); + const trocaSeries = this.buildHistorySeries(trocaFiltered, 11, 12); + const muregCutoff = this.startOfDay(new Date(now.getTime() - 30 * 86400000)); + const muregsUltimos30Dias = muregFiltered.filter((event) => event.date && this.startOfDay(event.date) >= muregCutoff).length; + const trocasUltimos30Dias = trocaFiltered.filter((event) => event.date && this.startOfDay(event.date) >= muregCutoff).length; + + const franquiaOrder = ['Sem franquia', 'Até 10 GB', '10 a 20 GB', '20 a 50 GB', 'Acima de 50 GB']; + this.franquiaLabels = franquiaOrder; + this.franquiaValues = franquiaOrder.map((label) => franquiaBandMap.get(label) ?? 0); + + this.adicionaisLabels = [...this.adicionaisLabelsPadrao]; + this.adicionaisValues = [ + additionalCounts.gvd, + additionalCounts.skeelo, + additionalCounts.news, + additionalCounts.travel, + additionalCounts.sync, + additionalCounts.dispositivo, + ]; + this.adicionaisTotals = this.adicionaisLabels.map(() => null); + this.travelLabels = ['Com Travel', 'Sem Travel']; + this.travelValues = [travelCom, Math.max(totalLinhas - travelCom, 0)]; + this.tipoChipLabels = ['e-SIM', 'SIMCARD', 'Outros']; + this.tipoChipValues = [eSim, simCard, outrosChip]; + + this.muregLabels = muregSeries.labels; + this.muregValues = muregSeries.values; + this.trocaLabels = trocaSeries.labels; + this.trocaValues = trocaSeries.values; + this.vigenciaLabels = vigenciaAxis.labels; + this.vigenciaValues = vigenciaAxis.keys.map((key) => vigenciaFuturaMap.get(key) ?? 0); + this.vigBuckets = vigenciaBuckets; + + this.statusResumo = { + total: totalLinhas, + ativos: ativas, + bloqueadas, + perdaRoubo, + bloq120, + reservas, + outras, + }; + + this.rebuildAdicionaisComparativo({ + totalLinesWithAnyPaidAdditional: comAdicionais, + totalLinesWithNoPaidAdditional: semAdicionais, + }); + + if (shouldUpdateCoreMetrics) { + this.dashboardRaw = { + totalLinhas, + clientesUnicos: clientesSet.size, + ativos: ativas, + bloqueados: bloqueadas, + reservas, + bloqueadosPerdaRoubo: perdaRoubo, + bloqueados120Dias: bloq120, + bloqueadosOutros: outras, + totalMuregs: muregFiltered.length, + muregsUltimos30Dias, + totalTrocas: trocaFiltered.length, + trocasUltimos30Dias, + totalVigenciaLinhas: vigenciaTotal, + vigenciaVencidos: vigenciaBuckets.vencidos, + vigenciaAVencer30: vigenciaBuckets.aVencer0a30, + userDataRegistros: totalLinhas, + userDataComCpf: 0, + userDataComEmail: 0, + }; + } + + if (shouldUpdateCoreMetrics) { + this.insights = this.buildSyntheticInsights( + totalLinhas, + ativas, + franquiaVivoTotalGb, + franquiaLineTotalGb, + comAdicionais, + semAdicionais, + pfLinhas, + pjLinhas + ); + } + + this.buildResumoDerivedFromLines(lines, clienteMap, planoMap, reservaDddMap, pfLinhas, pjLinhas); + this.buildVivoComparison(lines); + if (shouldUpdateCoreMetrics) { + this.rebuildPrimaryKpis(); + } + + this.insightsError = null; + this.resumoError = null; + this.resumoLoading = false; + this.resumoReady = true; + } + + private buildSyntheticInsights( + totalLinhas: number, + totalAtivas: number, + totalFranquiaVivo: number, + totalFranquiaLine: number, + comAdicionais: number, + semAdicionais: number, + pfLinhas: number, + pjLinhas: number + ): DashboardGeralInsightsDto { + return { + kpis: { + totalLinhas, + totalAtivas, + vivo: { + qtdLinhas: totalLinhas, + totalFranquiaGb: totalFranquiaVivo, + totalFranquiaLine: totalFranquiaLine, + }, + travelMundo: { + comTravel: this.travelValues[0] ?? 0, + semTravel: this.travelValues[1] ?? 0, + totalValue: 0, + }, + adicionais: { + totalLinesWithAnyPaidAdditional: comAdicionais, + totalLinesWithNoPaidAdditional: semAdicionais, + }, + totaisLine: [ + { tipo: 'PF', qtdLinhas: pfLinhas, valorTotalLine: null, lucroTotalLine: null }, + { tipo: 'PJ', qtdLinhas: pjLinhas, valorTotalLine: null, lucroTotalLine: null }, + ], + }, + charts: { + linhasPorFranquia: { + labels: this.franquiaLabels, + values: this.franquiaValues, + }, + adicionaisPagosPorServico: { + labels: this.adicionaisLabels, + values: this.adicionaisValues, + totals: this.adicionaisTotals, + }, + travelMundo: { + labels: this.travelLabels, + values: this.travelValues, + }, + tipoChip: { + labels: this.tipoChipLabels, + values: this.tipoChipValues, + }, + }, + }; + } + + private buildResumoDerivedFromLines( + lines: DashboardLineListItemDto[], + clienteMap: Map, + planoMap: Map, + reservaDddMap: Map, + pfLinhas: number, + pjLinhas: number + ): void { + const topClientes = Array.from(clienteMap.entries()) + .map(([cliente, qtd]) => ({ cliente, linhas: qtd })) + .sort((a, b) => b.linhas - a.linhas || a.cliente.localeCompare(b.cliente, 'pt-BR', { sensitivity: 'base' })) + .slice(0, this.resumoTopN); + + const topPlanos = Array.from(planoMap.entries()) + .map(([plano, qtd]) => ({ plano, linhas: qtd })) + .sort((a, b) => b.linhas - a.linhas || a.plano.localeCompare(b.plano, 'pt-BR', { sensitivity: 'base' })) + .slice(0, this.resumoTopN); + + const topReserva = Array.from(reservaDddMap.entries()) + .map(([ddd, total]) => ({ ddd, total, linhas: total })) + .sort((a, b) => b.total - a.total) + .slice(0, this.resumoTopN); + + this.resumoTopClientes = topClientes; + this.resumoClientesLabels = topClientes.map((item) => item.cliente); + this.resumoClientesValues = topClientes.map((item) => item.linhas); + + this.resumoTopPlanos = topPlanos; + this.resumoPlanosLabels = topPlanos.map((item) => item.plano); + this.resumoPlanosValues = topPlanos.map((item) => item.linhas); + + this.resumoTopReserva = topReserva; + this.resumoReservaLabels = topReserva.map((item) => item.ddd); + this.resumoReservaValues = topReserva.map((item) => item.total); + + this.resumoPfPjLabels = ['Pessoa Física', 'Pessoa Jurídica']; + this.resumoPfPjValues = [pfLinhas, pjLinhas]; + this.resumoDiferencaPjPf = { + pfLinhas, + pjLinhas, + totalLinhas: lines.length, + }; + } + + private buildVivoComparison(lines: DashboardLineListItemDto[]): void { + const comparison: DashboardVivoComparison = { + macrophonyLinhas: 0, + lineMovelLinhas: 0, + macrophonyComAdicionais: 0, + macrophonySemAdicionais: 0, + lineMovelComAdicionais: 0, + lineMovelSemAdicionais: 0, + }; + + lines.forEach((line) => { + const conta = this.readNode(line as any, 'conta', 'Conta'); + const empresaConta = this.readNode( + line as any, + 'contaEmpresa', + 'ContaEmpresa', + 'empresaConta', + 'EmpresaConta', + 'empresa_conta', + 'Empresa_Conta', + 'empresa (conta)', + 'EMPRESA (CONTA)', + 'empresa', + 'Empresa' + ); + + const context = resolveOperadoraContext({ + conta, + empresaConta, + accountCompanies: this.accountCompanies, + }); + if (context.operadora !== 'VIVO') return; + + const hasAdditional = this.hasAnyPaidAdditional(line); + if (context.vivoEmpresaGrupo === 'MACROPHONY') { + comparison.macrophonyLinhas += 1; + if (hasAdditional) comparison.macrophonyComAdicionais += 1; + else comparison.macrophonySemAdicionais += 1; + } else if (context.vivoEmpresaGrupo === 'LINE MOVEL') { + comparison.lineMovelLinhas += 1; + if (hasAdditional) comparison.lineMovelComAdicionais += 1; + else comparison.lineMovelSemAdicionais += 1; + } + }); + + this.vivoComparison = comparison; + } + + private hasAnyPaidAdditional(line: DashboardLineListItemDto): boolean { + return ( + this.readLineNumber(line, 'gestaoVozDados', 'GestaoVozDados') > 0 + || this.readLineNumber(line, 'skeelo', 'Skeelo') > 0 + || this.readLineNumber(line, 'vivoNewsPlus', 'VivoNewsPlus') > 0 + || this.readLineNumber(line, 'vivoTravelMundo', 'VivoTravelMundo') > 0 + || this.readLineNumber(line, 'vivoSync', 'VivoSync') > 0 + || this.readLineNumber(line, 'vivoGestaoDispositivo', 'VivoGestaoDispositivo') > 0 + ); + } + + private filterHistoryEventsByLines( + events: DashboardHistoryEvent[], + lineIds: Set, + lineDigits: Set + ): DashboardHistoryEvent[] { + return events.filter((event) => { + const eventLineId = String(event.mobileLineId ?? '').trim(); + if (eventLineId && lineIds.has(eventLineId)) return true; + + const linhaNova = this.normalizeLineDigits(event.linhaNova); + if (linhaNova && lineDigits.has(linhaNova)) return true; + + const linhaAntiga = this.normalizeLineDigits(event.linhaAntiga); + if (linhaAntiga && lineDigits.has(linhaAntiga)) return true; + + return false; + }); + } + + private buildHistorySeries(events: DashboardHistoryEvent[], monthsBack: number, count: number): { labels: string[]; values: number[] } { + const axis = this.buildMonthAxis(monthsBack, count); + const map = new Map(); + axis.keys.forEach((key) => map.set(key, 0)); + + events.forEach((event) => { + if (!event.date) return; + const key = `${event.date.getFullYear()}-${this.pad2(event.date.getMonth() + 1)}`; + if (!map.has(key)) return; + map.set(key, (map.get(key) ?? 0) + 1); + }); + + return { + labels: axis.labels, + values: axis.keys.map((key) => map.get(key) ?? 0), + }; + } + + private buildMonthAxis(monthsBack: number, count: number): { labels: string[]; keys: string[] } { + const now = new Date(); + const labels: string[] = []; + const keys: string[] = []; + + for (let i = monthsBack; i >= monthsBack - count + 1; i -= 1) { + const month = new Date(now.getFullYear(), now.getMonth() - i, 1); + const key = `${month.getFullYear()}-${this.pad2(month.getMonth() + 1)}`; + const label = month + .toLocaleDateString('pt-BR', { month: 'short', year: '2-digit' }) + .replace('.', '') + .replace(' de ', '/') + .trim(); + keys.push(key); + labels.push(label); + } + + return { labels, keys }; + } + + private parseDateValue(value: unknown): Date | null { + if (value instanceof Date) { + return Number.isNaN(value.getTime()) ? null : value; + } + + const raw = String(value ?? '').trim(); + if (!raw) return null; + + const br = raw.match(/^(\d{2})\/(\d{2})\/(\d{4})/); + if (br) { + const parsed = new Date(Number(br[3]), Number(br[2]) - 1, Number(br[1])); + return Number.isNaN(parsed.getTime()) ? null : parsed; + } + + const parsed = new Date(raw); + return Number.isNaN(parsed.getTime()) ? null : parsed; + } + + private normalizeLineDigits(value: unknown): string { + return String(value ?? '').replace(/\D/g, '').trim(); + } + + private startOfDay(value: Date): Date { + return new Date(value.getFullYear(), value.getMonth(), value.getDate()); } private applyDto(dto: DashboardDto) { @@ -1171,13 +2212,157 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { .replace(/[^A-Z0-9]/g, ''); } + private buildLineFieldAliases(camelCaseKey: string, pascalCaseKey: string): string[] { + const snakeCaseKey = pascalCaseKey + .replace(/([a-z0-9])([A-Z])/g, '$1_$2') + .toLowerCase(); + const flatCaseKey = snakeCaseKey.replace(/_/g, ''); + const spacedKey = pascalCaseKey.replace(/([a-z0-9])([A-Z])/g, '$1 $2'); + const kebabKey = snakeCaseKey.replace(/_/g, '-'); + + const aliases = new Set([ + camelCaseKey, + pascalCaseKey, + snakeCaseKey, + flatCaseKey, + kebabKey, + spacedKey, + spacedKey.toLowerCase(), + spacedKey.toUpperCase(), + flatCaseKey.toUpperCase(), + ]); + + return Array.from(aliases); + } + + private normalizeLooseFieldKey(value: unknown): string { + return String(value ?? '') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^A-Za-z0-9]/g, '') + .toUpperCase() + .trim(); + } + + private findLooseFieldValue(source: any, aliases: string[]): any { + if (!source || typeof source !== 'object') return undefined; + const targets = aliases + .map((alias) => this.normalizeLooseFieldKey(alias)) + .filter(Boolean); + if (!targets.length) return undefined; + + const entries = Object.entries(source as Record); + + for (const [entryKey, value] of entries) { + const normalizedEntry = this.normalizeLooseFieldKey(entryKey); + if (!normalizedEntry) continue; + if (targets.includes(normalizedEntry)) return value; + } + + for (const [entryKey, value] of entries) { + const normalizedEntry = this.normalizeLooseFieldKey(entryKey); + if (!normalizedEntry) continue; + if (targets.some((target) => normalizedEntry.includes(target) || target.includes(normalizedEntry))) { + return value; + } + } + + return undefined; + } + + private readLineRawField( + line: DashboardLineListItemDto, + camelCaseKey: keyof DashboardLineListItemDto, + pascalCaseKey: string + ): any { + const aliases = this.buildLineFieldAliases(String(camelCaseKey), pascalCaseKey); + const raw = this.readNode(line as any, ...aliases); + if (raw !== undefined) return raw; + const looseRaw = this.findLooseFieldValue(line as any, aliases); + if (looseRaw !== undefined) return looseRaw; + + const nested = this.readNode( + line as any, + 'financeiro', + 'Financeiro', + 'dadosFinanceiros', + 'DadosFinanceiros' + ); + if (nested && typeof nested === 'object') { + const nestedRaw = this.readNode(nested, ...aliases); + if (nestedRaw !== undefined) return nestedRaw; + return this.findLooseFieldValue(nested, aliases); + } + + return undefined; + } + + private readUnknownNumericField(source: any, camelCaseKey: string, pascalCaseKey: string): number | null { + if (!source || typeof source !== 'object') return null; + const aliases = this.buildLineFieldAliases(camelCaseKey, pascalCaseKey); + const raw = + this.readNode(source, ...aliases) + ?? this.findLooseFieldValue(source, aliases); + if (raw !== undefined) { + return this.toNumberOrNull(raw); + } + + const nested = this.readNode( + source, + 'financeiro', + 'Financeiro', + 'dadosFinanceiros', + 'DadosFinanceiros' + ); + if (nested && typeof nested === 'object') { + const nestedRaw = + this.readNode(nested, ...aliases) + ?? this.findLooseFieldValue(nested, aliases); + return this.toNumberOrNull(nestedRaw); + } + + return null; + } + + private readCachedFranquiaByLine( + line: DashboardLineListItemDto, + camelCaseKey: keyof DashboardLineListItemDto + ): number | null { + const id = String(this.readNode(line as any, 'id', 'Id') ?? '').trim(); + if (!id) return null; + + const cached = this.lineFranquiaCacheById.get(id); + if (!cached) return null; + + if (camelCaseKey === 'franquiaVivo') return cached.franquiaVivo; + if (camelCaseKey === 'franquiaLine') return cached.franquiaLine; + return null; + } + private readLineNumber(line: DashboardLineListItemDto, camelCaseKey: keyof DashboardLineListItemDto, pascalCaseKey: string): number { - const raw = this.readNode(line as any, String(camelCaseKey), pascalCaseKey); - return this.toNumberOrNull(raw) ?? 0; + const raw = this.readLineRawField(line, camelCaseKey, pascalCaseKey); + const value = this.toNumberOrNull(raw); + if (value !== null) return value; + + const cached = this.readCachedFranquiaByLine(line, camelCaseKey); + return cached ?? 0; + } + + private sumLineField( + lines: DashboardLineListItemDto[] | null | undefined, + camelCaseKey: keyof DashboardLineListItemDto, + pascalCaseKey: string + ): number { + if (!Array.isArray(lines) || lines.length === 0) return 0; + return lines.reduce((acc, line) => { + const value = this.readLineNumber(line, camelCaseKey, pascalCaseKey); + if (!Number.isFinite(value)) return acc; + return acc + value; + }, 0); } private readLineString(line: DashboardLineListItemDto, camelCaseKey: keyof DashboardLineListItemDto, pascalCaseKey: string): string { - const raw = this.readNode(line as any, String(camelCaseKey), pascalCaseKey) ?? ''; + const raw = this.readLineRawField(line, camelCaseKey, pascalCaseKey) ?? ''; return String(raw); } @@ -1335,13 +2520,15 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { ?? (this.resumo?.vivoLineResumos ?? []).reduce( (acc, row) => acc + (this.toNumberOrNull(row?.franquiaTotal) ?? 0), 0 - ); + ) + ?? (this.isOperadoraFiltroAtivo + ? (this.sumLineField(this.filteredLinesCache, 'franquiaVivo', 'FranquiaVivo') ?? 0) + : 0); add( 'franquia_vivo_total', - 'Total Franquia Vivo', + 'Total Franquia Contratada', this.formatDataAllowance(franquiaVivoTotal), - 'bi bi-diagram-3-fill', - 'Soma das franquias (Geral)' + 'bi bi-diagram-3-fill' ); const franquiaLineTotal = this.toNumberOrNull(insights?.vivo?.totalFranquiaLine) @@ -1349,7 +2536,10 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { ?? (this.resumo?.vivoLineResumos ?? []).reduce( (acc, row) => acc + (this.toNumberOrNull(row?.franquiaLine) ?? 0), 0 - ); + ) + ?? (this.isOperadoraFiltroAtivo + ? (this.sumLineField(this.filteredLinesCache, 'franquiaLine', 'FranquiaLine') ?? 0) + : 0); add( 'franquia_line_total', 'Total Franquia Line', @@ -1401,6 +2591,8 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { this.chartAdicionaisPagos?.nativeElement, this.chartTipoChip?.nativeElement, this.chartTravelMundo?.nativeElement, + this.chartVivoEmpresasLinhas?.nativeElement, + this.chartVivoEmpresasAdicionais?.nativeElement, ].filter(Boolean) as HTMLCanvasElement[]; if (!canvases.length) return; @@ -1610,6 +2802,83 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { } }); } + + if (this.showVivoComparison) { + if (this.chartVivoEmpresasLinhas?.nativeElement) { + this.chartVivoComparacaoLinhas = new Chart(this.chartVivoEmpresasLinhas.nativeElement, { + type: 'bar', + data: { + labels: ['MACROPHONY', 'LINE MÓVEL'], + datasets: [{ + label: 'Linhas', + data: [this.vivoComparison.macrophonyLinhas, this.vivoComparison.lineMovelLinhas], + backgroundColor: [palette.brand, palette.blue], + borderRadius: 8, + borderWidth: 0, + }], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { + callbacks: { + label: (ctx) => ` ${this.formatInt(ctx.raw as number)} linhas`, + }, + }, + }, + scales: { + x: { grid: { display: false }, border: { display: false } }, + y: { beginAtZero: true, grid: { color: '#f1f5f9' }, border: { display: false } }, + }, + }, + }); + } + + if (this.chartVivoEmpresasAdicionais?.nativeElement) { + this.chartVivoComparacaoAdicionais = new Chart(this.chartVivoEmpresasAdicionais.nativeElement, { + type: 'bar', + data: { + labels: ['MACROPHONY', 'LINE MÓVEL'], + datasets: [ + { + label: 'Com adicionais', + data: [this.vivoComparison.macrophonyComAdicionais, this.vivoComparison.lineMovelComAdicionais], + backgroundColor: palette.purple, + borderRadius: 6, + borderWidth: 0, + }, + { + label: 'Sem adicionais', + data: [this.vivoComparison.macrophonySemAdicionais, this.vivoComparison.lineMovelSemAdicionais], + backgroundColor: '#cbd5e1', + borderRadius: 6, + borderWidth: 0, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'bottom' }, + tooltip: { + callbacks: { + label: (ctx) => ` ${ctx.dataset.label}: ${this.formatInt(ctx.raw as number)}`, + }, + }, + }, + scales: { + x: { stacked: false, grid: { display: false }, border: { display: false } }, + y: { beginAtZero: true, grid: { color: '#f1f5f9' }, border: { display: false } }, + }, + }, + }); + } + } + + this.refreshExpandedChartIfOpen(); } private buildResumoCharts() { @@ -1695,6 +2964,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { }); } + this.refreshExpandedChartIfOpen(); } // Helper for consistent bar charts @@ -1796,6 +3066,8 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { this.chartFranquia?.destroy(); this.chartAdicionais?.destroy(); this.chartTipoChipDistribuicao?.destroy(); + this.chartVivoComparacaoLinhas?.destroy(); + this.chartVivoComparacaoAdicionais?.destroy(); this.chartPie = undefined; this.chartAdicionaisComparativoDoughnut = undefined; @@ -1807,6 +3079,8 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { this.chartFranquia = undefined; this.chartAdicionais = undefined; this.chartTipoChipDistribuicao = undefined; + this.chartVivoComparacaoLinhas = undefined; + this.chartVivoComparacaoAdicionais = undefined; } private destroyResumoCharts() { @@ -1821,6 +3095,177 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { this.chartResumoPfPj = undefined; } + private getChartInstanceByModalKey(key: DashboardChartModalKey): Chart | undefined { + switch (key) { + case 'status': return this.chartPie; + case 'adicionaisComparativo': return this.chartAdicionaisComparativoDoughnut; + case 'vigenciaBuckets': return this.chartVigSuper; + case 'travel': return this.chartTravel; + case 'linhasFranquia': return this.chartFranquia; + case 'adicionaisPagos': return this.chartAdicionais; + case 'tipoChip': return this.chartTipoChipDistribuicao; + case 'vivoEmpresasLinhas': return this.chartVivoComparacaoLinhas; + case 'vivoEmpresasAdicionais': return this.chartVivoComparacaoAdicionais; + case 'resumoTopClientes': return this.chartResumoClientes; + case 'resumoTopPlanos': return this.chartResumoPlanos; + case 'resumoPfPj': return this.chartResumoPfPj; + case 'resumoReservaDdd': return this.chartResumoReserva; + case 'mureg12': return this.chartMureg; + case 'troca12': return this.chartTroca; + case 'vigenciaMesAno': return this.chartVigMesAno; + default: return undefined; + } + } + + private refreshExpandedChartIfOpen(): void { + if (!this.chartModalOpen || !this.chartModalKey) return; + const sourceChart = this.getChartInstanceByModalKey(this.chartModalKey); + if (!sourceChart) return; + this.updateChartModalInfo(sourceChart); + this.renderExpandedChart(sourceChart); + } + + private renderExpandedChart(sourceChart: Chart): void { + if (!this.chartModalOpen) return; + + const canvas = this.chartExpandedCanvas?.nativeElement; + if (!canvas) return; + + this.destroyExpandedChart(); + + const sourceConfig = sourceChart.config as any; + const expandedConfig = { + type: sourceConfig.type, + data: this.cloneChartValue(sourceConfig.data), + options: this.cloneChartValue(sourceConfig.options ?? {}), + plugins: this.cloneChartValue(sourceConfig.plugins ?? []), + } as any; + + expandedConfig.options = expandedConfig.options ?? {}; + expandedConfig.options.responsive = true; + expandedConfig.options.maintainAspectRatio = false; + + this.chartExpanded = new Chart(canvas, expandedConfig); + } + + private destroyExpandedChart(): void { + try { + this.chartExpanded?.destroy(); + } catch {} + this.chartExpanded = undefined; + } + + private cloneChartValue(value: T): T { + if (value === null || value === undefined) return value; + if (typeof value === 'function') return value; + if (typeof value !== 'object') return value; + if (value instanceof Date) return new Date(value.getTime()) as T; + if (Array.isArray(value)) { + return value.map((entry) => this.cloneChartValue(entry)) as T; + } + + const output: Record = {}; + Object.entries(value as Record).forEach(([key, entry]) => { + output[key] = this.cloneChartValue(entry); + }); + return output as T; + } + + private updateChartModalInfo(sourceChart: Chart): void { + const labelsRaw = Array.isArray(sourceChart.data?.labels) ? sourceChart.data.labels : []; + const datasetsRaw = Array.isArray(sourceChart.data?.datasets) ? sourceChart.data.datasets : []; + + const datasetHeaders = datasetsRaw.map((dataset, index) => { + const raw = (dataset as any)?.label; + const text = String(raw ?? '').trim(); + return text || `Série ${index + 1}`; + }); + + const maxDataLength = datasetsRaw.reduce((max, dataset) => { + const data = Array.isArray((dataset as any)?.data) ? (dataset as any).data : []; + return Math.max(max, data.length); + }, 0); + + const rowCount = Math.max(labelsRaw.length, maxDataLength); + + const rows: DashboardChartModalInfoRow[] = []; + for (let index = 0; index < rowCount; index += 1) { + const label = this.resolveChartModalRowLabel(labelsRaw, index); + const cells = datasetsRaw.map((dataset, datasetIndex) => { + const dataArray = Array.isArray((dataset as any)?.data) ? (dataset as any).data : []; + const numericValue = this.toChartPointNumber(dataArray[index]); + return { + dataset: datasetHeaders[datasetIndex] ?? `Série ${datasetIndex + 1}`, + valueText: this.formatChartModalValue(numericValue), + numericValue, + }; + }); + rows.push({ label, cells }); + } + + const totals = datasetHeaders.map((dataset, datasetIndex) => { + const sum = rows.reduce((acc, row) => { + const value = row.cells[datasetIndex]?.numericValue; + if (value === null) return acc; + return acc + value; + }, 0); + return { + dataset, + totalText: this.formatChartModalValue(sum), + }; + }); + + this.chartModalDatasetHeaders = datasetHeaders; + this.chartModalInfoRows = rows; + this.chartModalDatasetTotals = totals; + } + + private resolveChartModalRowLabel(labelsRaw: unknown[], index: number): string { + const raw = labelsRaw[index]; + const text = String(raw ?? '').trim(); + if (text) return text; + return `Item ${index + 1}`; + } + + private toChartPointNumber(value: unknown): number | null { + if (value === null || value === undefined) return null; + + const direct = this.toNumberOrNull(value); + if (direct !== null) return direct; + + if (typeof value === 'object') { + const nested = value as Record; + const fromY = this.toNumberOrNull(nested.y); + if (fromY !== null) return fromY; + const fromR = this.toNumberOrNull(nested.r); + if (fromR !== null) return fromR; + } + + return null; + } + + private formatChartModalValue(value: number | null): string { + if (value === null) return '-'; + if (Number.isInteger(value)) return this.formatInt(value); + return value.toLocaleString('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); + } + + private lockBodyScrollForChartModal(): void { + if (!isPlatformBrowser(this.platformId)) return; + const body = window.document.body; + if (this.bodyOverflowBeforeChartModal === null) { + this.bodyOverflowBeforeChartModal = body.style.overflow; + } + body.style.overflow = 'hidden'; + } + + private restoreBodyScrollForChartModal(): void { + if (!isPlatformBrowser(this.platformId)) return; + if (this.bodyOverflowBeforeChartModal === null) return; + window.document.body.style.overflow = this.bodyOverflowBeforeChartModal; + this.bodyOverflowBeforeChartModal = null; + } + private normalizeResumo(data: ResumoResponse): ResumoResponse { // Helper to ensure arrays are arrays return { @@ -1894,7 +3339,12 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { return Number.isNaN(n) ? null : n; } + private pad2(value: number): string { + return value.toString().padStart(2, '0'); + } + trackByKpiKey = (_: number, item: KpiCard) => item.key; + trackByOperadoraFilter = (_: number, item: { value: OperadoraFiltro }) => item.value; isKpiClickable(card: KpiCard): boolean { return !!this.kpiNavigationMap[card.key]; @@ -1908,7 +3358,8 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { }); } - onKpiCardKeydown(event: KeyboardEvent, card: KpiCard): void { + onKpiCardKeydown(event: Event, card: KpiCard): void { + if (!(event instanceof KeyboardEvent)) return; if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); this.onKpiClick(card); diff --git a/src/app/pages/geral/geral.html b/src/app/pages/geral/geral.html index 2937efe..a46a1cb 100644 --- a/src/app/pages/geral/geral.html +++ b/src/app/pages/geral/geral.html @@ -75,180 +75,214 @@ -
-
- - - - - - - - - - - -
- - -
- + + - - -
- - {{ client }} - - -
- + + + + - - -
- - -
-
- - -
-
-
Modo
-
- - - -
+
+
+
+
-
-
Serviços
-
- +
+ +
+
+ + +
+ + +
+ + +
+
- diff --git a/src/app/pages/geral/geral.scss b/src/app/pages/geral/geral.scss index 38116a6..f3fa7c0 100644 --- a/src/app/pages/geral/geral.scss +++ b/src/app/pages/geral/geral.scss @@ -123,11 +123,35 @@ .btn-glass { border-radius: 12px; font-weight: 900; background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(3, 15, 170, 0.25); color: var(--blue); &:hover { transform: translateY(-2px); border-color: var(--brand); background: #fff; } } /* Filtros e Multi-Select */ -.filters-row { display: flex; justify-content: center; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 10px; } +.filters-stack { + display: flex; + flex-direction: column; + gap: 10px; +} + +.filters-row { + display: flex; + justify-content: center; + align-items: flex-end; + gap: 12px; + flex-wrap: wrap; + margin-top: 0; + position: relative; + z-index: 30; + overflow: visible; +} + +.filters-row-top { + justify-content: center; +} + +.filters-row-bottom { + justify-content: center; +} .filter-tabs { display: flex; gap: 4px; padding: 4px; background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(17, 18, 20, 0.08); border-radius: 12px; backdrop-filter: blur(8px); } .filter-tab { border: none; background: transparent; padding: 8px 16px; border-radius: 8px; font-size: 0.85rem; font-weight: 700; color: var(--muted); transition: all 0.2s ease; display: flex; align-items: center; gap: 6px; &:hover { color: var(--text); background: rgba(255, 255, 255, 0.5); } &.active { background: #fff; color: var(--brand); box-shadow: 0 2px 8px rgba(227, 61, 207, 0.15); } &:disabled { opacity: 0.5; cursor: not-allowed; } } -.client-filter-wrap { position: relative; } +.client-filter-wrap { position: relative; z-index: 40; } .btn-client-filter { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 12px; border: 1px solid rgba(17, 18, 20, 0.08); background: rgba(255, 255, 255, 0.6); color: var(--muted); font-weight: 700; font-size: 0.85rem; backdrop-filter: blur(8px); transition: all 0.2s; min-height: 38px; height: auto; flex-wrap: wrap; &:hover { background: #fff; border-color: var(--blue); color: var(--blue); } &.active, &.has-selection { background: #fff; border-color: var(--brand); } } .chips-container { display: flex; flex-wrap: wrap; gap: 6px; max-width: 400px; } .client-chip { display: inline-flex; align-items: center; background: rgba(227, 61, 207, 0.1); color: var(--brand); border: 1px solid rgba(227, 61, 207, 0.2); border-radius: 6px; padding: 2px 6px; font-size: 0.75rem; font-weight: 800; cursor: default; user-select: none; } @@ -140,6 +164,34 @@ .additional-filter-wrap { position: relative; + z-index: 40; +} + +.operadora-empresa-filters { + display: flex; + align-items: flex-end; + gap: 10px; + flex-wrap: wrap; + position: relative; + z-index: 50; +} + +.filter-select-box { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 190px; + position: relative; + z-index: 50; +} + +.filter-select-label { + font-size: 0.66rem; + font-weight: 900; + letter-spacing: 0.05em; + text-transform: uppercase; + color: rgba(17, 18, 20, 0.58); + padding-left: 2px; } .btn-additional-filter { @@ -249,6 +301,18 @@ } } +@media (max-width: 768px) { + .operadora-empresa-filters { + width: 100%; + justify-content: center; + } + + .filter-select-box { + flex: 1 1 220px; + min-width: 0; + } +} + /* KPIs */ .geral-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 20px; margin-bottom: 16px; width: 100%; @media (max-width: 992px) { grid-template-columns: repeat(2, 1fr); } @media (max-width: 576px) { grid-template-columns: 1fr; } } .geral-kpis.geral-kpis-client { diff --git a/src/app/pages/geral/geral.spec.ts b/src/app/pages/geral/geral.spec.ts index 05f32d1..07f461d 100644 --- a/src/app/pages/geral/geral.spec.ts +++ b/src/app/pages/geral/geral.spec.ts @@ -73,4 +73,36 @@ describe('Geral', () => { expect(component.createBatchLines[0].linha).toBe('11888888888'); expect(component.createBatchLines[0]['planoContrato']).toBe('PLANO B'); }); + + it('should apply TIM filter in client-side pipeline using conta TIM textual', () => { + component.filterOperadora = 'TIM'; + component.filterContaEmpresa = ''; + component.filterStatus = 'ALL'; + component.additionalMode = 'ALL'; + component.selectedAdditionalServices = []; + + const filtered = (component as any).applyAdditionalFiltersClientSide([ + { id: '1', item: 1, conta: 'TIM', linha: '11911111111', cliente: 'A', usuario: 'U', vencConta: null, status: 'ATIVO' }, + { id: '2', item: 2, conta: '455371844', linha: '11922222222', cliente: 'B', usuario: 'U', vencConta: null, status: 'ATIVO' }, + ]); + + expect(filtered.length).toBe(1); + expect(filtered[0].conta).toBe('TIM'); + }); + + it('should combine operadora and empresa filters for VIVO MACROPHONY', () => { + component.filterOperadora = 'VIVO'; + component.filterContaEmpresa = 'VIVO MACROPHONY'; + component.filterStatus = 'ALL'; + component.additionalMode = 'ALL'; + component.selectedAdditionalServices = []; + + const filtered = (component as any).applyAdditionalFiltersClientSide([ + { id: '1', item: 1, conta: '460161507', linha: '11911111111', cliente: 'A', usuario: 'U', vencConta: null, status: 'ATIVO' }, + { id: '2', item: 2, conta: '0435288088', linha: '11922222222', cliente: 'B', usuario: 'U', vencConta: null, status: 'ATIVO' }, + ]); + + expect(filtered.length).toBe(1); + expect(filtered[0].conta).toBe('460161507'); + }); }); diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts index 0bd8044..2410ed4 100644 --- a/src/app/pages/geral/geral.ts +++ b/src/app/pages/geral/geral.ts @@ -45,11 +45,20 @@ import { buildBatchMassPreview, mergeMassRows } from './batch-mass-input.util'; +import { + DEFAULT_ACCOUNT_COMPANIES, + mergeAccountCompaniesWithDefaults, + normalizeConta as normalizeContaValue, + resolveEmpresaByConta, + resolveOperadoraContext, + sameConta as sameContaValue, +} from '../../utils/account-operator.util'; type SortDir = 'asc' | 'desc'; type CreateMode = 'NEW_CLIENT' | 'NEW_LINE_IN_GROUP'; type CreateEntryMode = 'SINGLE' | 'BATCH'; type AdditionalMode = 'ALL' | 'WITH' | 'WITHOUT'; +type OperadoraFilterMode = 'ALL' | 'VIVO' | 'CLARO' | 'TIM'; type AdditionalServiceKey = 'gvd' | 'skeelo' | 'news' | 'travel' | 'sync' | 'dispositivo'; type BlockedStatusMode = 'ALL' | 'PERDA_ROUBO' | 'BLOQUEIO_120'; @@ -79,6 +88,9 @@ interface ApiPagedResult { interface ApiLineList { id: string; item: number; + conta?: string | null; + contaEmpresa?: string | null; + empresaConta?: string | null; linha: string | null; chip?: string | null; cliente: string | null; @@ -361,6 +373,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { blockedStatusMode: BlockedStatusMode = 'ALL'; additionalMode: AdditionalMode = 'ALL'; selectedAdditionalServices: AdditionalServiceKey[] = []; + filterOperadora: OperadoraFilterMode = 'ALL'; + filterContaEmpresa = ''; readonly additionalServiceOptions: Array<{ key: AdditionalServiceKey; label: string }> = [ { key: 'gvd', label: 'Gestão Voz e Dados' }, { key: 'skeelo', label: 'Skeelo' }, @@ -369,6 +383,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { { key: 'sync', label: 'Vivo Sync' }, { key: 'dispositivo', label: 'Vivo Gestão Dispositivo' } ]; + readonly operadoraFilterOptions: Array<{ label: string; value: OperadoraFilterMode }> = [ + { label: 'Todas operadoras', value: 'ALL' }, + { label: 'VIVO', value: 'VIVO' }, + { label: 'CLARO', value: 'CLARO' }, + { label: 'TIM', value: 'TIM' }, + ]; clientsList: string[] = []; loadingClientsList = false; @@ -472,12 +492,10 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { 'M2M 50MB' ]; - private readonly fallbackAccountCompanies: AccountCompanyOption[] = [ - { empresa: 'CLARO LINE MÓVEL', contas: ['172593311', '172593840'] }, - { empresa: 'VIVO MACROPHONY', contas: ['0430237019', '0437488125', '0449508564', '0454371844'] }, - { empresa: 'VIVO LINE MÓVEL', contas: ['0435288088'] }, - { empresa: 'TIM LINE MÓVEL', contas: ['0072046192'] } - ]; + private readonly fallbackAccountCompanies: AccountCompanyOption[] = DEFAULT_ACCOUNT_COMPANIES.map((group) => ({ + empresa: group.empresa, + contas: [...group.contas], + })); accountCompanies: AccountCompanyOption[] = [...this.fallbackAccountCompanies]; loadingAccountCompanies = false; @@ -489,6 +507,15 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return this.accountCompanies.map((x) => x.empresa); } + get contaEmpresaFilterOptions(): Array<{ label: string; value: string }> { + const empresas = this.getContaEmpresaOptionsByOperadora(this.filterOperadora); + const merged = this.mergeOption(this.filterContaEmpresa, empresas); + return [ + { label: 'Todas empresas', value: '' }, + ...merged.map((empresa) => ({ label: empresa, value: empresa })), + ]; + } + get contaOptionsForCreate(): string[] { return this.getContasByEmpresa(this.createModel?.contaEmpresa); } @@ -740,8 +767,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return this.additionalMode !== 'ALL' || this.selectedAdditionalServices.length > 0; } + get hasOperadoraEmpresaFiltersApplied(): boolean { + return this.filterOperadora !== 'ALL' || !!this.filterContaEmpresa.trim(); + } + get hasClientSideFiltersApplied(): boolean { - return this.hasAdditionalFiltersApplied || this.filterStatus === 'BLOCKED'; + return this.hasAdditionalFiltersApplied || this.filterStatus === 'BLOCKED' || this.hasOperadoraEmpresaFiltersApplied; } get additionalModeLabel(): string { @@ -1042,17 +1073,19 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.http.get(`${this.apiBase}/account-companies`).subscribe({ next: (data) => { const normalized = this.normalizeAccountCompanies(data); - this.accountCompanies = - normalized.length > 0 ? normalized : [...this.fallbackAccountCompanies]; + const source = normalized.length > 0 ? normalized : this.fallbackAccountCompanies; + this.accountCompanies = mergeAccountCompaniesWithDefaults(source); this.loadingAccountCompanies = false; + this.syncContaEmpresaFilterByOperadora(); this.syncContaEmpresaSelection(this.createModel); this.syncContaEmpresaSelection(this.editModel); this.cdr.detectChanges(); }, error: () => { - this.accountCompanies = [...this.fallbackAccountCompanies]; + this.accountCompanies = mergeAccountCompaniesWithDefaults(this.fallbackAccountCompanies); this.loadingAccountCompanies = false; + this.syncContaEmpresaFilterByOperadora(); this.syncContaEmpresaSelection(this.createModel); this.syncContaEmpresaSelection(this.editModel); @@ -1494,6 +1527,32 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.refreshData(); } + setOperadoraFilter(mode: OperadoraFilterMode) { + if (this.isClientRestricted) return; + this.filterOperadora = mode; + this.syncContaEmpresaFilterByOperadora(); + this.expandedGroup = null; + this.groupLines = []; + this.searchResolvedClient = null; + this.page = 1; + + this.loadClients(); + this.refreshData(); + } + + setContaEmpresaFilter(empresa: string) { + if (this.isClientRestricted) return; + const next = (empresa ?? '').toString().trim(); + this.filterContaEmpresa = next; + this.expandedGroup = null; + this.groupLines = []; + this.searchResolvedClient = null; + this.page = 1; + + this.loadClients(); + this.refreshData(); + } + private applyBaseFilters(params: HttpParams): HttpParams { let next = params; @@ -1577,6 +1636,10 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return false; } + if (!this.matchesOperadoraContaEmpresaFilters(line)) { + return false; + } + const selected = this.selectedAdditionalServices; const hasSelected = selected.length > 0; @@ -1600,6 +1663,40 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return true; } + private matchesOperadoraContaEmpresaFilters(line: ApiLineList): boolean { + const hasOperadora = this.filterOperadora !== 'ALL'; + const selectedEmpresa = this.filterContaEmpresa.trim(); + const hasEmpresa = !!selectedEmpresa; + if (!hasOperadora && !hasEmpresa) return true; + + const conta = (line as any)?.conta ?? (line as any)?.Conta ?? ''; + const empresaConta = (line as any)?.contaEmpresa + ?? (line as any)?.empresaConta + ?? (line as any)?.ContaEmpresa + ?? (line as any)?.EmpresaConta + ?? (line as any)?.empresa_conta + ?? (line as any)?.Empresa_Conta + ?? (line as any)?.['empresa (conta)'] + ?? (line as any)?.['EMPRESA (CONTA)'] + ?? ''; + + const context = resolveOperadoraContext({ + conta, + empresaConta, + accountCompanies: this.accountCompanies, + }); + + if (hasOperadora && context.operadora !== this.filterOperadora) { + return false; + } + + if (!hasEmpresa) return true; + + const resolvedEmpresa = (context.empresaConta || this.findEmpresaByConta(conta) || '').toString().trim(); + if (!resolvedEmpresa) return false; + return this.normalizeFilterToken(resolvedEmpresa) === this.normalizeFilterToken(selectedEmpresa); + } + private applyAdditionalFiltersClientSide(lines: ApiLineList[]): ApiLineList[] { if (!Array.isArray(lines) || lines.length === 0) return []; return lines.filter((line) => this.matchesAdditionalFilters(line)); @@ -2411,6 +2508,13 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { parts.push(this.selectedAdditionalServices.join('-')); } + if (this.filterOperadora !== 'ALL') { + parts.push(`operadora-${this.filterOperadora.toLowerCase()}`); + } + if (this.filterContaEmpresa.trim()) { + parts.push(`empresa-${this.normalizeFilterToken(this.filterContaEmpresa).toLowerCase()}`); + } + return parts.join('_'); } @@ -4538,26 +4642,46 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return found ? [...found.contas] : []; } - private findEmpresaByConta(conta: any): string { - const target = this.normalizeConta(conta); - if (!target) return ''; + private getContaEmpresaOptionsByOperadora(mode: OperadoraFilterMode): string[] { + const empresas = this.mergeOptionList([], this.accountCompanies.map((group) => group?.empresa ?? '')) + .filter((empresa) => !!(empresa ?? '').toString().trim()); - const found = this.accountCompanies.find((group) => - (group.contas ?? []).some((c) => this.sameConta(c, target)) - ); - return found?.empresa ?? ''; + const filtered = mode === 'ALL' + ? empresas + : empresas.filter((empresa) => { + const operadora = resolveOperadoraContext({ + empresaConta: empresa, + accountCompanies: this.accountCompanies, + }).operadora; + return operadora === mode; + }); + + return filtered.sort((a, b) => a.localeCompare(b, 'pt-BR', { sensitivity: 'base' })); + } + + private syncContaEmpresaFilterByOperadora(): void { + const selected = this.filterContaEmpresa.trim(); + if (!selected) return; + + const available = this.getContaEmpresaOptionsByOperadora(this.filterOperadora); + const normalizedSelected = this.normalizeFilterToken(selected); + const hasSelected = available.some((empresa) => this.normalizeFilterToken(empresa) === normalizedSelected); + + if (!hasSelected) { + this.filterContaEmpresa = ''; + } + } + + private findEmpresaByConta(conta: any): string { + return resolveEmpresaByConta(conta, this.accountCompanies); } private normalizeConta(value: any): string { - const raw = (value ?? '').toString().trim(); - if (!raw) return ''; - if (!/^\d+$/.test(raw)) return raw.toUpperCase(); - const noLeadingZero = raw.replace(/^0+/, ''); - return noLeadingZero || '0'; + return normalizeContaValue(value); } private sameConta(a: any, b: any): boolean { - return this.normalizeConta(a) === this.normalizeConta(b); + return sameContaValue(a, b); } private syncContaEmpresaSelection(model: any) { diff --git a/src/app/utils/account-operator.util.spec.ts b/src/app/utils/account-operator.util.spec.ts new file mode 100644 index 0000000..ed1c014 --- /dev/null +++ b/src/app/utils/account-operator.util.spec.ts @@ -0,0 +1,80 @@ +import { + DEFAULT_ACCOUNT_COMPANIES, + mergeAccountCompaniesWithDefaults, + normalizeConta, + resolveEmpresaByConta, + resolveOperadoraContext, + sameConta, +} from './account-operator.util'; + +describe('account-operator.util', () => { + it('normaliza contas removendo zeros a esquerda', () => { + expect(normalizeConta('0455371844')).toBe('455371844'); + expect(normalizeConta('000187890982')).toBe('187890982'); + }); + + it('compara contas normalizadas', () => { + expect(sameConta('0435288088', '435288088')).toBeTrue(); + expect(sameConta('172593311', '172593840')).toBeFalse(); + }); + + it('resolve empresa por conta com regras deterministicas obrigatorias', () => { + expect(resolveEmpresaByConta('455371844', [])).toBe('VIVO MACROPHONY'); + expect(resolveEmpresaByConta('460161507', [])).toBe('VIVO MACROPHONY'); + expect(resolveEmpresaByConta('187890982', [])).toBe('CLARO LINE MÓVEL'); + expect(resolveEmpresaByConta('TIM', [])).toBe('TIM LINE MÓVEL'); + }); + + it('mescla lista da API com defaults sem perder contas obrigatorias', () => { + const merged = mergeAccountCompaniesWithDefaults([ + { empresa: 'VIVO MACROPHONY', contas: ['0430237019'] }, + ]); + + const vivo = merged.find((group) => group.empresa === 'VIVO MACROPHONY'); + const contas = (vivo?.contas ?? []).map((value) => normalizeConta(value)); + + expect(contas).toContain(normalizeConta('455371844')); + expect(contas).toContain(normalizeConta('460161507')); + expect(contas).toContain(normalizeConta('0430237019')); + }); + + it('classifica operadora e grupo da vivo por contexto', () => { + const vivo = resolveOperadoraContext({ + conta: '455371844', + accountCompanies: DEFAULT_ACCOUNT_COMPANIES, + }); + expect(vivo.operadora).toBe('VIVO'); + expect(vivo.vivoEmpresaGrupo).toBe('MACROPHONY'); + + const claro = resolveOperadoraContext({ + conta: '187890982', + accountCompanies: DEFAULT_ACCOUNT_COMPANIES, + }); + expect(claro.operadora).toBe('CLARO'); + expect(claro.vivoEmpresaGrupo).toBeNull(); + + const tim = resolveOperadoraContext({ + empresaConta: 'TIM LINE MÓVEL', + accountCompanies: DEFAULT_ACCOUNT_COMPANIES, + }); + expect(tim.operadora).toBe('TIM'); + + const timByConta = resolveOperadoraContext({ + conta: 'TIM', + accountCompanies: DEFAULT_ACCOUNT_COMPANIES, + }); + expect(timByConta.operadora).toBe('TIM'); + }); + + it('prioriza mapeamento deterministico por conta mesmo com empresa da linha divergente', () => { + const vivoDeterministico = resolveOperadoraContext({ + conta: '455371844', + empresaConta: 'VIVO LINE MÓVEL', + accountCompanies: DEFAULT_ACCOUNT_COMPANIES, + }); + + expect(vivoDeterministico.operadora).toBe('VIVO'); + expect(vivoDeterministico.empresaConta).toBe('VIVO MACROPHONY'); + expect(vivoDeterministico.vivoEmpresaGrupo).toBe('MACROPHONY'); + }); +}); diff --git a/src/app/utils/account-operator.util.ts b/src/app/utils/account-operator.util.ts new file mode 100644 index 0000000..3dab6fc --- /dev/null +++ b/src/app/utils/account-operator.util.ts @@ -0,0 +1,176 @@ +import { normalizeAccentInsensitive } from './text-normalization.util'; + +export type OperadoraNome = 'VIVO' | 'CLARO' | 'TIM' | 'OUTRA'; +export type OperadoraFiltro = 'TODOS' | 'VIVO' | 'CLARO' | 'TIM'; +export type VivoEmpresaGrupo = 'MACROPHONY' | 'LINE MOVEL' | 'OUTRA'; + +export interface AccountCompanyOption { + empresa: string; + contas: string[]; +} + +export interface OperadoraResolution { + operadora: OperadoraNome; + empresaConta: string; + vivoEmpresaGrupo: VivoEmpresaGrupo | null; +} + +export const DEFAULT_ACCOUNT_COMPANIES: AccountCompanyOption[] = [ + { empresa: 'CLARO LINE MÓVEL', contas: ['172593311', '172593840', '187890982'] }, + { empresa: 'VIVO MACROPHONY', contas: ['0430237019', '0437488125', '0449508564', '0454371844', '455371844', '460161507'] }, + { empresa: 'VIVO LINE MÓVEL', contas: ['0435288088'] }, + { empresa: 'TIM LINE MÓVEL', contas: ['TIM'] }, +]; + +const DEFAULT_EMPRESA_BY_CONTA = buildDefaultEmpresaByConta(); + +function buildDefaultEmpresaByConta(): Map { + const result = new Map(); + + DEFAULT_ACCOUNT_COMPANIES.forEach((group) => { + (group.contas ?? []).forEach((conta) => { + const normalized = normalizeConta(conta); + if (!normalized) return; + result.set(normalized, group.empresa); + }); + }); + + return result; +} + +function normalizeEmpresaKey(value: unknown): string { + return normalizeAccentInsensitive(value, 'upper').replace(/[^A-Z0-9]/g, ''); +} + +function normalizeContas(contas: unknown): string[] { + if (!Array.isArray(contas)) return []; + + const result: string[] = []; + const seen = new Set(); + + contas.forEach((value) => { + const trimmed = String(value ?? '').trim(); + if (!trimmed) return; + const normalized = normalizeConta(trimmed); + if (!normalized || seen.has(normalized)) return; + seen.add(normalized); + result.push(trimmed); + }); + + return result; +} + +export function normalizeConta(value: unknown): string { + const raw = String(value ?? '').trim(); + if (!raw) return ''; + + if (!/^\d+$/.test(raw)) { + return normalizeAccentInsensitive(raw, 'upper'); + } + + const noLeadingZero = raw.replace(/^0+/, ''); + return noLeadingZero || '0'; +} + +export function sameConta(a: unknown, b: unknown): boolean { + return normalizeConta(a) === normalizeConta(b); +} + +export function mergeAccountCompaniesWithDefaults( + source: AccountCompanyOption[] | null | undefined +): AccountCompanyOption[] { + const merged = new Map(); + const contaSeenByEmpresa = new Map>(); + + const addGroup = (empresaRaw: unknown, contasRaw: unknown) => { + const empresa = String(empresaRaw ?? '').trim(); + if (!empresa) return; + + const key = normalizeEmpresaKey(empresa); + const contas = normalizeContas(contasRaw); + + if (!merged.has(key)) { + merged.set(key, { empresa, contas: [] }); + contaSeenByEmpresa.set(key, new Set()); + } + + const record = merged.get(key); + const seen = contaSeenByEmpresa.get(key); + if (!record || !seen) return; + + contas.forEach((conta) => { + const normalized = normalizeConta(conta); + if (!normalized || seen.has(normalized)) return; + seen.add(normalized); + record.contas.push(conta); + }); + }; + + (source ?? []).forEach((group) => addGroup(group?.empresa, group?.contas)); + DEFAULT_ACCOUNT_COMPANIES.forEach((group) => addGroup(group.empresa, group.contas)); + + return Array.from(merged.values()); +} + +export function resolveEmpresaByConta( + conta: unknown, + accountCompanies: AccountCompanyOption[] | null | undefined +): string { + const target = normalizeConta(conta); + if (!target) return ''; + + const deterministic = DEFAULT_EMPRESA_BY_CONTA.get(target); + if (deterministic) return deterministic; + + const found = (accountCompanies ?? []).find((group) => + (group.contas ?? []).some((candidate) => sameConta(candidate, target)) + ); + return found?.empresa ?? ''; +} + +function resolveOperadoraByEmpresa(empresa: unknown): OperadoraNome { + const normalized = normalizeEmpresaKey(empresa); + if (!normalized) return 'OUTRA'; + if (normalized.includes('CLARO')) return 'CLARO'; + if (normalized.includes('TIM')) return 'TIM'; + if (normalized.includes('VIVO') || normalized.includes('MACROPHONY')) return 'VIVO'; + return 'OUTRA'; +} + +function resolveVivoEmpresaGrupo(empresa: unknown): VivoEmpresaGrupo { + const normalized = normalizeEmpresaKey(empresa); + if (!normalized) return 'OUTRA'; + if (normalized.includes('MACROPHONY')) return 'MACROPHONY'; + if (normalized.includes('LINEMOVEL') || normalized.includes('LINEMOV')) return 'LINE MOVEL'; + return 'OUTRA'; +} + +export function resolveOperadoraContext(input: { + conta?: unknown; + empresaConta?: unknown; + accountCompanies?: AccountCompanyOption[] | null; +}): OperadoraResolution { + const contaRaw = String(input.conta ?? '').trim(); + const contaEmpresaRaw = String(input.empresaConta ?? '').trim(); + const empresaFromConta = resolveEmpresaByConta(input.conta, input.accountCompanies); + // Regras por conta (determinísticas) têm prioridade sobre texto livre da linha. + const empresaConta = empresaFromConta || contaEmpresaRaw; + + let operadora = resolveOperadoraByEmpresa(empresaConta); + if (operadora === 'OUTRA' && empresaFromConta) { + operadora = resolveOperadoraByEmpresa(empresaFromConta); + } + if (operadora === 'OUTRA' && contaRaw) { + operadora = resolveOperadoraByEmpresa(contaRaw); + } + + const vivoEmpresaGrupo = operadora === 'VIVO' + ? resolveVivoEmpresaGrupo(empresaConta || empresaFromConta || contaRaw) + : null; + + return { + operadora, + empresaConta: empresaConta || '', + vivoEmpresaGrupo, + }; +} From c609953352a2f298bd1e5b6e890f8b7e850669e3 Mon Sep 17 00:00:00 2001 From: Leon Date: Tue, 10 Mar 2026 17:07:04 -0300 Subject: [PATCH 7/7] feat: adicionando autitoria completa MVE --- src/app/app.routes.ts | 2 + src/app/app.ts | 1 + src/app/components/header/header.html | 3 + src/app/components/header/header.ts | 1 + src/app/pages/dashboard/dashboard.html | 16 +- src/app/pages/dashboard/dashboard.scss | 38 +- src/app/pages/dashboard/dashboard.ts | 39 +- src/app/pages/geral/geral.html | 3 + src/app/pages/geral/geral.ts | 113 +++- .../historico-chips/historico-chips.html | 274 +++++++++ .../pages/historico-chips/historico-chips.ts | 554 ++++++++++++++++++ .../historico-linhas/historico-linhas.html | 16 +- .../historico-linhas/historico-linhas.ts | 31 +- .../pages/mve-auditoria/mve-auditoria.html | 181 ++++-- .../pages/mve-auditoria/mve-auditoria.scss | 301 +++++++++- src/app/pages/mve-auditoria/mve-auditoria.ts | 163 +++++- src/app/services/historico.service.ts | 30 +- 17 files changed, 1633 insertions(+), 133 deletions(-) create mode 100644 src/app/pages/historico-chips/historico-chips.html create mode 100644 src/app/pages/historico-chips/historico-chips.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index f0b8b83..af24341 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -21,6 +21,7 @@ import { Resumo } from './pages/resumo/resumo'; import { Parcelamentos } from './pages/parcelamentos/parcelamentos'; import { Historico } from './pages/historico/historico'; import { HistoricoLinhas } from './pages/historico-linhas/historico-linhas'; +import { HistoricoChips } from './pages/historico-chips/historico-chips'; import { Perfil } from './pages/perfil/perfil'; import { SystemProvisionUserPage } from './pages/system-provision-user/system-provision-user'; import { SolicitacoesLinhas } from './pages/solicitacoes-linhas/solicitacoes-linhas'; @@ -43,6 +44,7 @@ export const routes: Routes = [ { path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, sysadminOrFinanceiroGuard], title: 'Parcelamentos' }, { path: 'historico', component: Historico, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico' }, { path: 'historico-linhas', component: HistoricoLinhas, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico de Linhas' }, + { path: 'historico-chips', component: HistoricoChips, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico de Chips' }, { path: 'solicitacoes', component: SolicitacoesLinhas, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Solicitações' }, { path: 'auditoria-mve', component: MveAuditoriaPage, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Auditoria MVE' }, { path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' }, diff --git a/src/app/app.ts b/src/app/app.ts index da02a75..2a1b14c 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -43,6 +43,7 @@ export class AppComponent { '/parcelamentos', '/historico', '/historico-linhas', + '/historico-chips', '/perfil', '/system', ]; diff --git a/src/app/components/header/header.html b/src/app/components/header/header.html index 00114a2..649881f 100644 --- a/src/app/components/header/header.html +++ b/src/app/components/header/header.html @@ -556,6 +556,9 @@ Histórico de Linhas + + Histórico de Chips + Solicitações diff --git a/src/app/components/header/header.ts b/src/app/components/header/header.ts index b8f0eda..be50d40 100644 --- a/src/app/components/header/header.ts +++ b/src/app/components/header/header.ts @@ -100,6 +100,7 @@ export class Header implements AfterViewInit, OnDestroy { '/parcelamentos', '/historico', '/historico-linhas', + '/historico-chips', '/solicitacoes', '/auditoria-mve', '/perfil', diff --git a/src/app/pages/dashboard/dashboard.html b/src/app/pages/dashboard/dashboard.html index 89eb1e0..13d6f61 100644 --- a/src/app/pages/dashboard/dashboard.html +++ b/src/app/pages/dashboard/dashboard.html @@ -61,7 +61,21 @@
{{ k.title }} - {{ k.value }} +
+ {{ k.value }} + + + + - + +
diff --git a/src/app/pages/dashboard/dashboard.scss b/src/app/pages/dashboard/dashboard.scss index e1076ec..f518a56 100644 --- a/src/app/pages/dashboard/dashboard.scss +++ b/src/app/pages/dashboard/dashboard.scss @@ -289,11 +289,47 @@ letter-spacing: 0.02em; } +.hero-value-row { + display: inline-flex; + align-items: center; + gap: 10px; + margin-top: 2px; +} + .hero-value { font-size: 24px; font-weight: 800; color: var(--text-main); - margin-top: 2px; + line-height: 1; +} + +.hero-trend { + min-width: 22px; + height: 22px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 800; + border: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(148, 163, 184, 0.12); + + &.trend-up { + color: #15803d; + background: rgba(34, 197, 94, 0.16); + border-color: rgba(34, 197, 94, 0.22); + } + + &.trend-down { + color: #b91c1c; + background: rgba(239, 68, 68, 0.16); + border-color: rgba(239, 68, 68, 0.22); + } + + &.trend-stable { + color: #64748b; + } } .hero-hint { diff --git a/src/app/pages/dashboard/dashboard.ts b/src/app/pages/dashboard/dashboard.ts index b9a878e..68fa500 100644 --- a/src/app/pages/dashboard/dashboard.ts +++ b/src/app/pages/dashboard/dashboard.ts @@ -32,11 +32,14 @@ import { } from '../../utils/account-operator.util'; // --- Interfaces (Mantidas intactas para não quebrar contrato) --- +type KpiTrendDirection = 'up' | 'down' | 'stable'; + type KpiCard = { key: string; title: string; value: string; icon: string; + trend: KpiTrendDirection; hint?: string; }; @@ -107,6 +110,7 @@ type DashboardKpisDto = { type DashboardDto = { kpis: DashboardKpisDto; + kpiTrends?: Record | null; topClientes: TopClienteDto[]; serieMuregUltimos12Meses: SerieMesDto[]; serieTrocaUltimos12Meses: SerieMesDto[]; @@ -490,6 +494,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { private filteredLinesCache: DashboardLineListItemDto[] = []; private operatorDatasetReady = false; private lineFranquiaCacheById = new Map(); + private kpiTrendMap: Record = {}; private readonly baseApi: string; private readonly kpiNavigationMap: Record = { @@ -632,6 +637,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { this.dashboardApiCache = null; this.loading = false; this.dashboardRaw = null; + this.kpiTrendMap = {}; this.kpis = []; this.errorMsg = this.isNetworkError(error) ? 'Falha ao carregar o Dashboard. Verifique a conexão.' @@ -648,11 +654,13 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { this.resumoReady = false; try { - const [operacionais, reservas] = await Promise.all([ + const [operacionais, reservas, dashboardDto] = await Promise.all([ this.fetchAllDashboardLines(false), this.fetchAllDashboardLines(true), + this.fetchDashboardReal().catch(() => null), ]); const allLines = [...operacionais, ...reservas]; + this.syncKpiTrendMap(dashboardDto?.kpiTrends ?? null); this.applyClientLineAggregates(allLines); this.loading = false; @@ -666,6 +674,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { this.resumoLoading = false; this.resumoReady = false; this.dataReady = false; + this.kpiTrendMap = {}; this.errorMsg = this.isNetworkError(error) ? 'Falha ao carregar o Dashboard. Verifique a conexão.' : 'Falha ao carregar os dados do cliente.'; @@ -1796,6 +1805,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { private applyDto(dto: DashboardDto) { const k = dto.kpis; this.dashboardRaw = k; + this.syncKpiTrendMap(dto.kpiTrends ?? null); this.muregLabels = (dto.serieMuregUltimos12Meses || []).map(x => x.mes); this.muregValues = (dto.serieMuregUltimos12Meses || []).map(x => x.total); @@ -2471,6 +2481,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { title: 'Linhas Ativas', value: this.formatInt(overview.ativas), icon: 'bi bi-check2-circle', + trend: this.getKpiTrend('linhas_ativas'), hint: 'Status ativo', }, { @@ -2478,6 +2489,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { title: 'Franquia Line Total', value: this.formatDataAllowance(overview.franquiaLineTotalGb), icon: 'bi bi-wifi', + trend: this.getKpiTrend('franquia_line_total'), hint: 'Franquia contratada', }, { @@ -2485,6 +2497,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { title: 'Planos Contratados', value: this.formatInt(overview.planosContratados), icon: 'bi bi-diagram-3-fill', + trend: this.getKpiTrend('planos_contratados'), hint: 'Planos ativos na base', }, { @@ -2492,6 +2505,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { title: 'Usuários com Linha', value: this.formatInt(overview.usuariosComLinha), icon: 'bi bi-people-fill', + trend: this.getKpiTrend('usuarios_com_linha'), hint: 'Usuários vinculados', }, ]; @@ -2505,7 +2519,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { const add = (key: string, title: string, value: string, icon: string, hint?: string) => { if (used.has(key)) return; used.add(key); - cards.push({ key, title, value, icon, hint }); + cards.push({ key, title, value, icon, trend: this.getKpiTrend(key), hint }); }; const insights = this.insights?.kpis; @@ -2574,6 +2588,27 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { this.kpis = cards; } + private syncKpiTrendMap(raw: Record | null | undefined): void { + const next: Record = {}; + if (raw && typeof raw === 'object') { + Object.entries(raw).forEach(([key, value]) => { + next[key] = this.normalizeKpiTrend(value); + }); + } + this.kpiTrendMap = next; + } + + private normalizeKpiTrend(value: unknown): KpiTrendDirection { + const token = String(value ?? '').trim().toLowerCase(); + if (token === 'up') return 'up'; + if (token === 'down') return 'down'; + return 'stable'; + } + + private getKpiTrend(key: string): KpiTrendDirection { + return this.kpiTrendMap[key] ?? 'stable'; + } + // --- CHART BUILDERS (Generic) --- private tryBuildCharts() { if (!isPlatformBrowser(this.platformId)) return; diff --git a/src/app/pages/geral/geral.html b/src/app/pages/geral/geral.html index a46a1cb..11a4511 100644 --- a/src/app/pages/geral/geral.html +++ b/src/app/pages/geral/geral.html @@ -91,6 +91,9 @@ + +
+
+ {{ toastMessage }} +
+
+
+ +
+ + + + + +
+
+
+
+
+ Chip +
+ +
+
Histórico de Chips
+ Timeline das alterações feitas em um chip específico. +
+ +
+ + +
+
+ +
+
+
+ + Filtros +
+
+ + +
+
+ +
+
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+
+ Eventos (filtro) + {{ total }} +
+
+ Trocas de Chip (página) + {{ chipCountInPage }} +
+
+ Trocas de Número (página) + {{ trocaCountInPage }} +
+
+ Status (página) + {{ statusCountInPage }} +
+
+
+ +
+
+
+ +
+ + + +
+ Nenhuma alteração encontrada para os filtros informados. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Data/HoraUsuárioOrigemAçãoResumo da alteraçãoDetalhes
{{ formatDateTime(log.occurredAtUtc) }} +
+ {{ displayUserName(log) }} + {{ log.userEmail || '-' }} +
+
+ {{ log.page || '-' }} + + {{ formatAction(log.action) }} + + +
{{ summary.title }}
+
{{ summary.description }}
+
+ {{ formatChangeValue(summary.before) }} + + {{ formatChangeValue(summary.after) }} +
+
+ DDD: {{ formatChangeValue(summary.beforeDdd) }} {{ formatChangeValue(summary.afterDdd) }} +
+
+
+ +
+
+
+
+ Mudanças de campos +
+ +
+
+
+ {{ change.field }} + + {{ changeTypeLabel(change.changeType) }} + +
+
+ {{ formatChangeValue(change.oldValue) }} + + {{ formatChangeValue(change.newValue) }} +
+
+
+
+ +
Sem mudanças detalhadas nesse evento.
+
+
+
+
+
+
+ + +
+
+
diff --git a/src/app/pages/historico-chips/historico-chips.ts b/src/app/pages/historico-chips/historico-chips.ts new file mode 100644 index 0000000..b7a637b --- /dev/null +++ b/src/app/pages/historico-chips/historico-chips.ts @@ -0,0 +1,554 @@ +import { Component, OnInit, ElementRef, ViewChild, ChangeDetectorRef, Inject, PLATFORM_ID } from '@angular/core'; +import { CommonModule, isPlatformBrowser } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { HttpErrorResponse } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; + +import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { + HistoricoService, + AuditLogDto, + AuditChangeType, + AuditFieldChangeDto, + ChipHistoricoQuery +} from '../../services/historico.service'; +import { TableExportService } from '../../services/table-export.service'; +import { + buildPageNumbers, + clampPage, + computePageEnd, + computePageStart, + computeTotalPages +} from '../../utils/pagination.util'; + +interface SelectOption { + value: string; + label: string; +} + +type EventTone = 'mureg' | 'troca' | 'status' | 'linha' | 'chip' | 'generic'; + +interface EventSummary { + title: string; + description: string; + before?: string | null; + after?: string | null; + beforeDdd?: string | null; + afterDdd?: string | null; + tone: EventTone; +} + +@Component({ + selector: 'app-historico-chips', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent], + templateUrl: './historico-chips.html', + styleUrls: ['../historico-linhas/historico-linhas.scss'], +}) +export class HistoricoChips implements OnInit { + @ViewChild('successToast', { static: false }) successToast!: ElementRef; + + logs: AuditLogDto[] = []; + loading = false; + exporting = false; + error = false; + errorMsg = ''; + toastMessage = ''; + + expandedLogId: string | null = null; + + page = 1; + pageSize = 10; + pageSizeOptions = [10, 20, 50, 100]; + total = 0; + + filterChip = ''; + filterPageName = ''; + filterAction = ''; + filterUser = ''; + dateFrom = ''; + dateTo = ''; + + readonly pageOptions: SelectOption[] = [ + { value: '', label: 'Todas as origens' }, + { value: 'Geral', label: 'Geral' }, + { value: 'Troca de número', label: 'Troca de número' }, + { value: 'Chips Virgens e Recebidos', label: 'Chips Virgens e Recebidos' }, + ]; + + readonly actionOptions: SelectOption[] = [ + { value: '', label: 'Todas as ações' }, + { value: 'CREATE', label: 'Criação' }, + { value: 'UPDATE', label: 'Atualização' }, + { value: 'DELETE', label: 'Exclusão' }, + ]; + + private readonly summaryCache = new Map(); + private readonly idFieldExceptions = new Set(['iccid']); + + constructor( + private readonly historicoService: HistoricoService, + private readonly cdr: ChangeDetectorRef, + @Inject(PLATFORM_ID) private readonly platformId: object, + private readonly tableExportService: TableExportService + ) {} + + ngOnInit(): void { + this.fetch(); + } + + applyFilters(): void { + this.page = 1; + this.fetch(); + } + + refresh(): void { + this.fetch(); + } + + clearFilters(): void { + this.filterChip = ''; + this.filterPageName = ''; + this.filterAction = ''; + this.filterUser = ''; + this.dateFrom = ''; + this.dateTo = ''; + this.page = 1; + this.logs = []; + this.total = 0; + this.error = false; + this.errorMsg = ''; + this.summaryCache.clear(); + this.fetch(); + } + + onPageSizeChange(): void { + this.page = 1; + this.fetch(); + } + + goToPage(target: number): void { + this.page = clampPage(target, this.totalPages); + this.fetch(); + } + + toggleDetails(log: AuditLogDto, event?: Event): void { + if (event) event.stopPropagation(); + this.expandedLogId = this.expandedLogId === log.id ? null : log.id; + } + + async onExport(): Promise { + if (this.exporting) return; + + this.exporting = true; + try { + const allLogs = await this.fetchAllLogsForExport(); + if (!allLogs.length) { + await this.showToast('Nenhum evento encontrado para exportar.'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `historico_chips_${timestamp}`, + sheetName: 'HistoricoChips', + rows: allLogs, + columns: [ + { header: 'Data/Hora', type: 'datetime', value: (log) => log.occurredAtUtc ?? '' }, + { header: 'Usuario', value: (log) => this.displayUserName(log) }, + { header: 'E-mail', value: (log) => log.userEmail ?? '' }, + { header: 'Origem', value: (log) => log.page ?? '' }, + { header: 'Acao', value: (log) => this.formatAction(log.action) }, + { header: 'Evento', value: (log) => this.summaryFor(log).title }, + { header: 'Resumo', value: (log) => this.summaryFor(log).description }, + { header: 'Valor Anterior', value: (log) => this.summaryFor(log).before ?? '' }, + { header: 'Valor Novo', value: (log) => this.summaryFor(log).after ?? '' }, + { header: 'Mudancas', value: (log) => this.formatChangesSummary(log) }, + ], + }); + + await this.showToast(`Planilha exportada com ${allLogs.length} evento(s).`); + } catch { + await this.showToast('Erro ao exportar histórico de chips.'); + } finally { + this.exporting = false; + } + } + + formatDateTime(value?: string | null): string { + if (!value) return '-'; + const dt = new Date(value); + if (Number.isNaN(dt.getTime())) return '-'; + return dt.toLocaleString('pt-BR'); + } + + displayUserName(log: AuditLogDto): string { + const name = (log.userName || '').trim(); + return name ? name : 'SISTEMA'; + } + + formatAction(action?: string | null): string { + const value = (action || '').toUpperCase(); + if (!value) return '-'; + if (value === 'CREATE') return 'Criação'; + if (value === 'UPDATE') return 'Atualização'; + if (value === 'DELETE') return 'Exclusão'; + return 'Outro'; + } + + actionClass(action?: string | null): string { + const value = (action || '').toUpperCase(); + if (value === 'CREATE') return 'action-create'; + if (value === 'UPDATE') return 'action-update'; + if (value === 'DELETE') return 'action-delete'; + return 'action-default'; + } + + changeTypeLabel(type?: AuditChangeType | string | null): string { + if (!type) return 'Alterado'; + if (type === 'added') return 'Adicionado'; + if (type === 'removed') return 'Removido'; + return 'Alterado'; + } + + changeTypeClass(type?: AuditChangeType | string | null): string { + if (type === 'added') return 'change-added'; + if (type === 'removed') return 'change-removed'; + return 'change-modified'; + } + + formatChangeValue(value?: string | null): string { + if (value === undefined || value === null || value === '') return '-'; + return String(value); + } + + summaryFor(log: AuditLogDto): EventSummary { + const cached = this.summaryCache.get(log.id); + if (cached) return cached; + const summary = this.buildEventSummary(log); + this.summaryCache.set(log.id, summary); + return summary; + } + + toneClass(tone: EventTone): string { + return `tone-${tone}`; + } + + trackByLog(_: number, log: AuditLogDto): string { + return log.id; + } + + trackByField(_: number, change: AuditFieldChangeDto): string { + return `${change.field}-${change.oldValue ?? ''}-${change.newValue ?? ''}`; + } + + visibleChanges(log: AuditLogDto): AuditFieldChangeDto[] { + return this.publicChanges(log); + } + + get normalizedChipTerm(): string { + return (this.filterChip || '').trim(); + } + + get hasChipFilter(): boolean { + return !!this.normalizedChipTerm; + } + + get totalPages(): number { + return computeTotalPages(this.total || 0, this.pageSize); + } + + get pageNumbers(): number[] { + return buildPageNumbers(this.page, this.totalPages); + } + + get pageStart(): number { + return computePageStart(this.total || 0, this.page, this.pageSize); + } + + get pageEnd(): number { + return computePageEnd(this.total || 0, this.page, this.pageSize); + } + + get chipCountInPage(): number { + return this.logs.filter((log) => this.summaryFor(log).tone === 'chip').length; + } + + get trocaCountInPage(): number { + return this.logs.filter((log) => this.summaryFor(log).tone === 'troca').length; + } + + get statusCountInPage(): number { + return this.logs.filter((log) => this.summaryFor(log).tone === 'status').length; + } + + private fetch(): void { + this.loading = true; + this.error = false; + this.errorMsg = ''; + this.expandedLogId = null; + + const query: ChipHistoricoQuery = { + ...this.buildBaseQuery(), + chip: this.normalizedChipTerm || undefined, + page: this.page, + pageSize: this.pageSize, + }; + + this.historicoService.listByChip(query).subscribe({ + next: (res) => { + this.logs = res.items || []; + this.total = res.total || 0; + this.page = res.page || this.page; + this.pageSize = res.pageSize || this.pageSize; + this.loading = false; + this.rebuildSummaryCache(); + }, + error: (err: HttpErrorResponse) => { + this.loading = false; + this.error = true; + this.logs = []; + this.total = 0; + this.summaryCache.clear(); + if (err?.status === 403) { + this.errorMsg = 'Acesso restrito.'; + return; + } + this.errorMsg = 'Erro ao carregar histórico do chip. Tente novamente.'; + } + }); + } + + private async fetchAllLogsForExport(): Promise { + const pageSize = 500; + let page = 1; + let expectedTotal = 0; + const all: AuditLogDto[] = []; + + while (page <= 500) { + const response = await firstValueFrom( + this.historicoService.listByChip({ + ...this.buildBaseQuery(), + chip: this.normalizedChipTerm || undefined, + page, + pageSize, + }) + ); + + 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 buildBaseQuery(): Omit { + return { + pageName: this.filterPageName || undefined, + action: this.filterAction || undefined, + user: this.filterUser?.trim() || undefined, + dateFrom: this.toIsoDate(this.dateFrom, false) || undefined, + dateTo: this.toIsoDate(this.dateTo, true) || undefined, + }; + } + + private rebuildSummaryCache(): void { + this.summaryCache.clear(); + this.logs.forEach((log) => { + this.summaryCache.set(log.id, this.buildEventSummary(log)); + }); + } + + private buildEventSummary(log: AuditLogDto): EventSummary { + const page = (log.page || '').toLowerCase(); + const entity = (log.entityName || '').toLowerCase(); + + const linhaChange = this.findChange(log, 'linha'); + const statusChange = this.findChange(log, 'status'); + const chipChange = this.findChange(log, 'chip', 'iccid', 'numerodochip'); + const linhaAntiga = this.findChange(log, 'linhaantiga'); + const linhaNova = this.findChange(log, 'linhanova'); + + const trocaLike = entity === 'trocanumeroline' || page.includes('troca'); + if (trocaLike) { + const before = this.firstFilled(linhaAntiga?.newValue, linhaAntiga?.oldValue, linhaChange?.oldValue); + const after = this.firstFilled(linhaNova?.newValue, linhaNova?.oldValue, linhaChange?.newValue); + return { + title: 'Troca de Número', + description: 'Linha antiga substituída por uma nova.', + before, + after, + beforeDdd: this.extractDdd(before), + afterDdd: this.extractDdd(after), + tone: 'troca', + }; + } + + if (chipChange) { + return { + title: 'Alteração de Chip', + description: 'ICCID/chip atualizado na linha.', + before: this.firstFilled(chipChange.oldValue), + after: this.firstFilled(chipChange.newValue), + tone: 'chip', + }; + } + + if (statusChange) { + const oldStatus = this.firstFilled(statusChange.oldValue); + const newStatus = this.firstFilled(statusChange.newValue); + const wasBlocked = this.isBlockedStatus(oldStatus); + const isBlocked = this.isBlockedStatus(newStatus); + let description = 'Status da linha atualizado.'; + if (!wasBlocked && isBlocked) description = 'Linha foi bloqueada.'; + if (wasBlocked && !isBlocked) description = 'Linha foi desbloqueada.'; + return { + title: 'Status da Linha', + description, + before: oldStatus, + after: newStatus, + tone: 'status', + }; + } + + if (linhaChange) { + return { + title: 'Alteração da Linha', + description: 'Número da linha foi atualizado.', + before: this.firstFilled(linhaChange.oldValue), + after: this.firstFilled(linhaChange.newValue), + beforeDdd: this.extractDdd(linhaChange.oldValue), + afterDdd: this.extractDdd(linhaChange.newValue), + tone: 'linha', + }; + } + + const first = this.publicChanges(log)[0]; + if (first) { + return { + title: 'Outras alterações', + description: `Campo ${first.field} foi atualizado.`, + before: this.firstFilled(first.oldValue), + after: this.firstFilled(first.newValue), + tone: 'generic', + }; + } + + return { + title: 'Sem detalhes', + description: 'Não há mudanças detalhadas registradas para este evento.', + tone: 'generic', + }; + } + + private findChange(log: AuditLogDto, ...fields: string[]): AuditFieldChangeDto | null { + if (!fields.length) return null; + const normalizedTargets = new Set(fields.map((field) => this.normalizeField(field))); + return (log.changes || []).find((change) => normalizedTargets.has(this.normalizeField(change.field))) || null; + } + + private normalizeField(value?: string | null): string { + return (value ?? '') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-zA-Z0-9]/g, '') + .toLowerCase() + .trim(); + } + + private firstFilled(...values: Array): string | null { + for (const value of values) { + const normalized = (value ?? '').toString().trim(); + if (normalized) return normalized; + } + return null; + } + + private formatChangesSummary(log: AuditLogDto): string { + const changes = this.publicChanges(log); + if (!changes.length) return ''; + return changes + .map((change) => { + const field = change?.field ?? 'campo'; + const oldValue = this.formatChangeValue(change?.oldValue); + const newValue = this.formatChangeValue(change?.newValue); + return `${field}: ${oldValue} -> ${newValue}`; + }) + .join(' | '); + } + + private publicChanges(log: AuditLogDto): AuditFieldChangeDto[] { + return (log?.changes ?? []).filter((change) => !this.isHiddenIdField(change?.field)); + } + + private isHiddenIdField(field?: string | null): boolean { + const normalized = this.normalizeField(field); + if (!normalized) return false; + if (this.idFieldExceptions.has(normalized)) return false; + if (normalized === 'id') return true; + return normalized.endsWith('id'); + } + + private isBlockedStatus(status?: string | null): boolean { + const normalized = (status ?? '').toLowerCase().trim(); + if (!normalized) return false; + return ( + normalized.includes('bloque') || + normalized.includes('perda') || + normalized.includes('roubo') || + normalized.includes('suspens') + ); + } + + private extractDdd(value?: string | null): string | null { + const digits = this.digitsOnly(value); + if (!digits) return null; + + if (digits.startsWith('55') && digits.length >= 12) { + return digits.slice(2, 4); + } + if (digits.length >= 10) { + return digits.slice(0, 2); + } + if (digits.length >= 2) { + return digits.slice(0, 2); + } + return null; + } + + private digitsOnly(value?: string | null): string { + return (value ?? '').replace(/\D/g, ''); + } + + private toIsoDate(value: string, endOfDay: boolean): string | null { + if (!value) return null; + const time = endOfDay ? '23:59:59' : '00:00:00'; + const date = new Date(`${value}T${time}`); + if (isNaN(date.getTime())) return null; + return date.toISOString(); + } + + private async showToast(message: string): Promise { + 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); + } + } +} diff --git a/src/app/pages/historico-linhas/historico-linhas.html b/src/app/pages/historico-linhas/historico-linhas.html index eb67ad6..a000b4a 100644 --- a/src/app/pages/historico-linhas/historico-linhas.html +++ b/src/app/pages/historico-linhas/historico-linhas.html @@ -30,10 +30,10 @@
- - @@ -58,11 +58,11 @@
- + @@ -135,10 +135,6 @@
-
- Informe a linha no filtro para carregar o histórico detalhado. -
-
@@ -148,8 +144,8 @@
-
- Nenhuma alteração encontrada para a linha informada. +
+ Nenhuma alteração encontrada para os filtros informados.
diff --git a/src/app/pages/historico-linhas/historico-linhas.ts b/src/app/pages/historico-linhas/historico-linhas.ts index 9e1b5cd..22c5307 100644 --- a/src/app/pages/historico-linhas/historico-linhas.ts +++ b/src/app/pages/historico-linhas/historico-linhas.ts @@ -96,7 +96,7 @@ export class HistoricoLinhas implements OnInit { ) {} ngOnInit(): void { - // Tela inicia aguardando o usuário informar a linha. + this.fetch(); } applyFilters(): void { @@ -121,6 +121,7 @@ export class HistoricoLinhas implements OnInit { this.error = false; this.errorMsg = ''; this.summaryCache.clear(); + this.fetch(); } onPageSizeChange(): void { @@ -141,12 +142,6 @@ export class HistoricoLinhas implements OnInit { async onExport(): Promise { if (this.exporting) return; - const lineTerm = this.normalizedLineTerm; - if (!lineTerm) { - await this.showToast('Informe a linha para exportar.'); - return; - } - this.exporting = true; try { const allLogs = await this.fetchAllLogsForExport(); @@ -292,17 +287,6 @@ export class HistoricoLinhas implements OnInit { } private fetch(): void { - const lineTerm = this.normalizedLineTerm; - if (!lineTerm) { - this.logs = []; - this.total = 0; - this.error = true; - this.errorMsg = 'Informe a linha para consultar o histórico.'; - this.loading = false; - this.summaryCache.clear(); - return; - } - this.loading = true; this.error = false; this.errorMsg = ''; @@ -310,7 +294,7 @@ export class HistoricoLinhas implements OnInit { const query: LineHistoricoQuery = { ...this.buildBaseQuery(), - line: lineTerm, + line: this.normalizedLineTerm || undefined, page: this.page, pageSize: this.pageSize, }; @@ -330,10 +314,6 @@ export class HistoricoLinhas implements OnInit { this.logs = []; this.total = 0; this.summaryCache.clear(); - if (err?.status === 400) { - this.errorMsg = err?.error?.message || 'Informe uma linha válida.'; - return; - } if (err?.status === 403) { this.errorMsg = 'Acesso restrito.'; return; @@ -344,9 +324,6 @@ export class HistoricoLinhas implements OnInit { } private async fetchAllLogsForExport(): Promise { - const lineTerm = this.normalizedLineTerm; - if (!lineTerm) return []; - const pageSize = 500; let page = 1; let expectedTotal = 0; @@ -356,7 +333,7 @@ export class HistoricoLinhas implements OnInit { const response = await firstValueFrom( this.historicoService.listByLine({ ...this.buildBaseQuery(), - line: lineTerm, + line: this.normalizedLineTerm || undefined, page, pageSize, }) diff --git a/src/app/pages/mve-auditoria/mve-auditoria.html b/src/app/pages/mve-auditoria/mve-auditoria.html index 69711c1..26ccc58 100644 --- a/src/app/pages/mve-auditoria/mve-auditoria.html +++ b/src/app/pages/mve-auditoria/mve-auditoria.html @@ -34,7 +34,7 @@ Ultima conferencia Carregando... - @@ -45,8 +45,8 @@
Conferencia

- Use o relatorio da Vivo para conferir se o status da linha esta igual ao do sistema. - Os outros campos nao entram como erro nesta tela. + Use o relatorio da Vivo para conferir se status, linha e chip estão alinhados com o sistema. + Mudanças só de DDD continuam sendo sinalizadas apenas para revisão manual.

@@ -110,30 +110,72 @@
Com diferenca - {{ audit.summary.totalStatusDivergences }} + {{ totalDifferencesCount }}
Prontas para atualizar - {{ syncableStatusIssues.length }} + {{ syncableIssues.length }} +
+
+ Revisão manual + {{ manualReviewIssuesCount }}
-
+
Só no sistema: {{ audit.summary.totalOnlyInSystem }} Só no relatório: {{ audit.summary.totalOnlyInReport }} + Avisos/ignorados: {{ ignoredIssuesCount }}
-
- - - +
+
+ + + +
+ +
+ + + + +
@@ -141,7 +183,7 @@ @@ -153,49 +195,112 @@
-
+
-
Nenhuma diferenca de status encontrada para o filtro atual.
+
Nenhuma divergência encontrada para o filtro atual.
-
+
- - + + - - + - - - -
NúmeroStatus no sistemaStatus no relatorioSistemaRelatório Situação Ação
- {{ issue.numeroLinha || '-' }} +
+
+ {{ issue.numeroLinha || issue.reportSnapshot?.numeroLinha || issue.systemSnapshot?.numeroLinha || '-' }} +
- {{ statusLabel(issue.systemStatus) }} + +
+
+ Sistema + Cadastro atual +
+
+
+ Linha + Alterada +
+ {{ formatValue(issue.systemSnapshot?.numeroLinha) }} +
+
+
+ Chip + Alterado +
+ {{ formatValue(issue.systemSnapshot?.chip) }} +
+
+
+ Status + Divergente +
+ {{ statusLabel(issue.systemStatus) }} +
+
- {{ statusLabel(issue.reportStatus) }} + +
+
+ Relatório + Importado do MVE +
+
+
+ Linha + Nova +
+ {{ formatValue(issue.reportSnapshot?.numeroLinha) }} +
+
+
+ Chip + Novo +
+ {{ formatValue(issue.reportSnapshot?.chip) }} +
+
+
+ Status + MVE +
+ {{ statusLabel(issue.reportStatus) }} +
+
-
Status diferente.
+
+
+
+ {{ issueKindLabel(issue) }} +
+
+
+
{{ issue.notes }}
+
- Pode atualizar - Atualizada - Sem ação + +
+ Pode atualizar + Atualizada + Revisar +
-