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(); try { const raw = localStorage.getItem('vigenciaAcknowledgedIds'); const ids = raw ? (JSON.parse(raw) as string[]) : []; return new Set(ids); } catch { return new Set(); } } 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 }; } }