Compare commits
2 Commits
d87825b370
...
e233ad3b7d
| Author | SHA1 | Date |
|---|---|---|
|
|
e233ad3b7d | |
|
|
4bbf22152a |
|
|
@ -11,7 +11,6 @@ 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
|
||||||
|
|
||||||
|
|
@ -30,7 +29,5 @@ 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,11 +110,6 @@
|
||||||
<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,3 +1,4 @@
|
||||||
|
// 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';
|
||||||
|
|
@ -14,10 +15,7 @@ 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
|
||||||
|
|
@ -28,7 +26,6 @@ export class Header {
|
||||||
'/dadosusuarios',
|
'/dadosusuarios',
|
||||||
'/vigencia',
|
'/vigencia',
|
||||||
'/trocanumero',
|
'/trocanumero',
|
||||||
'/parcelamento', // ✅ ADICIONADO: Parcelamento
|
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -38,19 +35,14 @@ 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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
: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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
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, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
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