Refina layout dos cards de notificações
This commit is contained in:
parent
8a12a2e0d0
commit
72a79479a8
|
|
@ -50,11 +50,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="notification-item" *ngFor="let n of notifications">
|
<div class="notification-item" *ngFor="let n of notifications">
|
||||||
<span class="notification-tag" [class.danger]="n.tipo === 'Vencido'" [class.warn]="n.tipo === 'AVencer'">
|
<div class="notification-top">
|
||||||
{{ n.tipo === 'Vencido' ? 'Vencido' : 'A vencer' }}
|
<span class="notification-tag" [class.danger]="n.tipo === 'Vencido'" [class.warn]="n.tipo === 'AVencer'">
|
||||||
</span>
|
{{ n.tipo === 'Vencido' ? 'Vencido' : 'A vencer' }}
|
||||||
<div class="notification-title">{{ n.titulo }}</div>
|
</span>
|
||||||
<div class="notification-message">{{ n.mensagem }}</div>
|
<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)">
|
<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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
color: rgba(17, 18, 20, 0.92);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0 0 12px;
|
|
||||||
font-size: 13px;
|
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;
|
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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue