Merge pull request #21 from eduardolopesx03/front-end-sem-relacional
Salvando alterações do front-end com banco tabelas sem relacao
This commit is contained in:
commit
e233ad3b7d
|
|
@ -11,7 +11,6 @@ import { authGuard } from './guards/auth.guard';
|
|||
import { DadosUsuarios } from './pages/dados-usuarios/dados-usuarios';
|
||||
import { VigenciaComponent } from './pages/vigencia/vigencia';
|
||||
import { TrocaNumero } from './pages/troca-numero/troca-numero';
|
||||
import { Parcelamento } from './pages/parcelamento/parcelamento';
|
||||
|
||||
// ✅ NOVO: TROCA DE NÚMERO
|
||||
|
||||
|
|
@ -30,7 +29,5 @@ export const routes: Routes = [
|
|||
// ✅ NOVO: rota da página Troca de Número
|
||||
{ path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard] },
|
||||
|
||||
{ path: 'parcelamento', component: Parcelamento, canActivate: [authGuard] },
|
||||
|
||||
{ path: '**', redirectTo: '' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -110,11 +110,6 @@
|
|||
<i class="bi bi-arrow-left-right me-2"></i> Troca de Número
|
||||
</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()">
|
||||
<i class="bi bi-clipboard-data me-2"></i> Controle de Contratos
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// header.ts
|
||||
import { Component, HostListener, Inject } from '@angular/core';
|
||||
import { RouterLink, Router, NavigationEnd } from '@angular/router';
|
||||
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||
|
|
@ -14,10 +15,7 @@ export class Header {
|
|||
isScrolled = false;
|
||||
isHome = true;
|
||||
|
||||
// ✅ menu hamburguer
|
||||
menuOpen = false;
|
||||
|
||||
// ✅ define quando mostrar header “logado”
|
||||
isLoggedHeader = false;
|
||||
|
||||
// ✅ rotas internas que usam menu lateral
|
||||
|
|
@ -28,7 +26,6 @@ export class Header {
|
|||
'/dadosusuarios',
|
||||
'/vigencia',
|
||||
'/trocanumero',
|
||||
'/parcelamento', // ✅ ADICIONADO: Parcelamento
|
||||
];
|
||||
|
||||
constructor(
|
||||
|
|
@ -38,19 +35,14 @@ export class Header {
|
|||
this.router.events.subscribe((event) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
const rawUrl = event.urlAfterRedirects || event.url;
|
||||
|
||||
// normaliza (remove query/hash)
|
||||
const url = rawUrl.split('?')[0].split('#')[0];
|
||||
|
||||
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) =>
|
||||
url === p || url.startsWith(p + '/')
|
||||
);
|
||||
|
||||
// ✅ ao trocar de rota, fecha o menu
|
||||
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