line-gestao-frontend/src/app/pages/resumo/resumo.html

616 lines
27 KiB
HTML

<section class="resumo-page">
<div class="wrap">
<div class="resumo-container">
<div class="page-head" data-animate>
<div class="title-group">
<span class="badge-pill"><i class="bi bi-graph-up"></i> Dashboard</span>
<div class="flex-title">
<h2 class="page-title">Resumo Gerencial</h2>
<div class="status-wrapper">
<div class="status loading" *ngIf="loading">
<i class="bi bi-arrow-repeat spin"></i> Atualizando dados...
</div>
<div class="status error" *ngIf="!loading && errorMessage">
<i class="bi bi-exclamation-triangle"></i> {{ errorMessage }}
</div>
<div class="status success" *ngIf="!loading && !errorMessage">
<i class="bi bi-check-circle"></i> Atualizado
</div>
</div>
</div>
<p class="page-subtitle">Visão consolidada de performance, contratos e indicadores operacionais.</p>
</div>
<div class="header-actions">
<button class="btn-ghost" type="button" (click)="refresh()" [disabled]="loading">
<i class="bi bi-arrow-clockwise" [class.spin]="loading"></i>
<span>Atualizar</span>
</button>
</div>
</div>
<div class="tab-bar" data-animate>
<button
type="button"
class="tab-btn"
*ngFor="let tab of tabs"
[class.active]="activeTab === tab.key"
(click)="setTab(tab.key)">
<i [class]="tab.icon"></i>
<span>{{ tab.label }}</span>
</button>
</div>
<div class="tab-panel" *ngIf="activeTab === 'planos'">
<div class="section-hero" data-animate>
<div class="hero-content">
<div class="hero-text">
<h3>Planos & Contratos</h3>
<p>{{ showFinancial ? 'Performance financeira agrupada por modalidade de plano.' : 'Distribuição e volume de linhas por modalidade de plano.' }}</p>
</div>
<div class="hero-kpis">
<div class="kpi-card kpi-card--total-lines">
<span class="kpi-lbl">Total Linhas</span>
<strong class="kpi-val">{{ formatNumber(planosTotals?.totalLinhasTotal) }}</strong>
</div>
<div class="kpi-card" *ngIf="showFinancial">
<span class="kpi-lbl">Valor Total</span>
<strong class="kpi-val text-brand">{{ formatMoney(planosTotals?.valorTotal) }}</strong>
</div>
<div class="kpi-card" *ngIf="showFinancial">
<span class="kpi-lbl">Contratos</span>
<strong class="kpi-val">{{ formatMoney(contratosTotals?.valorTotal) }}</strong>
</div>
</div>
</div>
</div>
<div class="section-grid planos-charts" data-animate>
<div class="chart-card" *ngIf="showFinancial">
<div class="card-header-clean">
<h3>Top Planos (Valor)</h3>
<p>Os planos com maior representatividade financeira.</p>
</div>
<div class="chart-area">
<canvas #chartPlanos></canvas>
</div>
</div>
<div class="chart-card" [class.full-span]="!showFinancial">
<div class="card-header-clean">
<h3>Top Planos (Volume)</h3>
<p>Quantidade de linhas ativas por tipo de plano.</p>
</div>
<div class="chart-area">
<canvas #chartPlanosLinhas></canvas>
</div>
</div>
</div>
<details class="section-card" open>
<summary>
<div class="summary-content">
<h4>Macrophony - Planos</h4>
<span>Detalhamento granular dos planos e suas variações.</span>
</div>
<div class="summary-icon"><i class="bi bi-chevron-down"></i></div>
</summary>
<div class="macrophony-block" [class.compact]="macrophonyCompact">
<div class="table-tools">
<div class="search-box">
<i class="bi bi-search"></i>
<input
type="text"
placeholder="Pesquisar..."
[value]="macrophonySearch"
(input)="onMacrophonySearch($any($event.target).value)" />
</div>
<div class="tools-right">
<label class="select-label">
Exibir
<select [value]="macrophonyPageSize" (change)="onMacrophonyPageSizeChange($any($event.target).value)">
<option *ngFor="let size of macrophonyPageOptions" [value]="size">{{ size }}</option>
</select>
</label>
<div class="divider-v"></div>
<button class="btn-icon-text" type="button" (click)="toggleMacrophonyCompact()">
<i class="bi" [class.bi-arrows-angle-expand]="macrophonyCompact" [class.bi-arrows-collapse]="!macrophonyCompact"></i>
<span class="hide-mobile">{{ macrophonyCompact ? 'Expandir' : 'Compactar' }}</span>
</button>
<button class="btn-icon-text" type="button" (click)="exportMacrophonyCsv()">
<i class="bi bi-download"></i>
<span class="hide-mobile">Exportar</span>
</button>
</div>
</div>
<div class="macrophony-list">
<div class="empty-state" *ngIf="!loading && macrophonyView.length === 0">
<i class="bi bi-inbox"></i>
<p>Nenhum dado encontrado.</p>
</div>
<div class="macrophony-group" *ngFor="let group of macrophonyView; trackBy: trackByIndex">
<div class="macrophony-row" (click)="toggleMacrophonyGroup(group.key)">
<div class="row-trigger">
<button class="group-toggle" type="button">
<i class="bi" [class.bi-chevron-down]="isMacrophonyOpen(group.key)" [class.bi-chevron-right]="!isMacrophonyOpen(group.key)"></i>
</button>
</div>
<div class="group-main">
<div class="group-title">{{ group.plano }}</div>
<div class="group-meta">
<span class="badge-tag">GB {{ group.gbLabel }}</span>
<span class="badge-tag secondary">{{ formatNumber(group.totalLinhas) }} linhas</span>
</div>
</div>
<div class="group-metrics" *ngIf="showFinancial">
<div class="metric">
<span class="lbl">Valor Total</span>
<strong class="val-money">{{ formatMoney(group.valorTotal) }}</strong>
</div>
<div class="metric hide-mobile">
<span class="lbl">Média Un.</span>
<strong>{{ formatMoney(group.valorUnitMedio) }}</strong>
</div>
</div>
<div class="group-actions">
<button class="btn-mini" type="button" (click)="openMacrophonyDetail(group); $event.stopPropagation()">
Detalhes
</button>
</div>
</div>
<div class="macrophony-details" *ngIf="isMacrophonyOpen(group.key)">
<div class="table-wrap is-nested">
<table class="data-table" [class.compact]="macrophonyCompact">
<thead>
<tr>
<th>Plano / Variação</th>
<th>Franquia</th>
<th class="text-right" *ngIf="showFinancial">Valor Un.</th>
<th class="text-right">Linhas</th>
<th class="text-right" *ngIf="showFinancial">Total</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of group.rows; trackBy: trackByIndex">
<td>
<div class="cell-flex">
{{ row.planoContrato || '-' }}
<i *ngIf="isVivoTravel(row.vivoTravel)" class="bi bi-airplane-fill text-brand" title="Vivo Travel"></i>
</div>
</td>
<td>{{ formatGb(row.gb) }}</td>
<td class="text-right num-font" *ngIf="showFinancial">{{ formatMoney(row.valorIndividualComSvas) }}</td>
<td class="text-right num-font">{{ formatNumber(row.totalLinhas) }}</td>
<td class="text-right num-font font-bold" *ngIf="showFinancial">{{ formatMoney(row.valorTotal) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="table-footer">
<div class="table-count">
Exibindo {{ macrophonyPageStart }}-{{ macrophonyPageEnd }} de {{ macrophonyFilteredGroups.length }} grupos
</div>
<div class="pagination">
<button class="page-btn" (click)="goToMacrophonyPage(macrophonyPage - 1)" [disabled]="macrophonyPage === 1">
<i class="bi bi-chevron-left"></i>
</button>
<button
class="page-btn"
*ngFor="let p of macrophonyPageNumbers"
[class.active]="p === macrophonyPage"
(click)="goToMacrophonyPage(p)">{{ p }}</button>
<button class="page-btn" (click)="goToMacrophonyPage(macrophonyPage + 1)" [disabled]="macrophonyPage === macrophonyTotalPages">
<i class="bi bi-chevron-right"></i>
</button>
</div>
</div>
<div class="macrophony-summary" *ngIf="planosTotals">
<div class="summary-item">
<span>Total de Linhas</span>
<strong>{{ formatNumber(planosTotals.totalLinhasTotal) }}</strong>
</div>
<div class="summary-item highlight" *ngIf="showFinancial">
<span>Valor Total Global</span>
<strong>{{ formatMoney(planosTotals.valorTotal) }}</strong>
</div>
</div>
</div>
</details>
<details class="section-card">
<summary>
<div class="summary-content">
<h4>Resumo de Contratos</h4>
<span>Visão consolidada por tipo de contrato vigente.</span>
</div>
<div class="summary-icon"><i class="bi bi-chevron-down"></i></div>
</summary>
<ng-container *ngTemplateOutlet="groupedTableBlock; context: { group: groupPlanoContrato, footer: 'contratos', file: 'plano-contrato' }"></ng-container>
</details>
</div>
<div class="tab-panel" *ngIf="activeTab === 'clientes'">
<div class="section-hero" data-animate>
<div class="hero-content">
<div class="hero-text">
<h3>Clientes & Performance</h3>
<p>{{ showFinancial ? 'Analise a rentabilidade e custos por cliente.' : 'Distribuição e volume de linhas por cliente.' }}</p>
</div>
<div class="hero-kpis">
<div class="kpi-card kpi-card--total-lines">
<span class="kpi-lbl">Total Linhas</span>
<strong class="kpi-val">{{ formatNumber(clientesTotals?.qtdLinhasTotal) }}</strong>
</div>
<div class="kpi-card" *ngIf="showFinancial">
<span class="kpi-lbl">Receita Line</span>
<strong class="kpi-val">{{ formatMoney(clientesTotals?.valorContratoLine) }}</strong>
</div>
<div class="kpi-card highlight" *ngIf="showFinancial">
<span class="kpi-lbl">Lucro Total</span>
<strong class="kpi-val text-success">{{ formatMoney(clientesTotals?.lucro) }}</strong>
</div>
</div>
</div>
</div>
<div class="section-grid full-chart" data-animate>
<div class="chart-card">
<div class="card-header-clean">
<h3>{{ showFinancial ? 'Top Clientes (Lucratividade)' : 'Top Clientes (Qtd. Linhas)' }}</h3>
<p>{{ showFinancial ? 'Clientes ordenados pelo maior retorno financeiro.' : 'Clientes com maior volume de linhas.' }}</p>
</div>
<div class="chart-area">
<canvas #chartClientes></canvas>
</div>
</div>
</div>
<details class="section-card" open>
<summary>
<div class="summary-content">
<h4>Detalhamento Vivo x Line Móvel</h4>
<span>Comparativo de custos, receitas e margem por cliente.</span>
</div>
<div class="summary-icon"><i class="bi bi-chevron-down"></i></div>
</summary>
<ng-container *ngTemplateOutlet="groupedTableBlock; context: { group: groupClientes, footer: 'clientes', file: 'clientes-vivo-line' }"></ng-container>
</details>
</div>
<div class="tab-panel" *ngIf="activeTab === 'totais'">
<div class="section-hero" data-animate>
<div class="hero-content">
<div class="hero-text">
<h3>Totais Line</h3>
<p>Consolidado entre Pessoa Física (PF) e Jurídica (PJ).</p>
</div>
<div class="hero-kpis">
<div class="kpi-card">
<span class="kpi-lbl">PF Linhas</span>
<strong class="kpi-val">{{ formatNumber(findLineTotal(['PF','PESSOA FISICA'])?.qtdLinhas) }}</strong>
</div>
<div class="kpi-card">
<span class="kpi-lbl">PJ Linhas</span>
<strong class="kpi-val">{{ formatNumber(findLineTotal(['PJ','PESSOA JURIDICA'])?.qtdLinhas) }}</strong>
</div>
<div class="kpi-card" *ngIf="showFinancial">
<span class="kpi-lbl">Lucro Consolidado</span>
<strong class="kpi-val text-success">{{ formatMoney(totaisLineLucroConsolidado) }}</strong>
</div>
</div>
</div>
</div>
<div class="section-grid full-chart" data-animate>
<div class="chart-card">
<div class="card-header-clean">
<h3>Distribuição PF vs PJ</h3>
<p>Proporção da base de linhas ativas.</p>
</div>
<div class="chart-area">
<canvas #chartTotais></canvas>
</div>
</div>
</div>
<details class="section-card" open>
<summary>
<div class="summary-content">
<h4>Detalhamento Totais</h4>
<span>Tabela analítica dos totais processados.</span>
</div>
<div class="summary-icon"><i class="bi bi-chevron-down"></i></div>
</summary>
<ng-container *ngTemplateOutlet="groupedTableBlock; context: { group: groupTotaisLine, footer: 'none', file: 'totais-line' }"></ng-container>
</details>
<details class="section-card" open>
<summary>
<div class="summary-content">
<h4>Distribuição por GB</h4>
<span>Tabela GB / QTD / SOMA importada da aba RESUMO.</span>
</div>
<div class="summary-icon"><i class="bi bi-chevron-down"></i></div>
</summary>
<div class="grouped-block">
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th>GB</th>
<th class="text-right">QTD</th>
<th class="text-right">SOMA</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of gbDistribuicaoRows; trackBy: trackByIndex">
<td><span class="num-font">{{ formatGb(row.gb) }}</span></td>
<td class="text-right"><span class="num-font">{{ formatNumber(row.qtd) }}</span></td>
<td class="text-right"><span class="num-font">{{ formatMoney(row.soma) }}</span></td>
</tr>
<tr class="total-row" *ngIf="gbDistribuicaoRows.length">
<td>Total</td>
<td class="text-right"><span class="num-font">{{ formatNumber(gbDistribuicaoTotalLinhas) }}</span></td>
<td class="text-right"><span class="num-font">{{ formatMoney(gbDistribuicaoSomaTotal) }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</details>
</div>
<div class="tab-panel" *ngIf="activeTab === 'reserva'">
<div class="section-hero" data-animate>
<div class="hero-content">
<div class="hero-text">
<h3>Estoque de Reserva</h3>
<p>Monitoramento de linhas disponíveis por DDD.</p>
</div>
<div class="hero-kpis">
<div class="kpi-card">
<span class="kpi-lbl">Linhas em Estoque</span>
<strong class="kpi-val">{{ formatNumber(reservaTotals?.qtdLinhasTotal) }}</strong>
</div>
<div class="kpi-card" *ngIf="showFinancial">
<span class="kpi-lbl">Custo de Reserva</span>
<strong class="kpi-val">{{ formatNumber(reservaTotals?.total) }}</strong>
</div>
</div>
</div>
</div>
<div class="section-grid full-chart" data-animate>
<div class="chart-card">
<div class="card-header-clean">
<h3>Concentração por DDD</h3>
<p>Regiões com maior volume de linhas em reserva.</p>
</div>
<div class="chart-area">
<canvas #chartReserva></canvas>
</div>
</div>
</div>
<details class="section-card" open>
<summary>
<div class="summary-content">
<h4>Detalhamento por DDD</h4>
<span>Lista completa de estoque agrupada geograficamente.</span>
</div>
<div class="summary-icon"><i class="bi bi-chevron-down"></i></div>
</summary>
<ng-container *ngTemplateOutlet="groupedTableBlock; context: { group: groupReserva, footer: 'reserva', file: 'reserva-ddd' }"></ng-container>
</details>
</div>
</div>
</div>
<ng-template #groupedTableBlock let-group="group" let-footer="footer" let-file="file">
<div class="grouped-block" [class.compact]="group.compact">
<div class="table-tools">
<div class="search-box">
<i class="bi bi-search"></i>
<input
type="text"
placeholder="Pesquisar..."
[value]="group.search"
(input)="onGroupedSearch(group, $any($event.target).value)" />
</div>
<div class="tools-right">
<button class="btn-icon-text" type="button" (click)="toggleGroupedCompact(group)">
<i class="bi" [class.bi-arrows-angle-expand]="group.compact" [class.bi-arrows-collapse]="!group.compact"></i>
<span class="hide-mobile">{{ group.compact ? 'Expandir' : 'Compactar' }}</span>
</button>
<button class="btn-icon-text" type="button" (click)="exportGroupedCsv(group, file)">
<i class="bi bi-download"></i>
<span class="hide-mobile">Exportar</span>
</button>
</div>
</div>
<div class="grouped-list">
<div class="empty-state" *ngIf="!loading && group.view.length === 0">
<p>Nenhum registro encontrado.</p>
</div>
<div class="grouped-group" *ngFor="let item of group.view; trackBy: trackByIndex">
<div class="grouped-row" (click)="toggleGroupedOpen(group, item.key)">
<div class="row-trigger">
<button class="group-toggle" type="button">
<i class="bi" [class.bi-chevron-down]="isGroupedOpen(group, item.key)" [class.bi-chevron-right]="!isGroupedOpen(group, item.key)"></i>
</button>
</div>
<div class="group-main">
<div class="group-title">{{ item.title }}</div>
<div class="group-subtitle" *ngIf="item.subtitle">{{ item.subtitle }}</div>
</div>
<div class="group-metrics">
<div class="metric" *ngFor="let metric of item.metrics">
<span class="lbl">{{ metric.label }}</span>
<strong class="num-font" [ngClass]="metric.tone">{{ metric.value }}</strong>
</div>
</div>
<div class="group-actions">
<button class="btn-mini" type="button" (click)="openGroupedDetail(group, item); $event.stopPropagation()">
Ver Tabela
</button>
</div>
</div>
<div class="grouped-details" *ngIf="isGroupedOpen(group, item.key)">
<div class="table-wrap is-nested">
<table class="data-table" [class.compact]="group.compact">
<thead>
<tr>
<th
*ngFor="let col of group.table.columns"
[class.text-right]="col.align === 'right'"
[class.text-center]="col.align === 'center'">
{{ col.label }}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of item.rows; trackBy: trackByIndex" [class.diff-row]="getTableRowClass(group.table, row)">
<td
*ngFor="let col of group.table.columns"
[class.text-right]="col.align === 'right'"
[class.text-center]="col.align === 'center'">
<span class="num-font" *ngIf="!col.badge" [ngClass]="col.tone ? getToneClass(col.value(row)) : null">
{{ formatCell(col, row) }}
</span>
<span *ngIf="col.badge" class="badge-tag">{{ formatCell(col, row) }}</span>
<i *ngIf="col.icon && col.icon(row)" [class]="col.icon(row)"></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="grouped-summary-bar" *ngIf="showFinancial && footer === 'clientes' && clientesTotals">
<div class="sum-col">
<span class="lbl">Receita Line</span>
<strong>{{ formatMoney(clientesTotals.valorContratoLine) }}</strong>
</div>
<div class="sum-col">
<span class="lbl">Custo Vivo</span>
<strong>{{ formatMoney(clientesTotals.valorContratoVivo) }}</strong>
</div>
<div class="sum-col highlight">
<span class="lbl">Lucro Líquido</span>
<strong class="text-success">{{ formatMoney(clientesTotals.lucro) }}</strong>
</div>
</div>
<div class="table-footer">
<div class="table-count">Mostrando {{ getGroupedPageStart(group) }}-{{ getGroupedPageEnd(group) }} de {{ group.filtered.length }}</div>
<div class="pagination">
<button class="page-btn" (click)="goToGroupedPage(group, group.page - 1)" [disabled]="group.page === 1">
<i class="bi bi-chevron-left"></i>
</button>
<button
class="page-btn"
*ngFor="let p of getGroupedPageNumbers(group)"
[class.active]="p === group.page"
(click)="goToGroupedPage(group, p)">{{ p }}</button>
<button class="page-btn" (click)="goToGroupedPage(group, group.page + 1)" [disabled]="group.page === getGroupedTotalPages(group)">
<i class="bi bi-chevron-right"></i>
</button>
</div>
</div>
</div>
<div class="grouped-backdrop" *ngIf="group.detailOpen" (click)="closeGroupedDetail(group)"></div>
<div class="grouped-modal" *ngIf="group.detailOpen">
<div class="grouped-card" (click)="$event.stopPropagation()">
<div class="detail-head">
<h4>{{ group.detailGroup?.title }}</h4>
<button class="btn-icon" type="button" (click)="closeGroupedDetail(group)"><i class="bi bi-x-lg"></i></button>
</div>
<div class="grouped-modal-body">
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th *ngFor="let col of group.table.columns" [class.text-right]="col.align === 'right'">
{{ col.label }}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of group.detailGroup?.rows; trackBy: trackByIndex">
<td *ngFor="let col of group.table.columns" [class.text-right]="col.align === 'right'">
<span class="num-font">{{ formatCell(col, row) }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</ng-template>
<div class="macrophony-backdrop" *ngIf="macrophonyDetailOpen" (click)="closeMacrophonyDetail()"></div>
<div class="macrophony-modal" *ngIf="macrophonyDetailOpen">
<div class="macrophony-card" (click)="$event.stopPropagation()">
<div class="detail-head">
<div>
<span class="detail-super">Detalhes do Plano</span>
<h4>{{ macrophonyDetailGroup?.plano }}</h4>
</div>
<button class="btn-icon" type="button" (click)="closeMacrophonyDetail()"><i class="bi bi-x-lg"></i></button>
</div>
<div class="macrophony-modal-body">
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th>Variação</th>
<th>GB</th>
<th class="text-right" *ngIf="showFinancial">Valor Un.</th>
<th class="text-right">Total Linhas</th>
<th class="text-right" *ngIf="showFinancial">Valor Total</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of macrophonyDetailGroup?.rows; trackBy: trackByIndex">
<td>{{ row.planoContrato || '-' }}</td>
<td>{{ formatGb(row.gb) }}</td>
<td class="text-right num-font" *ngIf="showFinancial">{{ formatMoney(row.valorIndividualComSvas) }}</td>
<td class="text-right num-font">{{ formatNumber(row.totalLinhas) }}</td>
<td class="text-right num-font" *ngIf="showFinancial">{{ formatMoney(row.valorTotal) }}</td>
</tr>
</tbody>
<tfoot *ngIf="macrophonyDetailGroup">
<tr class="total-row">
<td [attr.colspan]="showFinancial ? 3 : 2">Total deste grupo</td>
<td class="text-right num-font">{{ formatNumber(macrophonyDetailGroup.totalLinhas) }}</td>
<td class="text-right num-font" *ngIf="showFinancial">{{ formatMoney(macrophonyDetailGroup.valorTotal) }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</section>