line-gestao-frontend/src/app/components/header/header.ts

708 lines
21 KiB
TypeScript

import { Component, HostListener, Inject, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
import { RouterLink, Router, NavigationEnd } from '@angular/router';
import { CommonModule, isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID } from '@angular/core';
import { filter } from 'rxjs/operators';
import { AuthService } from '../../services/auth.service';
import { NotificationsService, NotificationDto } from '../../services/notifications.service';
import { UsersService, CreateUserPayload, ApiFieldError } from '../../services/users.service';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors, FormsModule } from '@angular/forms';
import { HttpErrorResponse } from '@angular/common/http';
import { CustomSelectComponent } from '../custom-select/custom-select';
@Component({
selector: 'app-header',
standalone: true,
imports: [RouterLink, CommonModule, ReactiveFormsModule, FormsModule, CustomSelectComponent],
templateUrl: './header.html',
styleUrls: ['./header.scss'],
})
export class Header implements AfterViewInit {
isScrolled = false;
menuOpen = false;
optionsOpen = false;
notificationsOpen = false;
createUserOpen = false;
manageUsersOpen = false;
isLoggedHeader = false;
isHome = false;
isAdmin = false;
notifications: NotificationDto[] = [];
notificationsLoading = false;
notificationsError = false;
private notificationsLoaded = false;
private pendingToastCheck = false;
private lastNotificationsLoadAt = 0;
private readonly notificationsRefreshMs = 60_000;
readonly notificationsPreviewLimit = 40;
@ViewChild('notifToast') notifToast?: ElementRef;
createUserForm: FormGroup;
createUserSubmitting = false;
createUserErrors: ApiFieldError[] = [];
createUserForbidden = false;
createUserSuccess = '';
readonly permissionOptions = [
{ value: 'admin', label: 'Administrador' },
{ value: 'gestor', label: 'Gestor' },
];
manageUsersLoading = false;
manageUsersErrors: ApiFieldError[] = [];
manageUsersSuccess = '';
manageUsers: any[] = [];
manageSearch = '';
managePage = 1;
managePageSize = 10;
manageTotal = 0;
editUserForm: FormGroup;
editUserSubmitting = false;
editUserErrors: ApiFieldError[] = [];
editUserSuccess = '';
editUserTarget: any | null = null;
private readonly loggedPrefixes = [
'/geral',
'/mureg',
'/faturamento',
'/dadosusuarios',
'/vigencia',
'/trocanumero',
'/dashboard',
'/notificacoes',
'/chips-controle-recebidos',
'/resumo',
'/parcelamentos',
'/historico',
'/perfil',
];
constructor(
private router: Router,
private authService: AuthService,
private notificationsService: NotificationsService,
private usersService: UsersService,
private fb: FormBuilder,
@Inject(PLATFORM_ID) private platformId: object
) {
this.createUserForm = this.fb.group(
{
nome: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
senha: ['', [Validators.required, Validators.minLength(6)]],
confirmarSenha: ['', [Validators.required, Validators.minLength(6)]],
permissao: ['', [Validators.required]],
},
{ validators: this.passwordsMatchValidator }
);
this.editUserForm = this.fb.group({
nome: [''],
email: [''],
senha: [''],
confirmarSenha: [''],
permissao: [''],
ativo: [true],
});
this.syncHeaderState(this.router.url);
this.syncPermissions();
this.router.events
.pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
.subscribe((event) => {
const rawUrl = event.urlAfterRedirects || event.url;
this.syncHeaderState(rawUrl);
this.syncPermissions();
this.menuOpen = false;
this.optionsOpen = false;
this.notificationsOpen = false;
if (this.isLoggedHeader) {
this.ensureNotificationsLoaded();
}
});
if (this.isLoggedHeader) {
this.ensureNotificationsLoaded();
}
}
private syncHeaderState(rawUrl: string) {
let url = (rawUrl || '').split('?')[0].split('#')[0];
if (url && !url.startsWith('/')) url = `/${url}`;
url = url.replace(/\/+$/, '');
this.isHome = (url === '/' || url === '');
this.isLoggedHeader = this.loggedPrefixes.some((p) =>
url === p || url.startsWith(p + '/')
);
}
private syncPermissions() {
if (!isPlatformBrowser(this.platformId)) {
this.isAdmin = false;
return;
}
this.isAdmin = this.authService.hasRole('admin');
}
toggleMenu() {
this.menuOpen = !this.menuOpen;
}
closeMenu() {
this.menuOpen = false;
}
toggleOptions() {
this.optionsOpen = !this.optionsOpen;
if (this.optionsOpen) this.notificationsOpen = false;
}
closeOptions() {
this.optionsOpen = false;
}
goToProfile() {
this.closeOptions();
this.router.navigate(['/perfil']);
}
openCreateUserModal() {
if (!this.isAdmin) return;
this.createUserOpen = true;
this.closeOptions();
this.resetCreateUserState();
}
closeCreateUserModal() {
this.createUserOpen = false;
this.resetCreateUserState();
}
openManageUsersModal() {
if (!this.isAdmin) return;
this.manageUsersOpen = true;
this.closeOptions();
this.resetManageUsersState();
this.fetchManageUsers(1);
}
closeManageUsersModal() {
this.manageUsersOpen = false;
this.resetManageUsersState();
}
toggleNotifications() {
this.notificationsOpen = !this.notificationsOpen;
if (this.notificationsOpen) {
this.optionsOpen = false;
if (!this.notificationsLoaded) {
this.loadNotifications(false);
return;
}
if (Date.now() - this.lastNotificationsLoadAt > this.notificationsRefreshMs) {
// Atualiza em background para não bloquear a abertura visual do dropdown.
setTimeout(() => this.loadNotifications(false), 0);
}
}
}
closeNotifications() {
this.notificationsOpen = false;
}
markNotificationRead(notification: NotificationDto) {
if (notification.lida) return;
this.notificationsService.markAsRead(notification.id).subscribe({
next: () => {
notification.lida = true;
notification.lidaEm = new Date().toISOString();
},
});
}
getVigenciaLabel(notification: NotificationDto): string {
return this.getNotificationTipo(notification) === 'Vencido' ? 'Venceu em' : 'Vence em';
}
getVigenciaDate(notification: NotificationDto): string {
const raw =
notification.dtTerminoFidelizacao ??
notification.referenciaData ??
notification.data;
if (!raw) return '-';
const parsed = this.parseDateOnly(raw);
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';
}
getNotificationDaysToExpire(notification: NotificationDto): number | null {
const reference = notification.dtTerminoFidelizacao ?? notification.referenciaData;
const parsed = this.parseDateOnly(reference);
if (!parsed) {
return notification.diasParaVencer ?? null;
}
const today = this.startOfDay(new Date());
const msPerDay = 24 * 60 * 60 * 1000;
return Math.round((parsed.getTime() - today.getTime()) / msPerDay);
}
abbreviateName(value?: string | null): string {
const name = (value ?? '').trim();
if (!name) return '-';
const parts = name.split(/\s+/).filter(Boolean);
if (parts.length === 1) return parts[0];
const maxLen = 18;
const full = parts.join(' ');
if (full.length <= maxLen) return full;
if (parts.length >= 3) {
const candidate = `${parts[0]} ${parts[1]} ${parts[2][0]}.`;
if (candidate.length <= maxLen) return candidate;
return `${parts[0]} ${parts[1][0]}.`;
}
const two = `${parts[0]} ${parts[1]}`;
if (two.length <= maxLen) return two;
return `${parts[0]} ${parts[1][0]}.`;
}
get unreadCount() {
return this.notifications.filter(n => !n.lida).length;
}
get notificationsPreview() {
return this.notifications.slice(0, this.notificationsPreviewLimit);
}
get hasNotificationsTruncated() {
return this.notifications.length > this.notificationsPreviewLimit;
}
trackByNotificationId(_: number, notification: NotificationDto) {
return notification.id;
}
logout() {
this.authService.logout();
this.optionsOpen = false;
this.notificationsOpen = false;
this.isAdmin = false;
this.router.navigate(['/']);
}
@HostListener('window:scroll', [])
onWindowScroll() {
if (!isPlatformBrowser(this.platformId)) return;
this.isScrolled = window.scrollY > 10;
}
@HostListener('document:click', [])
onDocumentClick() {
this.optionsOpen = false;
this.notificationsOpen = false;
}
@HostListener('document:keydown.escape', [])
onEsc() {
if (!isPlatformBrowser(this.platformId)) return;
this.closeMenu();
this.closeOptions();
this.closeNotifications();
this.closeCreateUserModal();
this.closeManageUsersModal();
}
acknowledgeNotification(notification: NotificationDto) {
if (!isPlatformBrowser(this.platformId)) return;
const acknowledged = this.getAcknowledgedIds();
acknowledged.add(notification.id);
localStorage.setItem('vigenciaAcknowledgedIds', JSON.stringify(Array.from(acknowledged)));
}
acknowledgeCurrentToast() {
const current = this.toastNotification;
if (!current) return;
this.acknowledgeNotification(current);
}
ngAfterViewInit() {
if (this.pendingToastCheck) {
this.pendingToastCheck = false;
void this.maybeShowVigenciaToast();
}
}
private ensureNotificationsLoaded() {
if (this.notificationsLoaded || this.notificationsLoading) return;
this.loadNotifications(true);
}
private loadNotifications(showToast = true) {
if (!isPlatformBrowser(this.platformId)) return;
if (this.notificationsLoading) return;
this.notificationsLoading = true;
this.notificationsError = false;
this.notificationsService.list().subscribe({
next: (data) => {
this.notifications = data || [];
this.notificationsLoaded = true;
this.notificationsLoading = false;
this.lastNotificationsLoadAt = Date.now();
if (showToast) {
void this.maybeShowVigenciaToast();
}
},
error: () => {
this.notificationsError = true;
this.notificationsLoading = false;
},
});
}
private async maybeShowVigenciaToast() {
if (!isPlatformBrowser(this.platformId)) return;
if (!this.notifToast) {
this.pendingToastCheck = true;
return;
}
const pending = this.getPendingVigenciaToast();
if (!pending) return;
const bs = await import('bootstrap');
const toast = bs.Toast.getOrCreateInstance(this.notifToast.nativeElement, { autohide: false });
toast.show();
}
get toastNotification() {
return this.getPendingVigenciaToast();
}
private getPendingVigenciaToast() {
const acknowledged = this.getAcknowledgedIds();
return this.notifications.find(
n =>
this.getNotificationTipo(n) === 'AVencer' &&
this.getNotificationDaysToExpire(n) === 5 &&
!acknowledged.has(n.id)
);
}
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 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>();
}
}
submitCreateUser() {
if (this.createUserSubmitting) return;
if (this.createUserForm.invalid) {
this.createUserForm.markAllAsTouched();
return;
}
if (!this.isAdmin) {
this.createUserForbidden = true;
return;
}
this.createUserSubmitting = true;
this.setCreateFormDisabled(true);
this.createUserErrors = [];
this.createUserForbidden = false;
this.createUserSuccess = '';
const payload = this.createUserForm.value as CreateUserPayload;
this.usersService.create(payload).subscribe({
next: (created) => {
this.createUserSubmitting = false;
this.setCreateFormDisabled(false);
this.createUserSuccess = `Usuario ${created.nome} criado com sucesso.`;
this.createUserForm.reset({ permissao: '' });
},
error: (err: HttpErrorResponse) => {
this.createUserSubmitting = false;
this.setCreateFormDisabled(false);
if (err.status === 401 || err.status === 403) {
this.createUserForbidden = true;
return;
}
const apiErrors = err?.error?.errors;
if (Array.isArray(apiErrors)) {
this.createUserErrors = apiErrors.map((e: any) => ({
field: e?.field,
message: e?.message || 'Erro ao criar usuario.',
}));
} else {
this.createUserErrors = [{ message: 'Erro ao criar usuario.' }];
}
},
});
}
fetchManageUsers(goToPage?: number) {
if (goToPage) this.managePage = goToPage;
this.manageUsersLoading = true;
this.manageUsersErrors = [];
this.manageUsersSuccess = '';
this.usersService
.list({
search: this.manageSearch?.trim() || undefined,
page: this.managePage,
pageSize: this.managePageSize,
})
.subscribe({
next: (res) => {
this.manageUsers = res.items || [];
this.manageTotal = res.total || 0;
this.manageUsersLoading = false;
},
error: () => {
this.manageUsers = [];
this.manageTotal = 0;
this.manageUsersLoading = false;
},
});
}
onManageSearch() {
this.managePage = 1;
this.fetchManageUsers();
}
clearManageSearch() {
this.manageSearch = '';
this.managePage = 1;
this.fetchManageUsers();
}
manageGoToPage(p: number) {
this.managePage = p;
this.fetchManageUsers();
}
get manageTotalPages(): number {
return Math.max(1, Math.ceil((this.manageTotal || 0) / (this.managePageSize || 10)));
}
get managePageNumbers(): number[] {
const total = this.manageTotalPages;
const current = this.managePage;
const max = 5;
let start = Math.max(1, current - 2);
let end = Math.min(total, start + (max - 1));
start = Math.max(1, end - (max - 1));
const pages: number[] = [];
for (let i = start; i <= end; i++) pages.push(i);
return pages;
}
openEditUser(user: any) {
this.editUserTarget = null;
this.editUserErrors = [];
this.editUserSuccess = '';
this.editUserSubmitting = false;
this.setEditFormDisabled(false);
this.editUserForm.reset({ nome: '', email: '', senha: '', confirmarSenha: '', permissao: '', ativo: true });
this.usersService.getById(user.id).subscribe({
next: (full) => {
this.editUserTarget = full;
this.editUserForm.reset({
nome: full.nome ?? '',
email: full.email ?? '',
senha: '',
confirmarSenha: '',
permissao: full.permissao ?? '',
ativo: full.ativo ?? true,
});
},
error: () => {
this.editUserErrors = [{ message: 'Erro ao carregar usuario.' }];
},
});
}
cancelEditUser() {
this.editUserTarget = null;
this.editUserErrors = [];
this.editUserSuccess = '';
this.editUserSubmitting = false;
this.setEditFormDisabled(false);
this.editUserForm.reset({ nome: '', email: '', senha: '', confirmarSenha: '', permissao: '', ativo: true });
}
submitEditUser() {
if (this.editUserSubmitting || !this.editUserTarget) return;
const payload: any = {};
const nome = (this.editUserForm.get('nome')?.value || '').toString().trim();
const email = (this.editUserForm.get('email')?.value || '').toString().trim();
const permissao = (this.editUserForm.get('permissao')?.value || '').toString().trim();
const ativo = !!this.editUserForm.get('ativo')?.value;
if (nome && nome !== (this.editUserTarget.nome || '').trim()) payload.nome = nome;
if (email && email !== (this.editUserTarget.email || '').trim()) payload.email = email;
if (permissao && permissao !== (this.editUserTarget.permissao || '').trim()) payload.permissao = permissao;
if ((this.editUserTarget.ativo ?? true) !== ativo) payload.ativo = ativo;
const senha = (this.editUserForm.get('senha')?.value || '').toString();
const confirmar = (this.editUserForm.get('confirmarSenha')?.value || '').toString();
if (senha || confirmar) {
if (!senha || !confirmar) {
this.editUserErrors = [{ message: 'Para alterar a senha, preencha senha e confirmacao.' }];
return;
}
if (senha.length < 6) {
this.editUserErrors = [{ message: 'Senha deve ter no minimo 6 caracteres.' }];
return;
}
if (senha !== confirmar) {
this.editUserErrors = [{ message: 'As senhas nao conferem.' }];
return;
}
payload.senha = senha;
payload.confirmarSenha = confirmar;
}
if (Object.keys(payload).length === 0) {
this.editUserErrors = [{ message: 'Nenhuma alteracao detectada.' }];
return;
}
this.editUserSubmitting = true;
this.setEditFormDisabled(true);
this.editUserErrors = [];
this.editUserSuccess = '';
this.usersService.update(this.editUserTarget.id, payload).subscribe({
next: (updated) => {
this.editUserSubmitting = false;
this.setEditFormDisabled(false);
this.editUserSuccess = `Usuario ${updated.nome} atualizado.`;
this.editUserTarget = updated;
this.fetchManageUsers(this.managePage);
},
error: (err: HttpErrorResponse) => {
this.editUserSubmitting = false;
this.setEditFormDisabled(false);
const apiErrors = err?.error?.errors;
if (Array.isArray(apiErrors)) {
this.editUserErrors = apiErrors.map((e: any) => ({
field: e?.field,
message: e?.message || 'Erro ao atualizar usuario.',
}));
} else {
this.editUserErrors = [{ message: err?.error?.message || 'Erro ao atualizar usuario.' }];
}
},
});
}
confirmDeleteUser(user: any) {
if (!confirm(`Excluir usuario ${user.nome}?`)) return;
this.usersService.update(user.id, { ativo: false }).subscribe({
next: () => this.fetchManageUsers(this.managePage),
});
}
hasFieldError(field: string): boolean {
return this.getFieldErrors(field).length > 0;
}
getFieldErrors(field: string): string[] {
const key = this.normalizeField(field);
return this.createUserErrors
.filter((e) => this.normalizeField(e.field) === key)
.map((e) => e.message);
}
get passwordMismatch(): boolean {
return !!this.createUserForm.errors?.['passwordsMismatch'];
}
private resetCreateUserState() {
this.createUserErrors = [];
this.createUserForbidden = false;
this.createUserSuccess = '';
this.createUserSubmitting = false;
this.setCreateFormDisabled(false);
this.createUserForm.reset({ permissao: '' });
}
private resetManageUsersState() {
this.manageUsersErrors = [];
this.manageUsersSuccess = '';
this.manageUsersLoading = false;
this.manageUsers = [];
this.manageSearch = '';
this.managePage = 1;
this.manageTotal = 0;
this.cancelEditUser();
}
private normalizeField(field?: string | null): string {
return (field || '').trim().toLowerCase();
}
private setCreateFormDisabled(disabled: boolean) {
if (disabled) this.createUserForm.disable({ emitEvent: false });
else this.createUserForm.enable({ emitEvent: false });
}
private setEditFormDisabled(disabled: boolean) {
if (disabled) this.editUserForm.disable({ emitEvent: false });
else this.editUserForm.enable({ emitEvent: false });
}
private passwordsMatchValidator(group: AbstractControl): ValidationErrors | null {
const senha = group.get('senha')?.value;
const confirmar = group.get('confirmarSenha')?.value;
if (!senha || !confirmar) return null;
return senha === confirmar ? null : { passwordsMismatch: true };
}
}