import { Component, HostListener, Inject, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core'; import { RouterLink, Router, NavigationEnd } from '@angular/router'; import { CommonModule, isPlatformBrowser } from '@angular/common'; import { PLATFORM_ID } from '@angular/core'; import { HttpClient } from '@angular/common/http'; 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'; import { Subscription } from 'rxjs'; import { environment } from '../../../environments/environment'; import { confirmActionModal, confirmDeletionWithTyping, showDeletionWarning } from '../../utils/destructive-confirmation'; import { buildApiBaseUrl } from '../../utils/api-base.util'; @Component({ selector: 'app-header', standalone: true, imports: [RouterLink, CommonModule, ReactiveFormsModule, FormsModule, CustomSelectComponent], templateUrl: './header.html', styleUrls: ['./header.scss'], }) export class Header implements AfterViewInit, OnDestroy { isScrolled = false; private headerResizeObserver?: ResizeObserver; private headerTransitionEndCleanup?: (() => void) | null = null; menuOpen = false; optionsOpen = false; notificationsOpen = false; createUserOpen = false; manageUsersOpen = false; isLoggedHeader = false; isHome = false; isSysAdmin = false; isGestor = false; isFinanceiro = false; canViewAll = false; canViewFinancialPages = false; canViewMveAudit = false; clientTenantDisplayName = ''; private clientTenantNameTenantId: string | null = null; private readonly baseApi: string; notifications: NotificationDto[] = []; notificationsLoading = false; notificationsError = false; notificationsView: 'pendentes' | 'lidas' = 'pendentes'; notificationsBulkReadLoading = false; notificationsBulkUnreadLoading = false; private notificationsLoaded = false; private pendingToastCheck = false; private lastNotificationsLoadAt = 0; private readonly notificationsRefreshMs = 60_000; readonly notificationsPreviewLimit = 40; @ViewChild('notifToast') notifToast?: ElementRef; private readonly subs = new Subscription(); createUserForm: FormGroup; createUserSubmitting = false; createUserErrors: ApiFieldError[] = []; createUserForbidden = false; createUserSuccess = ''; readonly permissionOptions = [ { value: 'sysadmin', label: 'SysAdmin' }, { value: 'gestor', label: 'Gestor' }, { value: 'financeiro', label: 'Financeiro' }, { value: 'cliente', label: 'Cliente' }, ]; manageUsersLoading = false; manageUsersErrors: ApiFieldError[] = []; manageUsersSuccess = ''; private manageUsersFeedbackTimer: ReturnType | null = null; manageMode: 'users' | 'clients' = 'users'; 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', '/historico-linhas', '/historico-chips', '/solicitacoes', '/auditoria-mve', '/perfil', '/system', ]; constructor( private router: Router, private authService: AuthService, private notificationsService: NotificationsService, private usersService: UsersService, private http: HttpClient, private fb: FormBuilder, private hostElement: ElementRef, @Inject(PLATFORM_ID) private platformId: object ) { this.baseApi = buildApiBaseUrl(environment.apiUrl); 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(); } this.subs.add( this.notificationsService.events$.subscribe((ev) => { if (ev.type === 'read') { const byId = new Set(ev.ids); this.notifications.forEach((n) => { if (byId.has(n.id)) { n.lida = true; n.lidaEm = ev.readAtIso; } }); return; } if (ev.type === 'unread') { const byId = new Set(ev.ids); this.notifications.forEach((n) => { if (byId.has(n.id)) { n.lida = false; n.lidaEm = null; } }); return; } if (ev.type === 'readAll') { this.notifications.forEach((n) => { if (!n.lida) { n.lida = true; n.lidaEm = ev.readAtIso; } }); return; } if (ev.type === 'unreadAll') { this.notifications.forEach((n) => { if (n.lida) { n.lida = false; n.lidaEm = null; } }); return; } if (ev.type === 'reload') { // Para mudanças de escopo desconhecido, recarrega em background. if (this.isLoggedHeader) setTimeout(() => this.loadNotifications(false), 0); } }) ); } 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.isSysAdmin = false; this.isGestor = false; this.isFinanceiro = false; this.canViewAll = false; this.canViewFinancialPages = false; this.clientTenantDisplayName = ''; this.clientTenantNameTenantId = null; return; } const isSysAdmin = this.authService.hasRole('sysadmin'); const isGestor = this.authService.hasRole('gestor'); const isFinanceiro = this.authService.hasRole('financeiro'); this.isSysAdmin = isSysAdmin; this.isGestor = isGestor; this.isFinanceiro = isFinanceiro; this.canViewAll = isSysAdmin || isGestor || isFinanceiro; this.canViewFinancialPages = isSysAdmin || isFinanceiro; this.canViewMveAudit = isSysAdmin || isGestor; if (!this.isClientHeader) { this.clientTenantDisplayName = ''; this.clientTenantNameTenantId = null; return; } this.ensureClientTenantName(); } 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']); } goToSystemProvisionUser() { if (!this.isSysAdmin) return; this.closeOptions(); this.router.navigate(['/system/fornecer-usuario']); } openCreateUserModal() { if (!this.isSysAdmin) return; this.createUserOpen = true; this.closeOptions(); this.resetCreateUserState(); } closeCreateUserModal() { this.createUserOpen = false; this.resetCreateUserState(); } openManageUsersModal() { if (!this.isSysAdmin) return; this.manageMode = 'users'; this.manageUsersOpen = true; this.closeOptions(); this.resetManageUsersState(); this.fetchManageUsers(1); } openManageClientCredentialsModal() { if (!this.isSysAdmin) return; this.manageMode = 'clients'; this.manageUsersOpen = true; this.closeOptions(); this.resetManageUsersState(); this.fetchManageUsers(1); } closeManageUsersModal() { this.manageUsersOpen = false; this.resetManageUsersState(); this.manageMode = 'users'; } toggleNotifications() { this.notificationsOpen = !this.notificationsOpen; if (this.notificationsOpen) { this.notificationsView = 'pendentes'; 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(); // Evita que o toast volte a aparecer para a mesma notificação. this.acknowledgeNotification(notification); }, }); } markNotificationUnread(notification: NotificationDto) { if (!notification.lida) return; this.notificationsService.markAsUnread(notification.id).subscribe({ next: () => { notification.lida = false; notification.lidaEm = null; } }); } setNotificationsView(view: 'pendentes' | 'lidas') { this.notificationsView = view; } markAllNotificationsRead() { if (this.notificationsView === 'lidas') return; if (this.unreadCount === 0 || this.notificationsBulkReadLoading) return; this.notificationsBulkReadLoading = true; this.notificationsService.markAllAsRead().subscribe({ next: () => { // Evento do service já sincroniza o estado; aqui só finalizamos loading e acknowledge. const unreadIds = this.notifications.filter(n => !n.lida).map(n => n.id); if (unreadIds.length && isPlatformBrowser(this.platformId)) { const acknowledged = this.getAcknowledgedIds(); unreadIds.forEach(id => acknowledged.add(id)); localStorage.setItem('vigenciaAcknowledgedIds', JSON.stringify(Array.from(acknowledged))); } this.notificationsBulkReadLoading = false; }, error: () => { this.notificationsBulkReadLoading = false; } }); } markAllNotificationsUnread() { if (this.notificationsView !== 'lidas') return; if (this.notificationsVisibleCount === 0 || this.notificationsBulkUnreadLoading) return; this.notificationsBulkUnreadLoading = true; this.notificationsService.markAllAsUnread().subscribe({ next: () => { this.notificationsBulkUnreadLoading = false; }, error: () => { this.notificationsBulkUnreadLoading = false; } }); } onNotificationItemClick(notification: NotificationDto) { if (this.notificationsView === 'lidas') { this.markNotificationUnread(notification); return; } this.markNotificationRead(notification); } getVigenciaLabel(notification: NotificationDto): string { const tipo = this.getNotificationTipo(notification); if (tipo === 'Vencido') return 'Venceu em'; if (tipo === 'AVencer') return 'Vence em'; return 'Atualizado 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): string { if (notification.tipo === 'RenovacaoAutomatica') { return 'RenovacaoAutomatica'; } 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 isClientHeader(): boolean { return this.isLoggedHeader && !this.canViewAll; } get headerLogoSubtitle(): string { return this.isClientHeader ? 'Gestão Empresas' : 'Gestão'; } get headerLogoAriaLabel(): string { return `Line ${this.headerLogoSubtitle}`; } get clientTenantDisplayNameAbbrev(): string { return this.abbreviateClientTenantName(this.clientTenantDisplayName); } get notificationsVisible() { return this.notificationsView === 'lidas' ? this.notifications.filter(n => n.lida) : this.notifications.filter(n => !n.lida); } get notificationsVisibleCount() { return this.notificationsVisible.length; } get notificationsPreview() { return this.notificationsVisible.slice(0, this.notificationsPreviewLimit); } get hasNotificationsTruncated() { return this.notificationsVisibleCount > this.notificationsPreviewLimit; } trackByNotificationId(_: number, notification: NotificationDto) { return notification.id; } logout() { this.authService.logout(); this.optionsOpen = false; this.notificationsOpen = false; this.isSysAdmin = false; this.isGestor = false; this.isFinanceiro = false; this.canViewAll = false; this.canViewFinancialPages = false; this.router.navigate(['/']); } @HostListener('window:scroll', []) onWindowScroll() { if (!isPlatformBrowser(this.platformId)) return; this.isScrolled = window.scrollY > 10; this.scheduleHeaderOffsetSync(); } @HostListener('window:resize', []) onWindowResize() { if (!isPlatformBrowser(this.platformId)) return; this.scheduleHeaderOffsetSync(); } @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() { this.setupHeaderOffsetTracking(); 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 => !n.lida && this.getNotificationTipo(n) === 'AVencer' && this.getNotificationDaysToExpire(n) === 5 && !acknowledged.has(n.id) ); } ngOnDestroy(): void { this.headerResizeObserver?.disconnect(); this.headerResizeObserver = undefined; this.headerTransitionEndCleanup?.(); this.headerTransitionEndCleanup = null; if (this.manageUsersFeedbackTimer) { clearTimeout(this.manageUsersFeedbackTimer); this.manageUsersFeedbackTimer = null; } this.subs.unsubscribe(); } 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.isSysAdmin) { 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, permissao: this.isManageClientsMode ? 'cliente' : 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; const permissao = this.isManageClientsMode ? 'cliente' : (full.permissao ?? ''); this.editUserForm.reset({ nome: full.nome ?? '', email: full.email ?? '', senha: '', confirmarSenha: '', permissao, ativo: full.ativo ?? true, }); if (this.isManageClientsMode) { this.editUserForm.get('permissao')?.disable({ emitEvent: false }); } else { this.editUserForm.get('permissao')?.enable({ emitEvent: false }); } }, error: () => { this.editUserErrors = [{ message: this.isManageClientsMode ? 'Erro ao carregar credencial do cliente.' : 'Erro ao carregar usuário.' }]; }, }); } 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.isManageClientsMode ? 'cliente' : (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 (this.isManageClientsMode) { const targetPermissao = String(this.editUserTarget.permissao || '').trim().toLowerCase(); if (targetPermissao !== 'cliente') { payload.permissao = 'cliente'; } } else 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 = ''; const currentTarget = { ...this.editUserTarget }; this.usersService.update(this.editUserTarget.id, payload).subscribe({ next: () => { const merged = this.mergeUserUpdate(currentTarget, payload); this.editUserSubmitting = false; this.setEditFormDisabled(false); this.editUserSuccess = this.isManageClientsMode ? `Credencial de ${merged.nome} atualizada com sucesso.` : `Usuario ${merged.nome} atualizado com sucesso.`; this.editUserTarget = merged; this.editUserForm.patchValue({ nome: merged.nome ?? '', email: merged.email ?? '', permissao: this.isManageClientsMode ? 'cliente' : (merged.permissao ?? ''), ativo: merged.ativo ?? true, senha: '', confirmarSenha: '', }); this.upsertManageUser(merged); this.showManageUsersFeedback( this.isManageClientsMode ? `Credencial de ${merged.nome} atualizada com sucesso.` : `Usuario ${merged.nome} atualizado com sucesso.`, 'success' ); }, 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 || (this.isManageClientsMode ? 'Erro ao atualizar credencial do cliente.' : 'Erro ao atualizar usuario.'), })); } else { this.editUserErrors = [{ message: err?.error?.message || (this.isManageClientsMode ? 'Erro ao atualizar credencial do cliente.' : 'Erro ao atualizar usuario.') }]; } this.showManageUsersFeedback( this.editUserErrors[0]?.message || (this.isManageClientsMode ? 'Erro ao atualizar credencial do cliente.' : 'Erro ao atualizar usuario.'), 'error' ); }, }); } async confirmToggleUserStatus(user: any) { const nextActive = user.ativo === false; const actionLabel = nextActive ? 'reativar' : 'inativar'; const entity = this.isManageClientsMode ? 'Credencial do Cliente' : 'Usuário'; const entityLower = this.isManageClientsMode ? 'credencial do cliente' : 'usuário'; const confirmed = await confirmActionModal({ title: nextActive ? `Reativar ${entity}` : `Inativar ${entity}`, message: nextActive ? `Deseja reativar ${entityLower} ${user.nome}? O acesso ao sistema será liberado novamente.` : `Deseja inativar ${entityLower} ${user.nome}? O acesso ao sistema ficará bloqueado até reativação.`, confirmLabel: nextActive ? 'Reativar' : 'Inativar', tone: nextActive ? 'neutral' : 'warning', }); if (!confirmed) return; this.usersService.update(user.id, { ativo: nextActive }).subscribe({ next: () => { const updated = this.mergeUserUpdate(user, { ativo: nextActive }); this.upsertManageUser(updated); if (this.editUserTarget?.id === user.id) { this.editUserTarget = { ...this.editUserTarget, ativo: nextActive }; this.editUserForm.patchValue({ ativo: nextActive, senha: '', confirmarSenha: '' }); this.editUserErrors = []; this.editUserSuccess = this.isManageClientsMode ? `Credencial de ${user.nome} ${nextActive ? 'reativada' : 'inativada'} com sucesso.` : `Usuario ${user.nome} ${nextActive ? 'reativado' : 'inativado'} com sucesso.`; } }, error: (err: HttpErrorResponse) => { const message = err?.error?.message || `Erro ao ${actionLabel} ${this.isManageClientsMode ? 'credencial do cliente' : 'usuario'}.`; if (this.editUserTarget?.id === user.id) { this.editUserSuccess = ''; this.editUserErrors = [{ message }]; } }, }); } async confirmPermanentDeleteUser(user: any) { if (user?.ativo !== false) { const message = this.isManageClientsMode ? 'Inative a credencial antes de excluir permanentemente.' : 'Inative a conta antes de excluir permanentemente.'; if (this.editUserTarget?.id === user?.id) { this.editUserSuccess = ''; this.editUserErrors = [{ message }]; } else { await showDeletionWarning(message, 'Exclusão não permitida'); } return; } const confirmed = await confirmDeletionWithTyping( this.isManageClientsMode ? `a credencial do cliente ${user.nome}` : `o usuário ${user.nome}` ); if (!confirmed) return; this.usersService.delete(user.id).subscribe({ next: () => { this.removeManageUser(user.id); if (this.editUserTarget?.id === user.id) { this.cancelEditUser(); } }, error: (err: HttpErrorResponse) => { const apiErrors = err?.error?.errors; const message = Array.isArray(apiErrors) ? (apiErrors[0]?.message || (this.isManageClientsMode ? 'Erro ao excluir credencial do cliente.' : 'Erro ao excluir usuario.')) : (err?.error?.message || (this.isManageClientsMode ? 'Erro ao excluir credencial do cliente.' : 'Erro ao excluir usuario.')); if (this.editUserTarget?.id === user.id) { this.editUserSuccess = ''; this.editUserErrors = [{ message }]; return; } void showDeletionWarning(message, 'Falha na exclusão'); }, }); } 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() { if (this.manageUsersFeedbackTimer) { clearTimeout(this.manageUsersFeedbackTimer); this.manageUsersFeedbackTimer = null; } this.manageUsersErrors = []; this.manageUsersSuccess = ''; this.manageUsersLoading = false; this.manageUsers = []; this.manageSearch = ''; this.managePage = 1; this.manageTotal = 0; this.cancelEditUser(); } get isManageClientsMode(): boolean { return this.manageMode === 'clients'; } get manageModalTitle(): string { return this.isManageClientsMode ? 'Credenciais de Clientes' : 'Gestão de Usuários'; } get manageListTitle(): string { return this.isManageClientsMode ? 'Credenciais de Cliente' : 'Usuários'; } get manageSearchPlaceholder(): string { return this.isManageClientsMode ? 'Buscar por cliente, nome ou email...' : 'Buscar por nome ou email...'; } get editPermissionOptions() { return this.isManageClientsMode ? [{ value: 'cliente', label: 'Cliente' }] : this.permissionOptions; } 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 }); if (this.isManageClientsMode) { this.editUserForm.get('permissao')?.disable({ emitEvent: false }); } } } private upsertManageUser(user: any) { this.manageUsers = this.manageUsers.map((item) => item.id === user.id ? { ...item, ...user } : item ); } private removeManageUser(userId: string) { const prevLength = this.manageUsers.length; this.manageUsers = this.manageUsers.filter((item) => item.id !== userId); if (this.manageUsers.length !== prevLength && this.manageTotal > 0) { this.manageTotal = Math.max(0, this.manageTotal - 1); } if (!this.manageUsers.length && this.managePage > 1) { this.fetchManageUsers(this.managePage - 1); } } private mergeUserUpdate(current: any, payload: any) { return { ...current, nome: payload.nome ?? current.nome, email: payload.email ? String(payload.email).trim().toLowerCase() : current.email, permissao: payload.permissao ?? current.permissao, ativo: payload.ativo ?? current.ativo, }; } private showManageUsersFeedback(message: string, type: 'success' | 'error') { if (this.manageUsersFeedbackTimer) { clearTimeout(this.manageUsersFeedbackTimer); this.manageUsersFeedbackTimer = null; } if (type === 'success') { this.manageUsersErrors = []; this.manageUsersSuccess = message; } else { this.manageUsersSuccess = ''; this.manageUsersErrors = [{ message }]; } this.manageUsersFeedbackTimer = setTimeout(() => { this.manageUsersSuccess = ''; this.manageUsersErrors = []; this.manageUsersFeedbackTimer = null; }, 3000); } 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 }; } private setupHeaderOffsetTracking() { if (!isPlatformBrowser(this.platformId)) return; this.syncHeaderOffsetCssVar(); const headerEl = this.getHeaderElement(); if (!headerEl || typeof ResizeObserver === 'undefined') return; this.headerResizeObserver?.disconnect(); this.headerResizeObserver = new ResizeObserver(() => { this.syncHeaderOffsetCssVar(); }); this.headerResizeObserver.observe(headerEl); // Garante sincronismo do offset após a animação de "scrolled" do header. this.headerTransitionEndCleanup?.(); const onTransitionEnd = (ev: TransitionEvent) => { if (ev.target !== headerEl) return; if (ev.propertyName && ev.propertyName !== 'padding-top' && ev.propertyName !== 'padding-bottom') { return; } this.syncHeaderOffsetCssVar(); }; headerEl.addEventListener('transitionend', onTransitionEnd); this.headerTransitionEndCleanup = () => headerEl.removeEventListener('transitionend', onTransitionEnd); } private scheduleHeaderOffsetSync() { if (!isPlatformBrowser(this.platformId)) return; requestAnimationFrame(() => this.syncHeaderOffsetCssVar()); } private syncHeaderOffsetCssVar() { if (!isPlatformBrowser(this.platformId)) return; const headerEl = this.getHeaderElement(); if (!headerEl) return; const height = Math.ceil(headerEl.getBoundingClientRect().height || headerEl.offsetHeight || 0); if (height > 0) { document.documentElement.style.setProperty('--app-header-offset', `${height}px`); } } private getHeaderElement(): HTMLElement | null { return this.hostElement.nativeElement.querySelector('.app-header'); } private ensureClientTenantName() { const profile = this.authService.currentUserProfile; const tenantId = String(profile?.tenantId || '').trim(); if (!tenantId) { this.clientTenantDisplayName = ''; this.clientTenantNameTenantId = null; return; } if (this.clientTenantNameTenantId === tenantId && this.clientTenantDisplayName) { return; } this.clientTenantNameTenantId = tenantId; this.clientTenantDisplayName = ''; this.http.get(`${this.baseApi}/lines/clients`).subscribe({ next: (clients) => { const list = (clients || []) .map((x) => String(x ?? '').trim()) .filter((x) => !!x && x.localeCompare('RESERVA', 'pt-BR', { sensitivity: 'base' }) !== 0); this.clientTenantDisplayName = list[0] || this.resolveFallbackClientTenantName(); }, error: () => { this.clientTenantDisplayName = this.resolveFallbackClientTenantName(); }, }); } private resolveFallbackClientTenantName(): string { const profileName = String(this.authService.currentUserProfile?.nome || '').trim(); if (profileName) return profileName; return 'Cliente'; } private abbreviateClientTenantName(value: string): string { const name = (value || '').trim(); if (!name) return 'Cliente'; const maxLen = 30; if (name.length <= maxLen) return name; const parts = name.split(/\s+/).filter(Boolean); if (parts.length === 1) { return `${parts[0].slice(0, maxLen - 1)}…`; } const first = parts[0]; const last = parts[parts.length - 1]; let candidate = `${first} ${last}`; if (candidate.length <= maxLen) return candidate; const shortFirst = `${first.slice(0, Math.max(3, maxLen - (last.length + 3)))}.`; candidate = `${shortFirst} ${last}`; if (candidate.length <= maxLen) return candidate; return `${name.slice(0, maxLen - 1)}…`; } }