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

486 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
Component,
AfterViewInit,
OnInit,
OnDestroy,
ViewChild,
ElementRef,
Inject,
} from '@angular/core';
import { CommonModule, isPlatformBrowser } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { PLATFORM_ID } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import Chart from 'chart.js/auto';
type KpiCard = {
title: string;
value: string;
icon: string;
hint?: string;
};
type SerieMesDto = {
mes: string;
total: number;
};
type TopClienteDto = {
cliente: string;
linhas: number;
};
type MuregRecenteDto = {
id: string;
item: number;
linhaAntiga?: string | null;
linhaNova?: string | null;
iccid?: string | null;
dataDaMureg?: string | null;
cliente?: string | null;
mobileLineId: string;
};
type TrocaRecenteDto = {
id: string;
item: number;
linhaAntiga?: string | null;
linhaNova?: string | null;
iccid?: string | null;
dataTroca?: string | null;
motivo?: string | null;
};
type VigenciaBucketsDto = {
vencidos: number;
aVencer0a30: number;
aVencer31a60: number;
aVencer61a90: number;
acima90: number;
};
type DashboardKpisDto = {
totalLinhas: number;
clientesUnicos: number;
ativos: number;
bloqueados: number;
reservas: number;
bloqueadosPerdaRoubo: number;
bloqueados120Dias: number;
bloqueadosOutros: number;
totalMuregs: number;
muregsUltimos30Dias: number;
totalTrocas: number;
trocasUltimos30Dias: number;
totalVigenciaLinhas: number;
vigenciaVencidos: number;
vigenciaAVencer30: number;
userDataRegistros: number;
userDataComCpf: number;
userDataComEmail: number;
};
type RelatoriosDashboardDto = {
kpis: DashboardKpisDto;
topClientes: TopClienteDto[];
serieMuregUltimos12Meses: SerieMesDto[];
serieTrocaUltimos12Meses: SerieMesDto[];
muregsRecentes: MuregRecenteDto[];
trocasRecentes: TrocaRecenteDto[];
// ✅ vigência
serieVigenciaEncerramentosProx12Meses: SerieMesDto[];
vigenciaBuckets: VigenciaBucketsDto;
};
@Component({
selector: 'app-relatorios',
standalone: true,
imports: [CommonModule],
templateUrl: './relatorios.html',
styleUrls: ['./relatorios.scss'],
})
export class Relatorios implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('chartMureg12') chartMureg12?: ElementRef<HTMLCanvasElement>;
@ViewChild('chartTroca12') chartTroca12?: ElementRef<HTMLCanvasElement>;
@ViewChild('chartStatusPie') chartStatusPie?: ElementRef<HTMLCanvasElement>;
@ViewChild('chartVigenciaMesAno') chartVigenciaMesAno?: ElementRef<HTMLCanvasElement>;
@ViewChild('chartVigenciaSupervisao') chartVigenciaSupervisao?: ElementRef<HTMLCanvasElement>;
loading = true;
errorMsg: string | null = null;
kpis: KpiCard[] = [];
muregLabels: string[] = [];
muregValues: number[] = [];
trocaLabels: string[] = [];
trocaValues: number[] = [];
vigenciaLabels: string[] = [];
vigenciaValues: number[] = [];
vigBuckets: VigenciaBucketsDto = {
vencidos: 0,
aVencer0a30: 0,
aVencer31a60: 0,
aVencer61a90: 0,
acima90: 0,
};
topClientes: TopClienteDto[] = [];
muregsRecentes: MuregRecenteDto[] = [];
trocasRecentes: TrocaRecenteDto[] = [];
statusResumo = {
total: 0,
ativos: 0,
perdaRoubo: 0,
bloq120: 0,
reservas: 0,
outras: 0,
};
private viewReady = false;
private dataReady = false;
private chartMureg?: Chart;
private chartTroca?: Chart;
private chartPie?: Chart;
private chartVigMesAno?: Chart;
private chartVigSuper?: Chart;
private readonly baseApi: string;
// ✅ Paletas "padrão de dashboard" (fácil de entender)
private readonly STATUS_COLORS = {
ativos: '#2E7D32', // verde
perdaRoubo: '#D32F2F', // vermelho
bloq120: '#F57C00', // laranja
reservas: '#1976D2', // azul
outros: '#607D8B', // cinza
};
private readonly VIG_COLORS = {
vencidos: '#D32F2F', // vermelho
d0a30: '#F57C00', // laranja
d31a60: '#FBC02D', // amarelo
d61a90: '#1976D2', // azul
acima90: '#2E7D32', // verde
};
constructor(
private http: HttpClient,
@Inject(PLATFORM_ID) private platformId: object
) {
const raw = (environment.apiUrl || 'https://localhost:7205').replace(/\/+$/, '');
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
}
ngOnInit(): void {
this.loadDashboard();
}
ngAfterViewInit(): void {
this.viewReady = true;
this.tryBuildCharts();
}
ngOnDestroy(): void {
this.destroyCharts();
}
private async loadDashboard() {
this.loading = true;
this.errorMsg = null;
this.dataReady = false;
try {
const dto = await this.fetchDashboardReal();
this.applyDto(dto);
this.dataReady = true;
this.loading = false;
this.tryBuildCharts();
} catch {
this.loading = false;
this.errorMsg =
'Falha ao carregar Relatórios. Verifique se a API está rodando e o endpoint /api/relatorios/dashboard está acessível.';
}
}
private async fetchDashboardReal(): Promise<RelatoriosDashboardDto> {
if (!isPlatformBrowser(this.platformId)) throw new Error('SSR não suportado para charts');
const url = `${this.baseApi}/relatorios/dashboard`;
return await firstValueFrom(this.http.get<RelatoriosDashboardDto>(url));
}
private applyDto(dto: RelatoriosDashboardDto) {
const k = dto.kpis;
this.kpis = [
{ title: 'Linhas', value: this.formatInt(k.totalLinhas), icon: 'bi bi-sim', hint: 'Total cadastradas' },
{ title: 'Clientes', value: this.formatInt(k.clientesUnicos), icon: 'bi bi-building', hint: 'Clientes únicos' },
{ title: 'Ativos', value: this.formatInt(k.ativos), icon: 'bi bi-check2-circle', hint: 'Linhas ativas' },
{ title: 'Bloqueados', value: this.formatInt(k.bloqueados), icon: 'bi bi-slash-circle', hint: 'Somatório de bloqueios' },
{ title: 'Reservas', value: this.formatInt(k.reservas), icon: 'bi bi-inboxes', hint: 'Linhas em reserva' },
{ title: 'MUREGs (30d)', value: this.formatInt(k.muregsUltimos30Dias), icon: 'bi bi-arrow-repeat', hint: 'Últimos 30 dias' },
{ title: 'Trocas (30d)', value: this.formatInt(k.trocasUltimos30Dias), icon: 'bi bi-shuffle', hint: 'Últimos 30 dias' },
{ title: 'Vencidos', value: this.formatInt(k.vigenciaVencidos), icon: 'bi bi-exclamation-triangle', hint: 'Vigência vencida' },
{ title: 'A vencer (30d)', value: this.formatInt(k.vigenciaAVencer30), icon: 'bi bi-calendar2-week', hint: 'Vigência a vencer' },
{ title: 'Registros', value: this.formatInt(k.userDataRegistros), icon: 'bi bi-person-vcard', hint: 'Cadastros de usuário' },
];
this.muregLabels = (dto.serieMuregUltimos12Meses || []).map(x => x.mes);
this.muregValues = (dto.serieMuregUltimos12Meses || []).map(x => x.total);
this.trocaLabels = (dto.serieTrocaUltimos12Meses || []).map(x => x.mes);
this.trocaValues = (dto.serieTrocaUltimos12Meses || []).map(x => x.total);
this.vigenciaLabels = (dto.serieVigenciaEncerramentosProx12Meses || []).map(x => x.mes);
this.vigenciaValues = (dto.serieVigenciaEncerramentosProx12Meses || []).map(x => x.total);
this.vigBuckets = dto.vigenciaBuckets || this.vigBuckets;
this.topClientes = dto.topClientes || [];
this.muregsRecentes = dto.muregsRecentes || [];
this.trocasRecentes = dto.trocasRecentes || [];
this.statusResumo = {
total: k.totalLinhas ?? 0,
ativos: k.ativos ?? 0,
perdaRoubo: k.bloqueadosPerdaRoubo ?? 0,
bloq120: k.bloqueados120Dias ?? 0,
reservas: k.reservas ?? 0,
outras: k.bloqueadosOutros ?? 0,
};
}
private tryBuildCharts() {
if (!isPlatformBrowser(this.platformId)) return;
if (!this.viewReady || !this.dataReady) return;
requestAnimationFrame(() => {
const canvases = [
this.chartStatusPie?.nativeElement,
this.chartVigenciaMesAno?.nativeElement,
this.chartVigenciaSupervisao?.nativeElement,
this.chartMureg12?.nativeElement,
this.chartTroca12?.nativeElement,
].filter(Boolean) as HTMLCanvasElement[];
if (canvases.length === 0) return;
// evita render quando canvas ainda não mediu (bug comum que "some")
if (canvases.some(c => c.clientWidth === 0 || c.clientHeight === 0)) {
requestAnimationFrame(() => this.tryBuildCharts());
return;
}
this.buildCharts();
});
}
private buildCharts() {
if (!isPlatformBrowser(this.platformId)) return;
this.destroyCharts();
// ✅ Status das linhas (paleta padrão)
const cP = this.chartStatusPie?.nativeElement;
if (cP) {
this.chartPie = new Chart(cP, {
type: 'doughnut',
data: {
labels: [
'Ativos',
'Bloqueadas (perda/roubo)',
'Bloqueadas (120 dias)',
'Reservas',
'Bloqueadas (outros)'
],
datasets: [{
data: [
this.statusResumo.ativos,
this.statusResumo.perdaRoubo,
this.statusResumo.bloq120,
this.statusResumo.reservas,
this.statusResumo.outras,
],
borderWidth: 1,
backgroundColor: [
this.STATUS_COLORS.ativos,
this.STATUS_COLORS.perdaRoubo,
this.STATUS_COLORS.bloq120,
this.STATUS_COLORS.reservas,
this.STATUS_COLORS.outros,
],
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '62%',
plugins: {
legend: { position: 'bottom' },
tooltip: {
callbacks: { label: (ctx) => ` ${ctx.label}: ${this.formatInt(Number(ctx.raw || 0))}` },
},
},
},
});
}
// ✅ Contratos a encerrar (próximos 12 meses) - barra azul padrão
const cV1 = this.chartVigenciaMesAno?.nativeElement;
if (cV1) {
this.chartVigMesAno = new Chart(cV1, {
type: 'bar',
data: {
labels: this.vigenciaLabels,
datasets: [{
label: 'Encerramentos',
data: this.vigenciaValues,
borderWidth: 0,
backgroundColor: '#1976D2', // azul padrão
borderRadius: 10,
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, border: { display: false } },
x: { border: { display: false } },
},
},
});
}
// ✅ Vigência (supervisão) - paleta por urgência
const cV2 = this.chartVigenciaSupervisao?.nativeElement;
if (cV2) {
this.chartVigSuper = new Chart(cV2, {
type: 'doughnut',
data: {
labels: ['Vencidos', '030 dias', '3160 dias', '6190 dias', '> 90 dias'],
datasets: [{
data: [
this.vigBuckets.vencidos,
this.vigBuckets.aVencer0a30,
this.vigBuckets.aVencer31a60,
this.vigBuckets.aVencer61a90,
this.vigBuckets.acima90,
],
borderWidth: 1,
backgroundColor: [
this.VIG_COLORS.vencidos,
this.VIG_COLORS.d0a30,
this.VIG_COLORS.d31a60,
this.VIG_COLORS.d61a90,
this.VIG_COLORS.acima90,
],
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '62%',
plugins: {
legend: { position: 'bottom' },
tooltip: {
callbacks: { label: (ctx) => ` ${ctx.label}: ${this.formatInt(Number(ctx.raw || 0))}` },
},
},
},
});
}
// ✅ MUREG 12 meses
const cM = this.chartMureg12?.nativeElement;
if (cM) {
this.chartMureg = new Chart(cM, {
type: 'bar',
data: {
labels: this.muregLabels,
datasets: [{
label: 'MUREG',
data: this.muregValues,
borderWidth: 0,
backgroundColor: '#6A1B9A', // roxo (bem comum em dashboards)
borderRadius: 10,
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, border: { display: false } },
x: { border: { display: false } },
},
},
});
}
// ✅ Troca 12 meses
const cT = this.chartTroca12?.nativeElement;
if (cT) {
this.chartTroca = new Chart(cT, {
type: 'bar',
data: {
labels: this.trocaLabels,
datasets: [{
label: 'Troca',
data: this.trocaValues,
borderWidth: 0,
backgroundColor: '#00897B', // teal (bem comum)
borderRadius: 10,
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, border: { display: false } },
x: { border: { display: false } },
},
},
});
}
}
private destroyCharts() {
try { this.chartMureg?.destroy(); } catch {}
try { this.chartTroca?.destroy(); } catch {}
try { this.chartPie?.destroy(); } catch {}
try { this.chartVigMesAno?.destroy(); } catch {}
try { this.chartVigSuper?.destroy(); } catch {}
this.chartMureg = undefined;
this.chartTroca = undefined;
this.chartPie = undefined;
this.chartVigMesAno = undefined;
this.chartVigSuper = undefined;
}
private formatInt(v: number) {
return (v || 0).toLocaleString('pt-BR');
}
}