diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index f0b8b83..af24341 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -21,6 +21,7 @@ import { Resumo } from './pages/resumo/resumo';
import { Parcelamentos } from './pages/parcelamentos/parcelamentos';
import { Historico } from './pages/historico/historico';
import { HistoricoLinhas } from './pages/historico-linhas/historico-linhas';
+import { HistoricoChips } from './pages/historico-chips/historico-chips';
import { Perfil } from './pages/perfil/perfil';
import { SystemProvisionUserPage } from './pages/system-provision-user/system-provision-user';
import { SolicitacoesLinhas } from './pages/solicitacoes-linhas/solicitacoes-linhas';
@@ -43,6 +44,7 @@ export const routes: Routes = [
{ path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, sysadminOrFinanceiroGuard], title: 'Parcelamentos' },
{ path: 'historico', component: Historico, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico' },
{ path: 'historico-linhas', component: HistoricoLinhas, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico de Linhas' },
+ { path: 'historico-chips', component: HistoricoChips, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico de Chips' },
{ path: 'solicitacoes', component: SolicitacoesLinhas, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Solicitações' },
{ path: 'auditoria-mve', component: MveAuditoriaPage, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Auditoria MVE' },
{ path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' },
diff --git a/src/app/app.ts b/src/app/app.ts
index da02a75..2a1b14c 100644
--- a/src/app/app.ts
+++ b/src/app/app.ts
@@ -43,6 +43,7 @@ export class AppComponent {
'/parcelamentos',
'/historico',
'/historico-linhas',
+ '/historico-chips',
'/perfil',
'/system',
];
diff --git a/src/app/components/header/header.html b/src/app/components/header/header.html
index 00114a2..649881f 100644
--- a/src/app/components/header/header.html
+++ b/src/app/components/header/header.html
@@ -556,6 +556,9 @@
Histórico de Linhas
+
+ Histórico de Chips
+
Solicitações
diff --git a/src/app/components/header/header.ts b/src/app/components/header/header.ts
index b8f0eda..be50d40 100644
--- a/src/app/components/header/header.ts
+++ b/src/app/components/header/header.ts
@@ -100,6 +100,7 @@ export class Header implements AfterViewInit, OnDestroy {
'/parcelamentos',
'/historico',
'/historico-linhas',
+ '/historico-chips',
'/solicitacoes',
'/auditoria-mve',
'/perfil',
diff --git a/src/app/pages/dashboard/dashboard.html b/src/app/pages/dashboard/dashboard.html
index 89eb1e0..13d6f61 100644
--- a/src/app/pages/dashboard/dashboard.html
+++ b/src/app/pages/dashboard/dashboard.html
@@ -61,7 +61,21 @@
{{ k.title }}
-
{{ k.value }}
+
+ {{ k.value }}
+
+
+
+ -
+
+
diff --git a/src/app/pages/dashboard/dashboard.scss b/src/app/pages/dashboard/dashboard.scss
index e1076ec..f518a56 100644
--- a/src/app/pages/dashboard/dashboard.scss
+++ b/src/app/pages/dashboard/dashboard.scss
@@ -289,11 +289,47 @@
letter-spacing: 0.02em;
}
+.hero-value-row {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ margin-top: 2px;
+}
+
.hero-value {
font-size: 24px;
font-weight: 800;
color: var(--text-main);
- margin-top: 2px;
+ line-height: 1;
+}
+
+.hero-trend {
+ min-width: 22px;
+ height: 22px;
+ border-radius: 999px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: 800;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ background: rgba(148, 163, 184, 0.12);
+
+ &.trend-up {
+ color: #15803d;
+ background: rgba(34, 197, 94, 0.16);
+ border-color: rgba(34, 197, 94, 0.22);
+ }
+
+ &.trend-down {
+ color: #b91c1c;
+ background: rgba(239, 68, 68, 0.16);
+ border-color: rgba(239, 68, 68, 0.22);
+ }
+
+ &.trend-stable {
+ color: #64748b;
+ }
}
.hero-hint {
diff --git a/src/app/pages/dashboard/dashboard.ts b/src/app/pages/dashboard/dashboard.ts
index b9a878e..68fa500 100644
--- a/src/app/pages/dashboard/dashboard.ts
+++ b/src/app/pages/dashboard/dashboard.ts
@@ -32,11 +32,14 @@ import {
} from '../../utils/account-operator.util';
// --- Interfaces (Mantidas intactas para não quebrar contrato) ---
+type KpiTrendDirection = 'up' | 'down' | 'stable';
+
type KpiCard = {
key: string;
title: string;
value: string;
icon: string;
+ trend: KpiTrendDirection;
hint?: string;
};
@@ -107,6 +110,7 @@ type DashboardKpisDto = {
type DashboardDto = {
kpis: DashboardKpisDto;
+ kpiTrends?: Record | null;
topClientes: TopClienteDto[];
serieMuregUltimos12Meses: SerieMesDto[];
serieTrocaUltimos12Meses: SerieMesDto[];
@@ -490,6 +494,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
private filteredLinesCache: DashboardLineListItemDto[] = [];
private operatorDatasetReady = false;
private lineFranquiaCacheById = new Map();
+ private kpiTrendMap: Record = {};
private readonly baseApi: string;
private readonly kpiNavigationMap: Record = {
@@ -632,6 +637,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
this.dashboardApiCache = null;
this.loading = false;
this.dashboardRaw = null;
+ this.kpiTrendMap = {};
this.kpis = [];
this.errorMsg = this.isNetworkError(error)
? 'Falha ao carregar o Dashboard. Verifique a conexão.'
@@ -648,11 +654,13 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
this.resumoReady = false;
try {
- const [operacionais, reservas] = await Promise.all([
+ const [operacionais, reservas, dashboardDto] = await Promise.all([
this.fetchAllDashboardLines(false),
this.fetchAllDashboardLines(true),
+ this.fetchDashboardReal().catch(() => null),
]);
const allLines = [...operacionais, ...reservas];
+ this.syncKpiTrendMap(dashboardDto?.kpiTrends ?? null);
this.applyClientLineAggregates(allLines);
this.loading = false;
@@ -666,6 +674,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
this.resumoLoading = false;
this.resumoReady = false;
this.dataReady = false;
+ this.kpiTrendMap = {};
this.errorMsg = this.isNetworkError(error)
? 'Falha ao carregar o Dashboard. Verifique a conexão.'
: 'Falha ao carregar os dados do cliente.';
@@ -1796,6 +1805,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
private applyDto(dto: DashboardDto) {
const k = dto.kpis;
this.dashboardRaw = k;
+ this.syncKpiTrendMap(dto.kpiTrends ?? null);
this.muregLabels = (dto.serieMuregUltimos12Meses || []).map(x => x.mes);
this.muregValues = (dto.serieMuregUltimos12Meses || []).map(x => x.total);
@@ -2471,6 +2481,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
title: 'Linhas Ativas',
value: this.formatInt(overview.ativas),
icon: 'bi bi-check2-circle',
+ trend: this.getKpiTrend('linhas_ativas'),
hint: 'Status ativo',
},
{
@@ -2478,6 +2489,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
title: 'Franquia Line Total',
value: this.formatDataAllowance(overview.franquiaLineTotalGb),
icon: 'bi bi-wifi',
+ trend: this.getKpiTrend('franquia_line_total'),
hint: 'Franquia contratada',
},
{
@@ -2485,6 +2497,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
title: 'Planos Contratados',
value: this.formatInt(overview.planosContratados),
icon: 'bi bi-diagram-3-fill',
+ trend: this.getKpiTrend('planos_contratados'),
hint: 'Planos ativos na base',
},
{
@@ -2492,6 +2505,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
title: 'Usuários com Linha',
value: this.formatInt(overview.usuariosComLinha),
icon: 'bi bi-people-fill',
+ trend: this.getKpiTrend('usuarios_com_linha'),
hint: 'Usuários vinculados',
},
];
@@ -2505,7 +2519,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
const add = (key: string, title: string, value: string, icon: string, hint?: string) => {
if (used.has(key)) return;
used.add(key);
- cards.push({ key, title, value, icon, hint });
+ cards.push({ key, title, value, icon, trend: this.getKpiTrend(key), hint });
};
const insights = this.insights?.kpis;
@@ -2574,6 +2588,27 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
this.kpis = cards;
}
+ private syncKpiTrendMap(raw: Record | null | undefined): void {
+ const next: Record = {};
+ if (raw && typeof raw === 'object') {
+ Object.entries(raw).forEach(([key, value]) => {
+ next[key] = this.normalizeKpiTrend(value);
+ });
+ }
+ this.kpiTrendMap = next;
+ }
+
+ private normalizeKpiTrend(value: unknown): KpiTrendDirection {
+ const token = String(value ?? '').trim().toLowerCase();
+ if (token === 'up') return 'up';
+ if (token === 'down') return 'down';
+ return 'stable';
+ }
+
+ private getKpiTrend(key: string): KpiTrendDirection {
+ return this.kpiTrendMap[key] ?? 'stable';
+ }
+
// --- CHART BUILDERS (Generic) ---
private tryBuildCharts() {
if (!isPlatformBrowser(this.platformId)) return;
diff --git a/src/app/pages/geral/geral.html b/src/app/pages/geral/geral.html
index a46a1cb..11a4511 100644
--- a/src/app/pages/geral/geral.html
+++ b/src/app/pages/geral/geral.html
@@ -91,6 +91,9 @@
+