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 class="notification-item" *ngFor="let n of notifications">
<span class="notification-tag" [class.danger]="n.tipo === 'Vencido'" [class.warn]="n.tipo === 'AVencer'">
{{ n.tipo === 'Vencido' ? 'Vencido' : 'A vencer' }}
</span>
<div class="notification-title">{{ n.titulo }}</div>
<div class="notification-message">{{ n.mensagem }}</div>
<div class="notification-top">
<span class="notification-tag" [class.danger]="n.tipo === 'Vencido'" [class.warn]="n.tipo === 'AVencer'">
{{ n.tipo === 'Vencido' ? 'Vencido' : 'A vencer' }}
</span>
<span class="notification-line">{{ n.linha || '-' }}</span>
</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)">
{{ n.lida ? 'Lida' : 'Marcar como lida' }}
</button>
@ -117,6 +123,21 @@
</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) -->
<div class="menu-overlay" *ngIf="isLoggedHeader && menuOpen" (click)="closeMenu()"></div>

View File

@ -242,6 +242,31 @@
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;
@ -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 {
position: relative;
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 { CommonModule, isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID } from '@angular/core';
@ -24,6 +24,8 @@ export class Header {
notifications: NotificationDto[] = [];
notificationsLoading = false;
notificationsError = false;
private notificationsLoaded = false;
@ViewChild('notifToast') notifToast?: ElementRef;
private readonly loggedPrefixes = [
'/geral',
@ -54,7 +56,13 @@ export class Header {
this.menuOpen = false;
this.optionsOpen = false;
this.notificationsOpen = false;
if (this.isLoggedHeader) {
this.ensureNotificationsLoaded();
}
});
if (this.isLoggedHeader) {
this.ensureNotificationsLoaded();
}
}
private syncHeaderState(rawUrl: string) {
@ -137,6 +145,18 @@ export class Header {
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;
@ -144,7 +164,9 @@ export class Header {
this.notificationsService.list().subscribe({
next: (data) => {
this.notifications = data || [];
this.notificationsLoaded = true;
this.notificationsLoading = false;
this.maybeShowVigenciaToast();
},
error: () => {
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'">
{{ n.tipo === 'Vencido' ? 'Vencido' : 'A vencer' }}
</span>
<span class="date">{{ n.data | date:'dd/MM/yyyy' }}</span>
<span class="line-number">{{ n.linha || '-' }}</span>
</div>
<h3>{{ n.titulo }}</h3>
<p>{{ n.mensagem }}</p>
<div class="card-meta">
<span *ngIf="n.cliente">Cliente: {{ n.cliente }}</span>
<span *ngIf="n.linha">Linha: {{ n.linha }}</span>
<div class="card-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)="markAsRead(n)">

View File

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