line-gestao-frontend/src/app/pages/vigencia/vigencia.ts

573 lines
16 KiB
TypeScript

import { Component, OnDestroy, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpErrorResponse } from '@angular/common/http';
import { VigenciaService, VigenciaRow, VigenciaClientGroup, PagedResult, UpdateVigenciaRequest } from '../../services/vigencia.service';
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
import { AuthService } from '../../services/auth.service';
import { LinesService, MobileLineDetail } from '../../services/lines.service';
import { PlanAutoFillService } from '../../services/plan-autofill.service';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
type SortDir = 'asc' | 'desc';
type ToastType = 'success' | 'danger';
type ViewMode = 'lines' | 'groups';
interface LineOptionDto {
id: string;
item: number;
linha: string | null;
usuario: string | null;
label?: string;
}
@Component({
selector: 'app-vigencia',
standalone: true,
imports: [CommonModule, FormsModule, CustomSelectComponent],
templateUrl: './vigencia.html',
styleUrls: ['./vigencia.scss'],
})
export class VigenciaComponent implements OnInit, OnDestroy {
loading = false;
errorMsg = '';
// Filtros
search = '';
client = '';
clients: string[] = [];
// Paginação
page = 1;
pageSize = 10;
pageSizeOptions = [10, 20, 50, 100];
total = 0;
// Ordenação
sortBy = 'cliente';
sortDir: SortDir = 'asc';
// PADRÃO: GROUPS
viewMode: ViewMode = 'groups';
// Dados
groups: VigenciaClientGroup[] = [];
rows: VigenciaRow[] = [];
// === KPIs GERAIS (Vindos do Backend) ===
kpiTotalClientes = 0;
kpiTotalLinhas = 0;
kpiTotalVencidos = 0;
// === ACORDEÃO ===
expandedGroup: string | null = null;
expandedLoading = false;
groupRows: VigenciaRow[] = [];
// UI
detailsOpen = false;
selectedRow: VigenciaRow | null = null;
editOpen = false;
editSaving = false;
editModel: VigenciaRow | null = null;
editEfetivacao = '';
editTermino = '';
editingId: string | null = null;
deleteOpen = false;
deleteTarget: VigenciaRow | null = null;
createOpen = false;
createSaving = false;
createModel: any = {
selectedClient: '',
mobileLineId: '',
item: '',
conta: '',
linha: '',
cliente: '',
usuario: '',
planoContrato: '',
total: null
};
createEfetivacao = '';
createTermino = '';
lineOptionsCreate: LineOptionDto[] = [];
createClientsLoading = false;
createLinesLoading = false;
clientsFromGeral: string[] = [];
planOptions: string[] = [];
isAdmin = false;
toastOpen = false;
toastMessage = '';
toastType: ToastType = 'success';
private toastTimer: any = null;
private searchTimer: any = null;
constructor(
private vigenciaService: VigenciaService,
private authService: AuthService,
private linesService: LinesService,
private planAutoFill: PlanAutoFillService
) {}
ngOnInit(): void {
this.isAdmin = this.authService.hasRole('admin');
this.loadClients();
this.loadPlanRules();
this.fetch(1);
}
ngOnDestroy(): void {
if (this.searchTimer) clearTimeout(this.searchTimer);
if (this.toastTimer) clearTimeout(this.toastTimer);
}
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);
}
loadClients(): void {
this.vigenciaService.getClients().subscribe({
next: (list) => (this.clients = list ?? []),
error: () => (this.clients = []),
});
}
private async loadPlanRules() {
try {
await this.planAutoFill.load();
this.planOptions = this.planAutoFill.getPlanOptions();
} catch {
this.planOptions = [];
}
}
get totalPages(): number {
return Math.max(1, Math.ceil((this.total || 0) / (this.pageSize || 10)));
}
fetch(goToPage?: number): void {
if (goToPage) this.page = goToPage;
this.loading = true;
this.errorMsg = '';
if(goToPage && goToPage !== this.page) this.expandedGroup = null;
if (this.viewMode === 'groups') {
this.fetchGroups();
} else {
this.fetchLines();
}
}
private fetchGroups() {
this.vigenciaService.getGroups({
search: this.search?.trim(),
page: this.page,
pageSize: this.pageSize,
sortBy: this.sortBy,
sortDir: this.sortDir,
}).subscribe({
next: (res) => {
// ✅ Preenche Lista
this.groups = res.data.items || [];
this.total = res.data.total || 0;
// ✅ Preenche KPIs Globais
this.kpiTotalClientes = res.kpis.totalClientes;
this.kpiTotalLinhas = res.kpis.totalLinhas;
this.kpiTotalVencidos = res.kpis.totalVencidos;
this.loading = false;
},
error: (err) => this.handleError(err, 'Erro ao carregar clientes.'),
});
}
private fetchLines() {
this.vigenciaService.getVigencia({
search: this.search?.trim(),
client: this.client?.trim(),
page: this.page,
pageSize: this.pageSize,
sortBy: this.sortBy,
sortDir: this.sortDir,
}).subscribe({
next: (res) => {
this.rows = res.items || [];
this.total = res.total || 0;
this.loading = false;
},
error: (err) => this.handleError(err, 'Erro ao carregar linhas.'),
});
}
toggleGroup(g: VigenciaClientGroup): void {
if (this.expandedGroup === g.cliente) {
this.expandedGroup = null;
this.groupRows = [];
return;
}
this.expandedGroup = g.cliente;
this.expandedLoading = true;
this.groupRows = [];
this.vigenciaService.getVigencia({
client: g.cliente,
page: 1,
pageSize: 200,
sortBy: 'item',
sortDir: 'asc'
}).subscribe({
next: (res) => {
this.groupRows = res.items || [];
this.expandedLoading = false;
},
error: () => {
this.showToast('Erro ao carregar detalhes do cliente.', 'danger');
this.expandedLoading = false;
}
});
}
public isVencido(dateValue: any): boolean {
if(!dateValue) return false;
const d = this.parseAnyDate(dateValue);
if(!d) return false;
return this.startOfDay(d) < this.startOfDay(new Date());
}
public isAtivo(dateValue: any): boolean {
if(!dateValue) return true;
const d = this.parseAnyDate(dateValue);
if(!d) return true;
return this.startOfDay(d) >= this.startOfDay(new Date());
}
public parseAnyDate(value: any): Date | null {
if (!value) return null;
const d = new Date(value);
return isNaN(d.getTime()) ? null : d;
}
public startOfDay(d: Date): Date {
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
}
onSearchChange() {
if (this.searchTimer) clearTimeout(this.searchTimer);
this.searchTimer = setTimeout(() => this.fetch(1), 300);
}
clearFilters() {
this.search = '';
if (this.searchTimer) clearTimeout(this.searchTimer);
this.fetch(1);
}
openDetails(r: VigenciaRow) { this.selectedRow = r; this.detailsOpen = true; }
closeDetails() { this.detailsOpen = false; }
openEdit(r: VigenciaRow) {
if (!this.isAdmin) return;
this.editingId = r.id;
this.editModel = { ...r };
this.editEfetivacao = this.toDateInput(r.dtEfetivacaoServico);
this.editTermino = this.toDateInput(r.dtTerminoFidelizacao);
this.editOpen = true;
}
closeEdit() {
this.editOpen = false;
this.editSaving = false;
this.editModel = null;
this.editEfetivacao = '';
this.editTermino = '';
this.editingId = null;
}
saveEdit() {
if (!this.editModel || !this.editingId) return;
this.editSaving = true;
const payload: UpdateVigenciaRequest = {
item: this.toNullableNumber(this.editModel.item),
conta: this.editModel.conta,
linha: this.editModel.linha,
cliente: this.editModel.cliente,
usuario: this.editModel.usuario,
planoContrato: this.editModel.planoContrato,
dtEfetivacaoServico: this.dateInputToIso(this.editEfetivacao),
dtTerminoFidelizacao: this.dateInputToIso(this.editTermino),
total: this.toNullableNumber(this.editModel.total)
};
this.vigenciaService.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.', '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: '',
conta: '',
linha: '',
cliente: '',
usuario: '',
planoContrato: '',
total: null
};
this.createEfetivacao = '';
this.createTermino = '';
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.conta = '';
this.createModel.usuario = '';
this.createModel.planoContrato = '';
this.createModel.total = null;
this.createModel.cliente = c;
this.lineOptionsCreate = [];
if (c) this.loadLinesForClient(c);
}
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.conta = d.conta ?? '';
this.createModel.cliente = d.cliente ?? this.createModel.cliente ?? '';
this.createModel.usuario = d.usuario ?? '';
this.createModel.planoContrato = d.planoContrato ?? '';
this.createEfetivacao = this.toDateInput(d.dtEfetivacaoServico ?? null);
this.createTermino = this.toDateInput(d.dtTerminoFidelizacao ?? null);
this.ensurePlanOption(this.createModel.planoContrato);
if (!String(this.createModel.item ?? '').trim() && d.item) {
this.createModel.item = String(d.item);
}
this.onCreatePlanChange();
}
onCreatePlanChange() {
this.ensurePlanOption(this.createModel?.planoContrato);
this.applyPlanSuggestion(this.createModel);
}
onEditPlanChange() {
if (!this.editModel) return;
this.ensurePlanOption(this.editModel?.planoContrato);
this.applyPlanSuggestion(this.editModel);
}
private applyPlanSuggestion(model: any) {
const plan = (model?.planoContrato ?? '').toString().trim();
if (!plan) return;
const suggestion = this.planAutoFill.suggest(plan);
if (!suggestion) return;
if (suggestion.valorPlano != null) {
model.total = suggestion.valorPlano;
}
}
private ensurePlanOption(plan: any) {
const p = (plan ?? '').toString().trim();
if (!p) return;
if (!this.planOptions.includes(p)) {
this.planOptions = [p, ...this.planOptions];
}
}
saveCreate() {
if (!this.createModel) return;
this.applyPlanSuggestion(this.createModel);
const payload = {
item: this.toNullableNumber(this.createModel.item),
conta: this.createModel.conta,
linha: this.createModel.linha,
cliente: this.createModel.cliente,
usuario: this.createModel.usuario,
planoContrato: this.createModel.planoContrato,
dtEfetivacaoServico: this.dateInputToIso(this.createEfetivacao),
dtTerminoFidelizacao: this.dateInputToIso(this.createTermino),
total: this.toNullableNumber(this.createModel.total)
};
this.createSaving = true;
this.vigenciaService.create(payload).subscribe({
next: () => {
this.createSaving = false;
this.closeCreate();
this.fetch();
this.showToast('Vigência criada com sucesso!', 'success');
},
error: () => {
this.createSaving = false;
this.showToast('Erro ao criar vigência.', 'danger');
}
});
}
openDelete(r: VigenciaRow) {
if (!this.isAdmin) return;
this.deleteTarget = r;
this.deleteOpen = true;
}
cancelDelete() {
this.deleteOpen = false;
this.deleteTarget = null;
}
async confirmDelete() {
if (!this.deleteTarget) return;
if (!(await confirmDeletionWithTyping('este registro de vigência'))) return;
const id = this.deleteTarget.id;
this.vigenciaService.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');
}
});
}
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;
}
handleError(err: HttpErrorResponse, msg: string) {
this.loading = false;
this.expandedLoading = false;
this.errorMsg = (err.error as any)?.message || msg;
this.showToast(msg, 'danger');
}
showToast(msg: string, type: ToastType) {
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; }
}