Refina layout dos cards de notificações

This commit is contained in:
Eduardo Lopes 2026-01-22 16:56:54 -03:00
parent 8a12a2e0d0
commit 72a79479a8
5 changed files with 145 additions and 31 deletions

View File

@ -50,11 +50,17 @@
</div> </div>
<div class="notification-item" *ngFor="let n of notifications"> <div class="notification-item" *ngFor="let n of notifications">
<div class="notification-top">
<span class="notification-tag" [class.danger]="n.tipo === 'Vencido'" [class.warn]="n.tipo === 'AVencer'"> <span class="notification-tag" [class.danger]="n.tipo === 'Vencido'" [class.warn]="n.tipo === 'AVencer'">
{{ n.tipo === 'Vencido' ? 'Vencido' : 'A vencer' }} {{ n.tipo === 'Vencido' ? 'Vencido' : 'A vencer' }}
</span> </span>
<div class="notification-title">{{ n.titulo }}</div> <span class="notification-line">{{ n.linha || '-' }}</span>
<div class="notification-message">{{ n.mensagem }}</div> </div>
<div class="notification-info">
<div><strong>Linha:</strong> {{ n.linha || '-' }}</div>
<div><strong>Cliente:</strong> {{ n.cliente || '-' }}</div>
<div><strong>{{ n.tipo === 'Vencido' ? 'Venceu em' : 'Vence em' }}:</strong> {{ n.referenciaData ? (n.referenciaData | date:'dd/MM/yyyy') : '-' }}</div>
</div>
<button type="button" class="mark-read" (click)="markNotificationRead(n)"> <button type="button" class="mark-read" (click)="markNotificationRead(n)">
{{ n.lida ? 'Lida' : 'Marcar como lida' }} {{ n.lida ? 'Lida' : 'Marcar como lida' }}
</button> </button>
@ -117,6 +123,21 @@
</header> </header>
<div class="toast-container position-fixed top-0 end-0 p-3">
<div class="toast notification-toast" #notifToast>
<div class="toast-header">
<strong class="me-auto">Vigência próxima</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body" *ngIf="toastNotification as toastItem">
A linha {{ toastItem.linha || '-' }} vence em 5 dias.
<button type="button" class="btn-aware" (click)="acknowledgeNotification(toastItem)" data-bs-dismiss="toast">
Estou ciente
</button>
</div>
</div>
</div>
<!-- ✅ OVERLAY (logado) --> <!-- ✅ OVERLAY (logado) -->
<div class="menu-overlay" *ngIf="isLoggedHeader && menuOpen" (click)="closeMenu()"></div> <div class="menu-overlay" *ngIf="isLoggedHeader && menuOpen" (click)="closeMenu()"></div>

View File

