diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 06022b9..a6ea6af 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -11,7 +11,8 @@ import { authGuard } from './guards/auth.guard'; import { DadosUsuarios } from './pages/dados-usuarios/dados-usuarios'; import { VigenciaComponent } from './pages/vigencia/vigencia'; import { TrocaNumero } from './pages/troca-numero/troca-numero'; -import { Relatorios } from './pages/relatorios/relatorios'; +import { Dashboard } from './pages/dashboard/dashboard'; +import { Notificacoes } from './pages/notificacoes/notificacoes'; export const routes: Routes = [ { path: '', component: Home }, @@ -24,12 +25,13 @@ export const routes: Routes = [ { path: 'dadosusuarios', component: DadosUsuarios, canActivate: [authGuard] }, { path: 'vigencia', component: VigenciaComponent, canActivate: [authGuard] }, { path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard] }, + { path: 'notificacoes', component: Notificacoes, canActivate: [authGuard] }, // ✅ rota correta - { path: 'relatorios', component: Relatorios, canActivate: [authGuard] }, + { path: 'dashboard', component: Dashboard, canActivate: [authGuard] }, - // ✅ compatibilidade: se alguém acessar /portal/relatorios, manda pra /relatorios - { path: 'portal/relatorios', redirectTo: 'relatorios', pathMatch: 'full' }, + // ✅ compatibilidade: se alguém acessar /portal/dashboard, manda pra /dashboard + { path: 'portal/dashboard', redirectTo: 'dashboard', pathMatch: 'full' }, { path: '**', redirectTo: '' }, ]; diff --git a/src/app/app.ts b/src/app/app.ts index f4b13c7..021813a 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -33,7 +33,8 @@ export class AppComponent { '/dadosusuarios', '/vigencia', '/trocanumero', - '/relatorios', // ✅ ADICIONADO: esconde footer na página de relatórios + '/dashboard', // ✅ ADICIONADO: esconde footer na página de dashboard + '/notificacoes', ]; constructor( diff --git a/src/app/components/header/header.html b/src/app/components/header/header.html index 92c2497..de06fe3 100644 --- a/src/app/components/header/header.html +++ b/src/app/components/header/header.html @@ -3,19 +3,99 @@ -
- +
+
+ - - + +
+
+ + +
+
+ Notificações + Ver todas +
+ +
+
+ Carregando... +
+
+ Falha ao carregar notificações. +
+
+ Nenhuma notificação por aqui. +
+ +
+
+ + {{ n.tipo === 'Vencido' ? 'Vencido' : 'A vencer' }} + + {{ n.linha || '-' }} +
+
+ {{ n.linha || '-' }} - {{ n.usuario || n.cliente || '-' }} +
+
+
Linha: {{ n.linha || '-' }}
+
Usuário: {{ n.usuario || '-' }}
+
Cliente: {{ n.cliente || '-' }}
+
{{ n.tipo === 'Vencido' ? 'Venceu em' : 'Vence em' }}: {{ n.referenciaData ? (n.referenciaData | date:'dd/MM/yyyy') : '-' }}
+
+ +
+
+
-
- LineGestão + + +
+ + +
+ + +
- +
@@ -45,12 +125,23 @@
- -
- Somos a escolha certa para estar sempre conectado! -
+
+
+
+ Vigência próxima + +
+
+ A linha {{ toastItem.linha || '-' }} vence em 5 dias. + +
+
+
+ @@ -62,7 +153,7 @@ (click)="$event.stopPropagation()" >
- @@ -73,6 +164,10 @@
diff --git a/src/app/components/header/header.scss b/src/app/components/header/header.scss index 0275bf3..6c36e6f 100644 --- a/src/app/components/header/header.scss +++ b/src/app/components/header/header.scss @@ -30,6 +30,20 @@ gap: 12px; } +.logged-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + width: 100%; +} + +.logged-actions { + display: flex; + align-items: center; + gap: 10px; +} + /* Logo */ .logo-area { display: flex; @@ -37,24 +51,47 @@ gap: 10px; text-decoration: none; color: var(--text-main); + transition: transform 0.2s ease; .logo-icon { - width: 36px; - height: 36px; - background: linear-gradient(135deg, var(--brand-primary), #6A55FF); + width: 38px; + height: 38px; + background: conic-gradient( + from 210deg, + #1c38c9 0deg, + #3555ff 90deg, + #e33dcf 180deg, + #ff6b6b 250deg, + #2ecc71 320deg, + #1c38c9 360deg + ); color: #fff; - border-radius: 10px; + border-radius: 50%; display: grid; place-items: center; font-size: 18px; flex: 0 0 auto; + box-shadow: 0 10px 24px rgba(28, 56, 201, 0.25); } .logo-text { font-size: 20px; - font-weight: 700; - letter-spacing: -0.5px; - .highlight { color: var(--text-main); } + font-weight: 800; + letter-spacing: -0.4px; + text-transform: lowercase; + + .highlight { + background: linear-gradient(90deg, #1c38c9 0%, #6a55ff 45%, #e33dcf 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-transform: none; + margin-left: 4px; + font-weight: 700; + } + } + + &:hover { + transform: translateY(-1px); } } @@ -123,22 +160,261 @@ } } -/* Faixa home */ -.header-bar { - margin-top: 10px; - width: 100%; - height: 34px; - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient(90deg, #0B2BD6 0%, #6A55FF 40%, #E33DCF 100%); +/* ✅ Opções (logado) */ + +.btn-bell { + width: 42px; + height: 42px; + border-radius: 12px; + + i { + font-size: 18px; + } } -.header-bar-text { - color: #ffffff; - font-size: 15px; +.notifications-menu { + position: relative; + display: flex; + align-items: center; +} + +.badge-dot { + position: absolute; + top: -4px; + right: -4px; + min-width: 20px; + height: 20px; + padding: 0 5px; + border-radius: 999px; + background: #ef4444; + color: #fff; + font-size: 11px; font-weight: 800; - font-family: 'Poppins', sans-serif; + display: grid; + place-items: center; + box-shadow: 0 0 0 3px #fff; +} + +.notifications-dropdown { + position: absolute; + right: 0; + top: calc(100% + 8px); + width: min(360px, 82vw); + background: #fff; + border-radius: 16px; + border: 1px solid rgba(0,0,0,0.08); + box-shadow: 0 18px 40px rgba(0,0,0,0.12); + z-index: 1200; +} + +.notifications-head { + padding: 12px 14px 8px; + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 800; + color: rgba(17, 18, 20, 0.9); + border-bottom: 1px solid rgba(0,0,0,0.06); +} + +.see-all { + font-size: 12px; + color: var(--brand-primary); + text-decoration: none; + font-weight: 700; +} + +.notifications-body { + max-height: 320px; + overflow: auto; + padding: 6px 8px 10px; +} + +.notifications-state { + padding: 12px; + font-weight: 700; + color: rgba(17, 18, 20, 0.6); +} + +.notifications-state.warn { + color: #b45309; +} + +.notification-item { + background: rgba(248, 249, 255, 0.9); + border: 1px solid rgba(0,0,0,0.06); + border-radius: 12px; + padding: 10px 12px; + margin-bottom: 10px; + transition: transform 0.2s ease, box-shadow 0.2s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 10px 20px rgba(0,0,0,0.08); + } +} + +.notification-tag { + display: inline-flex; + align-items: center; + padding: 4px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + color: #1f2937; + background: rgba(3, 15, 170, 0.12); +} + +.notification-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.notification-line { + font-weight: 800; + font-size: 12px; + color: rgba(17, 18, 20, 0.65); +} + +.notification-info { + margin-top: 8px; + display: grid; + gap: 4px; + font-size: 12px; + color: rgba(17, 18, 20, 0.75); + + strong { + color: rgba(17, 18, 20, 0.9); + } +} + +.notification-tag.warn { + background: rgba(227, 61, 207, 0.16); + color: #8b2a7d; +} + +.notification-tag.danger { + background: rgba(239, 68, 68, 0.16); + color: #b91c1c; +} + +.notification-title { + margin-top: 6px; + font-weight: 800; + color: rgba(17, 18, 20, 0.9); +} + +.notification-message { + margin-top: 4px; + font-size: 12px; + color: rgba(17, 18, 20, 0.68); + line-height: 1.4; +} + +.mark-read { + margin-top: 8px; + padding: 6px 10px; + border-radius: 10px; + border: 1px solid rgba(0,0,0,0.08); + background: #fff; + font-size: 12px; + font-weight: 700; + cursor: pointer; + color: rgba(17, 18, 20, 0.8); + + &:hover { + border-color: rgba(3, 15, 170, 0.35); + color: #030faa; + } +} + +.notification-toast { + border-radius: 14px; + border: 1px solid rgba(0,0,0,0.08); + box-shadow: 0 18px 36px rgba(0,0,0,0.16); +} + +.notification-toast .toast-header { + border-bottom: 1px solid rgba(0,0,0,0.06); + font-weight: 800; +} + +.btn-aware { + display: inline-flex; + align-items: center; + margin-top: 10px; + padding: 6px 12px; + border-radius: 10px; + border: 1px solid rgba(0,0,0,0.1); + background: #fff; + font-size: 12px; + font-weight: 700; + cursor: pointer; +} + +.options-menu { + position: relative; + display: flex; + align-items: center; +} + +.options-trigger { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + border-radius: 12px; + border: 1px solid rgba(0,0,0,0.1); + background: #fff; + font-weight: 700; + color: var(--text-main); + cursor: pointer; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + + i { + font-size: 12px; + } + + &:hover { + border-color: rgba(227, 61, 207, 0.35); + box-shadow: 0 12px 22px rgba(0,0,0,0.08); + } +} + +.options-dropdown { + position: absolute; + right: 0; + top: calc(100% + 8px); + min-width: 200px; + padding: 8px 0; + border-radius: 14px; + border: 1px solid rgba(0,0,0,0.08); + background: #fff; + box-shadow: 0 18px 40px rgba(0,0,0,0.12); + z-index: 1200; +} + +.options-item { + display: flex; + align-items: center; + width: 100%; + padding: 10px 16px; + font-weight: 700; + color: rgba(17, 18, 20, 0.85); + text-decoration: none; + background: transparent; + border: none; + cursor: pointer; + + &:hover { + background: rgba(227, 61, 207, 0.08); + } + + &.danger { + color: #c2410c; + } } /* ========================= */ @@ -194,11 +470,19 @@ .side-logo-icon { width: 38px; height: 38px; - border-radius: 12px; + border-radius: 50%; display: grid; place-items: center; color: #fff; - background: linear-gradient(135deg, var(--brand-primary), #6A55FF); + background: conic-gradient( + from 210deg, + #1c38c9 0deg, + #3555ff 90deg, + #e33dcf 180deg, + #ff6b6b 250deg, + #2ecc71 320deg, + #1c38c9 360deg + ); i { font-size: 18px; } } @@ -206,7 +490,16 @@ font-weight: 900; font-size: 18px; letter-spacing: -0.4px; - .highlight { color: var(--text-main); } + text-transform: lowercase; + + .highlight { + background: linear-gradient(90deg, #1c38c9 0%, #6a55ff 45%, #e33dcf 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-transform: none; + margin-left: 4px; + font-weight: 700; + } } } diff --git a/src/app/components/header/header.ts b/src/app/components/header/header.ts index ebbc5ba..ed041d4 100644 --- a/src/app/components/header/header.ts +++ b/src/app/components/header/header.ts @@ -1,8 +1,10 @@ -import { Component, HostListener, Inject } from '@angular/core'; +import { Component, HostListener, Inject, ElementRef, ViewChild } from '@angular/core'; import { RouterLink, Router, NavigationEnd } from '@angular/router'; import { CommonModule, isPlatformBrowser } from '@angular/common'; import { PLATFORM_ID } from '@angular/core'; import { filter } from 'rxjs/operators'; +import { AuthService } from '../../services/auth.service'; +import { NotificationsService, NotificationDto } from '../../services/notifications.service'; @Component({ selector: 'app-header', @@ -15,8 +17,15 @@ export class Header { isScrolled = false; menuOpen = false; + optionsOpen = false; + notificationsOpen = false; isLoggedHeader = false; isHome = false; + notifications: NotificationDto[] = []; + notificationsLoading = false; + notificationsError = false; + private notificationsLoaded = false; + @ViewChild('notifToast') notifToast?: ElementRef; private readonly loggedPrefixes = [ '/geral', @@ -25,11 +34,14 @@ export class Header { '/dadosusuarios', '/vigencia', '/trocanumero', - '/relatorios', // ✅ ADICIONADO + '/dashboard', // ✅ ADICIONADO + '/notificacoes', ]; constructor( private router: Router, + private authService: AuthService, + private notificationsService: NotificationsService, @Inject(PLATFORM_ID) private platformId: object ) { // ✅ resolve no carregamento inicial @@ -42,7 +54,15 @@ export class Header { const rawUrl = event.urlAfterRedirects || event.url; this.syncHeaderState(rawUrl); this.menuOpen = false; + this.optionsOpen = false; + this.notificationsOpen = false; + if (this.isLoggedHeader) { + this.ensureNotificationsLoaded(); + } }); + if (this.isLoggedHeader) { + this.ensureNotificationsLoaded(); + } } private syncHeaderState(rawUrl: string) { @@ -63,15 +83,127 @@ export class Header { this.menuOpen = false; } + toggleOptions() { + this.optionsOpen = !this.optionsOpen; + if (this.optionsOpen) this.notificationsOpen = false; + } + + closeOptions() { + this.optionsOpen = false; + } + + toggleNotifications() { + this.notificationsOpen = !this.notificationsOpen; + if (this.notificationsOpen) { + this.optionsOpen = false; + this.loadNotifications(); + } + } + + closeNotifications() { + this.notificationsOpen = false; + } + + markNotificationRead(notification: NotificationDto) { + if (notification.lida) return; + this.notificationsService.markAsRead(notification.id).subscribe({ + next: () => { + notification.lida = true; + notification.lidaEm = new Date().toISOString(); + }, + }); + } + + get unreadCount() { + return this.notifications.filter(n => !n.lida).length; + } + + logout() { + this.authService.logout(); + this.optionsOpen = false; + this.notificationsOpen = false; + this.router.navigate(['/']); + } + @HostListener('window:scroll', []) onWindowScroll() { if (!isPlatformBrowser(this.platformId)) return; this.isScrolled = window.scrollY > 10; } + @HostListener('document:click', []) + onDocumentClick() { + this.optionsOpen = false; + this.notificationsOpen = false; + } + @HostListener('document:keydown.escape', []) onEsc() { if (!isPlatformBrowser(this.platformId)) return; this.closeMenu(); + this.closeOptions(); + this.closeNotifications(); + } + + acknowledgeNotification(notification: NotificationDto) { + if (!isPlatformBrowser(this.platformId)) return; + const acknowledged = this.getAcknowledgedIds(); + acknowledged.add(notification.id); + localStorage.setItem('vigenciaAcknowledgedIds', JSON.stringify(Array.from(acknowledged))); + } + + private ensureNotificationsLoaded() { + if (this.notificationsLoaded || this.notificationsLoading) return; + this.loadNotifications(); + } + + private loadNotifications() { + if (!isPlatformBrowser(this.platformId)) return; + this.notificationsLoading = true; + this.notificationsError = false; + this.notificationsService.list().subscribe({ + next: (data) => { + this.notifications = data || []; + this.notificationsLoaded = true; + this.notificationsLoading = false; + this.maybeShowVigenciaToast(); + }, + error: () => { + this.notificationsError = true; + this.notificationsLoading = false; + }, + }); + } + + private async maybeShowVigenciaToast() { + if (!this.notifToast || !isPlatformBrowser(this.platformId)) return; + const pending = this.getPendingVigenciaToast(); + if (!pending) return; + + const bs = await import('bootstrap'); + const toast = new bs.Toast(this.notifToast.nativeElement, { autohide: false }); + toast.show(); + } + + get toastNotification() { + return this.getPendingVigenciaToast(); + } + + private getPendingVigenciaToast() { + const acknowledged = this.getAcknowledgedIds(); + return this.notifications.find( + n => n.tipo === 'AVencer' && n.diasParaVencer === 5 && !acknowledged.has(n.id) + ); + } + + private getAcknowledgedIds() { + if (!isPlatformBrowser(this.platformId)) return new Set(); + try { + const raw = localStorage.getItem('vigenciaAcknowledgedIds'); + const ids = raw ? (JSON.parse(raw) as string[]) : []; + return new Set(ids); + } catch { + return new Set(); + } } } diff --git a/src/app/pages/relatorios/relatorios.html b/src/app/pages/dashboard/dashboard.html similarity index 98% rename from src/app/pages/relatorios/relatorios.html rename to src/app/pages/dashboard/dashboard.html index 5088116..35a2e2e 100644 --- a/src/app/pages/relatorios/relatorios.html +++ b/src/app/pages/dashboard/dashboard.html @@ -1,10 +1,10 @@ -
+
- Relatórios + Dashboard

Resumo e indicadores do ambiente.

diff --git a/src/app/pages/relatorios/relatorios.scss b/src/app/pages/dashboard/dashboard.scss similarity index 75% rename from src/app/pages/relatorios/relatorios.scss rename to src/app/pages/dashboard/dashboard.scss index 5c3a1f0..18a401f 100644 --- a/src/app/pages/relatorios/relatorios.scss +++ b/src/app/pages/dashboard/dashboard.scss @@ -2,6 +2,18 @@ display: block; width: 100%; overflow-x: hidden; + --brand-primary: #E33DCF; + --brand-blue: #030FAA; + --brand-deep: #B832A8; + --brand-violet: #6A55FF; + --brand-soft: rgba(227, 61, 207, 0.2); + --brand-blue-soft: rgba(3, 15, 170, 0.2); + --chart-pink: var(--brand-primary); + --chart-pink-dark: var(--brand-deep); + --chart-pink-soft: #F3B0E8; + --chart-blue: var(--brand-blue); + --chart-blue-soft: var(--brand-blue-soft); + --chart-violet: var(--brand-violet); } /* ✅ remove footer nessa página */ @@ -12,7 +24,7 @@ display: none !important; } -.relatorios-page { +.dashboard-page { width: 100%; overflow-x: hidden; } @@ -239,7 +251,7 @@ /* se quiser tirar o rosa do total, troque aqui */ .metric.total .meta .v { - color: #ff2d95; + color: var(--chart-pink); } .dot { @@ -250,12 +262,12 @@ } /* ✅ DOTS COM CORES "PADRÃO DE DASHBOARD" */ -.dot.d1 { background: #ff2d95; } /* total (mantém rosa no card de total, se você quiser) */ -.dot.d2 { background: #2E7D32; } /* Ativos - verde */ -.dot.d3 { background: #D32F2F; } /* Perda/Roubo - vermelho */ -.dot.d4 { background: #F57C00; } /* 120 dias - laranja */ -.dot.d5 { background: #1976D2; } /* Reservas - azul */ -.dot.d6 { background: #607D8B; } /* Outros - cinza */ +.dot.d1 { background: var(--chart-pink); } +.dot.d2 { background: var(--chart-blue); } +.dot.d3 { background: var(--chart-pink-dark); } +.dot.d4 { background: var(--chart-violet); } +.dot.d5 { background: var(--chart-pink-soft); } +.dot.d6 { background: var(--chart-blue-soft); } .meta .k { font-weight: 900; @@ -299,21 +311,25 @@ /* Table */ .table-wrap { - padding: 10px 12px 14px; + padding: 12px 12px 16px; overflow-x: auto; + background: rgba(255, 255, 255, 0.7); + border-radius: 16px; + border: 1px solid rgba(0, 0, 0, 0.06); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6); } .tablex { width: 100%; - border-collapse: collapse; + border-collapse: separate; + border-spacing: 0 8px; min-width: 720px; } .tablex th, .tablex td { - padding: 10px 10px; - border-bottom: 1px solid rgba(0, 0, 0, 0.06); - font-weight: 800; + padding: 12px 12px; + font-weight: 700; color: rgba(17, 18, 20, 0.8); text-align: left; white-space: nowrap; @@ -322,11 +338,47 @@ .tablex th { color: rgba(17, 18, 20, 0.65); font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; + padding-bottom: 6px; +} + +.tablex tbody tr { + background: #fff; + box-shadow: 0 8px 16px rgba(17, 18, 20, 0.06); + transition: transform 160ms ease, box-shadow 160ms ease; +} + +.tablex tbody tr:hover { + transform: translateY(-1px); + box-shadow: 0 12px 22px rgba(17, 18, 20, 0.12); +} + +.tablex tbody td { + border-top: 1px solid rgba(17, 18, 20, 0.06); + border-bottom: 1px solid rgba(17, 18, 20, 0.06); + background: rgba(255, 255, 255, 0.96); +} + +.tablex tbody td:first-child { + border-left: 1px solid rgba(17, 18, 20, 0.06); + border-top-left-radius: 12px; + border-bottom-left-radius: 12px; +} + +.tablex tbody td:last-child { + border-right: 1px solid rgba(17, 18, 20, 0.06); + border-top-right-radius: 12px; + border-bottom-right-radius: 12px; +} + +.tablex tbody tr:nth-child(even) td { + background: rgba(248, 249, 255, 0.9); } .muted { color: rgba(17, 18, 20, 0.55); - font-weight: 800; + font-weight: 700; } .cell-strong { diff --git a/src/app/pages/relatorios/relatorios.ts b/src/app/pages/dashboard/dashboard.ts similarity index 83% rename from src/app/pages/relatorios/relatorios.ts rename to src/app/pages/dashboard/dashboard.ts index 39e6358..d059397 100644 --- a/src/app/pages/relatorios/relatorios.ts +++ b/src/app/pages/dashboard/dashboard.ts @@ -88,7 +88,7 @@ type DashboardKpisDto = { userDataComEmail: number; }; -type RelatoriosDashboardDto = { +type DashboardDto = { kpis: DashboardKpisDto; topClientes: TopClienteDto[]; @@ -105,13 +105,13 @@ type RelatoriosDashboardDto = { }; @Component({ - selector: 'app-relatorios', + selector: 'app-dashboard', standalone: true, imports: [CommonModule], - templateUrl: './relatorios.html', - styleUrls: ['./relatorios.scss'], + templateUrl: './dashboard.html', + styleUrls: ['./dashboard.scss'], }) -export class Relatorios implements OnInit, AfterViewInit, OnDestroy { +export class Dashboard implements OnInit, AfterViewInit, OnDestroy { @ViewChild('chartMureg12') chartMureg12?: ElementRef; @ViewChild('chartTroca12') chartTroca12?: ElementRef; @ViewChild('chartStatusPie') chartStatusPie?: ElementRef; @@ -164,26 +164,10 @@ export class Relatorios implements OnInit, AfterViewInit, OnDestroy { private readonly baseApi: string; - // ✅ Paletas "padrão de dashboard" (fácil de entender) - private readonly STATUS_COLORS = { - ativos: '#2E7D32', // verde - perdaRoubo: '#D32F2F', // vermelho - bloq120: '#F57C00', // laranja - reservas: '#1976D2', // azul - outros: '#607D8B', // cinza - }; - - private readonly VIG_COLORS = { - vencidos: '#D32F2F', // vermelho - d0a30: '#F57C00', // laranja - d31a60: '#FBC02D', // amarelo - d61a90: '#1976D2', // azul - acima90: '#2E7D32', // verde - }; - constructor( private http: HttpClient, - @Inject(PLATFORM_ID) private platformId: object + @Inject(PLATFORM_ID) private platformId: object, + private hostRef: ElementRef ) { const raw = (environment.apiUrl || 'https://localhost:7205').replace(/\/+$/, ''); this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; @@ -218,17 +202,17 @@ export class Relatorios implements OnInit, AfterViewInit, OnDestroy { } catch { this.loading = false; this.errorMsg = - 'Falha ao carregar Relatórios. Verifique se a API está rodando e o endpoint /api/relatorios/dashboard está acessível.'; + 'Falha ao carregar Dashboard. Verifique se a API está rodando e o endpoint /api/relatorios/dashboard está acessível.'; } } - private async fetchDashboardReal(): Promise { + private async fetchDashboardReal(): Promise { if (!isPlatformBrowser(this.platformId)) throw new Error('SSR não suportado para charts'); const url = `${this.baseApi}/relatorios/dashboard`; - return await firstValueFrom(this.http.get(url)); + return await firstValueFrom(this.http.get(url)); } - private applyDto(dto: RelatoriosDashboardDto) { + private applyDto(dto: DashboardDto) { const k = dto.kpis; this.kpis = [ @@ -299,7 +283,9 @@ export class Relatorios implements OnInit, AfterViewInit, OnDestroy { this.destroyCharts(); - // ✅ Status das linhas (paleta padrão) + const palette = this.getPalette(); + + // ✅ Status das linhas (paleta do sistema) const cP = this.chartStatusPie?.nativeElement; if (cP) { this.chartPie = new Chart(cP, { @@ -322,11 +308,11 @@ export class Relatorios implements OnInit, AfterViewInit, OnDestroy { ], borderWidth: 1, backgroundColor: [ - this.STATUS_COLORS.ativos, - this.STATUS_COLORS.perdaRoubo, - this.STATUS_COLORS.bloq120, - this.STATUS_COLORS.reservas, - this.STATUS_COLORS.outros, + palette.status.ativos, + palette.status.perdaRoubo, + palette.status.bloq120, + palette.status.reservas, + palette.status.outros, ], }], }, @@ -355,7 +341,7 @@ export class Relatorios implements OnInit, AfterViewInit, OnDestroy { label: 'Encerramentos', data: this.vigenciaValues, borderWidth: 0, - backgroundColor: '#1976D2', // azul padrão + backgroundColor: palette.series.vigencia, borderRadius: 10, }], }, @@ -388,11 +374,11 @@ export class Relatorios implements OnInit, AfterViewInit, OnDestroy { ], borderWidth: 1, backgroundColor: [ - this.VIG_COLORS.vencidos, - this.VIG_COLORS.d0a30, - this.VIG_COLORS.d31a60, - this.VIG_COLORS.d61a90, - this.VIG_COLORS.acima90, + palette.vigencia.vencidos, + palette.vigencia.d0a30, + palette.vigencia.d31a60, + palette.vigencia.d61a90, + palette.vigencia.acima90, ], }], }, @@ -421,7 +407,7 @@ export class Relatorios implements OnInit, AfterViewInit, OnDestroy { label: 'MUREG', data: this.muregValues, borderWidth: 0, - backgroundColor: '#6A1B9A', // roxo (bem comum em dashboards) + backgroundColor: palette.series.mureg, borderRadius: 10, }], }, @@ -448,7 +434,7 @@ export class Relatorios implements OnInit, AfterViewInit, OnDestroy { label: 'Troca', data: this.trocaValues, borderWidth: 0, - backgroundColor: '#00897B', // teal (bem comum) + backgroundColor: palette.series.troca, borderRadius: 10, }], }, @@ -482,4 +468,35 @@ export class Relatorios implements OnInit, AfterViewInit, OnDestroy { private formatInt(v: number) { return (v || 0).toLocaleString('pt-BR'); } + + private getPalette() { + return { + status: { + ativos: this.readCssVar('--chart-blue', '#030FAA'), + perdaRoubo: this.readCssVar('--chart-pink-dark', '#B832A8'), + bloq120: this.readCssVar('--chart-violet', '#6A55FF'), + reservas: this.readCssVar('--chart-pink-soft', '#F3B0E8'), + outros: this.readCssVar('--chart-blue-soft', 'rgba(3, 15, 170, 0.2)'), + }, + vigencia: { + vencidos: this.readCssVar('--chart-pink', '#E33DCF'), + d0a30: this.readCssVar('--chart-violet', '#6A55FF'), + d31a60: this.readCssVar('--chart-blue', '#030FAA'), + d61a90: this.readCssVar('--chart-pink-dark', '#B832A8'), + acima90: this.readCssVar('--chart-pink-soft', '#F3B0E8'), + }, + series: { + vigencia: this.readCssVar('--chart-blue', '#030FAA'), + mureg: this.readCssVar('--chart-pink', '#E33DCF'), + troca: this.readCssVar('--chart-violet', '#6A55FF'), + }, + }; + } + + private readCssVar(name: string, fallback: string) { + if (!isPlatformBrowser(this.platformId)) return fallback; + const styles = getComputedStyle(this.hostRef.nativeElement); + const value = styles.getPropertyValue(name).trim(); + return value || fallback; + } } diff --git a/src/app/pages/faturamento/faturamento.html b/src/app/pages/faturamento/faturamento.html index 0beb345..aac77fe 100644 --- a/src/app/pages/faturamento/faturamento.html +++ b/src/app/pages/faturamento/faturamento.html @@ -115,7 +115,7 @@
-
+
Total Clientes @@ -123,7 +123,7 @@
-
+
Total Linhas @@ -131,7 +131,7 @@
-
+
Total Vivo @@ -139,7 +139,7 @@
-
+
Total Line @@ -147,7 +147,7 @@
-
+
Lucro @@ -229,7 +229,7 @@
-
+
Registros do Cliente Clique no “olho” para ver todos os detalhes
@@ -238,12 +238,12 @@ - @@ -254,19 +254,19 @@ diff --git a/src/app/pages/faturamento/faturamento.scss b/src/app/pages/faturamento/faturamento.scss index 4326aa4..766119a 100644 --- a/src/app/pages/faturamento/faturamento.scss +++ b/src/app/pages/faturamento/faturamento.scss @@ -80,7 +80,7 @@ .container-fat { width: 100%; - max-width: 1180px; + max-width: 1240px; position: relative; z-index: 1; margin-top: var(--page-top-gap); @@ -97,8 +97,9 @@ position: relative; display: flex; flex-direction: column; - max-height: calc(100vh - 18px) !important; - min-height: 0; + height: auto !important; + min-height: 80vh; + max-height: none !important; &::before { content: ''; @@ -448,6 +449,36 @@ } } +.kpi-stack { + flex-direction: column; + align-items: center; + gap: 6px; + text-align: center; + + .lbl, + .val { + white-space: normal; + } + + .val { + line-height: 1.1; + } +} + +.kpi-compact { + padding: 6px 12px; + min-height: 56px; + align-items: center; + + .val { + font-size: 1rem; + } + + .lbl { + font-size: 0.68rem; + } +} + .kpi-wide { min-width: 220px; padding: 14px 18px; @@ -471,6 +502,8 @@ .groups-container { padding: 16px; overflow-y: auto; + flex: 1; + min-height: 0; height: 100%; } @@ -552,6 +585,16 @@ to { opacity: 1; transform: translateY(0); } } +.group-actions-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: #fff; + border-bottom: 1px solid rgba(17,18,20,0.06); + gap: 12px; +} + .chip-muted { display: inline-flex; align-items: center; @@ -565,7 +608,11 @@ border: 1px solid rgba(17,18,20,0.06); } -.inner-table-wrap { max-height: 520px; overflow: auto; } +.inner-table-wrap { + max-height: none; + height: auto; + overflow-y: visible; +} /* TABLE */ .table-wrap { overflow: auto; height: 100%; } @@ -591,6 +638,9 @@ text-transform: uppercase; white-space: nowrap; text-align: center !important; + transition: color 0.2s ease; + + &:hover { color: var(--brand); } } tbody tr { @@ -614,6 +664,7 @@ .sort-caret { width: 14px; opacity: 0.3; &.active { opacity: 1; color: var(--brand); } } .td-clip { overflow: hidden; text-overflow: ellipsis; max-width: 260px; } .empty-state { background: rgba(255,255,255,0.4); } +.th-item .th-content { justify-content: center; } /* ACTIONS */ .action-group { display: flex; justify-content: center; gap: 6px; } diff --git a/src/app/pages/login/login.ts b/src/app/pages/login/login.ts index b9e6f67..34ef817 100644 --- a/src/app/pages/login/login.ts +++ b/src/app/pages/login/login.ts @@ -117,18 +117,18 @@ export class LoginComponent { const nome = this.getNameFromToken(token); console.log('👤 Nome extraído:', nome); - console.log('🔄 Tentando ir para /geral...'); - this.router.navigate(['/geral'], { + console.log('🔄 Tentando ir para /dashboard...'); + this.router.navigate(['/dashboard'], { state: { toastMessage: `Bem-vindo, ${nome}!` } }).then(sucesso => { if (sucesso) console.log('✅ Navegação funcionou!'); - else console.error('❌ Navegação falhou! A rota "/geral" existe?'); + else console.error('❌ Navegação falhou! A rota "/dashboard" existe?'); }); } catch (e) { console.error('❌ Erro ao processar token ou navegar:', e); // Força a ida mesmo se o nome falhar - this.router.navigate(['/geral']); + this.router.navigate(['/dashboard']); } }, error: (err) => { @@ -145,4 +145,4 @@ export class LoginComponent { if (error) return control.touched && control.hasError(error); return !!(control.touched && control.invalid); } -} \ No newline at end of file +} diff --git a/src/app/pages/mureg/mureg.html b/src/app/pages/mureg/mureg.html index 7c30f18..f688235 100644 --- a/src/app/pages/mureg/mureg.html +++ b/src/app/pages/mureg/mureg.html @@ -181,9 +181,15 @@ @@ -223,7 +229,7 @@ - + @@ -467,3 +473,109 @@ + + + + + + + + + + diff --git a/src/app/pages/mureg/mureg.scss b/src/app/pages/mureg/mureg.scss index 8ca24e1..3e30c8f 100644 --- a/src/app/pages/mureg/mureg.scss +++ b/src/app/pages/mureg/mureg.scss @@ -267,6 +267,8 @@ color: rgba(17,18,20,0.5); transition: all 0.2s; cursor: pointer; &:hover { background: rgba(17,18,20,0.05); color: var(--text); transform: translateY(-1px); } &.primary:hover { color: var(--blue); background: rgba(3,15,170,0.1); } + &.info:hover { color: var(--brand); background: rgba(227, 61, 207, 0.12); } + &.danger:hover { color: #dc3545; background: rgba(220, 53, 69, 0.12); } } /* FOOTER */ @@ -278,15 +280,18 @@ .modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); } .modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; } .modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; } +.modal-card.modal-xl-custom { width: min(920px, 90vw); max-height: 78vh; } +.modal-card.modal-sm { width: min(480px, 100%); } @keyframes modalPop { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } } .modal-header { padding: 16px 24px; border-bottom: 1px solid rgba(0,0,0,0.06); background: #fff; display: flex; justify-content: space-between; align-items: center; .modal-title { font-size: 1.1rem; font-weight: 800; color: var(--text); display: flex; align-items: center; gap: 12px; } .icon-bg { width: 32px; height: 32px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 16px; &.primary-soft { background: rgba(3, 15, 170, 0.1); color: var(--blue); } &.brand-soft { background: rgba(227, 61, 207, 0.1); color: var(--brand); } /* Adicionado */ + &.danger-soft { background: rgba(220, 53, 69, 0.12); color: #dc3545; } } .btn-icon { color: var(--muted); background: transparent; font-size: 1.2rem; border:none; cursor: pointer; &:hover { color: var(--brand); } } } -.modal-body { padding: 24px; overflow-y: auto; &.bg-light-gray { background-color: #f8f9fa; } } +.modal-body { padding: 20px; overflow-y: auto; &.bg-light-gray { background-color: #f8f9fa; } } /* FORM & DETAILS */ .details-dashboard { display: grid; grid-template-columns: 1fr; gap: 20px; } @@ -294,6 +299,18 @@ div.detail-box { background: #fff; border-radius: 16px; border: 1px solid rgba(0 div.box-header { padding: 10px 16px; font-size: 0.8rem; font-weight: 800; text-transform: uppercase; color: var(--muted); border-bottom: 1px solid rgba(0,0,0,0.04); background: #fdfdfd; display: flex; align-items: center; } div.box-body { padding: 16px; } +/* INFO GRID (detalhes) */ +.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 0; } +.info-item { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 5px 8px; background: rgba(245, 245, 247, 0.5); border-radius: 10px; border: 1px solid rgba(0,0,0,0.03); transition: background 0.2s; + &:hover { background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.05); } + &.span-2 { grid-column: span 2; } + .lbl { font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 800; color: var(--muted); margin-bottom: 2px; } + .val { font-size: 0.85rem; font-weight: 700; color: var(--text); word-break: break-word; line-height: 1.2; + &.fs-4 { font-size: 1rem !important; } + &.small-text { font-size: 0.7rem; font-family: monospace; } + } +} + /* EDIT FORM STYLES */ .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; @@ -307,4 +324,4 @@ div.box-body { padding: 16px; } .form-control { border-radius: 8px; border: 1px solid rgba(17,18,20,0.15); &:focus { border-color: var(--brand); box-shadow: 0 0 0 2px rgba(227,61,207,0.15); outline: none; } -} \ No newline at end of file +} diff --git a/src/app/pages/mureg/mureg.ts b/src/app/pages/mureg/mureg.ts index 0572137..fa633b6 100644 --- a/src/app/pages/mureg/mureg.ts +++ b/src/app/pages/mureg/mureg.ts @@ -129,6 +129,16 @@ export class Mureg implements AfterViewInit { editSaving = false; editModel: any = null; + // ====== DETAIL MODAL ====== + detailOpen = false; + detailLoading = false; + detailData: MuregDetailDto | null = null; + + // ====== DELETE MODAL ====== + deleteOpen = false; + deleteSaving = false; + deleteTarget: MuregRow | null = null; + // ====== CREATE MODAL ====== createOpen = false; createSaving = false; @@ -638,6 +648,76 @@ export class Mureg implements AfterViewInit { }); } + // ======================================================================= + // DETAIL MODAL + // ======================================================================= + onView(row: MuregRow) { + this.detailOpen = true; + this.detailLoading = true; + this.detailData = null; + + this.http.get(`${this.apiBase}/${row.id}`).subscribe({ + next: (data) => { + this.detailData = data; + this.detailLoading = false; + }, + error: async () => { + this.detailLoading = false; + await this.showToast('Erro ao carregar detalhes da Mureg.'); + } + }); + } + + closeDetail() { + this.detailOpen = false; + this.detailLoading = false; + this.detailData = null; + } + + // ======================================================================= + // DELETE MODAL + // ======================================================================= + onDelete(row: MuregRow) { + this.deleteTarget = row; + this.deleteOpen = true; + this.deleteSaving = false; + } + + closeDelete() { + this.deleteOpen = false; + this.deleteTarget = null; + this.deleteSaving = false; + } + + confirmDelete() { + if (!this.deleteTarget?.id) return; + + this.deleteSaving = true; + const targetId = this.deleteTarget.id; + const currentGroup = this.expandedGroup; + + this.http.delete(`${this.apiBase}/${targetId}`).subscribe({ + next: async () => { + this.deleteSaving = false; + await this.showToast('Mureg excluída com sucesso!'); + this.closeDelete(); + this.loadForGroups(); + + if (currentGroup) { + setTimeout(() => { + this.expandedGroup = currentGroup; + this.toggleGroup(currentGroup); + }, 400); + } + }, + error: async (err) => { + this.deleteSaving = false; + const msg = this.extractApiMessage(err) ?? 'Erro ao excluir Mureg.'; + await this.showToast(msg); + } + }); + } + // ======================================================================= // Helpers // ======================================================================= diff --git a/src/app/pages/notificacoes/notificacoes.html b/src/app/pages/notificacoes/notificacoes.html new file mode 100644 index 0000000..a90bf0a --- /dev/null +++ b/src/app/pages/notificacoes/notificacoes.html @@ -0,0 +1,81 @@ +
+
+
+
+
+

Notificações

+

Acompanhe vencimentos e avisos recentes.

+
+
+ + + + +
+
+ +
Carregando notificações...
+
Falha ao carregar notificações.
+
+ Nenhuma notificação encontrada. +
+
+ Nenhuma notificação para o filtro selecionado. +
+ +
+
+
+ + {{ n.tipo === 'Vencido' ? 'Vencido' : 'A vencer' }} + + {{ n.linha || '-' }} +
+ +
+ {{ n.linha || '-' }} - {{ n.usuario || n.cliente || '-' }} +
+ +
+
Linha: {{ n.linha || '-' }}
+
Usuário: {{ n.usuario || '-' }}
+
Cliente: {{ n.cliente || '-' }}
+
{{ n.tipo === 'Vencido' ? 'Venceu em' : 'Vence em' }}: {{ n.referenciaData ? (n.referenciaData | date:'dd/MM/yyyy') : '-' }}
+
+ + +
+
+
+
+
diff --git a/src/app/pages/notificacoes/notificacoes.scss b/src/app/pages/notificacoes/notificacoes.scss new file mode 100644 index 0000000..e071c74 --- /dev/null +++ b/src/app/pages/notificacoes/notificacoes.scss @@ -0,0 +1,178 @@ +:host { + display: block; +} + +.notificacoes-page { + width: 100%; +} + +.wrap { + padding: 24px 0 32px; +} + +.container { + width: 100%; + max-width: 1100px; + margin: 0 auto; + padding: 0 16px; +} + +.page-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + gap: 16px; + flex-wrap: wrap; + + h2 { + font-size: 24px; + font-weight: 800; + margin: 0 0 4px; + } + + p { + margin: 0; + color: rgba(17, 18, 20, 0.6); + font-weight: 600; + } +} + +.filters { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.filter-btn { + border: 1px solid rgba(0,0,0,0.08); + background: #fff; + padding: 8px 14px; + border-radius: 999px; + font-weight: 700; + font-size: 12px; + color: rgba(17, 18, 20, 0.7); + cursor: pointer; + transition: all 0.2s ease; + + &.active { + border-color: rgba(3, 15, 170, 0.4); + background: rgba(3, 15, 170, 0.08); + color: #030faa; + } + + &.warning.active { + border-color: rgba(227, 61, 207, 0.45); + background: rgba(227, 61, 207, 0.12); + color: #8b2a7d; + } + + &.danger.active { + border-color: rgba(239, 68, 68, 0.45); + background: rgba(239, 68, 68, 0.12); + color: #b91c1c; + } + + &.neutral.active { + border-color: rgba(15, 23, 42, 0.35); + background: rgba(15, 23, 42, 0.08); + color: #0f172a; + } +} + +.state { + padding: 12px 14px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.7); + border: 1px solid rgba(0,0,0,0.08); + font-weight: 700; + color: rgba(17, 18, 20, 0.6); +} + +.state.warn { + color: #b45309; +} + +.notifications-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; +} + +.notification-card { + background: #fff; + border-radius: 16px; + border: 1px solid rgba(0,0,0,0.08); + padding: 16px; + box-shadow: 0 18px 36px rgba(0,0,0,0.08); + transition: transform 0.2s ease, box-shadow 0.2s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 22px 44px rgba(0,0,0,0.12); + } + + .card-info { + margin-top: 12px; + display: grid; + gap: 6px; + font-size: 13px; + color: rgba(17, 18, 20, 0.72); + + strong { + color: rgba(17, 18, 20, 0.92); + } + } +} + +.card-title { + margin-top: 10px; + font-weight: 800; + color: rgba(17, 18, 20, 0.92); +} + +.card-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.tag { + display: inline-flex; + align-items: center; + padding: 4px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + background: rgba(3, 15, 170, 0.12); + color: #1f2937; +} + +.tag.warn { + background: rgba(227, 61, 207, 0.16); + color: #8b2a7d; +} + +.tag.danger { + background: rgba(239, 68, 68, 0.16); + color: #b91c1c; +} + +.line-number { + font-size: 12px; + color: rgba(17, 18, 20, 0.55); + font-weight: 700; +} + + +.mark-read { + margin-top: 12px; + padding: 8px 12px; + border-radius: 10px; + border: 1px solid rgba(0,0,0,0.08); + background: #fff; + font-size: 12px; + font-weight: 700; + cursor: pointer; +} diff --git a/src/app/pages/notificacoes/notificacoes.ts b/src/app/pages/notificacoes/notificacoes.ts new file mode 100644 index 0000000..6c2b0c1 --- /dev/null +++ b/src/app/pages/notificacoes/notificacoes.ts @@ -0,0 +1,66 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { NotificationsService, NotificationDto } from '../../services/notifications.service'; + +@Component({ + selector: 'app-notificacoes', + standalone: true, + imports: [CommonModule], + templateUrl: './notificacoes.html', + styleUrls: ['./notificacoes.scss'], +}) +export class Notificacoes implements OnInit { + notifications: NotificationDto[] = []; + filter: 'todas' | 'vencidas' | 'aVencer' | 'lidas' = 'todas'; + loading = false; + error = false; + + constructor(private notificationsService: NotificationsService) {} + + ngOnInit(): void { + this.loadNotifications(); + } + + markAsRead(notification: NotificationDto) { + if (notification.lida) return; + this.notificationsService.markAsRead(notification.id).subscribe({ + next: () => { + notification.lida = true; + notification.lidaEm = new Date().toISOString(); + }, + }); + } + + setFilter(value: 'todas' | 'vencidas' | 'aVencer' | 'lidas') { + this.filter = value; + } + + get filteredNotifications() { + if (this.filter === 'vencidas') { + return this.notifications.filter(n => n.tipo === 'Vencido'); + } + if (this.filter === 'aVencer') { + return this.notifications.filter(n => n.tipo === 'AVencer'); + } + if (this.filter === 'lidas') { + return this.notifications.filter(n => n.lida); + } + return this.notifications; + } + + private loadNotifications() { + this.loading = true; + this.error = false; + this.notificationsService.list().subscribe({ + next: (data) => { + this.notifications = data || []; + this.loading = false; + }, + error: () => { + this.error = true; + this.loading = false; + }, + }); + } +} diff --git a/src/app/services/notifications.service.ts b/src/app/services/notifications.service.ts new file mode 100644 index 0000000..07e16e8 --- /dev/null +++ b/src/app/services/notifications.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { environment } from '../../environments/environment'; + +export type NotificationTipo = 'AVencer' | 'Vencido'; + +export type NotificationDto = { + id: string; + tipo: NotificationTipo; + titulo: string; + mensagem: string; + data: string; + referenciaData?: string | null; + diasParaVencer?: number | null; + lida: boolean; + lidaEm?: string | null; + vigenciaLineId?: string | null; + cliente?: string | null; + linha?: string | null; + usuario?: string | null; +}; + +@Injectable({ providedIn: 'root' }) +export class NotificationsService { + private readonly baseApi: string; + + constructor(private http: HttpClient) { + const raw = (environment.apiUrl || '').replace(/\/+$/, ''); + this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + } + + list(): Observable { + return this.http.get(`${this.baseApi}/notifications`); + } + + markAsRead(id: string): Observable { + return this.http.patch(`${this.baseApi}/notifications/${id}/read`, {}); + } +}
-
ITEM {{ sortBy==='item' && sortDir==='desc' ? '▼' : '▲' }}
+
+
ITEM
-
QTD LINHAS {{ sortBy==='qtdlinhas' && sortDir==='desc' ? '▼' : '▲' }}
+
QTD LINHAS
VIVO
-
FRANQUIA {{ sortBy==='franquiavivo' && sortDir==='desc' ? '▼' : '▲' }}
+
FRANQUIA
-
VALOR (R$) {{ sortBy==='valorcontratovivo' && sortDir==='desc' ? '▼' : '▲' }}
+
VALOR (R$)
-
FRANQUIA {{ sortBy==='franquialine' && sortDir==='desc' ? '▼' : '▲' }}
+
FRANQUIA
-
VALOR (R$) {{ sortBy==='valorcontratoline' && sortDir==='desc' ? '▼' : '▲' }}
+
VALOR (R$)
+ +