line-gestao-frontend/src/app/pages/dados-usuarios/dados-usuarios.ts

603 lines
17 KiB
TypeScript

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<UserDataRow>) => {
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; }
}