Compare commits
2 Commits
6fd87ae2b3
...
20590bef57
| Author | SHA1 | Date |
|---|---|---|
|
|
20590bef57 | |
|
|
47485a867b |
|
|
@ -1,13 +1,22 @@
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
import { Home } from './pages/home/home';
|
import { Home } from './pages/home/home';
|
||||||
import { Register } from './pages/register/register';
|
import { Register } from './pages/register/register';
|
||||||
import { LoginComponent } from './pages/login/login';
|
import { LoginComponent } from './pages/login/login';
|
||||||
import { Geral } from './pages/geral/geral';
|
import { Geral } from './pages/geral/geral';
|
||||||
|
import { Mureg } from './pages/mureg/mureg';
|
||||||
|
import { Faturamento } from './pages/faturamento/faturamento';
|
||||||
|
|
||||||
import { authGuard } from './guards/auth.guard';
|
import { authGuard } from './guards/auth.guard';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: Home },
|
{ path: '', component: Home },
|
||||||
{ path: "register", component: Register },
|
{ path: 'register', component: Register },
|
||||||
{ path: "login", component: LoginComponent },
|
{ path: 'login', component: LoginComponent },
|
||||||
|
|
||||||
{ path: 'geral', component: Geral, canActivate: [authGuard] },
|
{ path: 'geral', component: Geral, canActivate: [authGuard] },
|
||||||
|
{ path: 'mureg', component: Mureg, canActivate: [authGuard] },
|
||||||
|
{ path: 'faturamento', component: Faturamento, canActivate: [authGuard] },
|
||||||
|
|
||||||
|
{ path: '**', redirectTo: '' },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
[class.header-scrolled]="isScrolled"
|
[class.header-scrolled]="isScrolled"
|
||||||
>
|
>
|
||||||
<div class="header-top">
|
<div class="header-top">
|
||||||
<!-- ESQUERDA: HAMBURGUER (só no /geral) + LOGO -->
|
<!-- ESQUERDA: HAMBURGUER (logado) + LOGO -->
|
||||||
<div class="left-area">
|
<div class="left-area">
|
||||||
<button
|
<button
|
||||||
*ngIf="isLoggedHeader"
|
*ngIf="isLoggedHeader"
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ✅ MENU HOME: só aparece fora do /geral -->
|
<!-- ✅ MENU HOME: só aparece fora do logado -->
|
||||||
<nav class="menu" *ngIf="!isLoggedHeader">
|
<nav class="menu" *ngIf="!isLoggedHeader">
|
||||||
<a href="https://www.linemovel.com.br/sobrenos" class="menu-item" target="_blank">O que é a Line Móvel?</a>
|
<a href="https://www.linemovel.com.br/sobrenos" class="menu-item" target="_blank">O que é a Line Móvel?</a>
|
||||||
<a href="https://www.linemovel.com.br/empresas" class="menu-item" target="_blank">Para sua empresa</a>
|
<a href="https://www.linemovel.com.br/empresas" class="menu-item" target="_blank">Para sua empresa</a>
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
<a href="https://www.linemovel.com.br/indique" class="menu-item" target="_blank">Indique um amigo</a>
|
<a href="https://www.linemovel.com.br/indique" class="menu-item" target="_blank">Indique um amigo</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- ✅ BOTÕES: só aparecem fora do /geral -->
|
<!-- ✅ BOTÕES: só aparecem fora do logado -->
|
||||||
<div class="btn-area" *ngIf="!isLoggedHeader">
|
<div class="btn-area" *ngIf="!isLoggedHeader">
|
||||||
<button type="button" class="btn btn-cadastrar" [routerLink]="['/register']">
|
<button type="button" class="btn btn-cadastrar" [routerLink]="['/register']">
|
||||||
Cadastre-se
|
Cadastre-se
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ✅ FAIXA (SÓ NA HOME) com degradê igual footer -->
|
<!-- ✅ FAIXA (SÓ NA HOME) -->
|
||||||
<div class="header-bar footer-gradient" *ngIf="isHome">
|
<div class="header-bar footer-gradient" *ngIf="isHome">
|
||||||
<span class="header-bar-text">
|
<span class="header-bar-text">
|
||||||
Somos a escolha certa para estar sempre conectado!
|
Somos a escolha certa para estar sempre conectado!
|
||||||
|
|
@ -52,14 +52,14 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- ✅ OVERLAY (só no /geral) -->
|
<!-- ✅ OVERLAY (logado) -->
|
||||||
<div
|
<div
|
||||||
class="menu-overlay"
|
class="menu-overlay"
|
||||||
*ngIf="isLoggedHeader && menuOpen"
|
*ngIf="isLoggedHeader && menuOpen"
|
||||||
(click)="closeMenu()"
|
(click)="closeMenu()"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- ✅ MENU LATERAL (só no /geral) -->
|
<!-- ✅ MENU LATERAL (logado) -->
|
||||||
<aside
|
<aside
|
||||||
*ngIf="isLoggedHeader"
|
*ngIf="isLoggedHeader"
|
||||||
class="side-menu"
|
class="side-menu"
|
||||||
|
|
@ -86,14 +86,24 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="side-menu-body">
|
<div class="side-menu-body">
|
||||||
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
|
||||||
<i class="bi bi-clipboard-data me-2"></i> Controle de Contratos
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-sim me-2"></i> Gerenciar Linhas
|
<i class="bi bi-sim me-2"></i> Gerenciar Linhas
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<!-- ✅ NOVO: FATURAMENTO -->
|
||||||
|
<a routerLink="/faturamento" class="side-item" (click)="closeMenu()">
|
||||||
|
<i class="bi bi-receipt me-2"></i> Faturamento
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a routerLink="/mureg" class="side-item" (click)="closeMenu()">
|
||||||
|
<i class="bi bi-table me-2"></i> Mureg
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
||||||
|
<i class="bi bi-clipboard-data me-2"></i> Controle de Contratos
|
||||||
|
</a>
|
||||||
|
|
||||||
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-people me-2"></i> Gerenciar Clientes
|
<i class="bi bi-people me-2"></i> Gerenciar Clientes
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -74,14 +74,12 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto; /* esquerda | centro | direita */
|
grid-template-columns: auto 1fr auto; /* esquerda | centro | direita */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
/* como agora é grid, não usamos space-between */
|
|
||||||
justify-content: unset;
|
justify-content: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu {
|
.menu {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center; /* ✅ centraliza os links no centro */
|
justify-content: center; /* ✅ centraliza os links */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +91,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================== */
|
/* ===================== */
|
||||||
/* HAMBURGUER (GERAL) */
|
/* HAMBURGUER (LOGADO) */
|
||||||
/* ===================== */
|
/* ===================== */
|
||||||
.hamburger-btn {
|
.hamburger-btn {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
|
|
@ -195,12 +193,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
/* ✅ nunca quebrar linha */
|
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
/* ✅ não empurrar botões */
|
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
|
|
@ -210,7 +206,6 @@
|
||||||
@media (max-width: 1100px) { gap: 12px; }
|
@media (max-width: 1100px) { gap: 12px; }
|
||||||
@media (max-width: 1024px) { gap: 10px; }
|
@media (max-width: 1024px) { gap: 10px; }
|
||||||
|
|
||||||
/* telas menores: some o menu */
|
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
@ -226,7 +221,6 @@
|
||||||
|
|
||||||
padding: 10px 10px;
|
padding: 10px 10px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
transition: transform 180ms ease, background 180ms ease, box-shadow 180ms ease;
|
transition: transform 180ms ease, background 180ms ease, box-shadow 180ms ease;
|
||||||
|
|
@ -297,7 +291,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-cadastrar {
|
.btn-cadastrar {
|
||||||
background: #E1E1E1; /* ✅ sólido (sem transparência) */
|
background: #E1E1E1;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,7 +327,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* degradê igual ao footer */
|
|
||||||
.footer-gradient {
|
.footer-gradient {
|
||||||
background: linear-gradient(90deg, #0B2BD6 0%, #6A55FF 40%, #E33DCF 100%);
|
background: linear-gradient(90deg, #0B2BD6 0%, #6A55FF 40%, #E33DCF 100%);
|
||||||
}
|
}
|
||||||
|
|
@ -348,9 +341,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================================================== */
|
/* ===================================================== */
|
||||||
/* MENU LATERAL (GERAL) */
|
/* MENU LATERAL (LOGADO) */
|
||||||
/* ===================================================== */
|
/* ===================================================== */
|
||||||
|
|
||||||
.menu-overlay {
|
.menu-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|
@ -454,7 +446,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========================================= */
|
/* ========================================= */
|
||||||
/* ✅ OVERRIDE BOOTSTRAP: SEM TRANSPARÊNCIA */
|
/* OVERRIDE BOOTSTRAP */
|
||||||
/* ========================================= */
|
/* ========================================= */
|
||||||
.btn.btn-cadastrar,
|
.btn.btn-cadastrar,
|
||||||
.btn.btn-login {
|
.btn.btn-login {
|
||||||
|
|
@ -473,11 +465,9 @@
|
||||||
filter: none !important;
|
filter: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* mantém as cores originais até quando clica */
|
|
||||||
.btn.btn-cadastrar:active { background: #E1E1E1 !important; }
|
.btn.btn-cadastrar:active { background: #E1E1E1 !important; }
|
||||||
.btn.btn-login:active { background: var(--brand) !important; border-color: var(--brand) !important; color: #fff !important; }
|
.btn.btn-login:active { background: var(--brand) !important; border-color: var(--brand) !important; color: #fff !important; }
|
||||||
|
|
||||||
/* remove “inset” do bootstrap */
|
|
||||||
.btn.btn-cadastrar:active,
|
.btn.btn-cadastrar:active,
|
||||||
.btn.btn-login:active {
|
.btn.btn-login:active {
|
||||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.10) !important;
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.10) !important;
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,11 @@ export class Header {
|
||||||
|
|
||||||
this.isHome = (url === '/' || url === '');
|
this.isHome = (url === '/' || url === '');
|
||||||
|
|
||||||
// ✅ considera header logado quando está em /geral
|
// ✅ considera header logado quando está em rotas internas
|
||||||
this.isLoggedHeader = url.startsWith('/geral');
|
// (agora inclui MUREG)
|
||||||
|
this.isLoggedHeader =
|
||||||
|
url.startsWith('/geral') ||
|
||||||
|
url.startsWith('/mureg');
|
||||||
|
|
||||||
// ✅ ao trocar de rota, fecha o menu
|
// ✅ ao trocar de rota, fecha o menu
|
||||||
this.menuOpen = false;
|
this.menuOpen = false;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,288 @@
|
||||||
|
<section class="billing-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>
|
||||||
|
<span class="page-blob blob-4" aria-hidden="true"></span>
|
||||||
|
|
||||||
|
<div class="container-billing">
|
||||||
|
<div class="billing-card" data-animate>
|
||||||
|
|
||||||
|
<!-- HEADER (igual Geral) -->
|
||||||
|
<div class="billing-header">
|
||||||
|
<div class="header-row-top">
|
||||||
|
<div class="title-badge" data-animate>
|
||||||
|
<i class="bi bi-cash-stack"></i> Financeiro
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-title" data-animate>
|
||||||
|
<h5 class="title mb-0">Faturamento</h5>
|
||||||
|
<small class="subtitle">Visualize dados de Pessoa Física e Pessoa Jurídica</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-actions d-flex gap-2 justify-content-end" data-animate>
|
||||||
|
<button class="btn btn-glass btn-sm" (click)="refresh()" [disabled]="loading">
|
||||||
|
<i class="bi bi-arrow-clockwise me-1"></i> Atualizar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FILTROS (tabs + controls no mesmo padrão do Geral) -->
|
||||||
|
<div class="filters-row mt-4" data-animate>
|
||||||
|
<!-- Tipo PF/PJ como tabs -->
|
||||||
|
<div class="filter-tabs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="filter-tab"
|
||||||
|
[class.active]="tipo === 'PF'"
|
||||||
|
(click)="setTipo('PF')"
|
||||||
|
[disabled]="loading">
|
||||||
|
<i class="bi bi-person me-1"></i> Pessoa Física
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="filter-tab"
|
||||||
|
[class.active]="tipo === 'PJ'"
|
||||||
|
(click)="setTipo('PJ')"
|
||||||
|
[disabled]="loading">
|
||||||
|
<i class="bi bi-building me-1"></i> Pessoa Jurídica
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cliente (select glass) -->
|
||||||
|
<div class="client-filter">
|
||||||
|
<div class="select-wrapper">
|
||||||
|
<select
|
||||||
|
class="form-select form-select-sm select-glass"
|
||||||
|
[(ngModel)]="client"
|
||||||
|
(change)="applyClientFilter()"
|
||||||
|
[disabled]="loading"
|
||||||
|
title="Filtrar por cliente">
|
||||||
|
<option value="">Todos os clientes</option>
|
||||||
|
<option *ngFor="let c of clients" [value]="c">{{ c }}</option>
|
||||||
|
</select>
|
||||||
|
<i class="bi bi-chevron-down select-icon"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary btn-sm btn-clear-client"
|
||||||
|
type="button"
|
||||||
|
(click)="clearClient()"
|
||||||
|
[disabled]="loading || !client"
|
||||||
|
title="Limpar filtro de cliente">
|
||||||
|
<i class="bi bi-x-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CONTROLS (search + page size igual Geral) -->
|
||||||
|
<div class="controls mt-3 mb-2" data-animate>
|
||||||
|
<div class="input-group input-group-sm search-group">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i
|
||||||
|
class="bi"
|
||||||
|
[class.bi-search]="!loading"
|
||||||
|
[class.bi-hourglass-split]="loading"
|
||||||
|
[class.text-brand]="loading"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Buscar cliente..."
|
||||||
|
[(ngModel)]="search"
|
||||||
|
(ngModelChange)="onSearchChange()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary btn-clear"
|
||||||
|
type="button"
|
||||||
|
(click)="search=''; onSearchChange()"
|
||||||
|
[disabled]="loading || !search"
|
||||||
|
title="Limpar busca">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-size d-flex align-items-center gap-2">
|
||||||
|
<span class="text-muted small fw-bold text-uppercase"
|
||||||
|
style="letter-spacing: 0.5px; font-size: 0.75rem;">
|
||||||
|
Itens por pág:
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="select-wrapper">
|
||||||
|
<select
|
||||||
|
class="form-select form-select-sm select-glass"
|
||||||
|
[(ngModel)]="pageSize"
|
||||||
|
(change)="changePageSize()"
|
||||||
|
[disabled]="loading">
|
||||||
|
<option [ngValue]="10">10</option>
|
||||||
|
<option [ngValue]="20">20</option>
|
||||||
|
<option [ngValue]="50">50</option>
|
||||||
|
<option [ngValue]="100">100</option>
|
||||||
|
</select>
|
||||||
|
<i class="bi bi-chevron-down select-icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ERRO -->
|
||||||
|
<div *ngIf="errorMessage" class="alert alert-danger mt-3 mb-0">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BODY (tabela no padrão Geral) -->
|
||||||
|
<div class="billing-body">
|
||||||
|
<div class="table-meta" *ngIf="result">
|
||||||
|
<span class="meta-pill">
|
||||||
|
<i class="bi bi-collection me-1"></i>
|
||||||
|
Total: <strong>{{ result.total }}</strong>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="meta-pill">
|
||||||
|
<i class="bi bi-layers me-1"></i>
|
||||||
|
Página: <strong>{{ page }}</strong> / <strong>{{ totalPages }}</strong>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="meta-loading" *ngIf="loading">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span> Carregando...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrap table-wrap-tall">
|
||||||
|
<table class="table table-modern align-middle text-center mb-0 billing-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="sortable" (click)="toggleSort('item')">
|
||||||
|
<div class="th-content justify-content-center">
|
||||||
|
ITEM
|
||||||
|
<span class="sort-caret" [class.active]="sortBy==='item'">
|
||||||
|
<i [class]="sortIcon('item')"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th class="sortable" (click)="toggleSort('cliente')">
|
||||||
|
<div class="th-content justify-content-center">
|
||||||
|
CLIENTE
|
||||||
|
<span class="sort-caret" [class.active]="sortBy==='cliente'">
|
||||||
|
<i [class]="sortIcon('cliente')"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th class="sortable" (click)="toggleSort('qtdlinhas')">
|
||||||
|
<div class="th-content justify-content-center">
|
||||||
|
QTD LINHAS
|
||||||
|
<span class="sort-caret" [class.active]="sortBy==='qtdlinhas'">
|
||||||
|
<i [class]="sortIcon('qtdlinhas')"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th class="sortable" (click)="toggleSort('franquiavivo')">
|
||||||
|
<div class="th-content justify-content-center">
|
||||||
|
FRANQUIA VIVO
|
||||||
|
<span class="sort-caret" [class.active]="sortBy==='franquiavivo'">
|
||||||
|
<i [class]="sortIcon('franquiavivo')"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th class="sortable" (click)="toggleSort('valorcontratovivo')">
|
||||||
|
<div class="th-content justify-content-center">
|
||||||
|
CONTRATO VIVO
|
||||||
|
<span class="sort-caret" [class.active]="sortBy==='valorcontratovivo'">
|
||||||
|
<i [class]="sortIcon('valorcontratovivo')"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th class="sortable" (click)="toggleSort('franquialine')">
|
||||||
|
<div class="th-content justify-content-center">
|
||||||
|
FRANQUIA LINE
|
||||||
|
<span class="sort-caret" [class.active]="sortBy==='franquialine'">
|
||||||
|
<i [class]="sortIcon('franquialine')"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th class="sortable" (click)="toggleSort('valorcontratoline')">
|
||||||
|
<div class="th-content justify-content-center">
|
||||||
|
CONTRATO LINE
|
||||||
|
<span class="sort-caret" [class.active]="sortBy==='valorcontratoline'">
|
||||||
|
<i [class]="sortIcon('valorcontratoline')"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th class="sortable" (click)="toggleSort('lucro')">
|
||||||
|
<div class="th-content justify-content-center">
|
||||||
|
LUCRO
|
||||||
|
<span class="sort-caret" [class.active]="sortBy==='lucro'">
|
||||||
|
<i [class]="sortIcon('lucro')"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th>
|
||||||
|
APARELHO
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th>
|
||||||
|
PAGAMENTO
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr *ngIf="!loading && (result.items?.length ?? 0) === 0">
|
||||||
|
<td colspan="10" class="text-center py-5 empty-state text-muted fw-bold">
|
||||||
|
<i class="bi bi-inbox me-2"></i>
|
||||||
|
Nenhum registro encontrado.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr *ngFor="let it of result.items; trackBy: trackById" class="table-row-item">
|
||||||
|
<td class="text-muted fw-bold">{{ it.item }}</td>
|
||||||
|
<td class="fw-black text-dark td-clip" [title]="it.cliente || ''">{{ it.cliente }}</td>
|
||||||
|
<td class="fw-bold">{{ it.qtdLinhas ?? 0 }}</td>
|
||||||
|
|
||||||
|
<td class="text-end">{{ brl(it.franquiaVivo) }}</td>
|
||||||
|
<td class="text-end">{{ brl(it.valorContratoVivo) }}</td>
|
||||||
|
<td class="text-end">{{ brl(it.franquiaLine) }}</td>
|
||||||
|
<td class="text-end">{{ brl(it.valorContratoLine) }}</td>
|
||||||
|
<td class="text-end lucro">{{ brl(it.lucro) }}</td>
|
||||||
|
|
||||||
|
<td class="text-truncate extra" [title]="it.aparelho || ''">{{ it.aparelho || '-' }}</td>
|
||||||
|
<td class="text-truncate extra" [title]="it.formaPagamento || ''">{{ it.formaPagamento || '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FOOTER / PAGINAÇÃO (padrão Geral) -->
|
||||||
|
<div class="billing-footer">
|
||||||
|
<div class="small text-muted fw-bold">
|
||||||
|
Página {{ page }} de {{ totalPages }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<ul class="pagination pagination-sm mb-0 pagination-modern">
|
||||||
|
<li class="page-item" [class.disabled]="loading || page <= 1">
|
||||||
|
<button class="page-link" (click)="prevPage()">Anterior</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="page-item" [class.disabled]="loading || page >= totalPages">
|
||||||
|
<button class="page-link" (click)="nextPage()">Próxima</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,566 @@
|
||||||
|
:host {
|
||||||
|
--brand: #E33DCF;
|
||||||
|
--blue: #030FAA;
|
||||||
|
--text: #111214;
|
||||||
|
--muted: rgba(17, 18, 20, 0.65);
|
||||||
|
|
||||||
|
--radius-xl: 22px;
|
||||||
|
--radius-lg: 16px;
|
||||||
|
--radius-md: 12px;
|
||||||
|
|
||||||
|
--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);
|
||||||
|
|
||||||
|
--page-top-gap: clamp(14px, 4vh, 40px);
|
||||||
|
--page-bottom-gap: clamp(110px, 14vh, 220px);
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
color: var(--text);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PAGE BG igual Geral */
|
||||||
|
.billing-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 0 12px var(--page-bottom-gap);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
background:
|
||||||
|
radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.14), transparent 60%),
|
||||||
|
radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.08), transparent 60%),
|
||||||
|
linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BLOBS igual Geral */
|
||||||
|
.page-blob {
|
||||||
|
position: fixed;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
filter: blur(34px);
|
||||||
|
opacity: 0.55;
|
||||||
|
z-index: 0;
|
||||||
|
background: radial-gradient(circle at 30% 30%, rgba(227,61,207,0.55), rgba(227,61,207,0.06));
|
||||||
|
animation: floaty 10s ease-in-out infinite;
|
||||||
|
|
||||||
|
&.blob-1 { width: 420px; height: 420px; top: -140px; left: -140px; }
|
||||||
|
&.blob-2 { width: 520px; height: 520px; top: -220px; right: -240px; animation-duration: 12s; }
|
||||||
|
&.blob-3 { width: 360px; height: 360px; bottom: -180px; left: 25%; animation-duration: 14s; }
|
||||||
|
&.blob-4 { width: 520px; height: 520px; bottom: -260px; right: -260px; animation-duration: 16s; opacity: .45; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes floaty {
|
||||||
|
0% { transform: translate(0, 0) scale(1); }
|
||||||
|
50% { transform: translate(18px, 10px) scale(1.03); }
|
||||||
|
100% { transform: translate(0, 0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-billing {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1180px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
margin-top: var(--page-top-gap);
|
||||||
|
margin-bottom: var(--page-bottom-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CARD glass */
|
||||||
|
.billing-card {
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--glass-bg);
|
||||||
|
border: var(--glass-border);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: calc(100vh - 18px) !important;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 1px;
|
||||||
|
border-radius: calc(var(--radius-xl) - 1px);
|
||||||
|
pointer-events: none;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HEADER */
|
||||||
|
.billing-header {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid rgba(17, 18, 20, 0.06);
|
||||||
|
background: linear-gradient(180deg, rgba(227,61,207,0.06), rgba(255,255,255,0.2));
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row-top {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
text-align: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.title-badge { justify-self: center; margin-bottom: 8px; }
|
||||||
|
.header-actions { justify-self: center; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-badge {
|
||||||
|
justify-self: start;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.78);
|
||||||
|
border: 1px solid rgba(227, 61, 207, 0.22);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
|
||||||
|
i { color: var(--brand); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
justify-self: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 950;
|
||||||
|
letter-spacing: -0.3px;
|
||||||
|
color: var(--text);
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: rgba(17, 18, 20, 0.65);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions { justify-self: end; }
|
||||||
|
|
||||||
|
/* BOTÕES */
|
||||||
|
.btn-glass {
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
border: 1px solid rgba(3, 15, 170, 0.25);
|
||||||
|
color: var(--blue);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--brand);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FILTROS */
|
||||||
|
.filters-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
border: 1px solid rgba(17, 18, 20, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tab {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--muted);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #fff;
|
||||||
|
color: var(--brand);
|
||||||
|
box-shadow: 0 2px 8px rgba(227, 61, 207, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CLIENTE: select glass + botão limpar */
|
||||||
|
.client-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear-client {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CONTROLS igual Geral */
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-group {
|
||||||
|
max-width: 320px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(17, 18, 20, 0.15);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--brand);
|
||||||
|
box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--muted);
|
||||||
|
padding-left: 14px;
|
||||||
|
padding-right: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
i { font-size: 1rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 10px 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text);
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&::placeholder { color: rgba(17, 18, 20, 0.4); font-weight: 500; }
|
||||||
|
&:focus { outline: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear {
|
||||||
|
display: inline-flex;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 0 12px;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover { color: #dc3545; }
|
||||||
|
i { font-size: 1rem; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size {
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
margin-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 170px;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-glass {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border: 1px solid rgba(17, 18, 20, 0.15);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--blue);
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 36px 8px 14px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fff;
|
||||||
|
border-color: var(--blue);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(3, 15, 170, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--brand);
|
||||||
|
box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-brand { color: var(--brand) !important; }
|
||||||
|
.fw-black { font-weight: 950; }
|
||||||
|
|
||||||
|
/* BODY */
|
||||||
|
.billing-body {
|
||||||
|
padding: 16px;
|
||||||
|
background: transparent;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* meta pills */
|
||||||
|
.table-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255,255,255,0.7);
|
||||||
|
border: 1px solid rgba(17,18,20,0.10);
|
||||||
|
font-weight: 800;
|
||||||
|
color: rgba(17,18,20,0.75);
|
||||||
|
|
||||||
|
i { color: var(--brand); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-loading {
|
||||||
|
margin-left: auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: rgba(17,18,20,0.65);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TABELA igual Geral */
|
||||||
|
.table-wrap {
|
||||||
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap-tall {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: clamp(760px, 82vh, 1800px) !important;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(17,18,20,0.10);
|
||||||
|
background: rgba(255,255,255,0.7);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-modern {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 1100px;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 2px solid rgba(227, 61, 207, 0.15);
|
||||||
|
padding: 12px;
|
||||||
|
color: rgba(17, 18, 20, 0.7);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 950;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: default;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
border-bottom: 1px solid rgba(17,18,20,0.05);
|
||||||
|
|
||||||
|
&:hover { background-color: rgba(227, 61, 207, 0.05); }
|
||||||
|
td { border-bottom: 1px solid rgba(17,18,20,0.04); }
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 12px;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text);
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover { color: var(--brand); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.th-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-caret {
|
||||||
|
opacity: 0.55;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.td-clip {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extra {
|
||||||
|
max-width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
background: rgba(255,255,255,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucro {
|
||||||
|
font-weight: 950;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FOOTER */
|
||||||
|
.billing-footer {
|
||||||
|
padding: 14px 24px;
|
||||||
|
border-top: 1px solid rgba(17, 18, 20, 0.06);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-modern .page-link {
|
||||||
|
color: var(--blue);
|
||||||
|
font-weight: 900;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(17,18,20,0.1);
|
||||||
|
background: rgba(255,255,255,0.6);
|
||||||
|
margin: 0 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: var(--brand);
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-modern .page-item.disabled .page-link {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RESPONSIVO */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.table-modern { min-width: 1000px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
:host {
|
||||||
|
--page-top-gap: 16px;
|
||||||
|
--page-bottom-gap: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap-tall { max-height: 70vh !important; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Faturamento } from './faturamento';
|
||||||
|
|
||||||
|
describe('Faturamento', () => {
|
||||||
|
let component: Faturamento;
|
||||||
|
let fixture: ComponentFixture<Faturamento>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Faturamento]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Faturamento);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import {
|
||||||
|
BillingService,
|
||||||
|
BillingItem,
|
||||||
|
PagedResult,
|
||||||
|
BillingSortBy,
|
||||||
|
SortDir,
|
||||||
|
TipoCliente
|
||||||
|
} from '../../services/billing';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-faturamento',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule],
|
||||||
|
templateUrl: './faturamento.html',
|
||||||
|
styleUrl: './faturamento.scss',
|
||||||
|
})
|
||||||
|
export class Faturamento implements OnInit, OnDestroy {
|
||||||
|
// ===== UI state =====
|
||||||
|
loading = false;
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
|
||||||
|
// ===== filtros =====
|
||||||
|
tipo: TipoCliente = 'PF';
|
||||||
|
search = '';
|
||||||
|
client = '';
|
||||||
|
page = 1;
|
||||||
|
pageSize = 20;
|
||||||
|
|
||||||
|
// ===== ordenação =====
|
||||||
|
sortBy: BillingSortBy = 'cliente';
|
||||||
|
sortDir: SortDir = 'asc';
|
||||||
|
|
||||||
|
// ===== dados =====
|
||||||
|
result: PagedResult<BillingItem> = { page: 1, pageSize: 20, total: 0, items: [] };
|
||||||
|
clients: string[] = [];
|
||||||
|
|
||||||
|
// ===== subs =====
|
||||||
|
private sub = new Subscription();
|
||||||
|
private searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
@ViewChild('errorToast', { static: false }) errorToast?: ElementRef<HTMLDivElement>;
|
||||||
|
|
||||||
|
constructor(private billingService: BillingService) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadClients();
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.sub.unsubscribe();
|
||||||
|
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Ações de filtro
|
||||||
|
// =========================
|
||||||
|
setTipo(tipo: TipoCliente) {
|
||||||
|
if (this.tipo === tipo) return;
|
||||||
|
this.tipo = tipo;
|
||||||
|
this.page = 1;
|
||||||
|
this.client = '';
|
||||||
|
this.loadClients();
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchChange() {
|
||||||
|
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||||
|
|
||||||
|
this.searchTimer = setTimeout(() => {
|
||||||
|
this.page = 1;
|
||||||
|
this.loadData();
|
||||||
|
}, 350);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyClientFilter() {
|
||||||
|
this.page = 1;
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearClient() {
|
||||||
|
this.client = '';
|
||||||
|
this.page = 1;
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.loadClients();
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
changePageSize() {
|
||||||
|
this.page = 1;
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Ordenação
|
||||||
|
// =========================
|
||||||
|
toggleSort(col: BillingSortBy) {
|
||||||
|
if (this.sortBy === col) {
|
||||||
|
this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
this.sortBy = col;
|
||||||
|
this.sortDir = 'asc';
|
||||||
|
}
|
||||||
|
this.page = 1;
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
sortIcon(col: BillingSortBy): string {
|
||||||
|
if (this.sortBy !== col) return 'bi bi-arrow-down-up';
|
||||||
|
return this.sortDir === 'asc' ? 'bi bi-sort-down' : 'bi bi-sort-up';
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Paginação
|
||||||
|
// =========================
|
||||||
|
get totalPages(): number {
|
||||||
|
const t = this.result?.total ?? 0;
|
||||||
|
return Math.max(1, Math.ceil(t / (this.pageSize || 20)));
|
||||||
|
}
|
||||||
|
|
||||||
|
goToPage(p: number) {
|
||||||
|
const tp = this.totalPages;
|
||||||
|
const next = Math.min(Math.max(1, p), tp);
|
||||||
|
if (next === this.page) return;
|
||||||
|
this.page = next;
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
prevPage() {
|
||||||
|
this.goToPage(this.page - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPage() {
|
||||||
|
this.goToPage(this.page + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Carregamento
|
||||||
|
// =========================
|
||||||
|
private loadClients() {
|
||||||
|
this.sub.add(
|
||||||
|
this.billingService.getClients(this.tipo).subscribe({
|
||||||
|
next: (list: string[]) => {
|
||||||
|
this.clients = list ?? [];
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.clients = [];
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
this.errorMessage = null;
|
||||||
|
|
||||||
|
this.sub.add(
|
||||||
|
this.billingService
|
||||||
|
.getPaged({
|
||||||
|
tipo: this.tipo,
|
||||||
|
search: this.search,
|
||||||
|
client: this.client,
|
||||||
|
page: this.page,
|
||||||
|
pageSize: this.pageSize,
|
||||||
|
sortBy: this.sortBy,
|
||||||
|
sortDir: this.sortDir,
|
||||||
|
})
|
||||||
|
.subscribe({
|
||||||
|
next: (res: PagedResult<BillingItem>) => {
|
||||||
|
this.result = res ?? { page: this.page, pageSize: this.pageSize, total: 0, items: [] };
|
||||||
|
this.page = this.result.page ?? this.page;
|
||||||
|
this.pageSize = this.result.pageSize ?? this.pageSize;
|
||||||
|
},
|
||||||
|
error: (err: any) => {
|
||||||
|
this.errorMessage = err?.error?.message || 'Erro ao carregar faturamento.';
|
||||||
|
this.result = { page: this.page, pageSize: this.pageSize, total: 0, items: [] };
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Helpers de display
|
||||||
|
// =========================
|
||||||
|
brl(v: number | null | undefined): string {
|
||||||
|
const n = typeof v === 'number' ? v : 0;
|
||||||
|
return n.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
|
||||||
|
}
|
||||||
|
|
||||||
|
num(v: number | null | undefined): string {
|
||||||
|
const n = typeof v === 'number' ? v : 0;
|
||||||
|
return n.toLocaleString('pt-BR');
|
||||||
|
}
|
||||||
|
|
||||||
|
trackById(_: number, it: BillingItem) {
|
||||||
|
return it.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,292 @@
|
||||||
|
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 10000;">
|
||||||
|
<div #successToast class="toast text-bg-success border-0 shadow" role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
|
<div class="toast-header border-bottom-0">
|
||||||
|
<strong class="me-auto text-primary">LineGestão</strong>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Fechar"></button>
|
||||||
|
</div>
|
||||||
|
<div class="toast-body bg-white rounded-bottom text-dark">
|
||||||
|
{{ toastMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="mureg-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>
|
||||||
|
<span class="page-blob blob-4" aria-hidden="true"></span>
|
||||||
|
|
||||||
|
<div class="container-mureg">
|
||||||
|
<div class="mureg-card" data-animate>
|
||||||
|
|
||||||
|
<div class="mureg-header">
|
||||||
|
<div class="header-row-top">
|
||||||
|
<div class="title-badge" data-animate>
|
||||||
|
<i class="bi bi-table"></i> MUREG
|
||||||
|
</div>
|
||||||
|
<div class="header-title" data-animate>
|
||||||
|
<h5 class="title mb-0">MUREG</h5>
|
||||||
|
<small class="subtitle">Gestão de registros MUREG</small>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions d-flex gap-2 justify-content-end" data-animate>
|
||||||
|
<button type="button" class="btn btn-brand btn-sm" (click)="onCreate()" [disabled]="loading">
|
||||||
|
<i class="bi bi-plus-circle me-1"></i> Nova Mureg
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mureg-kpis mt-4 animate-fade-in">
|
||||||
|
<div class="kpi">
|
||||||
|
<span class="lbl">Clientes</span>
|
||||||
|
<span class="val">
|
||||||
|
<span *ngIf="loading" class="spinner-border spinner-border-sm text-muted"></span>
|
||||||
|
<span *ngIf="!loading">{{ total || 0 }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="kpi">
|
||||||
|
<span class="lbl">Registros</span>
|
||||||
|
<span class="val">
|
||||||
|
<span *ngIf="loading" class="spinner-border spinner-border-sm text-muted"></span>
|
||||||
|
<span *ngIf="!loading">{{ groupLoadedRecords || 0 }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="kpi">
|
||||||
|
<span class="lbl text-brand">Trocas</span>
|
||||||
|
<span class="val text-brand">
|
||||||
|
<span *ngIf="loading" class="spinner-border spinner-border-sm text-muted"></span>
|
||||||
|
<span *ngIf="!loading">{{ groupTotalTrocas || 0 }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="kpi">
|
||||||
|
<span class="lbl text-success">ICCID</span>
|
||||||
|
<span class="val text-success">
|
||||||
|
<span *ngIf="loading" class="spinner-border spinner-border-sm text-muted"></span>
|
||||||
|
<span *ngIf="!loading">{{ groupTotalIccids || 0 }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls mt-3 mb-2" data-animate>
|
||||||
|
<div class="input-group input-group-sm search-group">
|
||||||
|
<span class="input-group-text"><i class="bi" [class.bi-search]="!loading" [class.bi-hourglass-split]="loading" [class.text-brand]="loading"></i></span>
|
||||||
|
<input class="form-control" placeholder="Pesquisar..." [(ngModel)]="searchTerm" (ngModelChange)="onSearch()" />
|
||||||
|
<button class="btn btn-outline-secondary btn-clear" type="button" (click)="clearSearch()" *ngIf="searchTerm"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-size d-flex align-items-center gap-2">
|
||||||
|
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">Itens por pág:</span>
|
||||||
|
<div class="select-wrapper">
|
||||||
|
<select class="form-select form-select-sm select-glass" [(ngModel)]="pageSize" (change)="onPageSizeChange()" [disabled]="loading">
|
||||||
|
<option [ngValue]="10">10</option>
|
||||||
|
<option [ngValue]="20">20</option>
|
||||||
|
<option [ngValue]="50">50</option>
|
||||||
|
<option [ngValue]="100">100</option>
|
||||||
|
</select>
|
||||||
|
<i class="bi bi-chevron-down select-icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mureg-body">
|
||||||
|
<div class="groups-container">
|
||||||
|
<div class="text-center p-5" *ngIf="loading"><span class="spinner-border text-brand"></span></div>
|
||||||
|
|
||||||
|
<div class="empty-group" *ngIf="!loading && pagedClientGroups.length === 0">
|
||||||
|
Nenhum dado encontrado.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group-list" *ngIf="!loading">
|
||||||
|
<div *ngFor="let g of pagedClientGroups" class="client-group-card" [class.expanded]="expandedGroup === g.cliente">
|
||||||
|
|
||||||
|
<div class="group-header" (click)="toggleGroup(g.cliente)">
|
||||||
|
<div class="group-info">
|
||||||
|
<h6 class="mb-0 fw-bold text-dark td-clip" [title]="g.cliente">{{ g.cliente }}</h6>
|
||||||
|
<div class="group-badges">
|
||||||
|
<span class="badge-pill total">{{ g.total }} Registros</span>
|
||||||
|
<span class="badge-pill swap" *ngIf="g.trocas > 0">{{ g.trocas }} Trocas</span>
|
||||||
|
<span class="badge-pill ok" *ngIf="g.comIccid > 0">{{ g.comIccid }} ICCID</span>
|
||||||
|
<span class="badge-pill warn" *ngIf="g.semIccid > 0">{{ g.semIccid }} Sem ICCID</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="group-toggle-icon"><i class="bi bi-chevron-down"></i></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group-body" *ngIf="expandedGroup === g.cliente">
|
||||||
|
<div class="d-flex justify-content-between align-items-center px-4 py-2 border-bottom bg-white">
|
||||||
|
<small class="text-muted fw-bold">Registros do Cliente</small>
|
||||||
|
<span class="chip-muted"><i class="bi bi-info-circle me-1"></i> Use o botão à direita para editar</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrap inner-table-wrap">
|
||||||
|
<table class="table table-modern align-middle text-center mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ITEM</th>
|
||||||
|
<th>LINHA ANTIGA</th>
|
||||||
|
<th>LINHA NOVA</th>
|
||||||
|
<th>ICCID</th>
|
||||||
|
<th>DATA MUREG</th>
|
||||||
|
<th>SITUAÇÃO</th>
|
||||||
|
<th style="min-width: 80px;">AÇÕES</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngIf="groupRows.length === 0">
|
||||||
|
<td colspan="7" class="text-center py-4 empty-state text-muted fw-bold">Nenhum registro.</td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngFor="let r of groupRows; trackBy: trackById" class="table-row-item">
|
||||||
|
<td class="text-muted fw-bold">{{ r.item || '-' }}</td>
|
||||||
|
<td class="text-dark">{{ r.linhaAntiga || '-' }}</td>
|
||||||
|
<td class="fw-black text-blue">{{ r.linhaNova || '-' }}</td>
|
||||||
|
<td class="small font-monospace">{{ r.iccid || '-' }}</td>
|
||||||
|
<td class="text-muted small fw-bold">{{ displayValue('dataDaMureg', r.dataDaMureg) }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-pill" [class.is-swap]="isTroca(r)" [class.is-same]="!isTroca(r)">
|
||||||
|
{{ isTroca(r) ? 'TROCA' : 'SEM TROCA' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-group justify-content-center">
|
||||||
|
<button class="btn-icon primary" (click)="onEditar(r)" title="Editar Registro"><i class="bi bi-pencil-square"></i></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mureg-footer">
|
||||||
|
<div class="small text-muted fw-bold">Mostrando {{ pageStart }}–{{ pageEnd }} de {{ total }} Clientes</div>
|
||||||
|
<nav><ul class="pagination pagination-sm mb-0 pagination-modern">
|
||||||
|
<li class="page-item" [class.disabled]="page === 1 || loading"><button class="page-link" (click)="goToPage(page - 1)">Anterior</button></li>
|
||||||
|
<li class="page-item" *ngFor="let p of pageNumbers" [class.active]="p === page"><button class="page-link" (click)="goToPage(p)">{{ p }}</button></li>
|
||||||
|
<li class="page-item" [class.disabled]="page === totalPages || loading"><button class="page-link" (click)="goToPage(page + 1)">Próxima</button></li>
|
||||||
|
</ul></nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="modal-backdrop-custom" *ngIf="editOpen || createOpen" (click)="closeEdit(); closeCreate()"></div>
|
||||||
|
|
||||||
|
<div class="modal-custom" *ngIf="editOpen">
|
||||||
|
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span>
|
||||||
|
Editar Registro Mureg
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<button class="btn btn-glass btn-sm" (click)="closeEdit()" [disabled]="editSaving">
|
||||||
|
<i class="bi bi-x-lg me-1"></i> Cancelar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-brand btn-sm" (click)="saveEdit()" [disabled]="!editModel || editSaving">
|
||||||
|
<span *ngIf="!editSaving"><i class="bi bi-check2-circle me-1"></i> Salvar</span>
|
||||||
|
<span *ngIf="editSaving"><span class="spinner-border spinner-border-sm me-2"></span> Salvando...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<ng-container *ngIf="editModel; else editLoadingTpl">
|
||||||
|
<div class="details-dashboard">
|
||||||
|
<div class="detail-box w-100">
|
||||||
|
<div class="box-header"><span><i class="bi bi-card-text me-2"></i> Informações</span></div>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Nome do Cliente</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.cliente" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Item</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.item" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Data Mureg</label>
|
||||||
|
<input class="form-control form-control-sm" type="date" [(ngModel)]="editModel.dataDaMureg" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Linha Antiga</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.linhaAntiga" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Linha Nova</label>
|
||||||
|
<input class="form-control form-control-sm fw-bold text-blue" [(ngModel)]="editModel.linhaNova" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>ICCID</label>
|
||||||
|
<input class="form-control form-control-sm font-monospace" [(ngModel)]="editModel.iccid" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #editLoadingTpl><div class="p-5 text-center text-muted">Preparando edição...</div></ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-custom" *ngIf="createOpen">
|
||||||
|
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg brand-soft"><i class="bi bi-plus-lg"></i></span>
|
||||||
|
Nova Mureg
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<button class="btn btn-glass btn-sm" (click)="closeCreate()" [disabled]="createSaving">
|
||||||
|
<i class="bi bi-x-lg me-1"></i> Cancelar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-brand btn-sm" (click)="saveCreate()" [disabled]="createSaving">
|
||||||
|
<span *ngIf="!createSaving"><i class="bi bi-check2-circle me-1"></i> Criar</span>
|
||||||
|
<span *ngIf="createSaving"><span class="spinner-border spinner-border-sm me-2"></span> Salvando...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<div class="details-dashboard">
|
||||||
|
<div class="detail-box w-100">
|
||||||
|
<div class="box-header"><span><i class="bi bi-pencil me-2"></i> Preencha os dados</span></div>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Nome do Cliente <span class="text-danger">*</span></label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="createModel.cliente" placeholder="Nome do Cliente" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Item</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="createModel.item" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Data Mureg</label>
|
||||||
|
<input class="form-control form-control-sm" type="date" [(ngModel)]="createModel.dataDaMureg" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Linha Antiga</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="createModel.linhaAntiga" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Linha Nova <span class="text-danger">*</span></label>
|
||||||
|
<input class="form-control form-control-sm fw-bold text-blue" [(ngModel)]="createModel.linhaNova" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>ICCID</label>
|
||||||
|
<input class="form-control form-control-sm font-monospace" [(ngModel)]="createModel.iccid" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,310 @@
|
||||||
|
/* ========================================================== */
|
||||||
|
/* VARIÁVEIS E GERAL */
|
||||||
|
/* ========================================================== */
|
||||||
|
:host {
|
||||||
|
--brand: #E33DCF;
|
||||||
|
--blue: #030FAA;
|
||||||
|
--text: #111214;
|
||||||
|
--muted: rgba(17, 18, 20, 0.65);
|
||||||
|
|
||||||
|
--success-bg: rgba(25, 135, 84, 0.1);
|
||||||
|
--success-text: #198754;
|
||||||
|
--warn-bg: rgba(255, 193, 7, 0.15);
|
||||||
|
--warn-text: #b58100;
|
||||||
|
|
||||||
|
--radius-xl: 22px;
|
||||||
|
--radius-lg: 16px;
|
||||||
|
--radius-md: 12px;
|
||||||
|
--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);
|
||||||
|
|
||||||
|
--swap-bg: rgba(227, 61, 207, 0.12);
|
||||||
|
--swap-text: #E33DCF;
|
||||||
|
--same-bg: rgba(3, 15, 170, 0.08);
|
||||||
|
--same-text: #030FAA;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
color: var(--text);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================================== */
|
||||||
|
/* LAYOUT PRINCIPAL */
|
||||||
|
/* ========================================================== */
|
||||||
|
.mureg-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 0 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
background:
|
||||||
|
radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.14), transparent 60%),
|
||||||
|
radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.08), transparent 60%),
|
||||||
|
linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%);
|
||||||
|
&::after {
|
||||||
|
content: ''; position: absolute; inset: 0; pointer-events: none;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-blob {
|
||||||
|
position: fixed; pointer-events: none; border-radius: 999px;
|
||||||
|
filter: blur(34px); opacity: 0.55; z-index: 0;
|
||||||
|
background: radial-gradient(circle at 30% 30%, rgba(227,61,207,0.55), rgba(227,61,207,0.06));
|
||||||
|
animation: floaty 10s ease-in-out infinite;
|
||||||
|
&.blob-1 { width: 420px; height: 420px; top: -140px; left: -140px; }
|
||||||
|
&.blob-2 { width: 520px; height: 520px; top: -220px; right: -240px; animation-duration: 12s; }
|
||||||
|
&.blob-3 { width: 360px; height: 360px; bottom: -180px; left: 25%; animation-duration: 14s; }
|
||||||
|
&.blob-4 { width: 520px; height: 520px; bottom: -260px; right: -260px; animation-duration: 16s; opacity: .45; }
|
||||||
|
}
|
||||||
|
@keyframes floaty {
|
||||||
|
0% { transform: translate(0, 0) scale(1); }
|
||||||
|
50% { transform: translate(18px, 10px) scale(1.03); }
|
||||||
|
100% { transform: translate(0, 0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-mureg {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1180px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mureg-card {
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--glass-bg);
|
||||||
|
border: var(--glass-border);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
position: relative;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
max-height: calc(100vh - 80px);
|
||||||
|
&::before {
|
||||||
|
content: ''; position: absolute; inset: 1px; border-radius: calc(var(--radius-xl) - 1px);
|
||||||
|
pointer-events: none; border: 1px solid rgba(255, 255, 255, 0.65); opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mureg-header {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid rgba(17, 18, 20, 0.06);
|
||||||
|
background: linear-gradient(180deg, rgba(227,61,207,0.06), rgba(255,255,255,0.2));
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row-top {
|
||||||
|
display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px;
|
||||||
|
@media (max-width: 768px) { grid-template-columns: 1fr; text-align: center; gap: 16px; .title-badge { justify-self: center; margin-bottom: 8px; } .header-actions { justify-self: center; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-badge {
|
||||||
|
justify-self: start; display: inline-flex; align-items: center; gap: 10px; padding: 6px 12px;
|
||||||
|
border-radius: 999px; background: rgba(255, 255, 255, 0.78);
|
||||||
|
border: 1px solid rgba(227, 61, 207, 0.22); backdrop-filter: blur(10px);
|
||||||
|
color: var(--text); font-size: 13px; font-weight: 800;
|
||||||
|
i { color: var(--brand); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title { justify-self: center; display: flex; flex-direction: column; align-items: center; text-align: center; }
|
||||||
|
.title { font-size: 26px; font-weight: 950; letter-spacing: -0.3px; color: var(--text); margin-top: 10px; margin-bottom: 0; }
|
||||||
|
.subtitle { color: rgba(17, 18, 20, 0.65); font-weight: 700; }
|
||||||
|
.header-actions { justify-self: end; }
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn-brand {
|
||||||
|
background-color: var(--brand); border-color: var(--brand); color: #fff; font-weight: 900; border-radius: 12px; transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
&:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(227, 61, 207, 0.25); filter: brightness(1.05); }
|
||||||
|
&:disabled { opacity: 0.7; cursor: not-allowed; transform: none; }
|
||||||
|
}
|
||||||
|
.btn-glass {
|
||||||
|
border-radius: 12px; font-weight: 900; background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(3, 15, 170, 0.25); color: var(--blue);
|
||||||
|
&:hover { transform: translateY(-2px); border-color: var(--brand); background: #fff; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* KPIs */
|
||||||
|
.mureg-kpis {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi {
|
||||||
|
background: rgba(255,255,255,0.7);
|
||||||
|
border: 1px solid rgba(17,18,20,0.08);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.02);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 15px rgba(227, 61, 207, 0.1);
|
||||||
|
background: #fff;
|
||||||
|
border-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lbl {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
&.text-success { color: var(--success-text) !important; }
|
||||||
|
&.text-brand { color: var(--brand) !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.val {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 950;
|
||||||
|
color: var(--text);
|
||||||
|
&.text-success { color: var(--success-text) !important; }
|
||||||
|
&.text-brand { color: var(--brand) !important; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Controls */
|
||||||
|
.controls { display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap; }
|
||||||
|
.search-group {
|
||||||
|
max-width: 270px; border-radius: 12px; overflow: hidden; display: flex; align-items: stretch; background: #fff; border: 1px solid rgba(17, 18, 20, 0.15); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); transition: all 0.2s ease;
|
||||||
|
&:focus-within { border-color: var(--brand); box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15); transform: translateY(-1px); }
|
||||||
|
.input-group-text { background: transparent; border: none; color: var(--muted); padding-left: 14px; padding-right: 8px; display: flex; align-items: center; i { font-size: 1rem; } }
|
||||||
|
.form-control { border: none; background: transparent; padding: 10px 0; font-size: 0.9rem; color: var(--text); box-shadow: none; &::placeholder { color: rgba(17, 18, 20, 0.4); font-weight: 500; } &:focus { outline: none; } }
|
||||||
|
.btn-clear { background: transparent; border: none; color: var(--muted); padding: 0 12px; display: flex; align-items: center; cursor: pointer; transition: color 0.2s; &:hover { color: #dc3545; } i { font-size: 1rem; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size { margin-left: auto; @media (max-width: 500px) { margin-left: 0; width: 100%; justify-content: space-between; } }
|
||||||
|
.select-wrapper { position: relative; display: inline-block; min-width: 90px; }
|
||||||
|
.select-glass {
|
||||||
|
appearance: none; -webkit-appearance: none; -moz-appearance: none;
|
||||||
|
background: rgba(255, 255, 255, 0.7); border: 1px solid rgba(17, 18, 20, 0.15); border-radius: 12px;
|
||||||
|
color: var(--blue); font-weight: 800; font-size: 0.9rem; text-align: left;
|
||||||
|
padding: 8px 36px 8px 14px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); cursor: pointer; transition: all 0.2s ease; width: 100%;
|
||||||
|
&:hover { background: #fff; border-color: var(--blue); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(3, 15, 170, 0.1); }
|
||||||
|
&:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.15); }
|
||||||
|
}
|
||||||
|
.select-icon { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--muted); font-size: 0.75rem; transition: transform 0.2s ease; }
|
||||||
|
.select-wrapper:hover .select-icon { color: var(--blue); }
|
||||||
|
|
||||||
|
/* Animation */
|
||||||
|
.animate-fade-in { animation: simpleFadeIn 0.5s ease-out forwards; }
|
||||||
|
@keyframes simpleFadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
|
||||||
|
/* BODY & GROUPS */
|
||||||
|
.mureg-body { padding: 0; background: transparent; flex: 1; overflow: hidden; display: flex; flex-direction: column; }
|
||||||
|
.groups-container { padding: 16px; overflow-y: auto; height: 100%; }
|
||||||
|
.group-list { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
.empty-group { background: rgba(255,255,255,0.7); border: 1px dashed rgba(17,18,20,0.12); border-radius: 16px; padding: 18px; text-align: center; font-weight: 800; color: var(--muted); }
|
||||||
|
|
||||||
|
.client-group-card {
|
||||||
|
background: #fff; border-radius: 16px; border: 1px solid rgba(17,18,20,0.08); overflow: hidden; transition: all 0.3s ease;
|
||||||
|
&:hover { border-color: var(--brand); box-shadow: 0 4px 12px rgba(227,61,207,0.1); }
|
||||||
|
&.expanded { border-color: var(--brand); box-shadow: 0 8px 24px rgba(227,61,207,0.12); }
|
||||||
|
}
|
||||||
|
.group-header { padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; background: linear-gradient(180deg, #fff, #fdfdfd); &:hover .group-toggle-icon { color: var(--brand); } }
|
||||||
|
.group-info { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.group-badges { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.badge-pill { font-size: 0.7rem; padding: 4px 10px; border-radius: 999px; font-weight: 800; text-transform: uppercase;
|
||||||
|
&.total { background: rgba(3,15,170,0.1); color: var(--blue); }
|
||||||
|
&.swap { background: var(--swap-bg); color: var(--swap-text); }
|
||||||
|
&.ok { background: var(--success-bg); color: var(--success-text); }
|
||||||
|
&.warn { background: var(--warn-bg); color: var(--warn-text); }
|
||||||
|
}
|
||||||
|
.group-toggle-icon { font-size: 1.2rem; color: var(--muted); transition: transform 0.3s ease; }
|
||||||
|
.client-group-card.expanded .group-toggle-icon { transform: rotate(180deg); color: var(--brand); }
|
||||||
|
.group-body { border-top: 1px solid rgba(17,18,20,0.06); background: #fbfbfc; animation: slideDown 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
|
||||||
|
@keyframes slideDown { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
|
||||||
|
.chip-muted { display: inline-flex; align-items: center; gap: 6px; font-size: 0.75rem; font-weight: 800; color: rgba(17,18,20,0.55); padding: 4px 10px; border-radius: 999px; background: rgba(17,18,20,0.04); border: 1px solid rgba(17,18,20,0.06); }
|
||||||
|
.inner-table-wrap { max-height: 450px; overflow-y: auto; }
|
||||||
|
|
||||||
|
/* TABLE */
|
||||||
|
.table-wrap { overflow-x: auto; overflow-y: auto; height: 100%; }
|
||||||
|
.table-modern {
|
||||||
|
width: 100%; min-width: 1000px; border-collapse: separate; border-spacing: 0;
|
||||||
|
thead th { position: sticky; top: 0; z-index: 10; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(8px); border-bottom: 2px solid rgba(227, 61, 207, 0.15); padding: 12px; color: rgba(17, 18, 20, 0.7); font-size: 0.8rem; font-weight: 950; letter-spacing: 0.05em; text-transform: uppercase; white-space: nowrap; cursor: pointer; transition: color 0.2s; text-align: center !important; &:hover { color: var(--brand); } }
|
||||||
|
tbody tr { transition: background-color 0.2s; border-bottom: 1px solid rgba(17,18,20,0.05); &:hover { background-color: rgba(227, 61, 207, 0.05); } td { border-bottom: 1px solid rgba(17,18,20,0.04); } }
|
||||||
|
td { padding: 12px; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.875rem; color: var(--text); text-align: center !important; }
|
||||||
|
}
|
||||||
|
.row-clickable { cursor: default; }
|
||||||
|
.text-brand { color: var(--brand) !important; }
|
||||||
|
.text-blue { color: var(--blue) !important; }
|
||||||
|
.fw-black { font-weight: 950; }
|
||||||
|
.td-clip { overflow: hidden; text-overflow: ellipsis; max-width: 250px; }
|
||||||
|
.empty-state { background: rgba(255,255,255,0.4); }
|
||||||
|
|
||||||
|
.status-pill { display: inline-block; vertical-align: middle; max-width: 100%; padding: 6px 14px; border-radius: 999px; font-weight: 950; font-size: 0.75rem; letter-spacing: 0.3px; text-align: center; text-transform: uppercase; border: 1px solid rgba(17,18,20,0.1); background: rgba(17,18,20,0.05); color: rgba(17,18,20,0.7);
|
||||||
|
&.is-swap { background: var(--swap-bg); border-color: rgba(227,61,207,0.25); color: var(--swap-text); }
|
||||||
|
&.is-same { background: var(--same-bg); border-color: rgba(3,15,170,0.25); color: var(--same-text); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ACTIONS */
|
||||||
|
.action-group { display: flex; justify-content: center; gap: 6px; }
|
||||||
|
.btn-icon {
|
||||||
|
width: 32px; height: 32px; border: none; background: transparent; border-radius: 8px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
color: rgba(17,18,20,0.5); transition: all 0.2s; cursor: pointer;
|
||||||
|
&:hover { background: rgba(17,18,20,0.05); color: var(--text); transform: translateY(-1px); }
|
||||||
|
&.primary:hover { color: var(--blue); background: rgba(3,15,170,0.1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FOOTER */
|
||||||
|
.mureg-footer { padding: 14px 24px; border-top: 1px solid rgba(17, 18, 20, 0.06); display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; flex-shrink: 0; @media (max-width: 768px) { justify-content: center; text-align: center; } }
|
||||||
|
.pagination-modern .page-link { color: var(--blue); font-weight: 900; border-radius: 10px; border: 1px solid rgba(17,18,20,0.1); background: rgba(255,255,255,0.6); margin: 0 2px; &:hover { transform: translateY(-1px); border-color: var(--brand); color: var(--brand); } }
|
||||||
|
.pagination-modern .page-item.active .page-link { background-color: var(--blue); border-color: var(--blue); color: #fff; }
|
||||||
|
|
||||||
|
/* MODALS */
|
||||||
|
.modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); }
|
||||||
|
.modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; }
|
||||||
|
.modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; }
|
||||||
|
@keyframes modalPop { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
||||||
|
.modal-header { padding: 16px 24px; border-bottom: 1px solid rgba(0,0,0,0.06); background: #fff; display: flex; justify-content: space-between; align-items: center; .modal-title { font-size: 1.1rem; font-weight: 800; color: var(--text); display: flex; align-items: center; gap: 12px; }
|
||||||
|
.icon-bg { width: 32px; height: 32px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 16px;
|
||||||
|
&.primary-soft { background: rgba(3, 15, 170, 0.1); color: var(--blue); }
|
||||||
|
&.brand-soft { background: rgba(227, 61, 207, 0.1); color: var(--brand); } /* Adicionado */
|
||||||
|
}
|
||||||
|
.btn-icon { color: var(--muted); background: transparent; font-size: 1.2rem; border:none; cursor: pointer; &:hover { color: var(--brand); } }
|
||||||
|
}
|
||||||
|
.modal-body { padding: 24px; overflow-y: auto; &.bg-light-gray { background-color: #f8f9fa; } }
|
||||||
|
|
||||||
|
/* FORM & DETAILS */
|
||||||
|
.details-dashboard { display: grid; grid-template-columns: 1fr; gap: 20px; }
|
||||||
|
div.detail-box { background: #fff; border-radius: 16px; border: 1px solid rgba(0,0,0,0.05); box-shadow: 0 2px 8px rgba(0,0,0,0.02); overflow: hidden; height: 100%; display: flex; flex-direction: column; }
|
||||||
|
div.box-header { padding: 10px 16px; font-size: 0.8rem; font-weight: 800; text-transform: uppercase; color: var(--muted); border-bottom: 1px solid rgba(0,0,0,0.04); background: #fdfdfd; display: flex; align-items: center; }
|
||||||
|
div.box-body { padding: 16px; }
|
||||||
|
|
||||||
|
/* EDIT FORM STYLES */
|
||||||
|
.form-grid {
|
||||||
|
display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
|
||||||
|
@media (max-width: 600px) { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
.form-field {
|
||||||
|
display: flex; flex-direction: column; gap: 6px;
|
||||||
|
label { font-size: 0.75rem; font-weight: 900; letter-spacing: 0.04em; text-transform: uppercase; color: rgba(17,18,20,0.65); }
|
||||||
|
&.span-2 { grid-column: span 2; @media (max-width: 600px) { grid-column: span 1; } }
|
||||||
|
}
|
||||||
|
.form-control {
|
||||||
|
border-radius: 8px; border: 1px solid rgba(17,18,20,0.15);
|
||||||
|
&:focus { border-color: var(--brand); box-shadow: 0 0 0 2px rgba(227,61,207,0.15); outline: none; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,433 @@
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
ViewChild,
|
||||||
|
Inject,
|
||||||
|
PLATFORM_ID,
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectorRef
|
||||||
|
} from '@angular/core';
|
||||||
|
import { isPlatformBrowser, CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import {
|
||||||
|
HttpClient,
|
||||||
|
HttpClientModule,
|
||||||
|
HttpParams
|
||||||
|
} from '@angular/common/http';
|
||||||
|
|
||||||
|
type MuregKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataDaMureg' | 'cliente';
|
||||||
|
|
||||||
|
interface MuregRow {
|
||||||
|
id: string;
|
||||||
|
item: string;
|
||||||
|
linhaAntiga: string;
|
||||||
|
linhaNova: string;
|
||||||
|
iccid: string;
|
||||||
|
dataDaMureg: string;
|
||||||
|
cliente: string;
|
||||||
|
raw: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiPagedResult<T> {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
total?: number;
|
||||||
|
items?: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClientGroup {
|
||||||
|
cliente: string;
|
||||||
|
total: number;
|
||||||
|
trocas: number;
|
||||||
|
comIccid: number;
|
||||||
|
semIccid: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule, HttpClientModule],
|
||||||
|
templateUrl: './mureg.html',
|
||||||
|
styleUrls: ['./mureg.scss']
|
||||||
|
})
|
||||||
|
export class Mureg implements AfterViewInit {
|
||||||
|
toastMessage = '';
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(PLATFORM_ID) private platformId: object,
|
||||||
|
private http: HttpClient,
|
||||||
|
private cdr: ChangeDetectorRef
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private readonly apiBase = 'https://localhost:7205/api/mureg';
|
||||||
|
|
||||||
|
// ====== DATA ======
|
||||||
|
clientGroups: ClientGroup[] = [];
|
||||||
|
pagedClientGroups: ClientGroup[] = [];
|
||||||
|
expandedGroup: string | null = null;
|
||||||
|
groupRows: MuregRow[] = [];
|
||||||
|
|
||||||
|
private rowsByClient = new Map<string, MuregRow[]>();
|
||||||
|
|
||||||
|
// KPIs
|
||||||
|
groupLoadedRecords = 0;
|
||||||
|
groupTotalTrocas = 0;
|
||||||
|
groupTotalIccids = 0;
|
||||||
|
|
||||||
|
// ====== FILTERS & PAGINATION ======
|
||||||
|
searchTerm = '';
|
||||||
|
private searchTimer: any = null;
|
||||||
|
page = 1;
|
||||||
|
pageSize = 10;
|
||||||
|
total = 0;
|
||||||
|
|
||||||
|
// ====== EDIT MODAL ======
|
||||||
|
editOpen = false;
|
||||||
|
editSaving = false;
|
||||||
|
editModel: any = null;
|
||||||
|
|
||||||
|
// ====== CREATE MODAL ======
|
||||||
|
createOpen = false;
|
||||||
|
createSaving = false;
|
||||||
|
createModel: any = {
|
||||||
|
cliente: '',
|
||||||
|
item: '',
|
||||||
|
linhaAntiga: '',
|
||||||
|
linhaNova: '',
|
||||||
|
iccid: '',
|
||||||
|
dataDaMureg: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
async ngAfterViewInit() {
|
||||||
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
this.initAnimations();
|
||||||
|
setTimeout(() => { this.refresh(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
private initAnimations() {
|
||||||
|
document.documentElement.classList.add('js-animate');
|
||||||
|
setTimeout(() => {
|
||||||
|
const items = document.querySelectorAll<HTMLElement>('[data-animate]');
|
||||||
|
items.forEach((el) => el.classList.add('is-visible'));
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.page = 1;
|
||||||
|
this.loadForGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch() {
|
||||||
|
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||||
|
this.searchTimer = setTimeout(() => {
|
||||||
|
this.page = 1;
|
||||||
|
this.expandedGroup = null;
|
||||||
|
this.groupRows = [];
|
||||||
|
this.loadForGroups();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSearch() {
|
||||||
|
this.searchTerm = '';
|
||||||
|
this.page = 1;
|
||||||
|
this.expandedGroup = null;
|
||||||
|
this.groupRows = [];
|
||||||
|
this.loadForGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageSizeChange() {
|
||||||
|
this.page = 1;
|
||||||
|
this.applyPagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
goToPage(p: number) {
|
||||||
|
this.page = Math.max(1, Math.min(this.totalPages, p));
|
||||||
|
this.applyPagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalPages() { return Math.ceil((this.total || 0) / this.pageSize) || 1; }
|
||||||
|
get pageNumbers() {
|
||||||
|
const total = this.totalPages;
|
||||||
|
const current = this.page;
|
||||||
|
const max = 5;
|
||||||
|
let start = Math.max(1, current - 2);
|
||||||
|
let end = Math.min(total, start + (max - 1));
|
||||||
|
start = Math.max(1, end - (max - 1));
|
||||||
|
const pages: number[] = [];
|
||||||
|
for (let i = start; i <= end; i++) pages.push(i);
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
get pageStart() { return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; }
|
||||||
|
get pageEnd() {
|
||||||
|
if (this.total === 0) return 0;
|
||||||
|
return Math.min(this.page * this.pageSize, this.total);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackById(_: number, row: MuregRow) { return row.id; }
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// LOAD LOGIC
|
||||||
|
// =======================================================================
|
||||||
|
private loadForGroups() {
|
||||||
|
this.loading = true;
|
||||||
|
const MAX_FETCH = 5000;
|
||||||
|
|
||||||
|
let params = new HttpParams()
|
||||||
|
.set('page', '1')
|
||||||
|
.set('pageSize', String(MAX_FETCH))
|
||||||
|
.set('search', (this.searchTerm ?? '').trim())
|
||||||
|
.set('sortBy', 'cliente')
|
||||||
|
.set('sortDir', 'asc');
|
||||||
|
|
||||||
|
this.http.get<ApiPagedResult<any> | any[]>(this.apiBase, { params }).subscribe({
|
||||||
|
next: (res: any) => {
|
||||||
|
const items = Array.isArray(res) ? res : (res.items ?? []);
|
||||||
|
const normalized = (items ?? []).map((x: any, idx: number) => this.normalizeRow(x, idx));
|
||||||
|
this.buildGroups(normalized);
|
||||||
|
this.applyPagination();
|
||||||
|
this.loading = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
},
|
||||||
|
error: async () => {
|
||||||
|
this.loading = false;
|
||||||
|
await this.showToast('Erro ao carregar MUREG.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildGroups(all: MuregRow[]) {
|
||||||
|
this.rowsByClient.clear();
|
||||||
|
const safeClient = (c: any) => (String(c ?? '').trim() || 'SEM CLIENTE');
|
||||||
|
|
||||||
|
for (const r of all) {
|
||||||
|
const key = safeClient(r.cliente);
|
||||||
|
r.cliente = key;
|
||||||
|
const arr = this.rowsByClient.get(key) ?? [];
|
||||||
|
arr.push(r);
|
||||||
|
this.rowsByClient.set(key, arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: ClientGroup[] = [];
|
||||||
|
let trocasTotal = 0;
|
||||||
|
let iccidsTotal = 0;
|
||||||
|
|
||||||
|
this.rowsByClient.forEach((arr, cliente) => {
|
||||||
|
const total = arr.length;
|
||||||
|
const trocas = arr.filter(x => this.isTroca(x)).length;
|
||||||
|
const comIccid = arr.filter(x => String(x.iccid ?? '').trim() !== '').length;
|
||||||
|
const semIccid = total - comIccid;
|
||||||
|
trocasTotal += trocas;
|
||||||
|
iccidsTotal += comIccid;
|
||||||
|
groups.push({ cliente, total, trocas, comIccid, semIccid });
|
||||||
|
});
|
||||||
|
|
||||||
|
groups.sort((a, b) => a.cliente.localeCompare(b.cliente, 'pt-BR', { sensitivity: 'base' }));
|
||||||
|
|
||||||
|
this.clientGroups = groups;
|
||||||
|
this.total = groups.length;
|
||||||
|
this.groupLoadedRecords = all.length;
|
||||||
|
this.groupTotalTrocas = trocasTotal;
|
||||||
|
this.groupTotalIccids = iccidsTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyPagination() {
|
||||||
|
const start = (this.page - 1) * this.pageSize;
|
||||||
|
const end = start + this.pageSize;
|
||||||
|
this.pagedClientGroups = this.clientGroups.slice(start, end);
|
||||||
|
if (this.expandedGroup && !this.pagedClientGroups.some(g => g.cliente === this.expandedGroup)) {
|
||||||
|
this.expandedGroup = null;
|
||||||
|
this.groupRows = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleGroup(cliente: string) {
|
||||||
|
if (this.expandedGroup === cliente) {
|
||||||
|
this.expandedGroup = null;
|
||||||
|
this.groupRows = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.expandedGroup = cliente;
|
||||||
|
const rows = this.rowsByClient.get(cliente) ?? [];
|
||||||
|
this.groupRows = [...rows].sort((a, b) => {
|
||||||
|
const ai = parseInt(String(a.item ?? '0'), 10);
|
||||||
|
const bi = parseInt(String(b.item ?? '0'), 10);
|
||||||
|
if (Number.isFinite(ai) && Number.isFinite(bi) && ai !== bi) return ai - bi;
|
||||||
|
return String(a.linhaNova ?? '').localeCompare(String(b.linhaNova ?? ''), 'pt-BR', { sensitivity: 'base' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isTroca(r: MuregRow): boolean {
|
||||||
|
const a = String(r.linhaAntiga ?? '').trim();
|
||||||
|
const b = String(r.linhaNova ?? '').trim();
|
||||||
|
if (!a || !b) return false;
|
||||||
|
return a !== b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeRow(x: any, idx: number): MuregRow {
|
||||||
|
const pick = (obj: any, keys: string[]): any => {
|
||||||
|
for (const k of keys) {
|
||||||
|
if (obj && obj[k] !== undefined && obj[k] !== null && String(obj[k]).trim() !== '') return obj[k];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const item = pick(x, ['item', 'ITEM', 'ITÉM']);
|
||||||
|
const linhaAntiga = pick(x, ['linhaAntiga', 'linha_antiga', 'LINHA ANTIGA']);
|
||||||
|
const linhaNova = pick(x, ['linhaNova', 'linha_nova', 'LINHA NOVA']);
|
||||||
|
const iccid = pick(x, ['iccid', 'ICCID']);
|
||||||
|
const dataDaMureg = pick(x, ['dataDaMureg', 'data_da_mureg', 'DATA DA MUREG']);
|
||||||
|
const cliente = pick(x, ['cliente', 'CLIENTE']);
|
||||||
|
const id = String(pick(x, ['id', 'ID']) || `${idx}-${item}-${linhaNova}-${iccid}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
item: String(item ?? ''),
|
||||||
|
linhaAntiga: String(linhaAntiga ?? ''),
|
||||||
|
linhaNova: String(linhaNova ?? ''),
|
||||||
|
iccid: String(iccid ?? ''),
|
||||||
|
dataDaMureg: String(dataDaMureg ?? ''),
|
||||||
|
cliente: String(cliente ?? ''),
|
||||||
|
raw: x
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== MODAL EDIÇÃO ======
|
||||||
|
|
||||||
|
// 1. Abrir modal
|
||||||
|
onEditar(r: MuregRow) {
|
||||||
|
this.editOpen = true;
|
||||||
|
this.editSaving = false;
|
||||||
|
|
||||||
|
this.editModel = {
|
||||||
|
id: r.id,
|
||||||
|
item: r.item,
|
||||||
|
linhaAntiga: r.linhaAntiga,
|
||||||
|
linhaNova: r.linhaNova,
|
||||||
|
iccid: r.iccid,
|
||||||
|
cliente: r.cliente,
|
||||||
|
dataDaMureg: this.isoToDateInput(r.dataDaMureg)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fechar modal
|
||||||
|
closeEdit() {
|
||||||
|
this.editOpen = false;
|
||||||
|
this.editModel = null;
|
||||||
|
this.editSaving = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Salvar (PUT)
|
||||||
|
saveEdit() {
|
||||||
|
if(!this.editModel || !this.editModel.id) return;
|
||||||
|
this.editSaving = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...this.editModel,
|
||||||
|
dataDaMureg: this.dateInputToIso(this.editModel.dataDaMureg)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.http.put(`${this.apiBase}/${this.editModel.id}`, payload).subscribe({
|
||||||
|
next: async () => {
|
||||||
|
this.editSaving = false;
|
||||||
|
await this.showToast('Registro atualizado com sucesso!');
|
||||||
|
this.closeEdit();
|
||||||
|
const currentGroup = this.expandedGroup;
|
||||||
|
this.loadForGroups();
|
||||||
|
if(currentGroup) setTimeout(() => this.expandedGroup = currentGroup, 400);
|
||||||
|
},
|
||||||
|
error: async () => {
|
||||||
|
this.editSaving = false;
|
||||||
|
await this.showToast('Erro ao salvar edição.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== MODAL CRIAÇÃO ======
|
||||||
|
|
||||||
|
onCreate() {
|
||||||
|
this.createOpen = true;
|
||||||
|
this.createSaving = false;
|
||||||
|
this.createModel = {
|
||||||
|
cliente: '',
|
||||||
|
item: '',
|
||||||
|
linhaAntiga: '',
|
||||||
|
linhaNova: '',
|
||||||
|
iccid: '',
|
||||||
|
dataDaMureg: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCreate() {
|
||||||
|
this.createOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCreate() {
|
||||||
|
if(!this.createModel.cliente || !this.createModel.linhaNova) {
|
||||||
|
this.showToast('Preencha Cliente e Linha Nova.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.createSaving = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...this.createModel,
|
||||||
|
dataDaMureg: this.dateInputToIso(this.createModel.dataDaMureg)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.http.post(this.apiBase, payload).subscribe({
|
||||||
|
next: async () => {
|
||||||
|
this.createSaving = false;
|
||||||
|
await this.showToast('Mureg criada com sucesso!');
|
||||||
|
this.closeCreate();
|
||||||
|
this.loadForGroups();
|
||||||
|
},
|
||||||
|
error: async () => {
|
||||||
|
this.createSaving = false;
|
||||||
|
await this.showToast('Erro ao criar Mureg.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers de Data
|
||||||
|
private isoToDateInput(iso: string | null | undefined): string {
|
||||||
|
if(!iso) return '';
|
||||||
|
const dt = new Date(iso);
|
||||||
|
if(Number.isNaN(dt.getTime())) return '';
|
||||||
|
return dt.toISOString().slice(0,10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private dateInputToIso(val: string | null | undefined): string | null {
|
||||||
|
if(!val) return null;
|
||||||
|
const dt = new Date(val);
|
||||||
|
if(Number.isNaN(dt.getTime())) return null;
|
||||||
|
return dt.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
displayValue(key: MuregKey, v: any): string {
|
||||||
|
if (v === null || v === undefined || String(v).trim() === '') return '-';
|
||||||
|
if (key === 'dataDaMureg') {
|
||||||
|
const s = String(v).trim();
|
||||||
|
const d = new Date(s);
|
||||||
|
if (!Number.isNaN(d.getTime())) {
|
||||||
|
return new Intl.DateTimeFormat('pt-BR').format(d);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showToast(message: string) {
|
||||||
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
this.toastMessage = message;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
if (!this.successToast?.nativeElement) return;
|
||||||
|
try {
|
||||||
|
const bs = await import('bootstrap');
|
||||||
|
const toastInstance = bs.Toast.getOrCreateInstance(this.successToast.nativeElement, { autohide: true, delay: 3000 });
|
||||||
|
toastInstance.show();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Billing } from './billing';
|
||||||
|
|
||||||
|
describe('Billing', () => {
|
||||||
|
let service: Billing;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(Billing);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
// src/app/services/billing.ts
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
export type SortDir = 'asc' | 'desc';
|
||||||
|
export type TipoCliente = 'PF' | 'PJ';
|
||||||
|
|
||||||
|
export type BillingSortBy =
|
||||||
|
| 'item'
|
||||||
|
| 'cliente'
|
||||||
|
| 'qtdlinhas'
|
||||||
|
| 'lucro'
|
||||||
|
| 'valorcontratovivo'
|
||||||
|
| 'valorcontratoline'
|
||||||
|
| 'franquiavivo'
|
||||||
|
| 'franquialine';
|
||||||
|
|
||||||
|
export interface PagedResult<T> {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
items: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BillingItem {
|
||||||
|
id: string;
|
||||||
|
tipo: TipoCliente;
|
||||||
|
item: number;
|
||||||
|
|
||||||
|
cliente: string | null;
|
||||||
|
|
||||||
|
qtdLinhas: number | null;
|
||||||
|
|
||||||
|
franquiaVivo: number | null;
|
||||||
|
valorContratoVivo: number | null;
|
||||||
|
|
||||||
|
franquiaLine: number | null;
|
||||||
|
valorContratoLine: number | null;
|
||||||
|
|
||||||
|
lucro: number | null;
|
||||||
|
|
||||||
|
aparelho: string | null;
|
||||||
|
formaPagamento: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BillingQuery {
|
||||||
|
tipo: TipoCliente;
|
||||||
|
search?: string;
|
||||||
|
client?: string;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
sortBy?: BillingSortBy;
|
||||||
|
sortDir?: SortDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class BillingService {
|
||||||
|
// ✅ Use HTTPS pra evitar redirect no preflight (CORS)
|
||||||
|
// Mesmas portas que você mostrou no log:
|
||||||
|
// https://localhost:7205
|
||||||
|
// http://localhost:5298
|
||||||
|
private baseUrl = 'https://localhost:7205/api/billing';
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
getPaged(q: BillingQuery): Observable<PagedResult<BillingItem>> {
|
||||||
|
const sortBy: BillingSortBy = (q.sortBy ?? 'cliente');
|
||||||
|
const sortDir: SortDir = (q.sortDir ?? 'asc');
|
||||||
|
|
||||||
|
let params = new HttpParams()
|
||||||
|
.set('tipo', q.tipo)
|
||||||
|
.set('page', String(q.page))
|
||||||
|
.set('pageSize', String(q.pageSize))
|
||||||
|
.set('sortBy', sortBy)
|
||||||
|
.set('sortDir', sortDir);
|
||||||
|
|
||||||
|
const search = (q.search ?? '').trim();
|
||||||
|
if (search) params = params.set('search', search);
|
||||||
|
|
||||||
|
const client = (q.client ?? '').trim();
|
||||||
|
if (client) params = params.set('client', client);
|
||||||
|
|
||||||
|
return this.http.get<PagedResult<BillingItem>>(this.baseUrl, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
getClients(tipo: TipoCliente): Observable<string[]> {
|
||||||
|
const params = new HttpParams().set('tipo', tipo);
|
||||||
|
return this.http.get<string[]>(`${this.baseUrl}/clients`, { params });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -51,17 +51,18 @@ export interface MobileLineDetail extends MobileLineList {
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class LinesService {
|
export class LinesService {
|
||||||
// ajuste aqui conforme sua API (mesmo host do auth)
|
// ✅ Mesma base do Swagger (evita redirect no preflight/CORS)
|
||||||
private baseUrl = 'http://localhost:5000/api/lines';
|
private baseUrl = 'https://localhost:7205/api/lines';
|
||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
getLines(page: number, pageSize: number, search: string): Observable<PagedResult<MobileLineList>> {
|
getLines(page: number, pageSize: number, search: string): Observable<PagedResult<MobileLineList>> {
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
.set('page', page)
|
.set('page', String(page))
|
||||||
.set('pageSize', pageSize);
|
.set('pageSize', String(pageSize));
|
||||||
|
|
||||||
if (search?.trim()) params = params.set('search', search.trim());
|
const s = (search ?? '').trim();
|
||||||
|
if (s) params = params.set('search', s);
|
||||||
|
|
||||||
return this.http.get<PagedResult<MobileLineList>>(this.baseUrl, { params });
|
return this.http.get<PagedResult<MobileLineList>>(this.baseUrl, { params });
|
||||||
}
|
}
|
||||||
|
|
@ -83,4 +84,26 @@ export class LinesService {
|
||||||
form.append('file', file);
|
form.append('file', file);
|
||||||
return this.http.post<{ imported: number }>(`${this.baseUrl}/import-excel`, form);
|
return this.http.post<{ imported: number }>(`${this.baseUrl}/import-excel`, form);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// (opcional) se você usa groups/clients no Geral, pode manter aqui também:
|
||||||
|
getClients(skil?: string): Observable<string[]> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
const s = (skil ?? '').trim();
|
||||||
|
if (s) params = params.set('skil', s);
|
||||||
|
return this.http.get<string[]>(`${this.baseUrl}/clients`, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroups(page: number, pageSize: number, skil?: string, search?: string) {
|
||||||
|
let params = new HttpParams()
|
||||||
|
.set('page', String(page))
|
||||||
|
.set('pageSize', String(pageSize));
|
||||||
|
|
||||||
|
const sk = (skil ?? '').trim();
|
||||||
|
if (sk) params = params.set('skil', sk);
|
||||||
|
|
||||||
|
const se = (search ?? '').trim();
|
||||||
|
if (se) params = params.set('search', se);
|
||||||
|
|
||||||
|
return this.http.get<PagedResult<any>>(`${this.baseUrl}/groups`, { params });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue