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

1251 lines
39 KiB
TypeScript

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<typeof setTimeout> | 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<HTMLElement>,
@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<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.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<string[]>(`${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)}`;
}
}