371 lines
11 KiB
TypeScript
371 lines
11 KiB
TypeScript
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, FormsModule],
|
|
templateUrl: './notificacoes.html',
|
|
styleUrls: ['./notificacoes.scss'],
|
|
})
|
|
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<string>();
|
|
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) {
|
|
if (notification.lida) return;
|
|
this.notificationsService.markAsRead(notification.id).subscribe({
|
|
next: () => {
|
|
notification.lida = true;
|
|
notification.lidaEm = new Date().toISOString();
|
|
},
|
|
});
|
|
}
|
|
|
|
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() {
|
|
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 {
|
|
if (!date) return '-';
|
|
const parsed = this.parseDateOnly(date);
|
|
if (!parsed) return '-';
|
|
return parsed.toLocaleDateString('pt-BR');
|
|
}
|
|
|
|
getNotificationTipo(notification: NotificationDto): 'Vencido' | 'AVencer' {
|
|
const reference = notification.dtTerminoFidelizacao ?? notification.referenciaData;
|
|
const parsed = this.parseDateOnly(reference);
|
|
if (!parsed) return notification.tipo;
|
|
|
|
const today = this.startOfDay(new Date());
|
|
return parsed < today ? 'Vencido' : 'AVencer';
|
|
}
|
|
|
|
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;
|
|
},
|
|
});
|
|
}
|
|
|
|
countByType(tipo: 'Vencido' | 'AVencer'): number {
|
|
return this.notifications.filter(n => this.getNotificationTipo(n) === tipo && !n.lida).length;
|
|
}
|
|
|
|
markAllAsRead() {
|
|
if (this.filter === 'lidas' || this.bulkLoading) return;
|
|
this.bulkLoading = true;
|
|
|
|
const selectedIds = Array.from(this.selectedIds);
|
|
const scopedIds = selectedIds.length
|
|
? selectedIds
|
|
: (this.filter !== 'todas' ? this.filteredNotifications.map(n => n.id) : []);
|
|
const filterParam = scopedIds.length ? undefined : this.getFilterParam();
|
|
this.notificationsService.markAllAsRead(filterParam, scopedIds.length ? scopedIds : undefined).subscribe({
|
|
next: () => {
|
|
const now = new Date().toISOString();
|
|
this.notifications = this.notifications.map((n) => {
|
|
if (scopedIds.length ? scopedIds.includes(n.id) : this.shouldMarkRead(n)) {
|
|
return { ...n, lida: true, lidaEm: now };
|
|
}
|
|
return n;
|
|
});
|
|
this.clearSelection();
|
|
this.bulkLoading = false;
|
|
},
|
|
error: () => {
|
|
this.bulkLoading = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
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;
|
|
|
|
const selectedIds = Array.from(this.selectedIds);
|
|
const scopedIds = selectedIds.length
|
|
? selectedIds
|
|
: (this.filter !== 'todas' ? this.filteredNotifications.map(n => n.id) : []);
|
|
const filterParam = scopedIds.length ? undefined : this.getFilterParam();
|
|
this.notificationsService.export(filterParam, scopedIds.length ? scopedIds : undefined).subscribe({
|
|
next: (res) => {
|
|
const blob = res.body;
|
|
if (!blob) {
|
|
this.exportLoading = false;
|
|
return;
|
|
}
|
|
|
|
const filename = this.extractFilename(res.headers.get('content-disposition')) || this.buildDefaultFilename();
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
URL.revokeObjectURL(url);
|
|
this.clearSelection();
|
|
this.exportLoading = false;
|
|
},
|
|
error: () => {
|
|
this.exportLoading = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
isSelected(notification: NotificationDto): boolean {
|
|
return this.selectedIds.has(notification.id);
|
|
}
|
|
|
|
toggleSelection(notification: NotificationDto) {
|
|
if (this.selectedIds.has(notification.id)) {
|
|
this.selectedIds.delete(notification.id);
|
|
} else {
|
|
this.selectedIds.add(notification.id);
|
|
}
|
|
}
|
|
|
|
get isAllSelected(): boolean {
|
|
const list = this.filteredNotifications;
|
|
return list.length > 0 && list.every(n => this.selectedIds.has(n.id));
|
|
}
|
|
|
|
toggleSelectAll() {
|
|
const list = this.filteredNotifications;
|
|
if (this.isAllSelected) {
|
|
this.clearSelection();
|
|
return;
|
|
}
|
|
list.forEach(n => this.selectedIds.add(n.id));
|
|
}
|
|
|
|
clearSelection() {
|
|
this.selectedIds.clear();
|
|
}
|
|
|
|
private getFilterParam(): string | undefined {
|
|
if (this.filter === 'aVencer') return 'a-vencer';
|
|
if (this.filter === 'vencidas') return 'vencidas';
|
|
if (this.filter === 'todas') return undefined;
|
|
return undefined;
|
|
}
|
|
|
|
private shouldMarkRead(n: NotificationDto): boolean {
|
|
if (this.filter === 'todas') return true;
|
|
if (this.filter === 'aVencer') return this.getNotificationTipo(n) === 'AVencer';
|
|
if (this.filter === 'vencidas') return this.getNotificationTipo(n) === 'Vencido';
|
|
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];
|
|
const parts = datePart.split('-');
|
|
if (parts.length === 3) {
|
|
const year = Number(parts[0]);
|
|
const month = Number(parts[1]);
|
|
const day = Number(parts[2]);
|
|
if (Number.isFinite(year) && Number.isFinite(month) && Number.isFinite(day)) {
|
|
return new Date(year, month - 1, day);
|
|
}
|
|
}
|
|
|
|
const fallback = new Date(raw);
|
|
if (Number.isNaN(fallback.getTime())) return null;
|
|
return this.startOfDay(fallback);
|
|
}
|
|
|
|
private startOfDay(date: Date): Date {
|
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
}
|
|
|
|
private extractFilename(contentDisposition: string | null): string | null {
|
|
if (!contentDisposition) return null;
|
|
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
|
|
if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
|
|
const normalMatch = contentDisposition.match(/filename=\"?([^\";]+)\"?/i);
|
|
return normalMatch?.[1] ?? null;
|
|
}
|
|
|
|
private buildDefaultFilename(): string {
|
|
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace('T', '').slice(0, 14);
|
|
return `notificacoes-${stamp}.xlsx`;
|
|
}
|
|
}
|