import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { DadosUsuariosService, UserDataClientGroup, UserDataRow, UserDataGroupResponse, PagedResult, UpdateUserDataRequest, CreateUserDataRequest } from '../../services/dados-usuarios.service'; import { AuthService } from '../../services/auth.service'; import { LinesService, MobileLineDetail } from '../../services/lines.service'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; type ViewMode = 'lines' | 'groups'; interface LineOptionDto { id: string; item: number; linha: string | null; usuario: string | null; label?: string; } interface SimpleOption { label: string; value: string; } @Component({ selector: 'app-dados-usuarios', standalone: true, imports: [CommonModule, FormsModule, CustomSelectComponent], templateUrl: './dados-usuarios.html', styleUrls: ['./dados-usuarios.scss'] }) export class DadosUsuarios implements OnInit { @ViewChild('successToast', { static: false }) successToast!: ElementRef; loading = false; errorMsg = ''; // Filtros search = ''; tipoFilter: 'PF' | 'PJ' = 'PF'; // Paginação page = 1; pageSize = 10; pageSizeOptions = [10, 20, 50, 100]; total = 0; // Ordenação sortBy = 'cliente'; sortDir: 'asc' | 'desc' = 'asc'; // PADRÃO: GROUPS (Acordeão) viewMode: ViewMode = 'groups'; // Dados groups: UserDataClientGroup[] = []; rows: UserDataRow[] = []; // KPIs kpiTotalRegistros = 0; kpiClientesUnicos = 0; kpiComCpf = 0; kpiComCnpj = 0; kpiComEmail = 0; // ACORDEÃO expandedGroup: string | null = null; expandedLoading = false; groupRows: UserDataRow[] = []; // Modal / Toast detailsOpen = false; selectedRow: UserDataRow | null = null; editOpen = false; editSaving = false; editModel: UserDataRow | null = null; editDateNascimento = ''; editingId: string | null = null; deleteOpen = false; deleteTarget: UserDataRow | null = null; createOpen = false; createSaving = false; createModel: any = null; createDateNascimento = ''; clientsFromGeral: string[] = []; lineOptionsCreate: LineOptionDto[] = []; readonly tipoPessoaOptions: SimpleOption[] = [ { label: 'Pessoa Física', value: 'PF' }, { label: 'Pessoa Jurídica', value: 'PJ' }, ]; createClientsLoading = false; createLinesLoading = false; isAdmin = false; toastOpen = false; toastMessage = ''; toastType: 'success' | 'danger' = 'success'; private toastTimer: any = null; private searchTimer: any = null; constructor( private service: DadosUsuariosService, private authService: AuthService, private linesService: LinesService ) {} ngOnInit(): void { this.isAdmin = this.authService.hasRole('sysadmin'); this.fetch(1); } // Alternar Visualização setView(mode: ViewMode): void { if (this.viewMode === mode) return; this.viewMode = mode; this.page = 1; this.expandedGroup = null; this.groupRows = []; this.sortBy = mode === 'groups' ? 'cliente' : 'item'; this.fetch(1); } get totalPages(): number { return Math.max(1, Math.ceil((this.total || 0) / (this.pageSize || 10))); } get pageStart(): number { return (this.page - 1) * this.pageSize + 1; } get pageEnd(): number { const end = this.page * this.pageSize; return end > this.total ? this.total : end; } get pageNumbers(): number[] { const total = this.totalPages; const current = this.page; 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; } fetch(goToPage?: number): void { if (goToPage) this.page = goToPage; this.loading = true; if(goToPage && goToPage !== this.page) this.expandedGroup = null; if (this.viewMode === 'groups') { this.fetchGroups(); } else { this.fetchLines(); // Fallback se quiser usar } } refresh() { this.fetch(1); } private fetchGroups() { this.service.getGroups({ search: this.search?.trim(), tipo: this.tipoFilter, page: this.page, pageSize: this.pageSize, sortBy: this.sortBy, sortDir: this.sortDir, }).subscribe({ next: (res: UserDataGroupResponse) => { this.groups = res.data.items || []; this.total = res.data.total || 0; this.kpiTotalRegistros = res.kpis.totalRegistros; this.kpiClientesUnicos = res.kpis.clientesUnicos; this.kpiComCpf = res.kpis.comCpf; this.kpiComCnpj = res.kpis.comCnpj; this.kpiComEmail = res.kpis.comEmail; this.loading = false; }, error: (err: HttpErrorResponse) => { this.loading = false; this.showToast('Erro ao carregar dados.', 'danger'); } }); } private fetchLines() { // Implementação opcional para modo lista plana } toggleGroup(g: UserDataClientGroup): void { if (this.expandedGroup === g.cliente) { this.expandedGroup = null; this.groupRows = []; return; } this.expandedGroup = g.cliente; this.expandedLoading = true; this.groupRows = []; this.service.getRows({ client: g.cliente, tipo: this.tipoFilter, page: 1, pageSize: 200, sortBy: 'item', sortDir: 'asc' }).subscribe({ next: (res: PagedResult) => { this.groupRows = res.items || []; this.expandedLoading = false; }, error: (err: HttpErrorResponse) => { this.showToast('Erro ao carregar usuários do cliente.', 'danger'); this.expandedLoading = false; } }); } onSearch() { if (this.searchTimer) clearTimeout(this.searchTimer); this.searchTimer = setTimeout(() => { this.page = 1; this.expandedGroup = null; this.fetch(); }, 400); } setTipoFilter(tipo: 'PF' | 'PJ') { if (this.tipoFilter === tipo) return; this.tipoFilter = tipo; this.page = 1; this.expandedGroup = null; this.groupRows = []; this.fetch(); } clearFilters() { this.search = ''; this.fetch(1); } onPageSizeChange() { this.page = 1; this.fetch(); } goToPage(p: number) { this.page = p; this.fetch(); } openDetails(row: UserDataRow) { this.service.getById(row.id).subscribe({ next: (fullData: UserDataRow) => { const tipo = this.normalizeTipo(fullData); this.selectedRow = { ...fullData, tipoPessoa: tipo, nome: fullData.nome || (tipo === 'PF' ? fullData.cliente : ''), razaoSocial: fullData.razaoSocial || (tipo === 'PJ' ? fullData.cliente : '') }; this.detailsOpen = true; }, error: (err: HttpErrorResponse) => this.showToast('Erro ao abrir detalhes', 'danger') }); } closeDetails() { this.detailsOpen = false; } openEdit(row: UserDataRow) { if (!this.isAdmin) return; this.service.getById(row.id).subscribe({ next: (fullData: UserDataRow) => { this.editingId = fullData.id; const tipo = this.normalizeTipo(fullData); this.editModel = { ...fullData, tipoPessoa: tipo, nome: fullData.nome || (tipo === 'PF' ? fullData.cliente : ''), razaoSocial: fullData.razaoSocial || (tipo === 'PJ' ? fullData.cliente : '') }; this.editDateNascimento = this.toDateInput(fullData.dataNascimento); this.editOpen = true; }, error: () => this.showToast('Erro ao abrir edição', 'danger') }); } closeEdit() { this.editOpen = false; this.editSaving = false; this.editModel = null; this.editDateNascimento = ''; this.editingId = null; } onEditTipoChange() { if (!this.editModel) return; const tipo = (this.editModel.tipoPessoa ?? 'PF').toString().toUpperCase(); this.editModel.tipoPessoa = tipo; if (tipo === 'PJ') { this.editModel.cpf = ''; if (!this.editModel.razaoSocial) this.editModel.razaoSocial = this.editModel.cliente; } else { this.editModel.cnpj = ''; if (!this.editModel.nome) this.editModel.nome = this.editModel.cliente; } } saveEdit() { if (!this.editModel || !this.editingId) return; this.editSaving = true; const tipo = (this.editModel.tipoPessoa ?? this.tipoFilter).toString().toUpperCase(); const cliente = tipo === 'PJ' ? (this.editModel.razaoSocial || this.editModel.cliente) : (this.editModel.nome || this.editModel.cliente); const payload: UpdateUserDataRequest = { item: this.toNullableNumber(this.editModel.item), linha: this.editModel.linha, cliente, tipoPessoa: tipo, nome: this.editModel.nome, razaoSocial: this.editModel.razaoSocial, cnpj: this.editModel.cnpj, cpf: this.editModel.cpf, rg: this.editModel.rg, email: this.editModel.email, endereco: this.editModel.endereco, celular: this.editModel.celular, telefoneFixo: this.editModel.telefoneFixo, dataNascimento: this.dateInputToIso(this.editDateNascimento) }; this.service.update(this.editingId, payload).subscribe({ next: () => { this.editSaving = false; this.closeEdit(); this.fetch(); this.showToast('Registro atualizado!', 'success'); }, error: () => { this.editSaving = false; this.showToast('Erro ao salvar alterações.', 'danger'); } }); } // ========================== // CREATE // ========================== openCreate() { if (!this.isAdmin) return; this.resetCreateModel(); this.createOpen = true; this.preloadGeralClients(); } closeCreate() { this.createOpen = false; this.createSaving = false; this.createModel = null; } private resetCreateModel() { this.createModel = { selectedClient: '', mobileLineId: '', item: '', linha: '', cliente: '', tipoPessoa: this.tipoFilter, nome: '', razaoSocial: '', cnpj: '', cpf: '', rg: '', email: '', endereco: '', celular: '', telefoneFixo: '' }; this.createDateNascimento = ''; this.lineOptionsCreate = []; this.createLinesLoading = false; this.createClientsLoading = false; } private preloadGeralClients() { this.createClientsLoading = true; this.linesService.getClients().subscribe({ next: (list) => { this.clientsFromGeral = list ?? []; this.createClientsLoading = false; }, error: () => { this.clientsFromGeral = []; this.createClientsLoading = false; } }); } onCreateClientChange() { const c = (this.createModel?.selectedClient ?? '').trim(); this.createModel.mobileLineId = ''; this.createModel.linha = ''; this.createModel.cliente = c; this.lineOptionsCreate = []; if (c) this.loadLinesForClient(c); } onCreateTipoChange() { const tipo = (this.createModel?.tipoPessoa ?? 'PF').toString().toUpperCase(); this.createModel.tipoPessoa = tipo; if (tipo === 'PJ') { this.createModel.cpf = ''; if (!this.createModel.razaoSocial) this.createModel.razaoSocial = this.createModel.cliente; } else { this.createModel.cnpj = ''; if (!this.createModel.nome) this.createModel.nome = this.createModel.cliente; } } private loadLinesForClient(cliente: string) { const c = (cliente ?? '').trim(); if (!c) return; this.createLinesLoading = true; this.linesService.getLinesByClient(c).subscribe({ next: (items: any[]) => { const mapped: LineOptionDto[] = (items ?? []) .filter(x => !!String(x?.id ?? '').trim()) .map(x => ({ id: String(x.id), item: Number(x.item ?? 0), linha: x.linha ?? null, usuario: x.usuario ?? null, label: `${x.item ?? ''} • ${x.linha ?? '-'} • ${x.usuario ?? 'SEM USUÁRIO'}` })) .filter(x => !!String(x.linha ?? '').trim()); this.lineOptionsCreate = mapped; this.createLinesLoading = false; }, error: () => { this.lineOptionsCreate = []; this.createLinesLoading = false; this.showToast('Erro ao carregar linhas da GERAL.', 'danger'); } }); } onCreateLineChange() { const id = String(this.createModel?.mobileLineId ?? '').trim(); if (!id) return; this.linesService.getById(id).subscribe({ next: (d: MobileLineDetail) => this.applyLineDetailToCreate(d), error: () => this.showToast('Erro ao carregar dados da linha.', 'danger') }); } private applyLineDetailToCreate(d: MobileLineDetail) { this.createModel.linha = d.linha ?? ''; this.createModel.cliente = d.cliente ?? this.createModel.cliente ?? ''; if (!String(this.createModel.item ?? '').trim() && d.item) { this.createModel.item = String(d.item); } if ((this.createModel.tipoPessoa ?? '').toUpperCase() === 'PJ') { if (!this.createModel.razaoSocial) this.createModel.razaoSocial = this.createModel.cliente; } else { if (!this.createModel.nome) this.createModel.nome = this.createModel.cliente; } } saveCreate() { if (!this.createModel) return; this.createSaving = true; const tipo = (this.createModel.tipoPessoa ?? this.tipoFilter).toString().toUpperCase(); const cliente = tipo === 'PJ' ? (this.createModel.razaoSocial || this.createModel.cliente) : (this.createModel.nome || this.createModel.cliente); const payload: CreateUserDataRequest = { item: this.toNullableNumber(this.createModel.item), linha: this.createModel.linha, cliente, tipoPessoa: tipo, nome: this.createModel.nome, razaoSocial: this.createModel.razaoSocial, cnpj: this.createModel.cnpj, cpf: this.createModel.cpf, rg: this.createModel.rg, email: this.createModel.email, endereco: this.createModel.endereco, celular: this.createModel.celular, telefoneFixo: this.createModel.telefoneFixo, dataNascimento: this.dateInputToIso(this.createDateNascimento) }; this.service.create(payload).subscribe({ next: () => { this.createSaving = false; this.closeCreate(); this.fetch(); this.showToast('Usuário criado com sucesso!', 'success'); }, error: () => { this.createSaving = false; this.showToast('Erro ao criar usuário.', 'danger'); } }); } openDelete(row: UserDataRow) { if (!this.isAdmin) return; this.deleteTarget = row; this.deleteOpen = true; } cancelDelete() { this.deleteOpen = false; this.deleteTarget = null; } async confirmDelete() { if (!this.deleteTarget) return; if (!(await confirmDeletionWithTyping('este registro de dados do usuário'))) return; const id = this.deleteTarget.id; this.service.remove(id).subscribe({ next: () => { this.deleteOpen = false; this.deleteTarget = null; this.fetch(); this.showToast('Registro removido.', 'success'); }, error: () => { this.deleteOpen = false; this.deleteTarget = null; this.showToast('Erro ao remover.', 'danger'); } }); } trackById(_: number, row: UserDataRow) { return row.id; } trackByCliente(_: number, g: UserDataClientGroup) { return g.cliente; } private toDateInput(value: string | null): string { if (!value) return ''; const d = new Date(value); if (isNaN(d.getTime())) return ''; return d.toISOString().slice(0, 10); } private dateInputToIso(value: string): string | null { if (!value) return null; const d = new Date(`${value}T00:00:00`); if (isNaN(d.getTime())) return null; return d.toISOString(); } private toNullableNumber(value: any): number | null { if (value === undefined || value === null || value === '') return null; const n = Number(value); return Number.isNaN(n) ? null : n; } private normalizeTipo(row: UserDataRow | null | undefined): 'PF' | 'PJ' { const t = (row?.tipoPessoa ?? '').toString().trim().toUpperCase(); if (t === 'PJ') return 'PJ'; if (t === 'PF') return 'PF'; if (row?.cnpj) return 'PJ'; return 'PF'; } showToast(msg: string, type: 'success' | 'danger') { this.toastMessage = msg; this.toastType = type; this.toastOpen = true; if(this.toastTimer) clearTimeout(this.toastTimer); this.toastTimer = setTimeout(() => this.toastOpen = false, 3000); } hideToast() { this.toastOpen = false; } }