Merge branch 'Filtros-Operadora' into dev
This commit is contained in:
commit
77973fc516
|
|
@ -640,6 +640,7 @@
|
||||||
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
|
|
@ -6944,6 +6945,7 @@
|
||||||
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
|
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cli-truncate": "^4.0.0",
|
"cli-truncate": "^4.0.0",
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
|
|
@ -9576,6 +9578,7 @@
|
||||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,24 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="operadora-filter-row fade-in-up" [style.animation-delay]="'80ms'" *ngIf="!isCliente">
|
||||||
|
<div class="operadora-filter-label">
|
||||||
|
<i class="bi bi-funnel-fill"></i>
|
||||||
|
<span>Filtro de Operadora</span>
|
||||||
|
</div>
|
||||||
|
<div class="filter-tabs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="filter-tab"
|
||||||
|
*ngFor="let option of operadoraFilters; trackBy: trackByOperadoraFilter"
|
||||||
|
[class.active]="operadoraFilter === option.value"
|
||||||
|
[disabled]="operadoraFilterLoading"
|
||||||
|
(click)="onOperadoraFilterChange(option.value)">
|
||||||
|
{{ option.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="hero-grid fade-in-up" [style.animation-delay]="'100ms'" *ngIf="!isCliente || clientOverview.hasData">
|
<div class="hero-grid fade-in-up" [style.animation-delay]="'100ms'" *ngIf="!isCliente || clientOverview.hasData">
|
||||||
<div
|
<div
|
||||||
class="hero-card"
|
class="hero-card"
|
||||||
|
|
@ -44,7 +62,6 @@
|
||||||
<div class="hero-data">
|
<div class="hero-data">
|
||||||
<span class="hero-label">{{ k.title }}</span>
|
<span class="hero-label">{{ k.title }}</span>
|
||||||
<span class="hero-value">{{ k.value }}</span>
|
<span class="hero-value">{{ k.value }}</span>
|
||||||
<span class="hero-hint" *ngIf="k.hint">{{ k.hint }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -66,7 +83,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body-split">
|
<div class="card-body-split">
|
||||||
<div class="chart-wrapper-pie">
|
<div
|
||||||
|
class="chart-wrapper-pie chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('status')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'status')">
|
||||||
<canvas #chartStatusPie></canvas>
|
<canvas #chartStatusPie></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-list">
|
<div class="status-list">
|
||||||
|
|
@ -107,7 +129,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body-adicionais">
|
<div class="card-body-adicionais">
|
||||||
<div class="chart-wrapper-pie-sm">
|
<div
|
||||||
|
class="chart-wrapper-pie-sm chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('adicionaisComparativo')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'adicionaisComparativo')">
|
||||||
<canvas #chartAdicionaisComparativo></canvas>
|
<canvas #chartAdicionaisComparativo></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="compare-list">
|
<div class="compare-list">
|
||||||
|
|
@ -143,7 +170,12 @@
|
||||||
<p>Status de vencimento atual</p>
|
<p>Status de vencimento atual</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-pie">
|
<div
|
||||||
|
class="chart-wrapper-pie chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('vigenciaBuckets')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'vigenciaBuckets')">
|
||||||
<canvas #chartVigenciaSupervisao></canvas>
|
<canvas #chartVigenciaSupervisao></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -156,7 +188,12 @@
|
||||||
<p>Linhas com e sem serviço ativo</p>
|
<p>Linhas com e sem serviço ativo</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-pie">
|
<div
|
||||||
|
class="chart-wrapper-pie chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('travel')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'travel')">
|
||||||
<canvas #chartTravelMundo></canvas>
|
<canvas #chartTravelMundo></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -170,7 +207,12 @@
|
||||||
<p>Distribuição da base por faixa de franquia</p>
|
<p>Distribuição da base por faixa de franquia</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-bar compact">
|
<div
|
||||||
|
class="chart-wrapper-bar compact chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('linhasFranquia')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'linhasFranquia')">
|
||||||
<canvas #chartLinhasPorFranquia></canvas>
|
<canvas #chartLinhasPorFranquia></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -182,7 +224,12 @@
|
||||||
<p>Quantidade de linhas por serviço adicional ativo</p>
|
<p>Quantidade de linhas por serviço adicional ativo</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-bar compact">
|
<div
|
||||||
|
class="chart-wrapper-bar compact chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('adicionaisPagos')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'adicionaisPagos')">
|
||||||
<canvas #chartAdicionaisPagos></canvas>
|
<canvas #chartAdicionaisPagos></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -194,13 +241,65 @@
|
||||||
<p>Quantidade de linhas e-SIM e SIMCARD</p>
|
<p>Quantidade de linhas e-SIM e SIMCARD</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-pie">
|
<div
|
||||||
|
class="chart-wrapper-pie chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('tipoChip')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'tipoChip')">
|
||||||
<canvas #chartTipoChip></canvas>
|
<canvas #chartTipoChip></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-section fade-in-up" [style.animation-delay]="'300ms'" *ngIf="showVivoComparison">
|
||||||
|
<div class="context-title">
|
||||||
|
<h2>Comparativo VIVO</h2>
|
||||||
|
<p>Comparação entre contas da operadora VIVO: MACROPHONY x LINE MÓVEL.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vivo-comparison-grid">
|
||||||
|
<div class="card-modern">
|
||||||
|
<div class="card-header-clean">
|
||||||
|
<div class="header-text">
|
||||||
|
<h3>Linhas por Empresa</h3>
|
||||||
|
<p>Volume total de linhas VIVO por empresa.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="chart-wrapper-bar compact-half chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('vivoEmpresasLinhas')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'vivoEmpresasLinhas')">
|
||||||
|
<canvas #chartVivoEmpresasLinhas></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-modern">
|
||||||
|
<div class="card-header-clean">
|
||||||
|
<div class="header-text">
|
||||||
|
<h3>Adicionais por Empresa</h3>
|
||||||
|
<p>Comparação de linhas com e sem adicionais pagos.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="chart-wrapper-bar compact-half chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('vivoEmpresasAdicionais')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'vivoEmpresasAdicionais')">
|
||||||
|
<canvas #chartVivoEmpresasAdicionais></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vivo-comparison-empty" *ngIf="vivoComparison.macrophonyLinhas === 0 && vivoComparison.lineMovelLinhas === 0">
|
||||||
|
Não há linhas VIVO vinculadas às empresas MACROPHONY ou LINE MÓVEL para o filtro atual.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="context-title fade-in-up" [style.animation-delay]="'320ms'">
|
<div class="context-title fade-in-up" [style.animation-delay]="'320ms'">
|
||||||
<h2>Página Resumo</h2>
|
<h2>Página Resumo</h2>
|
||||||
<p>Indicadores do Resumo focados em quantidade e distribuição de linhas.</p>
|
<p>Indicadores do Resumo focados em quantidade e distribuição de linhas.</p>
|
||||||
|
|
@ -247,22 +346,50 @@
|
||||||
<div class="analytics-grid" *ngIf="!resumoLoading && !resumoError">
|
<div class="analytics-grid" *ngIf="!resumoLoading && !resumoError">
|
||||||
<div class="mini-chart-card">
|
<div class="mini-chart-card">
|
||||||
<h6>Top Clientes (Qtd. Linhas)</h6>
|
<h6>Top Clientes (Qtd. Linhas)</h6>
|
||||||
<div class="chart-area"><canvas #chartResumoTopClientes></canvas></div>
|
<div
|
||||||
|
class="chart-area chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('resumoTopClientes')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'resumoTopClientes')">
|
||||||
|
<canvas #chartResumoTopClientes></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mini-chart-card">
|
<div class="mini-chart-card">
|
||||||
<h6>Top Planos (Qtd. Linhas)</h6>
|
<h6>Top Planos (Qtd. Linhas)</h6>
|
||||||
<div class="chart-area"><canvas #chartResumoTopPlanos></canvas></div>
|
<div
|
||||||
|
class="chart-area chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('resumoTopPlanos')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'resumoTopPlanos')">
|
||||||
|
<canvas #chartResumoTopPlanos></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mini-chart-card">
|
<div class="mini-chart-card">
|
||||||
<h6>PF vs PJ (Qtd. Linhas)</h6>
|
<h6>PF vs PJ (Qtd. Linhas)</h6>
|
||||||
<div class="chart-area"><canvas #chartResumoPfPjLinhas></canvas></div>
|
<div
|
||||||
|
class="chart-area chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('resumoPfPj')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'resumoPfPj')">
|
||||||
|
<canvas #chartResumoPfPjLinhas></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mini-chart-card">
|
<div class="mini-chart-card">
|
||||||
<h6>Reserva por DDD</h6>
|
<h6>Reserva por DDD</h6>
|
||||||
<div class="chart-area"><canvas #chartResumoReservaDdd></canvas></div>
|
<div
|
||||||
|
class="chart-area chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('resumoReservaDdd')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'resumoReservaDdd')">
|
||||||
|
<canvas #chartResumoReservaDdd></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mini-chart-card mini-metric-card">
|
<div class="mini-chart-card mini-metric-card">
|
||||||
|
|
@ -301,7 +428,12 @@
|
||||||
<p>Histórico mensal de mudanças de plano/aparelho</p>
|
<p>Histórico mensal de mudanças de plano/aparelho</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-bar compact-half">
|
<div
|
||||||
|
class="chart-wrapper-bar compact-half chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('mureg12')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'mureg12')">
|
||||||
<canvas #chartMureg12></canvas>
|
<canvas #chartMureg12></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -313,7 +445,12 @@
|
||||||
<p>Histórico mensal de trocas realizadas</p>
|
<p>Histórico mensal de trocas realizadas</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-bar compact-half">
|
<div
|
||||||
|
class="chart-wrapper-bar compact-half chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('troca12')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'troca12')">
|
||||||
<canvas #chartTroca12></canvas>
|
<canvas #chartTroca12></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -326,7 +463,12 @@
|
||||||
<p>Contratos a encerrar por mês</p>
|
<p>Contratos a encerrar por mês</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-bar compact-half">
|
<div
|
||||||
|
class="chart-wrapper-bar compact-half chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('vigenciaMesAno')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'vigenciaMesAno')">
|
||||||
<canvas #chartVigenciaMesAno></canvas>
|
<canvas #chartVigenciaMesAno></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -352,7 +494,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body-split">
|
<div class="card-body-split">
|
||||||
<div class="chart-wrapper-pie">
|
<div
|
||||||
|
class="chart-wrapper-pie chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('status')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'status')">
|
||||||
<canvas #chartStatusPie></canvas>
|
<canvas #chartStatusPie></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-list">
|
<div class="status-list">
|
||||||
|
|
@ -373,7 +520,12 @@
|
||||||
<p>Quantidade de linhas por faixa de franquia contratada</p>
|
<p>Quantidade de linhas por faixa de franquia contratada</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-bar compact-half">
|
<div
|
||||||
|
class="chart-wrapper-bar compact-half chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('linhasFranquia')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'linhasFranquia')">
|
||||||
<canvas #chartLinhasPorFranquia></canvas>
|
<canvas #chartLinhasPorFranquia></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -388,7 +540,12 @@
|
||||||
<p>Planos com maior volume na sua operação</p>
|
<p>Planos com maior volume na sua operação</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-bar compact">
|
<div
|
||||||
|
class="chart-wrapper-bar compact chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('resumoTopPlanos')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'resumoTopPlanos')">
|
||||||
<canvas #chartResumoTopPlanos></canvas>
|
<canvas #chartResumoTopPlanos></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -401,7 +558,12 @@
|
||||||
<p>Apenas usuários de fato (sem bloqueados/aguardando)</p>
|
<p>Apenas usuários de fato (sem bloqueados/aguardando)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-bar compact">
|
<div
|
||||||
|
class="chart-wrapper-bar compact chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('resumoTopClientes')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'resumoTopClientes')">
|
||||||
<canvas #chartResumoTopClientes></canvas>
|
<canvas #chartResumoTopClientes></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -414,7 +576,12 @@
|
||||||
<p>Distribuição entre e-SIM, SIMCARD e outros</p>
|
<p>Distribuição entre e-SIM, SIMCARD e outros</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-pie">
|
<div
|
||||||
|
class="chart-wrapper-pie chart-click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
(click)="openChartModal('tipoChip')"
|
||||||
|
(keydown)="onChartTargetKeydown($event, 'tipoChip')">
|
||||||
<canvas #chartTipoChip></canvas>
|
<canvas #chartTipoChip></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -441,5 +608,54 @@
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="chart-modal-overlay"
|
||||||
|
*ngIf="chartModalOpen"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
[attr.aria-label]="chartModalTitle || 'Gráfico expandido'"
|
||||||
|
(click)="closeChartModal()">
|
||||||
|
<div class="chart-modal-card" (click)="$event.stopPropagation()">
|
||||||
|
<div class="chart-modal-header">
|
||||||
|
<div class="chart-modal-title-wrap">
|
||||||
|
<h3>{{ chartModalTitle }}</h3>
|
||||||
|
<p *ngIf="chartModalSubtitle">{{ chartModalSubtitle }}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="chart-modal-close"
|
||||||
|
aria-label="Fechar gráfico expandido"
|
||||||
|
(click)="closeChartModal()">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="chart-modal-body">
|
||||||
|
<div class="chart-modal-content">
|
||||||
|
<div class="chart-modal-visual">
|
||||||
|
<canvas #chartExpandedCanvas></canvas>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="chart-modal-info"
|
||||||
|
*ngIf="chartModalInfoRows.length > 0"
|
||||||
|
[style.--dataset-cols]="chartModalDatasetHeaders.length">
|
||||||
|
<div class="chart-modal-info-head">
|
||||||
|
<span class="col-label">Categoria</span>
|
||||||
|
<span class="col-value" *ngFor="let dataset of chartModalDatasetHeaders">{{ dataset }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-modal-info-row" *ngFor="let row of chartModalInfoRows">
|
||||||
|
<span class="col-label">{{ row.label }}</span>
|
||||||
|
<span class="col-value" *ngFor="let cell of row.cells">{{ cell.valueText }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-modal-info-total" *ngIf="chartModalDatasetTotals.length > 0">
|
||||||
|
<span *ngFor="let total of chartModalDatasetTotals">
|
||||||
|
Total {{ total.dataset }}: <strong>{{ total.totalText }}</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,72 @@
|
||||||
@media(max-width: 768px) { flex-direction: column; align-items: flex-start; gap: 16px; }
|
@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 {
|
.badge-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -392,6 +458,22 @@
|
||||||
&.compact-half { height: 200px; }
|
&.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 {
|
.card-adicionais .card-body-adicionais {
|
||||||
padding: 14px 16px 12px;
|
padding: 14px 16px 12px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -630,7 +712,221 @@
|
||||||
@media(max-width: 1080px) { grid-template-columns: 1fr; }
|
@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 */
|
/* Utils */
|
||||||
.text-brand { color: var(--brand); }
|
.text-brand { color: var(--brand); }
|
||||||
.text-brand-dark { color: #b832a8; }
|
.text-brand-dark { color: #b832a8; }
|
||||||
.full-width { width: 100%; }
|
.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -75,180 +75,214 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filters-row mt-4" data-animate>
|
<div class="filters-stack mt-4" data-animate>
|
||||||
<div class="filter-tabs">
|
<div class="filters-row filters-row-top">
|
||||||
<button type="button" class="filter-tab" [class.active]="filterSkil === 'ALL'" (click)="setFilter('ALL')" [disabled]="loading">
|
<div class="filter-tabs">
|
||||||
Todos
|
<button type="button" class="filter-tab" [class.active]="filterSkil === 'ALL'" (click)="setFilter('ALL')" [disabled]="loading">
|
||||||
</button>
|
Todos
|
||||||
<ng-container *ngIf="!isClientRestricted">
|
|
||||||
<button type="button" class="filter-tab" [class.active]="filterSkil === 'PF'" (click)="setFilter('PF')" [disabled]="loading">
|
|
||||||
<i class="bi bi-person me-1"></i> Pessoa Física
|
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="filter-tab" [class.active]="filterSkil === 'PJ'" (click)="setFilter('PJ')" [disabled]="loading">
|
<ng-container *ngIf="!isClientRestricted">
|
||||||
<i class="bi bi-building me-1"></i> Pessoa Jurídica
|
<button type="button" class="filter-tab" [class.active]="filterSkil === 'PF'" (click)="setFilter('PF')" [disabled]="loading">
|
||||||
</button>
|
<i class="bi bi-person me-1"></i> Pessoa Física
|
||||||
<button type="button" class="filter-tab" [class.active]="filterSkil === 'RESERVA'" (click)="setFilter('RESERVA')" [disabled]="loading">
|
</button>
|
||||||
<i class="bi bi-archive me-1"></i> Reservas
|
<button type="button" class="filter-tab" [class.active]="filterSkil === 'PJ'" (click)="setFilter('PJ')" [disabled]="loading">
|
||||||
</button>
|
<i class="bi bi-building me-1"></i> Pessoa Jurídica
|
||||||
</ng-container>
|
</button>
|
||||||
<button type="button" class="filter-tab" [class.active]="filterStatus === 'BLOCKED'" (click)="toggleBlockedFilter()" [disabled]="loading">
|
<button type="button" class="filter-tab" [class.active]="filterSkil === 'RESERVA'" (click)="setFilter('RESERVA')" [disabled]="loading">
|
||||||
<i class="bi bi-slash-circle me-1"></i> Bloqueadas
|
<i class="bi bi-archive me-1"></i> Reservas
|
||||||
</button>
|
</button>
|
||||||
<ng-container *ngIf="filterStatus === 'BLOCKED'">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="filter-tab"
|
|
||||||
[class.active]="blockedStatusMode === 'PERDA_ROUBO'"
|
|
||||||
(click)="setBlockedStatusMode('PERDA_ROUBO')"
|
|
||||||
[disabled]="loading">
|
|
||||||
Perda/Roubo
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="filter-tab"
|
|
||||||
[class.active]="blockedStatusMode === 'BLOQUEIO_120'"
|
|
||||||
(click)="setBlockedStatusMode('BLOQUEIO_120')"
|
|
||||||
[disabled]="loading">
|
|
||||||
120 dias
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CLIENTE MULTI-SELECT -->
|
|
||||||
<div class="client-filter-wrap" *ngIf="!isClientRestricted" (click)="$event.stopPropagation()">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn-client-filter"
|
|
||||||
[class.has-selection]="selectedClients.length > 0"
|
|
||||||
(click)="toggleClientMenu()"
|
|
||||||
[disabled]="loading">
|
|
||||||
|
|
||||||
<ng-container *ngIf="selectedClients.length === 0">
|
|
||||||
<i class="bi bi-people-fill me-2"></i>
|
|
||||||
<span>Clientes</span>
|
|
||||||
<i class="bi bi-chevron-down ms-2 small"></i>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<button type="button" class="filter-tab" [class.active]="filterStatus === 'BLOCKED'" (click)="toggleBlockedFilter()" [disabled]="loading">
|
||||||
<ng-container *ngIf="selectedClients.length > 0">
|
<i class="bi bi-slash-circle me-1"></i> Bloqueadas
|
||||||
<div class="chips-container">
|
</button>
|
||||||
<span *ngFor="let client of selectedClients" class="client-chip" (click)="$event.stopPropagation()">
|
<ng-container *ngIf="filterStatus === 'BLOCKED'">
|
||||||
{{ client }}
|
<button
|
||||||
<i class="bi bi-x chip-close" (click)="removeClient(client, $event)"></i>
|
type="button"
|
||||||
</span>
|
class="filter-tab"
|
||||||
</div>
|
[class.active]="blockedStatusMode === 'PERDA_ROUBO'"
|
||||||
<i class="bi bi-chevron-down ms-1 small text-muted"></i>
|
(click)="setBlockedStatusMode('PERDA_ROUBO')"
|
||||||
|
[disabled]="loading">
|
||||||
|
Perda/Roubo
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="filter-tab"
|
||||||
|
[class.active]="blockedStatusMode === 'BLOQUEIO_120'"
|
||||||
|
(click)="setBlockedStatusMode('BLOQUEIO_120')"
|
||||||
|
[disabled]="loading">
|
||||||
|
120 dias
|
||||||
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="client-dropdown" *ngIf="showClientMenu">
|
|
||||||
<div class="dropdown-header-search">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
placeholder="Buscar na lista..."
|
|
||||||
[(ngModel)]="clientSearchTerm"
|
|
||||||
autofocus
|
|
||||||
(click)="$event.stopPropagation()">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dropdown-list">
|
|
||||||
<div class="dropdown-item-custom" [class.selected]="selectedClients.length === 0" (click)="selectClient(null)">
|
|
||||||
<i class="bi bi-grid me-2"></i> Todos os Clientes
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-container *ngFor="let client of filteredClientsList">
|
|
||||||
<div class="dropdown-item-custom" [class.selected]="isClientSelected(client)" (click)="selectClient(client)">
|
|
||||||
<div class="d-flex align-items-center justify-content-between w-100">
|
|
||||||
<span>{{ client }}</span>
|
|
||||||
<i class="bi bi-check-lg text-brand" *ngIf="isClientSelected(client)"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="additional-filter-wrap" *ngIf="!isClientRestricted" (click)="$event.stopPropagation()">
|
<div class="filters-row filters-row-bottom" *ngIf="!isClientRestricted">
|
||||||
<button
|
<div class="operadora-empresa-filters" (click)="$event.stopPropagation()">
|
||||||
type="button"
|
<div class="filter-select-box">
|
||||||
class="btn-client-filter btn-additional-filter"
|
<app-select
|
||||||
[class.has-selection]="hasAdditionalFiltersApplied"
|
class="select-glass"
|
||||||
(click)="toggleAdditionalMenu()"
|
size="sm"
|
||||||
[disabled]="loading">
|
[options]="operadoraFilterOptions"
|
||||||
|
labelKey="label"
|
||||||
<ng-container *ngIf="!hasAdditionalFiltersApplied">
|
valueKey="value"
|
||||||
<i class="bi bi-sliders2-vertical me-2"></i>
|
[(ngModel)]="filterOperadora"
|
||||||
<span>Adicionais</span>
|
(ngModelChange)="setOperadoraFilter($event)"
|
||||||
<i class="bi bi-chevron-down ms-2 small"></i>
|
[disabled]="loading"
|
||||||
</ng-container>
|
></app-select>
|
||||||
|
|
||||||
<ng-container *ngIf="hasAdditionalFiltersApplied">
|
|
||||||
<div class="chips-container">
|
|
||||||
<span class="client-chip">
|
|
||||||
{{ additionalModeLabel }}
|
|
||||||
</span>
|
|
||||||
<span *ngFor="let label of additionalSelectedLabels" class="client-chip">
|
|
||||||
{{ label }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<i class="bi bi-chevron-down ms-1 small text-muted"></i>
|
|
||||||
</ng-container>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="client-dropdown additional-dropdown" *ngIf="showAdditionalMenu">
|
|
||||||
<div class="additional-dropdown-section">
|
|
||||||
<div class="additional-dropdown-title">Modo</div>
|
|
||||||
<div class="additional-mode-tabs">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="additional-mode-btn"
|
|
||||||
[class.active]="additionalMode === 'ALL'"
|
|
||||||
(click)="setAdditionalMode('ALL')"
|
|
||||||
[disabled]="loading">
|
|
||||||
Todos os adicionais
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="additional-mode-btn"
|
|
||||||
[class.active]="additionalMode === 'WITH'"
|
|
||||||
(click)="setAdditionalMode('WITH')"
|
|
||||||
[disabled]="loading">
|
|
||||||
Com adicionais
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="additional-mode-btn"
|
|
||||||
[class.active]="additionalMode === 'WITHOUT'"
|
|
||||||
(click)="setAdditionalMode('WITHOUT')"
|
|
||||||
[disabled]="loading">
|
|
||||||
Sem adicionais
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="additional-dropdown-section">
|
<div class="filter-select-box">
|
||||||
<div class="additional-dropdown-title">Serviços</div>
|
<app-select
|
||||||
<div class="additional-services-chips">
|
class="select-glass"
|
||||||
<button
|
size="sm"
|
||||||
type="button"
|
[options]="contaEmpresaFilterOptions"
|
||||||
class="additional-chip-btn"
|
labelKey="label"
|
||||||
*ngFor="let svc of additionalServiceOptions"
|
valueKey="value"
|
||||||
[class.active]="isAdditionalServiceSelected(svc.key)"
|
[(ngModel)]="filterContaEmpresa"
|
||||||
(click)="toggleAdditionalService(svc.key)"
|
(ngModelChange)="setContaEmpresaFilter($event)"
|
||||||
[disabled]="loading">
|
[searchable]="true"
|
||||||
{{ svc.label }}
|
searchPlaceholder="Buscar empresa..."
|
||||||
</button>
|
[disabled]="loading || loadingAccountCompanies"
|
||||||
|
></app-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CLIENTE MULTI-SELECT -->
|
||||||
|
<div class="client-filter-wrap" (click)="$event.stopPropagation()">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-client-filter"
|
||||||
|
[class.has-selection]="selectedClients.length > 0"
|
||||||
|
(click)="toggleClientMenu()"
|
||||||
|
[disabled]="loading">
|
||||||
|
|
||||||
|
<ng-container *ngIf="selectedClients.length === 0">
|
||||||
|
<i class="bi bi-people-fill me-2"></i>
|
||||||
|
<span>Clientes</span>
|
||||||
|
<i class="bi bi-chevron-down ms-2 small"></i>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="selectedClients.length > 0">
|
||||||
|
<div class="chips-container">
|
||||||
|
<span *ngFor="let client of selectedClients" class="client-chip" (click)="$event.stopPropagation()">
|
||||||
|
{{ client }}
|
||||||
|
<i class="bi bi-x chip-close" (click)="removeClient(client, $event)"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-down ms-1 small text-muted"></i>
|
||||||
|
</ng-container>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="client-dropdown" *ngIf="showClientMenu">
|
||||||
|
<div class="dropdown-header-search">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
placeholder="Buscar na lista..."
|
||||||
|
[(ngModel)]="clientSearchTerm"
|
||||||
|
autofocus
|
||||||
|
(click)="$event.stopPropagation()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dropdown-list">
|
||||||
|
<div class="dropdown-item-custom" [class.selected]="selectedClients.length === 0" (click)="selectClient(null)">
|
||||||
|
<i class="bi bi-grid me-2"></i> Todos os Clientes
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngFor="let client of filteredClientsList">
|
||||||
|
<div class="dropdown-item-custom" [class.selected]="isClientSelected(client)" (click)="selectClient(client)">
|
||||||
|
<div class="d-flex align-items-center justify-content-between w-100">
|
||||||
|
<span>{{ client }}</span>
|
||||||
|
<i class="bi bi-check-lg text-brand" *ngIf="isClientSelected(client)"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="additional-dropdown-footer">
|
<div class="additional-filter-wrap" (click)="$event.stopPropagation()">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="additional-chip-btn clear"
|
class="btn-client-filter btn-additional-filter"
|
||||||
(click)="clearAdditionalFilters()"
|
[class.has-selection]="hasAdditionalFiltersApplied"
|
||||||
[disabled]="loading">
|
(click)="toggleAdditionalMenu()"
|
||||||
Limpar filtros adicionais
|
[disabled]="loading">
|
||||||
</button>
|
|
||||||
|
<ng-container *ngIf="!hasAdditionalFiltersApplied">
|
||||||
|
<i class="bi bi-sliders2-vertical me-2"></i>
|
||||||
|
<span>Adicionais</span>
|
||||||
|
<i class="bi bi-chevron-down ms-2 small"></i>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="hasAdditionalFiltersApplied">
|
||||||
|
<div class="chips-container">
|
||||||
|
<span class="client-chip">
|
||||||
|
{{ additionalModeLabel }}
|
||||||
|
</span>
|
||||||
|
<span *ngFor="let label of additionalSelectedLabels" class="client-chip">
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-down ms-1 small text-muted"></i>
|
||||||
|
</ng-container>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="client-dropdown additional-dropdown" *ngIf="showAdditionalMenu">
|
||||||
|
<div class="additional-dropdown-section">
|
||||||
|
<div class="additional-dropdown-title">Modo</div>
|
||||||
|
<div class="additional-mode-tabs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="additional-mode-btn"
|
||||||
|
[class.active]="additionalMode === 'ALL'"
|
||||||
|
(click)="setAdditionalMode('ALL')"
|
||||||
|
[disabled]="loading">
|
||||||
|
Todos os adicionais
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="additional-mode-btn"
|
||||||
|
[class.active]="additionalMode === 'WITH'"
|
||||||
|
(click)="setAdditionalMode('WITH')"
|
||||||
|
[disabled]="loading">
|
||||||
|
Com adicionais
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="additional-mode-btn"
|
||||||
|
[class.active]="additionalMode === 'WITHOUT'"
|
||||||
|
(click)="setAdditionalMode('WITHOUT')"
|
||||||
|
[disabled]="loading">
|
||||||
|
Sem adicionais
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="additional-dropdown-section">
|
||||||
|
<div class="additional-dropdown-title">Serviços</div>
|
||||||
|
<div class="additional-services-chips">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="additional-chip-btn"
|
||||||
|
*ngFor="let svc of additionalServiceOptions"
|
||||||
|
[class.active]="isAdditionalServiceSelected(svc.key)"
|
||||||
|
(click)="toggleAdditionalService(svc.key)"
|
||||||
|
[disabled]="loading">
|
||||||
|
{{ svc.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="additional-dropdown-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="additional-chip-btn clear"
|
||||||
|
(click)="clearAdditionalFilters()"
|
||||||
|
[disabled]="loading">
|
||||||
|
Limpar filtros adicionais
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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; } }
|
.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 */
|
/* 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-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; } }
|
.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); } }
|
.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; }
|
.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; }
|
.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 {
|
.additional-filter-wrap {
|
||||||
position: relative;
|
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 {
|
.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 */
|
/* 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 { 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 {
|
.geral-kpis.geral-kpis-client {
|
||||||
|
|
|
||||||
|
|
@ -73,4 +73,36 @@ describe('Geral', () => {
|
||||||
expect(component.createBatchLines[0].linha).toBe('11888888888');
|
expect(component.createBatchLines[0].linha).toBe('11888888888');
|
||||||
expect(component.createBatchLines[0]['planoContrato']).toBe('PLANO B');
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,20 @@ import {
|
||||||
buildBatchMassPreview,
|
buildBatchMassPreview,
|
||||||
mergeMassRows
|
mergeMassRows
|
||||||
} from './batch-mass-input.util';
|
} 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 SortDir = 'asc' | 'desc';
|
||||||
type CreateMode = 'NEW_CLIENT' | 'NEW_LINE_IN_GROUP';
|
type CreateMode = 'NEW_CLIENT' | 'NEW_LINE_IN_GROUP';
|
||||||
type CreateEntryMode = 'SINGLE' | 'BATCH';
|
type CreateEntryMode = 'SINGLE' | 'BATCH';
|
||||||
type AdditionalMode = 'ALL' | 'WITH' | 'WITHOUT';
|
type AdditionalMode = 'ALL' | 'WITH' | 'WITHOUT';
|
||||||
|
type OperadoraFilterMode = 'ALL' | 'VIVO' | 'CLARO' | 'TIM';
|
||||||
type AdditionalServiceKey = 'gvd' | 'skeelo' | 'news' | 'travel' | 'sync' | 'dispositivo';
|
type AdditionalServiceKey = 'gvd' | 'skeelo' | 'news' | 'travel' | 'sync' | 'dispositivo';
|
||||||
type BlockedStatusMode = 'ALL' | 'PERDA_ROUBO' | 'BLOQUEIO_120';
|
type BlockedStatusMode = 'ALL' | 'PERDA_ROUBO' | 'BLOQUEIO_120';
|
||||||
|
|
||||||
|
|
@ -86,6 +95,9 @@ interface ApiPagedResult<T> {
|
||||||
interface ApiLineList {
|
interface ApiLineList {
|
||||||
id: string;
|
id: string;
|
||||||
item: number;
|
item: number;
|
||||||
|
conta?: string | null;
|
||||||
|
contaEmpresa?: string | null;
|
||||||
|
empresaConta?: string | null;
|
||||||
linha: string | null;
|
linha: string | null;
|
||||||
chip?: string | null;
|
chip?: string | null;
|
||||||
cliente: string | null;
|
cliente: string | null;
|
||||||
|
|
@ -396,6 +408,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
blockedStatusMode: BlockedStatusMode = 'ALL';
|
blockedStatusMode: BlockedStatusMode = 'ALL';
|
||||||
additionalMode: AdditionalMode = 'ALL';
|
additionalMode: AdditionalMode = 'ALL';
|
||||||
selectedAdditionalServices: AdditionalServiceKey[] = [];
|
selectedAdditionalServices: AdditionalServiceKey[] = [];
|
||||||
|
filterOperadora: OperadoraFilterMode = 'ALL';
|
||||||
|
filterContaEmpresa = '';
|
||||||
readonly additionalServiceOptions: Array<{ key: AdditionalServiceKey; label: string }> = [
|
readonly additionalServiceOptions: Array<{ key: AdditionalServiceKey; label: string }> = [
|
||||||
{ key: 'gvd', label: 'Gestão Voz e Dados' },
|
{ key: 'gvd', label: 'Gestão Voz e Dados' },
|
||||||
{ key: 'skeelo', label: 'Skeelo' },
|
{ key: 'skeelo', label: 'Skeelo' },
|
||||||
|
|
@ -404,6 +418,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
{ key: 'sync', label: 'Vivo Sync' },
|
{ key: 'sync', label: 'Vivo Sync' },
|
||||||
{ key: 'dispositivo', label: 'Vivo Gestão Dispositivo' }
|
{ 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[] = [];
|
clientsList: string[] = [];
|
||||||
loadingClientsList = false;
|
loadingClientsList = false;
|
||||||
|
|
@ -521,12 +541,10 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
'M2M 50MB'
|
'M2M 50MB'
|
||||||
];
|
];
|
||||||
|
|
||||||
private readonly fallbackAccountCompanies: AccountCompanyOption[] = [
|
private readonly fallbackAccountCompanies: AccountCompanyOption[] = DEFAULT_ACCOUNT_COMPANIES.map((group) => ({
|
||||||
{ empresa: 'CLARO LINE MÓVEL', contas: ['172593311', '172593840'] },
|
empresa: group.empresa,
|
||||||
{ empresa: 'VIVO MACROPHONY', contas: ['0430237019', '0437488125', '0449508564', '0454371844'] },
|
contas: [...group.contas],
|
||||||
{ empresa: 'VIVO LINE MÓVEL', contas: ['0435288088'] },
|
}));
|
||||||
{ empresa: 'TIM LINE MÓVEL', contas: ['0072046192'] }
|
|
||||||
];
|
|
||||||
|
|
||||||
accountCompanies: AccountCompanyOption[] = [...this.fallbackAccountCompanies];
|
accountCompanies: AccountCompanyOption[] = [...this.fallbackAccountCompanies];
|
||||||
loadingAccountCompanies = false;
|
loadingAccountCompanies = false;
|
||||||
|
|
@ -538,6 +556,15 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return this.accountCompanies.map((x) => x.empresa);
|
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[] {
|
get contaOptionsForCreate(): string[] {
|
||||||
return this.getContasByEmpresa(this.createModel?.contaEmpresa);
|
return this.getContasByEmpresa(this.createModel?.contaEmpresa);
|
||||||
}
|
}
|
||||||
|
|
@ -789,8 +816,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return this.additionalMode !== 'ALL' || this.selectedAdditionalServices.length > 0;
|
return this.additionalMode !== 'ALL' || this.selectedAdditionalServices.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasOperadoraEmpresaFiltersApplied(): boolean {
|
||||||
|
return this.filterOperadora !== 'ALL' || !!this.filterContaEmpresa.trim();
|
||||||
|
}
|
||||||
|
|
||||||
get hasClientSideFiltersApplied(): boolean {
|
get hasClientSideFiltersApplied(): boolean {
|
||||||
return this.hasAdditionalFiltersApplied || this.filterStatus === 'BLOCKED';
|
return this.hasAdditionalFiltersApplied || this.filterStatus === 'BLOCKED' || this.hasOperadoraEmpresaFiltersApplied;
|
||||||
}
|
}
|
||||||
|
|
||||||
get additionalModeLabel(): string {
|
get additionalModeLabel(): string {
|
||||||
|
|
@ -1197,9 +1228,10 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.http.get<AccountCompanyOption[]>(`${this.apiBase}/account-companies`).subscribe({
|
this.http.get<AccountCompanyOption[]>(`${this.apiBase}/account-companies`).subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
const normalized = this.normalizeAccountCompanies(data);
|
const normalized = this.normalizeAccountCompanies(data);
|
||||||
this.accountCompanies =
|
const source = normalized.length > 0 ? normalized : this.fallbackAccountCompanies;
|
||||||
normalized.length > 0 ? normalized : [...this.fallbackAccountCompanies];
|
this.accountCompanies = mergeAccountCompaniesWithDefaults(source);
|
||||||
this.loadingAccountCompanies = false;
|
this.loadingAccountCompanies = false;
|
||||||
|
this.syncContaEmpresaFilterByOperadora();
|
||||||
|
|
||||||
this.syncContaEmpresaSelection(this.createModel);
|
this.syncContaEmpresaSelection(this.createModel);
|
||||||
this.syncContaEmpresaSelection(this.editModel);
|
this.syncContaEmpresaSelection(this.editModel);
|
||||||
|
|
@ -1208,8 +1240,9 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.accountCompanies = [...this.fallbackAccountCompanies];
|
this.accountCompanies = mergeAccountCompaniesWithDefaults(this.fallbackAccountCompanies);
|
||||||
this.loadingAccountCompanies = false;
|
this.loadingAccountCompanies = false;
|
||||||
|
this.syncContaEmpresaFilterByOperadora();
|
||||||
|
|
||||||
this.syncContaEmpresaSelection(this.createModel);
|
this.syncContaEmpresaSelection(this.createModel);
|
||||||
this.syncContaEmpresaSelection(this.editModel);
|
this.syncContaEmpresaSelection(this.editModel);
|
||||||
|
|
@ -1785,6 +1818,32 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.refreshData();
|
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 {
|
private applyBaseFilters(params: HttpParams): HttpParams {
|
||||||
let next = params;
|
let next = params;
|
||||||
|
|
||||||
|
|
@ -1868,6 +1927,10 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.matchesOperadoraContaEmpresaFilters(line)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const selected = this.selectedAdditionalServices;
|
const selected = this.selectedAdditionalServices;
|
||||||
const hasSelected = selected.length > 0;
|
const hasSelected = selected.length > 0;
|
||||||
|
|
||||||
|
|
@ -1891,6 +1954,40 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return true;
|
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[] {
|
private applyAdditionalFiltersClientSide(lines: ApiLineList[]): ApiLineList[] {
|
||||||
if (!Array.isArray(lines) || lines.length === 0) return [];
|
if (!Array.isArray(lines) || lines.length === 0) return [];
|
||||||
return lines.filter((line) => this.matchesAdditionalFilters(line));
|
return lines.filter((line) => this.matchesAdditionalFilters(line));
|
||||||
|
|
@ -2702,6 +2799,13 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
parts.push(this.selectedAdditionalServices.join('-'));
|
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('_');
|
return parts.join('_');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5144,26 +5248,46 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return found ? [...found.contas] : [];
|
return found ? [...found.contas] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private findEmpresaByConta(conta: any): string {
|
private getContaEmpresaOptionsByOperadora(mode: OperadoraFilterMode): string[] {
|
||||||
const target = this.normalizeConta(conta);
|
const empresas = this.mergeOptionList([], this.accountCompanies.map((group) => group?.empresa ?? ''))
|
||||||
if (!target) return '';
|
.filter((empresa) => !!(empresa ?? '').toString().trim());
|
||||||
|
|
||||||
const found = this.accountCompanies.find((group) =>
|
const filtered = mode === 'ALL'
|
||||||
(group.contas ?? []).some((c) => this.sameConta(c, target))
|
? empresas
|
||||||
);
|
: empresas.filter((empresa) => {
|
||||||
return found?.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 {
|
private normalizeConta(value: any): string {
|
||||||
const raw = (value ?? '').toString().trim();
|
return normalizeContaValue(value);
|
||||||
if (!raw) return '';
|
|
||||||
if (!/^\d+$/.test(raw)) return raw.toUpperCase();
|
|
||||||
const noLeadingZero = raw.replace(/^0+/, '');
|
|
||||||
return noLeadingZero || '0';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sameConta(a: any, b: any): boolean {
|
private sameConta(a: any, b: any): boolean {
|
||||||
return this.normalizeConta(a) === this.normalizeConta(b);
|
return sameContaValue(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncContaEmpresaSelection(model: any) {
|
private syncContaEmpresaSelection(model: any) {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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<string, string> {
|
||||||
|
const result = new Map<string, string>();
|
||||||
|
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
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<string, { empresa: string; contas: string[] }>();
|
||||||
|
const contaSeenByEmpresa = new Map<string, Set<string>>();
|
||||||
|
|
||||||
|
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<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue