diff --git a/package-lock.json b/package-lock.json
index be57a92..bb6e153 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -594,7 +594,6 @@
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.16.tgz",
"integrity": "sha512-e1LiQFZaajKqc00cY5FboIrWJZSMnZ64GDp5R0UejritYrqorQQQNOqP1W85BMuY2owibMmxVfX+dJg/Mc8PuQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -9114,17 +9113,6 @@
}
}
},
- "node_modules/xhr2": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz",
- "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index d541fc3..53c721b 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -27,14 +27,14 @@ export const routes: Routes = [
{ path: 'geral', component: Geral, canActivate: [authGuard], title: 'Geral' },
{ path: 'mureg', component: Mureg, canActivate: [authGuard], title: 'Mureg' },
- { path: 'faturamento', component: Faturamento, canActivate: [authGuard], title: 'Faturamento' },
+ { path: 'faturamento', component: Faturamento, canActivate: [authGuard, adminGuard], title: 'Faturamento' },
{ path: 'dadosusuarios', component: DadosUsuarios, canActivate: [authGuard], title: 'Dados dos Usuários' },
{ path: 'vigencia', component: VigenciaComponent, canActivate: [authGuard], title: 'Vigência' },
{ path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard], title: 'Troca de Número' },
{ path: 'notificacoes', component: Notificacoes, canActivate: [authGuard], title: 'Notificações' },
- { path: 'chips-controle-recebidos', component: ChipsControleRecebidos, canActivate: [authGuard], title: 'Chips Controle Recebidos' },
+ { path: 'chips-controle-recebidos', component: ChipsControleRecebidos, canActivate: [authGuard, adminGuard], title: 'Chips Controle Recebidos' },
{ path: 'resumo', component: Resumo, canActivate: [authGuard], title: 'Resumo' },
- { path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard], title: 'Parcelamentos' },
+ { path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, adminGuard], title: 'Parcelamentos' },
{ path: 'historico', component: Historico, canActivate: [authGuard, adminGuard], title: 'Histórico' },
{ path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' },
diff --git a/src/app/components/header/header.html b/src/app/components/header/header.html
index 0b3a0b1..9aead02 100644
--- a/src/app/components/header/header.html
+++ b/src/app/components/header/header.html
@@ -32,44 +32,85 @@
0">
-
-
-
- Notificações
- 0">{{ unreadCount }} nova(s)
-
-
Ver tudo
-
+
+
+
+ Notificações
+ 0">{{ unreadCount }} nova(s)
+
+
+
+
+
Ver tudo
+
+
-
-
-
+
+
+
+
+
+
+
- Falha ao carregar.
-
+
Falha ao carregar.
+
+
+
+
+
Não há notificações no momento.
+
+
+
+
+
+ Mostrando {{ notificationsPreviewLimit }} de {{ notificationsVisibleCount }} notificações
+
+
-
-
-
Tudo limpo por aqui!
-
-
-
-
-
- Mostrando {{ notificationsPreviewLimit }} de {{ notifications.length }} notificações
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/app/pages/dashboard/dashboard.ts b/src/app/pages/dashboard/dashboard.ts
index fb8fc8f..8171b8c 100644
--- a/src/app/pages/dashboard/dashboard.ts
+++ b/src/app/pages/dashboard/dashboard.ts
@@ -187,9 +187,9 @@ type ResumoTopReserva = {
};
type ResumoDiferencaPjPf = {
- valorTotalLine: number | null;
- lucroTotalLine: number | null;
- qtdLinhas: number | null;
+ pfLinhas: number | null;
+ pjLinhas: number | null;
+ totalLinhas: number | null;
};
@Component({
@@ -297,9 +297,9 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
resumoReservaLabels: string[] = [];
resumoReservaValues: number[] = [];
resumoDiferencaPjPf: ResumoDiferencaPjPf = {
- valorTotalLine: null,
- lucroTotalLine: null,
- qtdLinhas: null,
+ pfLinhas: null,
+ pjLinhas: null,
+ totalLinhas: null,
};
private viewReady = false;
@@ -611,14 +611,13 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
const lineTotals = this.getEffectiveLineTotals();
const pf = this.findLineTotal(lineTotals, ['PF', 'PESSOA FISICA']);
const pj = this.findLineTotal(lineTotals, ['PJ', 'PESSOA JURIDICA']);
- const diferenca = this.findLineTotal(lineTotals, ['DIFERENCA PJ X PF', 'DIFERENÇA PJ X PF', 'DIFERENCA']);
const pfLinhas = this.toNumberOrNull(pf?.qtdLinhas) ?? 0;
const pjLinhas = this.toNumberOrNull(pj?.qtdLinhas) ?? 0;
this.resumoDiferencaPjPf = {
- valorTotalLine: this.toNumberOrNull(diferenca?.valorTotalLine),
- lucroTotalLine: this.toNumberOrNull(diferenca?.lucroTotalLine),
- qtdLinhas: this.toNumberOrNull(diferenca?.qtdLinhas),
+ pfLinhas,
+ pjLinhas,
+ totalLinhas: pfLinhas + pjLinhas,
};
const clientesMap = new Map();
for (const c of this.resumo.vivoLineResumos ?? []) {
@@ -692,9 +691,9 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
this.resumoReservaLabels = [];
this.resumoReservaValues = [];
this.resumoDiferencaPjPf = {
- valorTotalLine: null,
- lucroTotalLine: null,
- qtdLinhas: null,
+ pfLinhas: null,
+ pjLinhas: null,
+ totalLinhas: null,
};
this.destroyResumoCharts();
this.rebuildPrimaryKpis();
diff --git a/src/app/pages/notificacoes/notificacoes.html b/src/app/pages/notificacoes/notificacoes.html
index f1442db..1376e22 100644
--- a/src/app/pages/notificacoes/notificacoes.html
+++ b/src/app/pages/notificacoes/notificacoes.html
@@ -8,10 +8,10 @@
Gerencie seus alertas de vencimento e avisos do sistema.
-
-
+
+
-
-
+
+
-
-
-
-
- Mostrando {{ filteredNotifications.length }} notificações
- 0">• {{ selectedIds.size }} selecionada(s)
-
-
-
+
+
+
+
+
+ Mostrando {{ filteredNotifications.length }} notificações
+ 0">• {{ selectedIds.size }} selecionada(s)
+
+
+
+
-
-
+
Exportando...
+
+
+
+
+
+
@@ -69,14 +101,14 @@
Não foi possível carregar as notificações.
-
-
-
-
-
Tudo em dia!
-
Você não tem nenhuma notificação pendente.
-
Nenhuma notificação neste filtro.
-
+
+
+
+
+
Tudo em dia!
+
Não há notificações no momento.
+
Nenhuma notificação neste filtro.
+
0">
-
-
-
+
+
+
diff --git a/src/app/pages/notificacoes/notificacoes.scss b/src/app/pages/notificacoes/notificacoes.scss
index 8aa8fcf..7d4a357 100644
--- a/src/app/pages/notificacoes/notificacoes.scss
+++ b/src/app/pages/notificacoes/notificacoes.scss
@@ -110,6 +110,47 @@ $border: #e5e7eb;
flex-wrap: wrap; justify-content: center;
}
+.search-row {
+ margin-top: 14px;
+ display: flex;
+ justify-content: center;
+}
+
+.search-box {
+ width: min(720px, 100%);
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 12px;
+ border-radius: 12px;
+ border: 1px solid rgba(0,0,0,0.08);
+ background: $white;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.03);
+
+ i { color: $text-secondary; }
+
+ input {
+ border: none;
+ outline: none;
+ background: transparent;
+ width: 100%;
+ font-size: 14px;
+ color: $text-main;
+ }
+}
+
+.clear-btn {
+ border: none;
+ background: transparent;
+ color: $text-secondary;
+ padding: 4px;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ &:hover { background: rgba(0,0,0,0.04); color: $text-main; }
+}
+
.pill {
border: none;
background: transparent;
diff --git a/src/app/pages/notificacoes/notificacoes.ts b/src/app/pages/notificacoes/notificacoes.ts
index 7b149da..bb509da 100644
--- a/src/app/pages/notificacoes/notificacoes.ts
+++ b/src/app/pages/notificacoes/notificacoes.ts
@@ -1,28 +1,83 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Subscription } from 'rxjs';
import { NotificationsService, NotificationDto } from '../../services/notifications.service';
@Component({
selector: 'app-notificacoes',
standalone: true,
- imports: [CommonModule],
+ imports: [CommonModule, FormsModule],
templateUrl: './notificacoes.html',
styleUrls: ['./notificacoes.scss'],
})
-export class Notificacoes implements OnInit {
+export class Notificacoes implements OnInit, OnDestroy {
notifications: NotificationDto[] = [];
filter: 'todas' | 'vencidas' | 'aVencer' | 'lidas' = 'todas';
+ search = '';
loading = false;
error = false;
bulkLoading = false;
+ bulkUnreadLoading = false;
exportLoading = false;
selectedIds = new Set();
+ private readonly subs = new Subscription();
constructor(private notificationsService: NotificationsService) {}
ngOnInit(): void {
this.loadNotifications();
+
+ this.subs.add(
+ this.notificationsService.events$.subscribe((ev) => {
+ if (ev.type === 'read') {
+ const ids = new Set(ev.ids);
+ this.notifications.forEach((n) => {
+ if (ids.has(n.id)) {
+ n.lida = true;
+ n.lidaEm = ev.readAtIso;
+ }
+ });
+ return;
+ }
+ if (ev.type === 'readAll') {
+ this.notifications.forEach((n) => {
+ if (!n.lida) {
+ n.lida = true;
+ n.lidaEm = ev.readAtIso;
+ }
+ });
+ return;
+ }
+ if (ev.type === 'unread') {
+ const ids = new Set(ev.ids);
+ this.notifications.forEach((n) => {
+ if (ids.has(n.id)) {
+ n.lida = false;
+ n.lidaEm = null;
+ }
+ });
+ return;
+ }
+ if (ev.type === 'unreadAll') {
+ this.notifications.forEach((n) => {
+ if (n.lida) {
+ n.lida = false;
+ n.lidaEm = null;
+ }
+ });
+ return;
+ }
+ if (ev.type === 'reload') {
+ this.loadNotifications();
+ }
+ })
+ );
+ }
+
+ ngOnDestroy(): void {
+ this.subs.unsubscribe();
}
markAsRead(notification: NotificationDto) {
@@ -35,22 +90,31 @@ export class Notificacoes implements OnInit {
});
}
+ markAsUnread(notification: NotificationDto) {
+ if (!notification.lida) return;
+ this.notificationsService.markAsUnread(notification.id).subscribe({
+ next: () => {
+ notification.lida = false;
+ notification.lidaEm = null;
+ },
+ });
+ }
+
setFilter(value: 'todas' | 'vencidas' | 'aVencer' | 'lidas') {
this.filter = value;
this.clearSelection();
}
get filteredNotifications() {
- if (this.filter === 'vencidas') {
- return this.notifications.filter(n => this.getNotificationTipo(n) === 'Vencido');
- }
- if (this.filter === 'aVencer') {
- return this.notifications.filter(n => this.getNotificationTipo(n) === 'AVencer');
- }
- if (this.filter === 'lidas') {
- return this.notifications.filter(n => n.lida);
- }
- return this.notifications;
+ const base = this.getBaseFilteredNotifications();
+ const q = (this.search || '').trim().toLowerCase();
+ if (!q) return base;
+ return base.filter(n => this.buildSearchText(n).includes(q));
+ }
+
+ clearSearch() {
+ this.search = '';
+ this.clearSelection();
}
formatDateLabel(date?: string | null): string {
@@ -115,6 +179,32 @@ export class Notificacoes implements OnInit {
});
}
+ markAllAsUnread() {
+ if (this.filter !== 'lidas' || this.bulkUnreadLoading) return;
+ this.bulkUnreadLoading = true;
+
+ const selectedIds = Array.from(this.selectedIds);
+ const scopedIds = selectedIds.length
+ ? selectedIds
+ : this.filteredNotifications.map(n => n.id);
+
+ this.notificationsService.markAllAsUnread(undefined, scopedIds.length ? scopedIds : undefined).subscribe({
+ next: () => {
+ this.notifications = this.notifications.map((n) => {
+ if (scopedIds.length ? scopedIds.includes(n.id) : n.lida) {
+ return { ...n, lida: false, lidaEm: null };
+ }
+ return n;
+ });
+ this.clearSelection();
+ this.bulkUnreadLoading = false;
+ },
+ error: () => {
+ this.bulkUnreadLoading = false;
+ }
+ });
+ }
+
exportNotifications() {
if (this.filter === 'lidas' || this.exportLoading) return;
this.exportLoading = true;
@@ -194,6 +284,55 @@ export class Notificacoes implements OnInit {
return false;
}
+ private getBaseFilteredNotifications(): NotificationDto[] {
+ if (this.filter === 'lidas') {
+ return this.notifications.filter(n => n.lida);
+ }
+ if (this.filter === 'vencidas') {
+ return this.notifications.filter(n => !n.lida && this.getNotificationTipo(n) === 'Vencido');
+ }
+ if (this.filter === 'aVencer') {
+ return this.notifications.filter(n => !n.lida && this.getNotificationTipo(n) === 'AVencer');
+ }
+ // "todas" aqui representa o inbox: pendentes (não lidas).
+ return this.notifications.filter(n => !n.lida);
+ }
+
+ private buildSearchText(n: NotificationDto): string {
+ const parts: string[] = [];
+ const push = (v?: string | null) => {
+ const t = (v ?? '').toString().trim();
+ if (t) parts.push(t);
+ };
+
+ push(n.cliente);
+ push(n.conta);
+ push(n.linha);
+ push(n.usuario);
+ push(n.planoContrato);
+ push(n.titulo);
+ push(n.mensagem);
+ push(n.data);
+ push(n.referenciaData ?? null);
+ push(n.dtEfetivacaoServico ?? null);
+ push(n.dtTerminoFidelizacao ?? null);
+
+ const efetivacao = this.formatDateSearch(n.dtEfetivacaoServico);
+ const termino = this.formatDateSearch(n.dtTerminoFidelizacao);
+ push(efetivacao);
+ push(termino);
+
+ return parts.join(' ').toLowerCase();
+ }
+
+ private formatDateSearch(raw?: string | null): string {
+ if (!raw) return '';
+ const parsed = this.parseDateOnly(raw);
+ if (!parsed) return '';
+ // Ex.: 12/02/2026 (facilita busca por padrão BR).
+ return parsed.toLocaleDateString('pt-BR');
+ }
+
private parseDateOnly(raw?: string | null): Date | null {
if (!raw) return null;
const datePart = raw.split('T')[0];
diff --git a/src/app/pages/resumo/resumo.html b/src/app/pages/resumo/resumo.html
index 68539bb..6edb6da 100644
--- a/src/app/pages/resumo/resumo.html
+++ b/src/app/pages/resumo/resumo.html
@@ -19,7 +19,7 @@
- Visão consolidada de performance, contratos e indicadores financeiros.
+ Visão consolidada de performance, contratos e indicadores operacionais.