Compare commits

...

2 Commits

Author SHA1 Message Date
Eduardo Lopes e233ad3b7d
Merge pull request #21 from eduardolopesx03/front-end-sem-relacional
Salvando alterações do front-end com banco tabelas sem relacao
2026-01-13 11:23:14 -03:00
Eduardo 4bbf22152a Salvando alterações do front-end com banco tabelas sem relacao 2026-01-13 11:19:25 -03:00
7 changed files with 1 additions and 518 deletions

View File

@ -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: '' },
]; ];

View File

@ -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>

View File

@ -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;
} }
}); });

View File

@ -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>

View File

@ -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);
}

View File

@ -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, '');
}
}

View File

@ -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 });
}
}