@ -242,6 +242,31 @@
background: rgba(3, 15, 170, 0.12); 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 { .notification-tag.warn {
background: rgba(227, 61, 207, 0.16); background: rgba(227, 61, 207, 0.16);
color: #8b2a7d; color: #8b2a7d;
@ -282,6 +307,30 @@
} }
} }
.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 { .options-menu {
position: relative; position: relative;
display: flex; display: flex;

View File

@ -1,4 +1,4 @@
import { Component, HostListener, Inject } from '@angular/core'; import { Component, HostListener, Inject, ElementRef, ViewChild } from '@angular/core';
import { RouterLink, Router, NavigationEnd } from '@angular/router'; import { RouterLink, Router, NavigationEnd } from '@angular/router';
import { CommonModule, isPlatformBrowser } from '@angular/common'; import { CommonModule, isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID } from '@angular/core'; import { PLATFORM_ID } from '@angular/core';
@ -24,6 +24,8 @@ export class Header {
notifications: NotificationDto[] = []; notifications: NotificationDto[] = [];
notificationsLoading = false; notificationsLoading = false;
notificationsError = false; notificationsError = false;
private notificationsLoaded = false;
@ViewChild('notifToast') notifToast?: ElementRef;
private readonly loggedPrefixes = [ private readonly loggedPrefixes = [
'/geral', '/geral',
@ -54,7 +56,13 @@ export class Header {
this.menuOpen = false; this.menuOpen = false;
this.optionsOpen = false; this.optionsOpen = false;
this.notificationsOpen = false; this.notificationsOpen = false;
if (this.isLoggedHeader) {
this.ensureNotificationsLoaded();
}
}); });
if (this.isLoggedHeader) {
this.ensureNotificationsLoaded();
}
} }
private syncHeaderState(rawUrl: string) { private syncHeaderState(rawUrl: string) {
@ -137,6 +145,18 @@ export class Header {
this.closeNotifications(); 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() { private loadNotifications() {
if (!isPlatformBrowser(this.platformId)) return; if (!isPlatformBrowser(this.platformId)) return;
this.notificationsLoading = true; this.notificationsLoading = true;
@ -144,7 +164,9 @@ export class Header {
this.notificationsService.list().subscribe({ this.notificationsService.list().subscribe({
next: (data) => { next: (data) => {
this.notifications = data || []; this.notifications = data || [];
this.notificationsLoaded = true;
this.notificationsLoading = false; this.notificationsLoading = false;
this.maybeShowVigenciaToast();
}, },
error: () => { error: () => {
this.notificationsError = true; this.notificationsError = true;
@ -152,4 +174,36 @@ export class Header {
}, },
}); });
} }
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<string>();
try {
const raw = localStorage.getItem('vigenciaAcknowledgedIds');
const ids = raw ? (JSON.parse(raw) as string[]) : [];
return new Set(ids);
} catch {
return new Set<string>();
}
}
} }

View File

@ -49,15 +49,13 @@
<span class="tag" [class.danger]="n.tipo === 'Vencido'" [class.warn]="n.tipo === 'AVencer'"> <span class="tag" [class.danger]="n.tipo === 'Vencido'" [class.warn]="n.tipo === 'AVencer'">
{{ n.tipo === 'Vencido' ? 'Vencido' : 'A vencer' }} {{ n.tipo === 'Vencido' ? 'Vencido' : 'A vencer' }}
</span> </span>
<span class="date">{{ n.data | date:'dd/MM/yyyy' }}</span> <span class="line-number">{{ n.linha || '-' }}</span>
</div> </div>
<h3>{{ n.titulo }}</h3> <div class="card-info">
<p>{{ n.mensagem }}</p> <div><strong>Linha:</strong> {{ n.linha || '-' }}</div>
<div><strong>Cliente:</strong> {{ n.cliente || '-' }}</div>
<div class="card-meta"> <div><strong>{{ n.tipo === 'Vencido' ? 'Venceu em' : 'Vence em' }}:</strong> {{ n.referenciaData ? (n.referenciaData | date:'dd/MM/yyyy') : '-' }}</div>
<span *ngIf="n.cliente">Cliente: {{ n.cliente }}</span>
<span *ngIf="n.linha">Linha: {{ n.linha }}</span>
</div> </div>
<button type="button" class="mark-read" (click)="markAsRead(n)"> <button type="button" class="mark-read" (click)="markAsRead(n)">

View File

@ -106,17 +106,16 @@
box-shadow: 0 22px 44px rgba(0,0,0,0.12); box-shadow: 0 22px 44px rgba(0,0,0,0.12);
} }
h3 { .card-info {
margin: 12px 0 8px; margin-top: 12px;
font-size: 16px; display: grid;
font-weight: 800; gap: 6px;
font-size: 13px;
color: rgba(17, 18, 20, 0.72);
strong {
color: rgba(17, 18, 20, 0.92); color: rgba(17, 18, 20, 0.92);
} }
p {
margin: 0 0 12px;
font-size: 13px;
color: rgba(17, 18, 20, 0.68);
} }
} }
@ -148,19 +147,12 @@
color: #b91c1c; color: #b91c1c;
} }
.date { .line-number {
font-size: 12px; font-size: 12px;
color: rgba(17, 18, 20, 0.55); color: rgba(17, 18, 20, 0.55);
font-weight: 700; font-weight: 700;
} }
.card-meta {
display: grid;
gap: 4px;
font-size: 12px;
color: rgba(17, 18, 20, 0.7);
font-weight: 700;
}
.mark-read { .mark-read {
margin-top: 12px; margin-top: 12px;