Compare commits
No commits in common. "e233ad3b7ded084918ac3a5261fcadd1ab78b67d" and "d87825b3707b38662405ad22788dea69b4a3e172" have entirely different histories.
e233ad3b7d
...
d87825b370
|
|
@ -11,6 +11,7 @@ import { authGuard } from './guards/auth.guard';
|
||||||
import { DadosUsuarios } from './pages/dados-usuarios/dados-usuarios';
|
import { DadosUsuarios } from './pages/dados-usuarios/dados-usuarios';
|
||||||
import { VigenciaComponent } from './pages/vigencia/vigencia';
|
import { VigenciaComponent } from './pages/vigencia/vigencia';
|
||||||
import { TrocaNumero } from './pages/troca-numero/troca-numero';
|
import { TrocaNumero } from './pages/troca-numero/troca-numero';
|
||||||
|
import { Parcelamento } from './pages/parcelamento/parcelamento';
|
||||||
|
|
||||||
// ✅ NOVO: TROCA DE NÚMERO
|
// ✅ NOVO: TROCA DE NÚMERO
|
||||||
|
|
||||||
|
|
@ -29,5 +30,7 @@ export const routes: Routes = [
|
||||||
// ✅ NOVO: rota da página Troca de Número
|
// ✅ NOVO: rota da página Troca de Número
|
||||||
{ path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard] },
|
{ path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard] },
|
||||||
|
|
||||||
|
{ path: 'parcelamento', component: Parcelamento, canActivate: [authGuard] },
|
||||||
|
|
||||||
{ path: '**', redirectTo: '' },
|
{ path: '**', redirectTo: '' },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,11 @@
|
||||||
<i class="bi bi-arrow-left-right me-2"></i> Troca de Número
|
<i class="bi bi-arrow-left-right me-2"></i> Troca de Número
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<!-- ✅ NOVO: PARCELAMENTO -->
|
||||||
|
<a routerLink="/parcelamento" class="side-item" (click)="closeMenu()">
|
||||||
|
<i class="bi bi-graph-up-arrow me-2"></i> Parcelamento
|
||||||
|
</a>
|
||||||
|
|
||||||
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-clipboard-data me-2"></i> Controle de Contratos
|
<i class="bi bi-clipboard-data me-2"></i> Controle de Contratos
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// header.ts
|
|
||||||
import { Component, HostListener, Inject } from '@angular/core';
|
import { Component, HostListener, Inject } from '@angular/core';
|
||||||
import { RouterLink, Router, NavigationEnd } from '@angular/router';
|
import { RouterLink, Router, NavigationEnd } from '@angular/router';
|
||||||
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||||
|
|
@ -15,7 +14,10 @@ export class Header {
|
||||||
isScrolled = false;
|
isScrolled = false;
|
||||||
isHome = true;
|
isHome = true;
|
||||||
|
|
||||||
|
// ✅ menu hamburguer
|
||||||
menuOpen = false;
|
menuOpen = false;
|
||||||
|
|
||||||
|
// ✅ define quando mostrar header “logado”
|
||||||
isLoggedHeader = false;
|
isLoggedHeader = false;
|
||||||
|
|
||||||
// ✅ rotas internas que usam menu lateral
|
// ✅ rotas internas que usam menu lateral
|
||||||
|
|
@ -26,6 +28,7 @@ export class Header {
|
||||||
'/dadosusuarios',
|
'/dadosusuarios',
|
||||||
'/vigencia',
|
'/vigencia',
|
||||||
'/trocanumero',
|
'/trocanumero',
|
||||||
|
'/parcelamento', // ✅ ADICIONADO: Parcelamento
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -35,14 +38,19 @@ export class Header {
|
||||||
this.router.events.subscribe((event) => {
|
this.router.events.subscribe((event) => {
|
||||||
if (event instanceof NavigationEnd) {
|
if (event instanceof NavigationEnd) {
|
||||||
const rawUrl = event.urlAfterRedirects || event.url;
|
const rawUrl = event.urlAfterRedirects || event.url;
|
||||||
|
|
||||||
|
// normaliza (remove query/hash)
|
||||||
const url = rawUrl.split('?')[0].split('#')[0];
|
const url = rawUrl.split('?')[0].split('#')[0];
|
||||||
|
|
||||||
this.isHome = (url === '/' || url === '');
|
this.isHome = (url === '/' || url === '');
|
||||||
|
|
||||||
|
// ✅ considera "logado" se a rota começa com qualquer prefixo interno
|
||||||
|
// aceita também subrotas, ex: /parcelamento/detalhes/123
|
||||||
this.isLoggedHeader = this.loggedPrefixes.some((p) =>
|
this.isLoggedHeader = this.loggedPrefixes.some((p) =>
|
||||||
url === p || url.startsWith(p + '/')
|
url === p || url.startsWith(p + '/')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ✅ ao trocar de rota, fecha o menu
|
||||||
this.menuOpen = false;
|
this.menuOpen = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
<section class="parcelamento-page">
|
||||||
|
<span class="page-blob blob-1" aria-hidden="true"></span>
|
||||||
|
<span class="page-blob blob-2" aria-hidden="true"></span>
|
||||||
|
<span class="page-blob blob-3" aria-hidden="true"></span>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="title">
|
||||||
|
<h2>Parcelamento</h2>
|
||||||
|
<p>KPIs e análise mensal do parcelamento importado da planilha.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters glass">
|
||||||
|
<div class="row g-2 align-items-end">
|
||||||
|
<div class="col-12 col-md-5">
|
||||||
|
<label class="form-label">Cliente</label>
|
||||||
|
<select class="form-select" [(ngModel)]="selectedClient">
|
||||||
|
<option value="">Todos</option>
|
||||||
|
<option *ngFor="let c of clients" [value]="c">{{ c }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label class="form-label">Linha</label>
|
||||||
|
<input class="form-control" placeholder="Ex: 7199..." [(ngModel)]="lineSearch" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-3 d-flex gap-2">
|
||||||
|
<button class="btn btn-primary w-100" (click)="onApplyFilters()" [disabled]="loading">
|
||||||
|
<i class="bi bi-funnel"></i> Filtrar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" (click)="onClearFilters()" [disabled]="loading" title="Limpar">
|
||||||
|
<i class="bi bi-x-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPIs -->
|
||||||
|
<div class="kpis">
|
||||||
|
<div class="kpi-card glass">
|
||||||
|
<span class="kpi-label">Total (c/ desconto)</span>
|
||||||
|
<span class="kpi-value">{{ money(kpis.totalComDesconto) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kpi-card glass">
|
||||||
|
<span class="kpi-label">Total (valor cheio)</span>
|
||||||
|
<span class="kpi-value">{{ money(kpis.totalValorCheio) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kpi-card glass">
|
||||||
|
<span class="kpi-label">Desconto total</span>
|
||||||
|
<span class="kpi-value">{{ money(kpis.totalDesconto) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kpi-card glass">
|
||||||
|
<span class="kpi-label">Linhas / Clientes</span>
|
||||||
|
<span class="kpi-value">{{ kpis.linhas }} <small>/ {{ kpis.clientes }}</small></span>
|
||||||
|
<span class="kpi-sub">{{ kpis.meses }} meses mapeados</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Charts -->
|
||||||
|
<div class="charts">
|
||||||
|
<div class="chart-card glass">
|
||||||
|
<div class="chart-head">
|
||||||
|
<h5>Valor por mês</h5>
|
||||||
|
<span class="muted">Soma mensal do parcelamento</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-area">
|
||||||
|
<canvas #monthlyCanvas></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-card glass">
|
||||||
|
<div class="chart-head">
|
||||||
|
<h5>Top 10 linhas</h5>
|
||||||
|
<span class="muted">Linhas com maior soma total</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-area">
|
||||||
|
<canvas #topLinesCanvas></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="loading" *ngIf="loading">
|
||||||
|
<div class="spinner-border" role="status"></div>
|
||||||
|
<span>Carregando...</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
:host {
|
||||||
|
--brand: #E33DCF;
|
||||||
|
--blue: #030FAA;
|
||||||
|
--text: #111214;
|
||||||
|
--muted: rgba(17, 18, 20, 0.65);
|
||||||
|
|
||||||
|
--radius-xl: 22px;
|
||||||
|
--shadow-card: 0 22px 46px rgba(17, 18, 20, 0.10);
|
||||||
|
--glass-bg: rgba(255, 255, 255, 0.82);
|
||||||
|
--glass-border: 1px solid rgba(227, 61, 207, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcelamento-page {
|
||||||
|
position: relative;
|
||||||
|
padding: clamp(14px, 3vw, 26px);
|
||||||
|
min-height: calc(100vh - 70px);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-blob {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 999px;
|
||||||
|
filter: blur(0.2px);
|
||||||
|
opacity: 0.20;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blob-1 { width: 240px; height: 240px; top: 16px; left: -50px; background: var(--brand); }
|
||||||
|
.blob-2 { width: 280px; height: 280px; top: 140px; right: -80px; background: var(--blue); opacity: .14; }
|
||||||
|
.blob-3 { width: 220px; height: 220px; bottom: 30px; left: 20%; background: var(--brand); opacity: .10; }
|
||||||
|
|
||||||
|
.glass {
|
||||||
|
background: var(--glass-bg);
|
||||||
|
border: var(--glass-border);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
h2 { margin: 0; font-weight: 800; letter-spacing: -0.3px; }
|
||||||
|
p { margin: 2px 0 0; color: var(--muted); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
padding: 14px;
|
||||||
|
.form-label { font-weight: 700; color: rgba(17,18,20,.78); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpis {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
margin-bottom: 14px;
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-card {
|
||||||
|
padding: 14px 16px;
|
||||||
|
|
||||||
|
.kpi-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(17,18,20,.70);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.55rem;
|
||||||
|
font-weight: 900;
|
||||||
|
margin-top: 6px;
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: rgba(17,18,20,.70);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-sub {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: .86rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
grid-template-columns: 1.2fr 1fr;
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
padding: 14px 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.chart-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
font-size: .9rem;
|
||||||
|
color: var(--muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-area {
|
||||||
|
height: 360px;
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
height: 320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
margin-top: 14px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||||
|
import { Component, ElementRef, Inject, PLATFORM_ID, ViewChild, AfterViewInit, OnInit } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
import Chart from 'chart.js/auto';
|
||||||
|
import {
|
||||||
|
ParcelamentoService,
|
||||||
|
ParcelamentoKpis,
|
||||||
|
ParcelamentoMonthlyPoint,
|
||||||
|
ParcelamentoTopLine
|
||||||
|
} from '../../services/parcelamento.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-parcelamento',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule, HttpClientModule],
|
||||||
|
templateUrl: './parcelamento.html',
|
||||||
|
styleUrl: './parcelamento.scss'
|
||||||
|
})
|
||||||
|
export class Parcelamento implements OnInit, AfterViewInit {
|
||||||
|
@ViewChild('monthlyCanvas') monthlyCanvas!: ElementRef<HTMLCanvasElement>;
|
||||||
|
@ViewChild('topLinesCanvas') topLinesCanvas!: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
private monthlyChart?: Chart;
|
||||||
|
private topLinesChart?: Chart;
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
clients: string[] = [];
|
||||||
|
selectedClient: string = '';
|
||||||
|
lineSearch: string = '';
|
||||||
|
|
||||||
|
kpis: ParcelamentoKpis = {
|
||||||
|
linhas: 0,
|
||||||
|
clientes: 0,
|
||||||
|
totalValorCheio: 0,
|
||||||
|
totalDesconto: 0,
|
||||||
|
totalComDesconto: 0,
|
||||||
|
meses: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
monthlySeries: ParcelamentoMonthlyPoint[] = [];
|
||||||
|
topLines: ParcelamentoTopLine[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private parcelamentoService: ParcelamentoService,
|
||||||
|
@Inject(PLATFORM_ID) private platformId: Object
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.loadClients();
|
||||||
|
await this.refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
|
this.renderCharts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildOpts() {
|
||||||
|
const cliente = this.selectedClient?.trim() || undefined;
|
||||||
|
const linha = this.onlyDigits(this.lineSearch) || undefined;
|
||||||
|
return { cliente, linha };
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadClients() {
|
||||||
|
try {
|
||||||
|
this.clients = await firstValueFrom(this.parcelamentoService.getClients());
|
||||||
|
} catch {
|
||||||
|
this.clients = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshAll() {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const opts = this.buildOpts();
|
||||||
|
|
||||||
|
this.kpis = await firstValueFrom(this.parcelamentoService.getKpis(opts));
|
||||||
|
this.monthlySeries = await firstValueFrom(this.parcelamentoService.getMonthlySeries(opts));
|
||||||
|
this.topLines = await firstValueFrom(
|
||||||
|
this.parcelamentoService.getTopLines({ cliente: opts.cliente, take: 10 })
|
||||||
|
);
|
||||||
|
|
||||||
|
this.renderCharts();
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClearFilters() {
|
||||||
|
this.selectedClient = '';
|
||||||
|
this.lineSearch = '';
|
||||||
|
this.refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
onApplyFilters() {
|
||||||
|
this.refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderCharts() {
|
||||||
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
if (!this.monthlyCanvas || !this.topLinesCanvas) return;
|
||||||
|
|
||||||
|
this.renderMonthlyChart();
|
||||||
|
this.renderTopLinesChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderMonthlyChart() {
|
||||||
|
const labels = this.monthlySeries.map(x => x.label);
|
||||||
|
const values = this.monthlySeries.map(x => x.total ?? 0);
|
||||||
|
|
||||||
|
if (this.monthlyChart) this.monthlyChart.destroy();
|
||||||
|
|
||||||
|
this.monthlyChart = new Chart(this.monthlyCanvas.nativeElement.getContext('2d')!, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [{ label: 'Valor por mês (R$)', data: values }]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: true },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx) => {
|
||||||
|
const y = (ctx.parsed as any)?.y;
|
||||||
|
return ` ${this.money(typeof y === 'number' ? y : 0)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
callback: (v: any) => this.money(Number(v) || 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTopLinesChart() {
|
||||||
|
const labels = this.topLines.map(x => (x.linha ?? '').toString());
|
||||||
|
const values = this.topLines.map(x => x.total ?? 0);
|
||||||
|
|
||||||
|
if (this.topLinesChart) this.topLinesChart.destroy();
|
||||||
|
|
||||||
|
this.topLinesChart = new Chart(this.topLinesCanvas.nativeElement.getContext('2d')!, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [{ label: 'Top 10 linhas (Total R$)', data: values }]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y',
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: true },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx) => {
|
||||||
|
const x = (ctx.parsed as any)?.x;
|
||||||
|
return ` ${this.money(typeof x === 'number' ? x : 0)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
callback: (v: any) => this.money(Number(v) || 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
money(v: number) {
|
||||||
|
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(v ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onlyDigits(s: string) {
|
||||||
|
return (s ?? '').replace(/\D/g, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
|
export interface ParcelamentoKpis {
|
||||||
|
linhas: number;
|
||||||
|
clientes: number;
|
||||||
|
totalValorCheio: number;
|
||||||
|
totalDesconto: number;
|
||||||
|
totalComDesconto: number;
|
||||||
|
meses: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParcelamentoMonthlyPoint {
|
||||||
|
month: string; // ex: "2026-01"
|
||||||
|
label: string; // ex: "JAN/2026"
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParcelamentoTopLine {
|
||||||
|
linha: string | null;
|
||||||
|
cliente: string | null;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class ParcelamentoService {
|
||||||
|
private readonly baseApi: string;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {
|
||||||
|
// ✅ igual ao seu VigenciaService
|
||||||
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ /api/parcelamento/clients
|
||||||
|
getClients(): Observable<string[]> {
|
||||||
|
return this.http.get<string[]>(`${this.baseApi}/parcelamento/clients`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ /api/parcelamento/kpis?cliente=...&linha=...
|
||||||
|
getKpis(opts?: { cliente?: string; linha?: string }): Observable<ParcelamentoKpis> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
if (opts?.cliente && opts.cliente.trim()) params = params.set('cliente', opts.cliente.trim());
|
||||||
|
if (opts?.linha && opts.linha.trim()) params = params.set('linha', opts.linha.trim());
|
||||||
|
return this.http.get<ParcelamentoKpis>(`${this.baseApi}/parcelamento/kpis`, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ /api/parcelamento/series/monthly?cliente=...&linha=...
|
||||||
|
getMonthlySeries(opts?: { cliente?: string; linha?: string }): Observable<ParcelamentoMonthlyPoint[]> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
if (opts?.cliente && opts.cliente.trim()) params = params.set('cliente', opts.cliente.trim());
|
||||||
|
if (opts?.linha && opts.linha.trim()) params = params.set('linha', opts.linha.trim());
|
||||||
|
return this.http.get<ParcelamentoMonthlyPoint[]>(`${this.baseApi}/parcelamento/series/monthly`, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ /api/parcelamento/top-lines?cliente=...&take=10
|
||||||
|
getTopLines(opts?: { cliente?: string; take?: number }): Observable<ParcelamentoTopLine[]> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
params = params.set('take', String(opts?.take ?? 10));
|
||||||
|
if (opts?.cliente && opts.cliente.trim()) params = params.set('cliente', opts.cliente.trim());
|
||||||
|
return this.http.get<ParcelamentoTopLine[]>(`${this.baseApi}/parcelamento/top-lines`, { params });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue