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 DashboardDto = { kpis: DashboardKpisDto; topClientes: TopClienteDto[]; serieMuregUltimos12Meses: SerieMesDto[]; serieTrocaUltimos12Meses: SerieMesDto[]; muregsRecentes: MuregRecenteDto[]; trocasRecentes: TrocaRecenteDto[]; // ✅ vigência serieVigenciaEncerramentosProx12Meses: SerieMesDto[]; vigenciaBuckets: VigenciaBucketsDto; }; @Component({ selector: 'app-dashboard', standalone: true, imports: [CommonModule], templateUrl: './dashboard.html', styleUrls: ['./dashboard.scss'], }) export class Dashboard implements OnInit, AfterViewInit, OnDestroy { @ViewChild('chartMureg12') chartMureg12?: ElementRef; @ViewChild('chartTroca12') chartTroca12?: ElementRef; @ViewChild('chartStatusPie') chartStatusPie?: ElementRef; @ViewChild('chartVigenciaMesAno') chartVigenciaMesAno?: ElementRef; @ViewChild('chartVigenciaSupervisao') chartVigenciaSupervisao?: ElementRef; 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; constructor( private http: HttpClient, @Inject(PLATFORM_ID) private platformId: object, private hostRef: ElementRef ) { 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 Dashboard. Verifique se a API está rodando e o endpoint /api/dashboard está acessível.'; } } private async fetchDashboardReal(): Promise { if (!isPlatformBrowser(this.platformId)) throw new Error('SSR não suportado para charts'); const url = `${this.baseApi}/dashboard`; return await firstValueFrom(this.http.get(url)); } private applyDto(dto: DashboardDto) { 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(); const palette = this.getPalette(); // ✅ Status das linhas (paleta do sistema) 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: [ palette.status.ativos, palette.status.perdaRoubo, palette.status.bloq120, palette.status.reservas, palette.status.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: palette.series.vigencia, 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', '0–30 dias', '31–60 dias', '61–90 dias', '> 90 dias'], datasets: [{ data: [ this.vigBuckets.vencidos, this.vigBuckets.aVencer0a30, this.vigBuckets.aVencer31a60, this.vigBuckets.aVencer61a90, this.vigBuckets.acima90, ], borderWidth: 1, backgroundColor: [ palette.vigencia.vencidos, palette.vigencia.d0a30, palette.vigencia.d31a60, palette.vigencia.d61a90, palette.vigencia.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: palette.series.mureg, 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: palette.series.troca, 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'); } private getPalette() { return { status: { ativos: this.readCssVar('--chart-blue', '#030FAA'), perdaRoubo: this.readCssVar('--chart-pink-dark', '#B832A8'), bloq120: this.readCssVar('--chart-violet', '#6A55FF'), reservas: this.readCssVar('--chart-pink-soft', '#F3B0E8'), outros: this.readCssVar('--chart-blue-soft', 'rgba(3, 15, 170, 0.2)'), }, vigencia: { vencidos: this.readCssVar('--chart-pink', '#E33DCF'), d0a30: this.readCssVar('--chart-violet', '#6A55FF'), d31a60: this.readCssVar('--chart-blue', '#030FAA'), d61a90: this.readCssVar('--chart-pink-dark', '#B832A8'), acima90: this.readCssVar('--chart-pink-soft', '#F3B0E8'), }, series: { vigencia: this.readCssVar('--chart-blue', '#030FAA'), mureg: this.readCssVar('--chart-pink', '#E33DCF'), troca: this.readCssVar('--chart-violet', '#6A55FF'), }, }; } private readCssVar(name: string, fallback: string) { if (!isPlatformBrowser(this.platformId)) return fallback; const styles = getComputedStyle(this.hostRef.nativeElement); const value = styles.getPropertyValue(name).trim(); return value || fallback; } }