line-gestao-frontend/src/app/pages/notificacoes/notificacoes.ts

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`;
}
}