feat: refatorado o código envolvendo boas práticas saas e implementação da tela de relatórios
This commit is contained in:
parent
4bbf22152a
commit
3584d4a373
|
|
@ -1,5 +1,8 @@
|
|||
<app-header></app-header>
|
||||
<app-header *ngIf="!isFullScreenPage"></app-header>
|
||||
|
||||
<router-outlet />
|
||||
<main class="app-main" [class.has-header]="!isFullScreenPage">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<app-footer></app-footer>
|
||||
<!-- ✅ Footer some em rotas logadas e também no login/register -->
|
||||
<app-footer *ngIf="!hideFooter"></app-footer>
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ import { authGuard } from './guards/auth.guard';
|
|||
import { DadosUsuarios } from './pages/dados-usuarios/dados-usuarios';
|
||||
import { VigenciaComponent } from './pages/vigencia/vigencia';
|
||||
import { TrocaNumero } from './pages/troca-numero/troca-numero';
|
||||
|
||||
// ✅ NOVO: TROCA DE NÚMERO
|
||||
|
||||
import { Relatorios } from './pages/relatorios/relatorios';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: Home },
|
||||
|
|
@ -25,9 +23,13 @@ export const routes: Routes = [
|
|||
{ path: 'faturamento', component: Faturamento, canActivate: [authGuard] },
|
||||
{ path: 'dadosusuarios', component: DadosUsuarios, canActivate: [authGuard] },
|
||||
{ path: 'vigencia', component: VigenciaComponent, canActivate: [authGuard] },
|
||||
|
||||
// ✅ NOVO: rota da página Troca de Número
|
||||
{ path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard] },
|
||||
|
||||
// ✅ rota correta
|
||||
{ path: 'relatorios', component: Relatorios, canActivate: [authGuard] },
|
||||
|
||||
// ✅ compatibilidade: se alguém acessar /portal/relatorios, manda pra /relatorios
|
||||
{ path: 'portal/relatorios', redirectTo: 'relatorios', pathMatch: 'full' },
|
||||
|
||||
{ path: '**', redirectTo: '' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,16 +1,65 @@
|
|||
import { Component, signal } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { Header } from './components/header/header';
|
||||
import { Footer } from './components/footer/footer';
|
||||
// src/app/app.ts
|
||||
import { Component, Inject, PLATFORM_ID } from '@angular/core';
|
||||
import { Router, NavigationEnd, RouterOutlet } from '@angular/router';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { Header } from './components/header/header';
|
||||
import { FooterComponent } from './components/footer/footer';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet, Header, Footer],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterOutlet,
|
||||
Header,
|
||||
FooterComponent
|
||||
],
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.scss'
|
||||
styleUrls: ['./app.scss'],
|
||||
})
|
||||
export class App {
|
||||
protected readonly title = signal('line-gestao-frontend');
|
||||
export class AppComponent {
|
||||
isFullScreenPage = false;
|
||||
hideFooter = false;
|
||||
|
||||
// ✅ páginas que devem esconder header/footer (tela cheia)
|
||||
private readonly fullScreenRoutes = ['/login', '/register'];
|
||||
|
||||
// ✅ rotas internas (LOGADO) que devem esconder footer
|
||||
private readonly loggedPrefixes = [
|
||||
'/geral',
|
||||
'/mureg',
|
||||
'/faturamento',
|
||||
'/dadosusuarios',
|
||||
'/vigencia',
|
||||
'/trocanumero',
|
||||
'/relatorios', // ✅ ADICIONADO: esconde footer na página de relatórios
|
||||
];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
@Inject(PLATFORM_ID) private platformId: object
|
||||
) {
|
||||
this.router.events.subscribe((event) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
const rawUrl = event.urlAfterRedirects || event.url;
|
||||
|
||||
// remove query/hash e barra final
|
||||
let url = rawUrl.split('?')[0].split('#')[0];
|
||||
url = url.replace(/\/+$/, '');
|
||||
|
||||
this.isFullScreenPage = this.fullScreenRoutes.includes(url);
|
||||
|
||||
const isLoggedRoute = this.loggedPrefixes.some(
|
||||
(p) => url === p || url.startsWith(p + '/')
|
||||
);
|
||||
|
||||
// ✅ footer some ao logar + também no login/register
|
||||
this.hideFooter = isLoggedRoute || this.isFullScreenPage;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ SSR espera importar { App } de './app/app'
|
||||
export { AppComponent as App };
|
||||
|
|
|
|||
|
|
@ -1,42 +1,32 @@
|
|||
<footer class="footer-container">
|
||||
<footer class="app-footer">
|
||||
<div class="footer-line"></div>
|
||||
|
||||
<!-- COLUNA ESQUERDA (TEXTOS) -->
|
||||
<div class="footer-left">
|
||||
<p>© 2024 Copyright | Line Móvel - Todos os Direitos Reservados</p>
|
||||
<p><strong>Razão Social:</strong> LINE MÓVEL - SERVIÇOS E VENDAS EM TELECOMUNICAÇÕES</p>
|
||||
<p><strong>CNPJ:</strong> 45.470.843/0001-90</p>
|
||||
<p>Av. Luís Viana Filho, Nº 7532 - Sala 1008</p>
|
||||
<div class="container">
|
||||
<div class="footer-inner">
|
||||
|
||||
<div class="footer-brand">
|
||||
<div class="logo-text">
|
||||
Line<span>Gestão</span>
|
||||
</div>
|
||||
<p class="footer-tagline">
|
||||
Inteligência para linhas corporativas.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-right">
|
||||
<nav class="footer-nav">
|
||||
<a href="https://www.linemovel.com.br/sobrenos" target="_blank">Sobre nós</a>
|
||||
<a href="https://www.linemovel.com.br/empresas" target="_blank">Para Empresas</a>
|
||||
<a href="#" class="disabled-link">Termos de Uso</a>
|
||||
<a href="#" class="disabled-link">Privacidade</a>
|
||||
</nav>
|
||||
|
||||
<div class="social-wrapper">
|
||||
<div class="social-section">
|
||||
<span class="social-label">Siga-nos</span>
|
||||
|
||||
<a href="#" class="social-icon">
|
||||
<i class="bi bi-instagram"></i>
|
||||
</a>
|
||||
|
||||
<a href="#" class="social-icon">
|
||||
<i class="bi bi-linkedin"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-button-wrapper">
|
||||
<app-cta-button
|
||||
label="Política de Privacidade"
|
||||
width="248.84"
|
||||
height="46.84"
|
||||
background="rgba(201, 30, 181, 0.76)"
|
||||
color="#FFFFFF"
|
||||
fontSize="11"
|
||||
fontWeight="700">
|
||||
</app-cta-button>
|
||||
<div class="footer-copy">
|
||||
<p>
|
||||
© {{ currentYear }} <strong>Ingline Systems</strong>.
|
||||
<br class="d-md-none"> Todos os direitos reservados.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</footer>
|
||||
|
|
@ -1,145 +1,102 @@
|
|||
/* ===================================== */
|
||||
/* FOOTER CONTAINER – VERSÃO MODERNA */
|
||||
/* ===================================== */
|
||||
.app-footer {
|
||||
background-color: #fff;
|
||||
padding: 0 0 32px 0;
|
||||
margin-top: auto; /* Garante que fique no fim se o conteúdo for curto */
|
||||
position: relative;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
/* Linha fina com gradiente da marca no topo */
|
||||
.footer-line {
|
||||
width: 100%;
|
||||
/* Degradê com as cores da marca */
|
||||
background: linear-gradient(90deg, #030FAA 0%, #6066FF 45%, #C91EB5 100%);
|
||||
padding: 10px 32px; /* bem mais baixo que antes */
|
||||
box-sizing: border-box;
|
||||
margin-top: -0.5px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(255,255,255,0) 0%,
|
||||
rgba(227, 61, 207, 0.3) 50%,
|
||||
rgba(255,255,255,0) 100%
|
||||
);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
|
||||
font-family: "Inter", sans-serif;
|
||||
color: #FFFFFF;
|
||||
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||
|
||||
/* Suave sombra pra destacar do conteúdo */
|
||||
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.12);
|
||||
|
||||
/* Telas médias e abaixo – empilha conteúdo */
|
||||
@media (max-width: 1199.98px) {
|
||||
/* Responsividade: Empilha no mobile */
|
||||
@media (max-width: 992px) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 12px 16px;
|
||||
gap: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================== */
|
||||
/* LADO ESQUERDO (TEXTOS) */
|
||||
/* ===================================== */
|
||||
|
||||
.footer-left {
|
||||
margin: 0; /* remove aqueles 100px enormes de antes */
|
||||
}
|
||||
|
||||
.footer-left p {
|
||||
margin: 0 0 2px 0; /* menos espaçamento vertical */
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 11.5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================== */
|
||||
/* LADO DIREITO (REDES + BOTÃO) */
|
||||
/* ===================================== */
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
|
||||
@media (max-width: 1199.98px) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
/* Redes sociais */
|
||||
|
||||
.social-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
@media (max-width: 1199.98px) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.social-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.social-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.social-icon i {
|
||||
font-size: 20px;
|
||||
color: #FFF;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* Identidade */
|
||||
.footer-brand {
|
||||
.logo-text {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-main, #0F172A);
|
||||
margin-bottom: 4px;
|
||||
|
||||
span { color: #000; } /* ou mantenha a cor base */
|
||||
}
|
||||
|
||||
.footer-tagline {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted, #64748B);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.social-icon i:hover {
|
||||
opacity: 0.8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Botão Política de Privacidade */
|
||||
|
||||
.footer-button-wrapper {
|
||||
/* Navegação Central */
|
||||
.footer-nav {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 24px;
|
||||
|
||||
@media (max-width: 1199.98px) {
|
||||
justify-content: center;
|
||||
@media (max-width: 576px) {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
justify-content: flex-start;
|
||||
a {
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted, #64748B);
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--brand-primary, #E33DCF);
|
||||
}
|
||||
|
||||
/* Estilo para links que ainda não existem (Termos/Privacidade) */
|
||||
&.disabled-link {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
&:hover { color: var(--text-muted, #64748B); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Copyright */
|
||||
.footer-copy {
|
||||
text-align: right;
|
||||
|
||||
@media (max-width: 992px) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
|
||||
strong {
|
||||
color: var(--text-main, #0F172A);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Footer } from './footer';
|
||||
import { FooterComponent } from './footer';
|
||||
|
||||
describe('Footer', () => {
|
||||
let component: Footer;
|
||||
let fixture: ComponentFixture<Footer>;
|
||||
let component: FooterComponent;
|
||||
let fixture: ComponentFixture<FooterComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Footer]
|
||||
imports: [FooterComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Footer);
|
||||
fixture = TestBed.createComponent(FooterComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { CtaButtonComponent } from '../cta-button/cta-button';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
imports: [CtaButtonComponent],
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './footer.html',
|
||||
styleUrl: './footer.scss',
|
||||
styleUrls: ['./footer.scss']
|
||||
})
|
||||
export class Footer {
|
||||
|
||||
export class FooterComponent {
|
||||
currentYear: number = new Date().getFullYear();
|
||||
}
|
||||
|
|
@ -1,63 +1,58 @@
|
|||
<header
|
||||
class="header-container"
|
||||
[class.header-scrolled]="isScrolled"
|
||||
>
|
||||
<div class="header-top">
|
||||
<!-- ESQUERDA: HAMBURGUER (logado) + LOGO -->
|
||||
<div class="left-area">
|
||||
<button
|
||||
*ngIf="isLoggedHeader"
|
||||
type="button"
|
||||
class="hamburger-btn"
|
||||
aria-label="Abrir menu"
|
||||
(click)="toggleMenu()"
|
||||
>
|
||||
<header class="app-header" [class.scrolled]="isScrolled">
|
||||
<div class="header-inner container">
|
||||
|
||||
<!-- ✅ LOGADO: hambúrguer + logo JUNTOS -->
|
||||
<ng-container *ngIf="isLoggedHeader; else publicHeader">
|
||||
<div class="left-logged">
|
||||
<button class="btn-icon" type="button" (click)="toggleMenu()" aria-label="Abrir menu">
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
|
||||
<!-- ✅ Logo SEMPRE aparece no header -->
|
||||
<a class="logo-area" routerLink="/">
|
||||
<img src="logo.png" alt="Logo" class="logo" />
|
||||
<div class="logo-text ms-2">
|
||||
<span class="line">Line</span><span class="gestao">Gestão</span>
|
||||
<a routerLink="/geral" class="logo-area" (click)="closeMenu()">
|
||||
<div class="logo-icon">
|
||||
<i class="bi bi-layers-fill"></i>
|
||||
</div>
|
||||
<div class="logo-text">
|
||||
Line<span class="highlight">Gestão</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- ✅ MENU HOME: só aparece fora do logado -->
|
||||
<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/empresas" class="menu-item" target="_blank">Para sua empresa</a>
|
||||
<a href="https://www.linemovel.com.br/proposta" class="menu-item" target="_blank">Solicite sua Proposta</a>
|
||||
<a href="https://www.linemovel.com.br/indique" class="menu-item" target="_blank">Indique um amigo</a>
|
||||
<!-- ✅ PÚBLICO (HOME): menu + botão -->
|
||||
<ng-template #publicHeader>
|
||||
<a routerLink="/" class="logo-area">
|
||||
<div class="logo-icon">
|
||||
<i class="bi bi-layers-fill"></i>
|
||||
</div>
|
||||
<div class="logo-text">
|
||||
Line<span class="highlight">Gestão</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<nav class="nav-links">
|
||||
<a href="https://www.linemovel.com.br/empresas" target="_blank" class="nav-link">Para Empresas</a>
|
||||
<a href="https://www.linemovel.com.br/proposta" target="_blank" class="nav-link">Proposta</a>
|
||||
<a href="https://www.linemovel.com.br/sobrenos" target="_blank" class="nav-link">Sobre</a>
|
||||
</nav>
|
||||
|
||||
<!-- ✅ BOTÕES: só aparecem fora do logado -->
|
||||
<div class="btn-area" *ngIf="!isLoggedHeader">
|
||||
<button type="button" class="btn btn-cadastrar" [routerLink]="['/register']">
|
||||
Cadastre-se
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-login" [routerLink]="['/login']">
|
||||
Login
|
||||
</button>
|
||||
<div class="header-actions">
|
||||
<a routerLink="/login" class="btn-login-header">
|
||||
Acessar Sistema <i class="bi bi-arrow-right-short"></i>
|
||||
</a>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ✅ FAIXA (SÓ NA HOME) -->
|
||||
<div class="header-bar footer-gradient" *ngIf="isHome">
|
||||
<span class="header-bar-text">
|
||||
Somos a escolha certa para estar sempre conectado!
|
||||
</span>
|
||||
<!-- ✅ faixa (só na home, opcional) -->
|
||||
<div class="header-bar" *ngIf="!isLoggedHeader && isHome">
|
||||
<span class="header-bar-text">Somos a escolha certa para estar sempre conectado!</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ✅ OVERLAY (logado) -->
|
||||
<div
|
||||
class="menu-overlay"
|
||||
*ngIf="isLoggedHeader && menuOpen"
|
||||
(click)="closeMenu()"
|
||||
></div>
|
||||
<div class="menu-overlay" *ngIf="isLoggedHeader && menuOpen" (click)="closeMenu()"></div>
|
||||
|
||||
<!-- ✅ MENU LATERAL (logado) -->
|
||||
<aside
|
||||
|
|
@ -67,82 +62,44 @@
|
|||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<div class="side-menu-header">
|
||||
<!-- ✅ Logo DENTRO do menu lateral -->
|
||||
<a class="logo-area" routerLink="/" (click)="closeMenu()">
|
||||
<img src="logo.png" alt="Logo" class="logo" />
|
||||
<div class="logo-text ms-2">
|
||||
<span class="line">Line</span><span class="gestao">Gestão</span>
|
||||
</div>
|
||||
<a class="side-logo" routerLink="/geral" (click)="closeMenu()">
|
||||
<span class="side-logo-icon"><i class="bi bi-layers-fill"></i></span>
|
||||
<span class="side-logo-text">Line<span class="highlight">Gestão</span></span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="close-btn"
|
||||
aria-label="Fechar menu"
|
||||
(click)="closeMenu()"
|
||||
>
|
||||
<button type="button" class="close-btn" aria-label="Fechar menu" (click)="closeMenu()">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="side-menu-body">
|
||||
|
||||
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-sim me-2"></i> Gerenciar Linhas
|
||||
<a routerLink="/geral" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-sim"></i> <span>Geral</span>
|
||||
</a>
|
||||
|
||||
<!-- ✅ FATURAMENTO -->
|
||||
<a routerLink="/faturamento" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-receipt me-2"></i> Faturamento
|
||||
<a routerLink="/mureg" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-table"></i> <span>Mureg</span>
|
||||
</a>
|
||||
|
||||
<!-- ✅ VIGÊNCIA -->
|
||||
<a routerLink="/vigencia" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-calendar-check me-2"></i> Vigência
|
||||
<a routerLink="/faturamento" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-receipt"></i> <span>Faturamento</span>
|
||||
</a>
|
||||
|
||||
<a routerLink="/mureg" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-table me-2"></i> Mureg
|
||||
<a routerLink="/vigencia" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-calendar-check"></i> <span>Vigência</span>
|
||||
</a>
|
||||
|
||||
<!-- ✅ TROCA DE NÚMERO -->
|
||||
<a routerLink="/trocanumero" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-arrow-left-right me-2"></i> Troca de Número
|
||||
<a routerLink="/trocanumero" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-arrow-left-right"></i> <span>Troca de Número</span>
|
||||
</a>
|
||||
|
||||
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-clipboard-data me-2"></i> Controle de Contratos
|
||||
<a routerLink="/dadosusuarios" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-person-lines-fill"></i> <span>Dados dos Usuários</span>
|
||||
</a>
|
||||
|
||||
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-people me-2"></i> Gerenciar Clientes
|
||||
</a>
|
||||
|
||||
<!-- ✅ DADOS DOS USUÁRIOS -->
|
||||
<a routerLink="/dadosusuarios" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-person-lines-fill me-2"></i> Dados dos Usuários
|
||||
</a>
|
||||
|
||||
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-bar-chart me-2"></i> Relatórios
|
||||
</a>
|
||||
|
||||
<hr class="my-2" />
|
||||
|
||||
<a href="https://www.linemovel.com.br/sobrenos" target="_blank" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-info-circle me-2"></i> O que é a Line Móvel?
|
||||
</a>
|
||||
|
||||
<a href="https://www.linemovel.com.br/empresas" target="_blank" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-building me-2"></i> Para sua empresa
|
||||
</a>
|
||||
|
||||
<a href="https://www.linemovel.com.br/proposta" target="_blank" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-file-earmark-text me-2"></i> Solicite sua Proposta
|
||||
</a>
|
||||
|
||||
<a href="https://www.linemovel.com.br/indique" target="_blank" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-megaphone me-2"></i> Indique um amigo
|
||||
<!-- ✅ CORRIGIDO + ESTILIZADO IGUAL AOS OUTROS -->
|
||||
<a routerLink="/relatorios" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-bar-chart-fill"></i> <span>Relatórios</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
|
|
|||
|
|
@ -1,333 +1,136 @@
|
|||
:host {
|
||||
--brand: #E33DCF;
|
||||
--blue: #030FAA;
|
||||
|
||||
--border: rgba(0, 0, 0, 0.10);
|
||||
|
||||
/* ✅ glass */
|
||||
--glass: rgba(255, 255, 255, 0.35);
|
||||
--glass-strong: rgba(255, 255, 255, 0.48);
|
||||
|
||||
--shadow-soft: 0 10px 26px rgba(0, 0, 0, 0.10);
|
||||
}
|
||||
|
||||
/* ===================== */
|
||||
/* HEADER PRINCIPAL */
|
||||
/* ===================== */
|
||||
.header-container {
|
||||
width: 100%;
|
||||
font-family: 'Inter', sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
position: sticky;
|
||||
.app-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 1200;
|
||||
|
||||
/* ✅ transparente/fosco */
|
||||
background: var(--glass);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
|
||||
border-bottom: 1px solid rgba(227, 61, 207, 0.12);
|
||||
transition: background 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
|
||||
}
|
||||
|
||||
.header-container.header-scrolled {
|
||||
background: var(--glass-strong);
|
||||
box-shadow: var(--shadow-soft);
|
||||
border-color: rgba(227, 61, 207, 0.18);
|
||||
}
|
||||
|
||||
/* ===================== */
|
||||
/* TOP AREA */
|
||||
/* ===================== */
|
||||
.header-top {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
padding: 0 32px;
|
||||
z-index: 1000;
|
||||
padding: 16px 0;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||
|
||||
&.scrolled {
|
||||
padding: 12px 0;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 22px;
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
padding: 0 22px;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
padding: 0 18px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
height: auto;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ✅ Centralizar menu em telas grandes (desktop) */
|
||||
@media (min-width: 993px) {
|
||||
.header-top {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto; /* esquerda | centro | direita */
|
||||
align-items: center;
|
||||
justify-content: unset;
|
||||
}
|
||||
|
||||
.menu {
|
||||
width: 100%;
|
||||
justify-content: center; /* ✅ centraliza os links */
|
||||
}
|
||||
}
|
||||
|
||||
.left-area {
|
||||
/* ✅ LOGADO: hambúrguer + logo lado a lado */
|
||||
.left-logged {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* ===================== */
|
||||
/* HAMBURGUER (LOGADO) */
|
||||
/* ===================== */
|
||||
.hamburger-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
|
||||
border: 1px solid rgba(0, 0, 0, 0.10);
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.hamburger-btn i {
|
||||
color: var(--brand);
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.hamburger-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 22px rgba(0, 0, 0, 0.12);
|
||||
border-color: rgba(227, 61, 207, 0.22);
|
||||
}
|
||||
|
||||
.hamburger-btn:active {
|
||||
transform: translateY(0) scale(0.99);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.10);
|
||||
}
|
||||
|
||||
/* ===================== */
|
||||
/* LOGO */
|
||||
/* ===================== */
|
||||
.logo {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
.logo-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
color: var(--text-main);
|
||||
|
||||
transition: transform 180ms ease, filter 180ms ease;
|
||||
}
|
||||
|
||||
.logo-area:hover {
|
||||
transform: translateY(-1px);
|
||||
filter: drop-shadow(0 10px 18px rgba(0, 0, 0, 0.10));
|
||||
}
|
||||
|
||||
/* TEXTO DA LOGO */
|
||||
.logo-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logo-text .line,
|
||||
.logo-text .gestao {
|
||||
font-weight: 800;
|
||||
font-size: 32px;
|
||||
|
||||
@media (max-width: 1280px) { font-size: 26px; }
|
||||
@media (max-width: 1100px) { font-size: 22px; }
|
||||
@media (max-width: 1024px) { font-size: 18px; }
|
||||
@media (max-width: 900px) { font-size: 17px; }
|
||||
@media (max-width: 768px) { font-size: 16px; }
|
||||
}
|
||||
|
||||
.logo-text .line { color: var(--blue); }
|
||||
.logo-text .gestao { color: #000000; }
|
||||
|
||||
/* ===================== */
|
||||
/* MENU (HOME) */
|
||||
/* ===================== */
|
||||
.menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
|
||||
gap: 18px;
|
||||
|
||||
@media (max-width: 1280px) { gap: 14px; }
|
||||
@media (max-width: 1100px) { gap: 12px; }
|
||||
@media (max-width: 1024px) { gap: 10px; }
|
||||
|
||||
@media (max-width: 992px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
|
||||
color: rgba(0, 0, 0, 0.78) !important;
|
||||
text-decoration: none !important;
|
||||
|
||||
padding: 10px 10px;
|
||||
border-radius: 12px;
|
||||
white-space: nowrap;
|
||||
|
||||
transition: transform 180ms ease, background 180ms ease, box-shadow 180ms ease;
|
||||
|
||||
@media (max-width: 1280px) { font-size: 13px; padding: 9px 9px; }
|
||||
@media (max-width: 1100px) { font-size: 12.5px; padding: 8px 8px; }
|
||||
@media (max-width: 1024px) { font-size: 12px; padding: 8px 8px; }
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
transform: translateY(-1px);
|
||||
background: rgba(227, 61, 207, 0.08);
|
||||
box-shadow: 0 10px 22px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* ===================== */
|
||||
/* BOTÕES (HOME) */
|
||||
/* ===================== */
|
||||
.btn-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
@media (max-width: 1100px) { gap: 10px; }
|
||||
}
|
||||
|
||||
.btn-cadastrar,
|
||||
.btn-login {
|
||||
width: 164px;
|
||||
height: 41px;
|
||||
border-radius: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
|
||||
border: 1px solid rgba(0, 0, 0, 0.10);
|
||||
cursor: pointer;
|
||||
|
||||
transition: transform 180ms ease, box-shadow 180ms ease, filter 180ms ease;
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
width: 140px;
|
||||
height: 38px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
width: 132px;
|
||||
.logo-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 13px;
|
||||
background: linear-gradient(135deg, var(--brand-primary), #6A55FF);
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 18px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
.logo-text {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
.highlight { color: var(--text-main); }
|
||||
}
|
||||
}
|
||||
|
||||
/* Nav (Desktop) */
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
|
||||
@media(max-width: 992px) { display: none; }
|
||||
|
||||
.nav-link {
|
||||
text-decoration: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
width: 46vw;
|
||||
max-width: 190px;
|
||||
&:hover { color: var(--brand-primary); }
|
||||
}
|
||||
}
|
||||
|
||||
.btn-cadastrar {
|
||||
background: #E1E1E1;
|
||||
color: #000;
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
background: var(--brand);
|
||||
border-color: var(--brand);
|
||||
color: #fff !important;
|
||||
.btn-login-header {
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
padding: 8px 20px;
|
||||
border-radius: 99px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--brand-primary);
|
||||
color: var(--brand-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-cadastrar:hover,
|
||||
.btn-login:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.12);
|
||||
.btn-icon {
|
||||
background: rgba(255,255,255,0.75);
|
||||
border: 1px solid rgba(0,0,0,0.10);
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
backdrop-filter: blur(12px);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
i { font-size: 24px; color: var(--text-main); }
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 14px 26px rgba(0,0,0,0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
filter: brightness(0.97);
|
||||
}
|
||||
|
||||
/* ===================== */
|
||||
/* FAIXA (HOME) */
|
||||
/* ===================== */
|
||||
/* Faixa home */
|
||||
.header-bar {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-gradient {
|
||||
background: linear-gradient(90deg, #0B2BD6 0%, #6A55FF 40%, #E33DCF 100%);
|
||||
}
|
||||
|
||||
|
|
@ -336,38 +139,31 @@
|
|||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
|
||||
@media (max-width: 480px) { font-size: 13px; }
|
||||
}
|
||||
|
||||
/* ===================================================== */
|
||||
/* ========================= */
|
||||
/* MENU LATERAL (LOGADO) */
|
||||
/* ===================================================== */
|
||||
/* ========================= */
|
||||
.menu-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
background: rgba(0, 0, 0, 0.38);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.side-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
height: 100vh;
|
||||
width: min(340px, 88vw);
|
||||
|
||||
width: min(360px, 88vw);
|
||||
z-index: 1150;
|
||||
transform: translateX(-102%);
|
||||
transition: transform 220ms ease;
|
||||
transition: transform 240ms ease;
|
||||
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
|
||||
border-right: 1px solid rgba(227, 61, 207, 0.18);
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.14);
|
||||
|
||||
|
|
@ -385,31 +181,46 @@
|
|||
gap: 12px;
|
||||
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: rgba(255,255,255,0.55);
|
||||
}
|
||||
|
||||
.side-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
color: var(--text-main);
|
||||
|
||||
.side-logo-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, var(--brand-primary), #6A55FF);
|
||||
i { font-size: 18px; }
|
||||
}
|
||||
|
||||
.side-logo-text {
|
||||
font-weight: 900;
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.4px;
|
||||
.highlight { color: var(--text-main); }
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
|
||||
border: 1px solid rgba(0,0,0,0.10);
|
||||
background: rgba(255, 255, 255, 0.60);
|
||||
|
||||
background: rgba(255, 255, 255, 0.70);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 22px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.close-btn:active {
|
||||
transform: translateY(0) scale(0.99);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.10);
|
||||
i { font-size: 18px; color: rgba(17,18,20,0.7); }
|
||||
}
|
||||
|
||||
.side-menu-body {
|
||||
|
|
@ -420,55 +231,38 @@
|
|||
.side-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 12px 12px;
|
||||
border-radius: 14px;
|
||||
|
||||
text-decoration: none;
|
||||
color: rgba(0, 0, 0, 0.80);
|
||||
color: rgba(17, 18, 20, 0.86);
|
||||
font-weight: 800;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
|
||||
transition: background 180ms ease, transform 180ms ease;
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
color: var(--brand-primary);
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.side-item i { color: var(--brand); }
|
||||
/* ✅ polimento: deixa o bar-chart com “peso” igual aos outros ícones */
|
||||
.bi-bar-chart-fill {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.side-item:hover {
|
||||
&:hover {
|
||||
background: rgba(227, 61, 207, 0.10);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.side-item:active {
|
||||
transform: translateY(0) scale(0.99);
|
||||
&.active {
|
||||
background: rgba(3, 15, 170, 0.10);
|
||||
border: 1px solid rgba(3, 15, 170, 0.12);
|
||||
}
|
||||
|
||||
/* ========================================= */
|
||||
/* OVERRIDE BOOTSTRAP */
|
||||
/* ========================================= */
|
||||
.btn.btn-cadastrar,
|
||||
.btn.btn-login {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.btn.btn-cadastrar:active,
|
||||
.btn.btn-login:active,
|
||||
.btn.btn-cadastrar:active:focus,
|
||||
.btn.btn-login:active:focus,
|
||||
.btn.btn-cadastrar:focus,
|
||||
.btn.btn-login:focus,
|
||||
.btn.btn-cadastrar:focus-visible,
|
||||
.btn.btn-login:focus-visible {
|
||||
opacity: 1 !important;
|
||||
filter: none !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-cadastrar:active,
|
||||
.btn.btn-login:active {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.10) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// header.ts
|
||||
import { Component, HostListener, Inject } from '@angular/core';
|
||||
import { RouterLink, Router, NavigationEnd } from '@angular/router';
|
||||
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||
import { PLATFORM_ID } from '@angular/core';
|
||||
import { filter } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
|
|
@ -13,12 +13,11 @@ import { PLATFORM_ID } from '@angular/core';
|
|||
})
|
||||
export class Header {
|
||||
isScrolled = false;
|
||||
isHome = true;
|
||||
|
||||
menuOpen = false;
|
||||
isLoggedHeader = false;
|
||||
isHome = false;
|
||||
|
||||
// ✅ rotas internas que usam menu lateral
|
||||
private readonly loggedPrefixes = [
|
||||
'/geral',
|
||||
'/mureg',
|
||||
|
|
@ -26,26 +25,34 @@ export class Header {
|
|||
'/dadosusuarios',
|
||||
'/vigencia',
|
||||
'/trocanumero',
|
||||
'/relatorios', // ✅ ADICIONADO
|
||||
];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
@Inject(PLATFORM_ID) private platformId: object
|
||||
) {
|
||||
this.router.events.subscribe((event) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
// ✅ resolve no carregamento inicial
|
||||
this.syncHeaderState(this.router.url);
|
||||
|
||||
// ✅ resolve em toda navegação
|
||||
this.router.events
|
||||
.pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
|
||||
.subscribe((event) => {
|
||||
const rawUrl = event.urlAfterRedirects || event.url;
|
||||
const url = rawUrl.split('?')[0].split('#')[0];
|
||||
this.syncHeaderState(rawUrl);
|
||||
this.menuOpen = false;
|
||||
});
|
||||
}
|
||||
|
||||
private syncHeaderState(rawUrl: string) {
|
||||
const url = (rawUrl || '').split('?')[0].split('#')[0];
|
||||
|
||||
this.isHome = (url === '/' || url === '');
|
||||
|
||||
this.isLoggedHeader = this.loggedPrefixes.some((p) =>
|
||||
url === p || url.startsWith(p + '/')
|
||||
);
|
||||
|
||||
this.menuOpen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleMenu() {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,203 +1,113 @@
|
|||
<section class="home-page">
|
||||
<section class="home-wrapper">
|
||||
|
||||
<!-- BACKGROUND GLOBAL -->
|
||||
<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>
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
|
||||
<div class="hero-inner">
|
||||
|
||||
<!-- COLUNA TEXTO (CENTRALIZADA) -->
|
||||
<div class="hero-copy">
|
||||
|
||||
<div class="hero-badge" data-animate>
|
||||
<i class="bi bi-stars"></i>
|
||||
SaaS para gestão de linhas corporativas
|
||||
<div class="home-global-bg">
|
||||
<div class="blob blob-1"></div>
|
||||
<div class="blob blob-2"></div>
|
||||
<div class="mesh-overlay"></div>
|
||||
</div>
|
||||
|
||||
<section class="hero-text-section">
|
||||
<h1 class="main-title" data-animate>
|
||||
<span class="first-line">Gerencie suas linhas móveis</span>
|
||||
<span class="second-line">com <strong>inteligência e praticidade</strong></span>
|
||||
<div class="content-scroll">
|
||||
|
||||
<div class="container hero-container">
|
||||
<div class="row align-items-center">
|
||||
|
||||
<div class="col-lg-6 hero-content fade-in-up">
|
||||
<div class="badge-saas">
|
||||
<span class="pulse"></span> Plataforma Inteligente
|
||||
</div>
|
||||
|
||||
<h1 class="display-title">
|
||||
Gestão de linhas corporativas <br>
|
||||
<span class="text-gradient">simples e eficiente.</span>
|
||||
</h1>
|
||||
|
||||
<p class="main-paragraph" data-animate>
|
||||
<strong class="brand-name">LineGestão</strong> é a solução completa para empresas que
|
||||
<strong class="highlight">desejam controlar suas linhas móveis com eficiência e segurança</strong>.
|
||||
Com recursos como <strong class="highlight">gerenciamento de clientes</strong>,
|
||||
<strong class="highlight">importação de dados via Excel</strong> e
|
||||
<strong class="highlight">monitoramento estratégico de contratos e linhas</strong>,
|
||||
você <strong class="highlight">simplifica processos</strong>,
|
||||
<strong class="highlight">reduz erros</strong> e
|
||||
<strong class="highlight">ganha mais controle sobre seus recursos corporativos</strong>.
|
||||
<p class="lead-text">
|
||||
Substitua planilhas complexas por um painel unificado. Controle contratos, usuários e custos com a segurança que a <strong>Ingline Systems</strong> garante.
|
||||
</p>
|
||||
|
||||
<div class="hero-actions" data-animate>
|
||||
<app-cta-button
|
||||
label="COMEÇAR AGORA"
|
||||
(clicked)="iniciar()">
|
||||
</app-cta-button>
|
||||
|
||||
<button type="button" class="cta-secondary" (click)="scrollToFeatures()">
|
||||
<i class="bi bi-arrow-down-circle"></i>
|
||||
Ver recursos
|
||||
<div class="hero-actions">
|
||||
<button (click)="goToLogin()" class="btn-primary-saas">
|
||||
Acessar Painel
|
||||
</button>
|
||||
<button (click)="scrollToFeatures()" class="btn-secondary-saas">
|
||||
Ver Recursos
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<div class="trust-strip">
|
||||
<span><i class="bi bi-shield-check"></i> Dados Seguros</span>
|
||||
<span><i class="bi bi-lightning-charge"></i> Setup Rápido</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CARD/MOCK (fica na direita) -->
|
||||
<div class="hero-mock" data-animate aria-label="Prévia visual do painel">
|
||||
<div class="mock-card">
|
||||
<div class="mock-top">
|
||||
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
|
||||
<span class="mock-title">Visão Geral</span>
|
||||
<div class="col-lg-6 fade-in-up" style="animation-delay: 0.2s;">
|
||||
<div class="mockup-perspective">
|
||||
<div class="mockup-card glass-card">
|
||||
<div class="mock-header">
|
||||
<div class="dots"><span></span><span></span><span></span></div>
|
||||
<div class="bar">LineGestão / Dashboard</div>
|
||||
</div>
|
||||
|
||||
<div class="mock-grid">
|
||||
<div class="mock-kpi">
|
||||
<span class="kpi-label">Linhas ativas</span>
|
||||
<span class="kpi-value">128</span>
|
||||
<span class="kpi-tag"><i class="bi bi-graph-up"></i> controle</span>
|
||||
<div class="mock-body">
|
||||
<div class="kpi-row">
|
||||
<div class="kpi-box">
|
||||
<small>Linhas Ativas</small>
|
||||
<strong>128</strong>
|
||||
</div>
|
||||
|
||||
<div class="mock-kpi">
|
||||
<span class="kpi-label">Contratos</span>
|
||||
<span class="kpi-value">12</span>
|
||||
<span class="kpi-tag"><i class="bi bi-file-earmark-text"></i> organizado</span>
|
||||
<div class="kpi-box">
|
||||
<small>Fatura Atual</small>
|
||||
<strong>R$ 4.2k</strong>
|
||||
</div>
|
||||
|
||||
<div class="mock-kpi">
|
||||
<span class="kpi-label">Clientes</span>
|
||||
<span class="kpi-value">34</span>
|
||||
<span class="kpi-tag"><i class="bi bi-people"></i> centralizado</span>
|
||||
</div>
|
||||
|
||||
<div class="mock-line">
|
||||
<div class="line-icon"><i class="bi bi-sim"></i></div>
|
||||
<div class="line-info">
|
||||
<div class="line-title">Linha 55XX9XXXXXXXX</div>
|
||||
<div class="line-sub">Status: Ativa • Operadora: Vivo</div>
|
||||
<div class="fake-chart">
|
||||
<div class="bar" style="height: 40%"></div>
|
||||
<div class="bar" style="height: 60%"></div>
|
||||
<div class="bar active" style="height: 85%"></div>
|
||||
<div class="bar" style="height: 55%"></div>
|
||||
</div>
|
||||
<div class="fake-list">
|
||||
<div class="item"><span></span><div class="line"></div></div>
|
||||
<div class="item"><span></span><div class="line"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="floating-badge">
|
||||
<i class="bi bi-check-circle-fill"></i> Controle Total
|
||||
</div>
|
||||
<div class="line-pill">OK</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- NOVO: CENTRALIZAR OS CARDS NO MEIO -->
|
||||
<div class="hero-metrics-wide" data-animate>
|
||||
<div class="hero-metrics">
|
||||
<div class="metric">
|
||||
<i class="bi bi-lightning-charge"></i>
|
||||
<div>
|
||||
<span class="metric-title">Setup rápido</span>
|
||||
<span class="metric-sub">comece em minutos</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<i class="bi bi-file-earmark-spreadsheet"></i>
|
||||
<div>
|
||||
<span class="metric-title">Excel → Sistema</span>
|
||||
<span class="metric-sub">importação inteligente</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<i class="bi bi-shield-check"></i>
|
||||
<div>
|
||||
<span class="metric-title">Mais segurança</span>
|
||||
<span class="metric-sub">menos erro manual</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- FEATURES -->
|
||||
<section id="features" class="features-section">
|
||||
<div class="container my-5">
|
||||
|
||||
<div class="section-head" data-animate>
|
||||
<h2 class="section-title">
|
||||
Tudo o que você precisa para <span class="brand">gestão de linhas</span>
|
||||
</h2>
|
||||
<p class="section-subtitle">
|
||||
Um painel simples, bonito e direto ao ponto — feito para empresa que quer controle.
|
||||
</p>
|
||||
<div class="container">
|
||||
<div class="text-center mb-5 fade-in-up">
|
||||
<h2 class="section-title">Tudo em um só lugar</h2>
|
||||
<p class="section-subtitle">O essencial para eliminar erros manuais e ganhar tempo.</p>
|
||||
</div>
|
||||
|
||||
<!-- NOVO: Centralizar os cards -->
|
||||
<div class="row justify-content-center feature-cards-row">
|
||||
|
||||
<div class="col-auto mb-4 feature-item" data-animate>
|
||||
<app-feature-card
|
||||
title="Monitoramento Completo"
|
||||
[textAlign]="'center'"
|
||||
iconClass="bi bi-laptop"
|
||||
description="<strong>Acompanhe contratos, valores e suas linhas</strong> com visão estratégica, garantindo controle total sobre seus recursos móveis."
|
||||
></app-feature-card>
|
||||
<div class="grid-features fade-in-up">
|
||||
<div class="feature-box">
|
||||
<div class="icon-sq"><i class="bi bi-laptop"></i></div>
|
||||
<h3>Monitoramento</h3>
|
||||
<p>Visão estratégica de contratos, valores e consumo em tempo real.</p>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mb-4 feature-item" data-animate>
|
||||
<app-feature-card
|
||||
title="Gerenciamento de Clientes"
|
||||
[textAlign]="'center'"
|
||||
iconClass="bi bi-people"
|
||||
description="<strong>Organize e acompanhe seus clientes</strong> com praticidade e segurança, garantindo uma gestão eficiente."
|
||||
></app-feature-card>
|
||||
<div class="feature-box">
|
||||
<div class="icon-sq"><i class="bi bi-file-earmark-spreadsheet"></i></div>
|
||||
<h3>Importação Excel</h3>
|
||||
<p>Traga seus dados legados em segundos, substituindo planilhas manuais.</p>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mb-4 feature-item" data-animate>
|
||||
<app-feature-card
|
||||
title="Importação via Excel"
|
||||
[textAlign]="'center'"
|
||||
iconClass="bi bi-table"
|
||||
description="<strong>Integre dados rapidamente</strong> sem esforço manual, substituindo planilhas por uma solução moderna e automatizada."
|
||||
></app-feature-card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="value-strip" data-animate>
|
||||
<div class="value">
|
||||
<i class="bi bi-check2-circle"></i>
|
||||
<span><strong>Menos planilha</strong>, mais controle.</span>
|
||||
</div>
|
||||
<div class="value">
|
||||
<i class="bi bi-check2-circle"></i>
|
||||
<span><strong>Mais agilidade</strong> no dia a dia.</span>
|
||||
</div>
|
||||
<div class="value">
|
||||
<i class="bi bi-check2-circle"></i>
|
||||
<span><strong>Mais segurança</strong> na gestão.</span>
|
||||
<div class="feature-box">
|
||||
<div class="icon-sq"><i class="bi bi-people"></i></div>
|
||||
<h3>Gestão de Usuários</h3>
|
||||
<p>Vincule linhas a colaboradores, organize centros de custo e evite desperdícios.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center button-section" data-animate>
|
||||
<div class="col-auto">
|
||||
<app-cta-button
|
||||
label="COMEÇAR AGORA"
|
||||
(clicked)="iniciar()">
|
||||
</app-cta-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div style="height: 80px;"></div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -1,544 +1,451 @@
|
|||
:host {
|
||||
--brand: #E33DCF;
|
||||
--brand-soft: rgba(227, 61, 207, 0.14);
|
||||
--brand-soft-2: rgba(227, 61, 207, 0.08);
|
||||
|
||||
--text: #111214;
|
||||
--muted: rgba(17, 18, 20, 0.70);
|
||||
|
||||
--radius-xl: 22px;
|
||||
--radius-lg: 16px;
|
||||
|
||||
display: block;
|
||||
--brand-primary: #E33DCF;
|
||||
--brand-soft: rgba(227, 61, 207, 0.15);
|
||||
--brand-bg-light: #FDF4FC; /* Um rosa/branco muito sutil */
|
||||
--text-main: #0F172A;
|
||||
--text-muted: #64748B;
|
||||
}
|
||||
|
||||
/* ✅ FUNDO GLOBAL (vale para a Home TODA até o footer) */
|
||||
.home-page {
|
||||
.home-wrapper {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
/* Não usamos overflow-hidden aqui para permitir o sticky footer funcionar nativamente */
|
||||
}
|
||||
|
||||
/* ========================================= */
|
||||
/* ✅ FUNDO GLOBAL FIXO (CORREÇÃO) */
|
||||
/* ========================================= */
|
||||
.home-global-bg {
|
||||
position: fixed; /* Fixo na tela */
|
||||
inset: 0; /* Cobre top, right, bottom, left */
|
||||
z-index: -1; /* Fica atrás de tudo */
|
||||
background-color: #F8FAFC; /* Cor base sólida */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
background:
|
||||
radial-gradient(900px 420px at 20% 10%, var(--brand-soft), transparent 60%),
|
||||
radial-gradient(820px 380px at 80% 30%, var(--brand-soft-2), transparent 60%),
|
||||
linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%);
|
||||
|
||||
/* névoa suave pra dar sensação de blur */
|
||||
&::after {
|
||||
content: '';
|
||||
/* O Mesh Overlay dá a textura geral */
|
||||
.mesh-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
background:
|
||||
radial-gradient(circle at 15% 10%, var(--brand-soft) 0%, transparent 40%),
|
||||
radial-gradient(circle at 85% 30%, rgba(3, 15, 170, 0.08) 0%, transparent 40%),
|
||||
linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.8) 100%);
|
||||
}
|
||||
|
||||
/* ✅ BLOBS FIXOS (continuam no scroll) */
|
||||
.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; }
|
||||
|
||||
@media (max-width: 992px) {
|
||||
opacity: 0.35;
|
||||
}
|
||||
/* Blobs flutuantes e suaves */
|
||||
.blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.6;
|
||||
animation: floatBlob 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ✅ garante que o conteúdo fique acima do fundo */
|
||||
.hero,
|
||||
.features-section,
|
||||
.container {
|
||||
.blob-1 {
|
||||
top: -100px;
|
||||
left: -100px;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: rgba(227, 61, 207, 0.12);
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
bottom: -150px;
|
||||
right: -100px;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: rgba(3, 15, 170, 0.08);
|
||||
animation-delay: -5s;
|
||||
}
|
||||
|
||||
@keyframes floatBlob {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(20px, 30px); }
|
||||
}
|
||||
|
||||
/* ========================================= */
|
||||
/* CONTEÚDO SCROLLÁVEL */
|
||||
/* ========================================= */
|
||||
.content-scroll {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: 1; /* Garante que o texto fique acima do fundo */
|
||||
padding-top: 120px; /* Espaço para o Header Fixed */
|
||||
}
|
||||
|
||||
/* =============================== */
|
||||
/* HERO */
|
||||
/* =============================== */
|
||||
.hero {
|
||||
padding: 56px 0 18px 0;
|
||||
/* ========================================= */
|
||||
/* HERO SECTION */
|
||||
/* ========================================= */
|
||||
.hero-container {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.hero-inner {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 0.9fr;
|
||||
gap: 36px;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: 992px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
/* texto centralizado */
|
||||
.hero-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* badge */
|
||||
.hero-badge {
|
||||
.badge-saas {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
width: fit-content;
|
||||
padding: 10px 14px;
|
||||
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-family: 'Inter', sans-serif;
|
||||
font-size: 14px;
|
||||
gap: 8px;
|
||||
background: white;
|
||||
border: 1px solid rgba(227, 61, 207, 0.2);
|
||||
color: var(--brand-primary);
|
||||
padding: 6px 14px;
|
||||
border-radius: 99px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 10px rgba(227, 61, 207, 0.05);
|
||||
|
||||
i { color: var(--brand); font-size: 16px; }
|
||||
.pulse {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--brand-primary);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 4px rgba(227, 61, 207, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.hero-text-section {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* título */
|
||||
.main-title {
|
||||
.display-title {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 52px;
|
||||
line-height: 1.05;
|
||||
margin: 18px 0 18px 0;
|
||||
font-size: 3.5rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -1.5px;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 24px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@media(max-width: 992px) { font-size: 2.8rem; }
|
||||
@media(max-width: 576px) { font-size: 2.2rem; }
|
||||
|
||||
@media (max-width: 1400px) { font-size: 44px; }
|
||||
@media (max-width: 1024px) { font-size: 38px; }
|
||||
@media (max-width: 768px) { font-size: 32px; }
|
||||
@media (max-width: 480px) { font-size: 28px; }
|
||||
.text-gradient {
|
||||
background: linear-gradient(90deg, var(--brand-primary) 0%, #030FAA 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.main-title .first-line,
|
||||
.main-title .second-line {
|
||||
font-weight: 650;
|
||||
display: block;
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
.lead-text {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 1.125rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.6;
|
||||
max-width: 520px;
|
||||
margin-bottom: 36px;
|
||||
|
||||
strong { color: var(--text-main); }
|
||||
}
|
||||
|
||||
.main-title strong {
|
||||
color: var(--brand);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-title strong::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 6px;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(227, 61, 207, 0.18);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* parágrafo */
|
||||
.main-paragraph {
|
||||
width: min(980px, 100%);
|
||||
margin: 0 auto 16px auto;
|
||||
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-size: 20px;
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
|
||||
@media (max-width: 1400px) { font-size: 19px; }
|
||||
@media (max-width: 1024px) { font-size: 18px; }
|
||||
@media (max-width: 768px) { font-size: 16px; }
|
||||
}
|
||||
|
||||
.main-paragraph .brand-name { color: var(--text); }
|
||||
.main-paragraph .highlight { color: var(--text); font-weight: 800; }
|
||||
.main-paragraph strong { font-weight: 800; }
|
||||
|
||||
/* botões */
|
||||
/* Botões */
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 16px;
|
||||
gap: 16px;
|
||||
margin-bottom: 40px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cta-secondary {
|
||||
height: 44px;
|
||||
padding: 0 14px;
|
||||
.btn-primary-saas {
|
||||
background: var(--brand-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 14px 32px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid rgba(17, 18, 20, 0.10);
|
||||
color: var(--text);
|
||||
font-weight: 800;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: transform 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
|
||||
|
||||
i { color: var(--brand); }
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 10px 25px -5px rgba(227, 61, 207, 0.3);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(227, 61, 207, 0.28);
|
||||
box-shadow: 0 12px 24px rgba(17, 18, 20, 0.10);
|
||||
background: #d42bbf;
|
||||
box-shadow: 0 15px 30px -5px rgba(227, 61, 207, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================== */
|
||||
/* MOCK (direita) */
|
||||
/* =============================== */
|
||||
.hero-mock {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
@media (max-width: 992px) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.mock-card {
|
||||
width: min(460px, 100%);
|
||||
border-radius: var(--radius-xl);
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1px solid rgba(227, 61, 207, 0.14);
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 22px 46px rgba(17, 18, 20, 0.10);
|
||||
overflow: hidden;
|
||||
|
||||
transform: perspective(900px) rotateY(-6deg) rotateX(2deg);
|
||||
transition: transform 200ms ease;
|
||||
.btn-secondary-saas {
|
||||
background: white;
|
||||
color: var(--text-main);
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
padding: 14px 24px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: perspective(900px) rotateY(-2deg) rotateX(1deg) translateY(-2px);
|
||||
background: #f8f9fa;
|
||||
border-color: rgba(0,0,0,0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.mock-top {
|
||||
.trust-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(180deg, rgba(227, 61, 207, 0.10), rgba(255, 255, 255, 0.20));
|
||||
gap: 24px;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(17, 18, 20, 0.12);
|
||||
}
|
||||
|
||||
.mock-title {
|
||||
margin-left: 6px;
|
||||
font-weight: 950;
|
||||
font-family: 'Inter', sans-serif;
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
.mock-grid {
|
||||
padding: 16px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
|
||||
@media (max-width: 380px) { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.mock-kpi {
|
||||
border-radius: var(--radius-lg);
|
||||
background: #fff;
|
||||
border: 1px solid rgba(17, 18, 20, 0.08);
|
||||
padding: 12px;
|
||||
|
||||
.kpi-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: rgba(17, 18, 20, 0.65);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
display: block;
|
||||
font-size: 26px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 950;
|
||||
color: var(--text);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.kpi-tag {
|
||||
display: inline-flex;
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(227, 61, 207, 0.20);
|
||||
background: rgba(227, 61, 207, 0.07);
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
i { color: var(--brand); }
|
||||
i { color: var(--brand-primary); font-size: 16px; }
|
||||
}
|
||||
|
||||
/* ========================================= */
|
||||
/* MOCKUP 3D (DIREITA) */
|
||||
/* ========================================= */
|
||||
.mockup-perspective {
|
||||
position: relative;
|
||||
perspective: 1000px;
|
||||
padding: 20px;
|
||||
|
||||
@media(max-width: 992px) {
|
||||
margin-top: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.mock-line {
|
||||
grid-column: 1 / -1;
|
||||
border-radius: var(--radius-lg);
|
||||
background: #fff;
|
||||
border: 1px solid rgba(17, 18, 20, 0.08);
|
||||
padding: 12px;
|
||||
.mockup-card {
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255,255,255,0.8);
|
||||
border-radius: 24px;
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 1px rgba(255,255,255,0.5) inset;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
transform: rotateY(-10deg) rotateX(4deg);
|
||||
transition: transform 0.5s ease;
|
||||
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
min-height: 340px;
|
||||
padding: 24px;
|
||||
|
||||
&:hover {
|
||||
transform: rotateY(-4deg) rotateX(2deg) translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Detalhes internos do Mock */
|
||||
.mock-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||
|
||||
.dots span {
|
||||
display: inline-block;
|
||||
width: 10px; height: 10px;
|
||||
background: #E2E8F0; border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.bar {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
.line-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
background: rgba(227, 61, 207, 0.10);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
i { color: var(--brand); font-size: 18px; }
|
||||
}
|
||||
|
||||
.line-title {
|
||||
font-weight: 950;
|
||||
font-family: 'Inter', sans-serif;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.line-sub {
|
||||
color: rgba(17, 18, 20, 0.65);
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.line-pill {
|
||||
padding: 8px 10px;
|
||||
border-radius: 999px;
|
||||
font-weight: 950;
|
||||
font-size: 12px;
|
||||
background: rgba(227, 61, 207, 0.10);
|
||||
border: 1px solid rgba(227, 61, 207, 0.22);
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================== */
|
||||
/* MÉTRICAS CENTRALIZADAS (MEIO) */
|
||||
/* =============================== */
|
||||
.hero-metrics-wide {
|
||||
margin-top: 18px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-metrics {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1px solid rgba(227, 61, 207, 0.16);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
i { color: var(--brand); font-size: 18px; }
|
||||
|
||||
.metric-title {
|
||||
display: block;
|
||||
font-weight: 900;
|
||||
color: var(--text);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.metric-sub {
|
||||
display: block;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 12px;
|
||||
color: rgba(17, 18, 20, 0.65);
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================== */
|
||||
/* FEATURES */
|
||||
/* =============================== */
|
||||
.features-section {
|
||||
padding: 18px 0 60px 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 950;
|
||||
color: var(--text);
|
||||
font-size: 30px;
|
||||
.kpi-box {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.02);
|
||||
border: 1px solid rgba(0,0,0,0.03);
|
||||
|
||||
@media (max-width: 768px) { font-size: 24px; }
|
||||
small {
|
||||
display: block;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
strong {
|
||||
font-size: 22px;
|
||||
color: var(--text-main);
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title .brand { color: var(--brand); }
|
||||
.fake-chart {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
height: 100px;
|
||||
margin-bottom: 24px;
|
||||
padding: 0 10px;
|
||||
|
||||
.bar {
|
||||
flex: 1;
|
||||
background: #F1F5F9;
|
||||
border-radius: 6px;
|
||||
transition: height 1s ease;
|
||||
}
|
||||
.bar.active {
|
||||
background: linear-gradient(180deg, var(--brand-primary) 0%, #B832A8 100%);
|
||||
box-shadow: 0 4px 10px rgba(227, 61, 207, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.fake-list .item {
|
||||
height: 36px;
|
||||
background: white;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(0,0,0,0.02);
|
||||
|
||||
span {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #F1F5F9;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.line {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: #F1F5F9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.floating-badge {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
left: -30px;
|
||||
background: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 50px;
|
||||
box-shadow: 0 15px 35px rgba(0,0,0,0.08);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: var(--text-main);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
animation: floatBadge 5s ease-in-out infinite;
|
||||
z-index: 10;
|
||||
|
||||
@media(max-width: 576px) { left: 0; bottom: -20px; }
|
||||
|
||||
i { color: #10B981; font-size: 18px; }
|
||||
}
|
||||
|
||||
@keyframes floatBadge {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
/* ========================================= */
|
||||
/* FEATURES SECTION */
|
||||
/* ========================================= */
|
||||
.features-section {
|
||||
padding: 80px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 10px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
margin-top: 10px;
|
||||
color: var(--muted);
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-size: 16px;
|
||||
color: var(--text-muted);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ✅ AQUI: 3 CARDS CENTRALIZADOS LADO A LADO NO NOTEBOOK */
|
||||
.feature-cards-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
gap: 26px;
|
||||
flex-wrap: nowrap; /* ✅ força ficar em 1 linha */
|
||||
|
||||
/* notebook / telas médias */
|
||||
@media (max-width: 1199.98px) {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
/* quando a tela ficar pequena de verdade, aí quebra */
|
||||
@media (max-width: 992px) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* Garante que o col-auto do bootstrap não atrapalhe */
|
||||
.feature-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* melhora o respiro no botão */
|
||||
.button-section {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
/* faixa de valores */
|
||||
.value-strip {
|
||||
.grid-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 32px;
|
||||
margin-top: 50px;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius-xl);
|
||||
background: rgba(255, 255, 255, 0.80);
|
||||
border: 1px solid rgba(227, 61, 207, 0.14);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 18px;
|
||||
flex-wrap: wrap;
|
||||
.feature-box {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(10px); /* Glassmorphism nos cards também */
|
||||
padding: 32px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.02);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
&:hover {
|
||||
transform: translateY(-8px);
|
||||
background: white;
|
||||
box-shadow: 0 20px 40px -5px rgba(0, 0, 0, 0.08);
|
||||
border-color: rgba(227, 61, 207, 0.1);
|
||||
|
||||
i { color: var(--brand); font-size: 18px; }
|
||||
span { color: var(--text); font-family: 'Inter', sans-serif; }
|
||||
.icon-sq {
|
||||
background: var(--brand-primary);
|
||||
color: white;
|
||||
transform: scale(1.1) rotate(-3deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================== */
|
||||
/* ✅ ANIMAÇÕES SSR-SAFE */
|
||||
/* =============================== */
|
||||
[data-animate] { opacity: 1; transform: none; }
|
||||
.icon-sq {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
background: rgba(227, 61, 207, 0.08);
|
||||
color: var(--brand-primary);
|
||||
border-radius: 14px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 22px;
|
||||
margin-bottom: 22px;
|
||||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.js-animate [data-animate] {
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 15px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utilitário de Animação */
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(14px);
|
||||
transition: opacity 600ms ease, transform 600ms ease;
|
||||
will-change: opacity, transform;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.js-animate [data-animate].is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* brilho suave no botão */
|
||||
:host ::ng-deep app-cta-button .btn,
|
||||
:host ::ng-deep app-cta-button button {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
:host ::ng-deep app-cta-button .btn::after,
|
||||
:host ::ng-deep app-cta-button button::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform: translateX(-120%) rotate(12deg);
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.55), transparent);
|
||||
animation: shine 3.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.page-blob { animation: none; }
|
||||
.js-animate [data-animate] { transition: none; transform: none; opacity: 1; }
|
||||
:host ::ng-deep app-cta-button .btn::after,
|
||||
:host ::ng-deep app-cta-button button::after { animation: none; }
|
||||
}
|
||||
|
||||
@keyframes floaty {
|
||||
0% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(18px, 10px) scale(1.03); }
|
||||
100% { transform: translate(0, 0) scale(1); }
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0%, 65% { transform: translateX(-120%) rotate(12deg); opacity: 0.0; }
|
||||
75% { opacity: 1; }
|
||||
100% { transform: translateX(120%) rotate(12deg); opacity: 0.0; }
|
||||
@keyframes fadeInUp {
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
|
@ -30,6 +30,10 @@ export class Home implements AfterViewInit {
|
|||
document.getElementById('features')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
goToLogin(): void {
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (!this.isBrowser) return;
|
||||
|
||||
|
|
@ -64,5 +68,6 @@ export class Home implements AfterViewInit {
|
|||
|
||||
items.forEach(el => io.observe(el));
|
||||
}, 0);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,73 +1,126 @@
|
|||
<div class="login-wrapper">
|
||||
<div class="login-card shadow-sm">
|
||||
|
||||
<!-- Título -->
|
||||
<div class="login-title mb-4">
|
||||
<h2 class="mb-0">Login</h2>
|
||||
<div class="login-left">
|
||||
|
||||
<div class="left-content fade-in-up">
|
||||
<div class="brand-header mb-4">
|
||||
<div class="brand-logo">
|
||||
<i class="bi bi-layers-fill"></i>
|
||||
<span>LineGestão</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
|
||||
<h1 class="welcome-title">Acesse sua conta</h1>
|
||||
<p class="welcome-subtitle">Informe suas credenciais para entrar na plataforma.</p>
|
||||
|
||||
<!-- Usuário -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Usuário</label>
|
||||
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" class="login-form">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">E-mail</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
type="email"
|
||||
id="email"
|
||||
formControlName="username"
|
||||
placeholder="Usuário" />
|
||||
|
||||
<div class="text-danger small mt-1" *ngIf="hasError('username')">
|
||||
Informe o usuário.
|
||||
</div>
|
||||
placeholder="admin@empresa.com"
|
||||
[class.error]="hasError('username')"
|
||||
>
|
||||
<div class="error-msg" *ngIf="hasError('username')">E-mail obrigatório ou inválido.</div>
|
||||
</div>
|
||||
|
||||
<!-- Senha -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Senha</label>
|
||||
<div class="form-group">
|
||||
<label for="password">Senha</label>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
[type]="showPassword ? 'text' : 'password'"
|
||||
id="password"
|
||||
formControlName="password"
|
||||
placeholder="Senha" />
|
||||
|
||||
<div class="text-danger small mt-1" *ngIf="hasError('password')">
|
||||
A senha deve ter pelo menos 6 caracteres.
|
||||
placeholder="••••••••••••"
|
||||
[class.error]="hasError('password')"
|
||||
>
|
||||
<button type="button" class="toggle-pass" (click)="togglePassword()">
|
||||
<i class="bi" [ngClass]="showPassword ? 'bi-eye-slash' : 'bi-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="error-msg" *ngIf="hasError('password')">Senha inválida.</div>
|
||||
</div>
|
||||
|
||||
<!-- Erro da API -->
|
||||
<div *ngIf="apiError" class="alert alert-danger py-2 mb-3">
|
||||
{{ apiError }}
|
||||
<div class="form-actions">
|
||||
<label class="custom-checkbox">
|
||||
<input type="checkbox" formControlName="rememberMe">
|
||||
<span class="checkmark"></span>
|
||||
Lembrar de mim
|
||||
</label>
|
||||
<a href="#" class="forgot-link">Esqueceu a senha?</a>
|
||||
</div>
|
||||
|
||||
<!-- Botão Entrar -->
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-100 login-btn-submit"
|
||||
[disabled]="isSubmitting">
|
||||
{{ isSubmitting ? 'Entrando...' : 'ENTRAR' }}
|
||||
<button type="submit" class="btn-primary-login" [disabled]="isSubmitting || loginForm.invalid">
|
||||
<span *ngIf="!isSubmitting">Entrar</span>
|
||||
<div *ngIf="isSubmitting" class="spinner"></div>
|
||||
</button>
|
||||
|
||||
|
||||
<div *ngIf="apiError" class="api-alert">
|
||||
<i class="bi bi-exclamation-circle"></i> {{ apiError }}
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="left-footer mt-5">
|
||||
<span>© 2026 Ingline Systems.</span>
|
||||
<a href="#">Privacidade</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast (Sucesso Login) -->
|
||||
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 2000;">
|
||||
<div
|
||||
#successToast
|
||||
class="toast text-bg-success"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
>
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">LineGestão</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Fechar"></button>
|
||||
<div class="login-right">
|
||||
<div class="right-content">
|
||||
<div class="text-area">
|
||||
<h2 class="hero-title">Gerencie suas linhas com<br>inteligência e controle.</h2>
|
||||
<p class="hero-desc">Acesse o dashboard completo para monitorar contratos, faturas e usuários.</p>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
{{ toastMessage }}
|
||||
|
||||
<div class="dashboard-mockup fade-in-up" style="animation-delay: 0.2s;">
|
||||
<div class="mock-top-bar">
|
||||
<div class="dots"><span></span><span></span></div>
|
||||
<div class="mock-search"></div>
|
||||
<div class="mock-profile"></div>
|
||||
</div>
|
||||
|
||||
<div class="mock-grid">
|
||||
<div class="mock-card highlight">
|
||||
<span>Gasto Mensal</span>
|
||||
<h3>R$ 12.450</h3>
|
||||
<div class="badge-mock"><i class="bi bi-arrow-down"></i> -2.4%</div>
|
||||
</div>
|
||||
|
||||
<div class="mock-card chart-card">
|
||||
<span>Linhas Ativas</span>
|
||||
<div class="fake-bars">
|
||||
<div class="bar" style="height: 40%"></div>
|
||||
<div class="bar active" style="height: 80%"></div>
|
||||
<div class="bar" style="height: 60%"></div>
|
||||
<div class="bar" style="height: 50%"></div>
|
||||
<div class="bar" style="height: 70%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mock-card table-card">
|
||||
<div class="table-row head"></div>
|
||||
<div class="table-row"><div class="avatar"></div><div class="line"></div><div class="status ok"></div></div>
|
||||
<div class="table-row"><div class="avatar"></div><div class="line"></div><div class="status warn"></div></div>
|
||||
<div class="table-row"><div class="avatar"></div><div class="line"></div><div class="status ok"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="float-card">
|
||||
<div class="icon-circle"><i class="bi bi-sim"></i></div>
|
||||
<div>
|
||||
<strong>128 Linhas</strong>
|
||||
<small>Monitoradas</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -1,216 +1,530 @@
|
|||
/* ========================= */
|
||||
/* TELA DE LOGIN */
|
||||
/* ========================= */
|
||||
:host {
|
||||
/* Paleta de Cores LineGestão */
|
||||
--brand-blue: #030FAA; /* Azul Principal */
|
||||
--brand-blue-dark: #020865; /* Azul Profundo (Gradiente) */
|
||||
--brand-pink: #E33DCF; /* Rosa (Detalhes/Acentos) */
|
||||
--text-main: #1e293b; /* Cinza Escuro (Texto) */
|
||||
--text-muted: #64748b; /* Cinza Médio (Legendas) */
|
||||
--bg-input: #f8fafc; /* Fundo dos inputs */
|
||||
--border-input: #e2e8f0; /* Borda dos inputs */
|
||||
}
|
||||
|
||||
/* Wrapper para centralizar o card entre header e footer */
|
||||
/* ================================================= */
|
||||
/* CONTAINER GLOBAL (TELA DIVIDIDA) */
|
||||
/* ================================================= */
|
||||
.login-wrapper {
|
||||
min-height: calc(100vh - 69.2px);
|
||||
|
||||
display: flex;
|
||||
justify-content: center; /* login fica no centro */
|
||||
align-items: center;
|
||||
|
||||
padding-top: 32px;
|
||||
padding-right: 12px;
|
||||
padding-bottom: 100px; /* mesmo “respiro” do cadastro */
|
||||
padding-left: 12px;
|
||||
|
||||
background-color: #efefef;
|
||||
|
||||
/* IMAGEM DESKTOP (PADRÃO) */
|
||||
background-image: url('../../../assets/wallpaper/registro_login.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right top;
|
||||
background-size: cover;
|
||||
|
||||
/* 🔑 fundo preso ao viewport (igual cadastro) */
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
/* NOTEBOOKS / TABLETS */
|
||||
@media (max-width: 992px) {
|
||||
.login-wrapper {
|
||||
padding-top: 24px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-bottom: 60px;
|
||||
|
||||
background-position: center top;
|
||||
background-size: cover;
|
||||
background-attachment: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
/* CELULARES – IMAGEM MOBILE */
|
||||
@media (max-width: 576px) {
|
||||
.login-wrapper {
|
||||
min-height: calc(100vh - 40px);
|
||||
padding-top: 20px;
|
||||
padding-bottom: 32px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
|
||||
background-image: url('../../../assets/wallpaper/mobile.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center top;
|
||||
background-size: cover;
|
||||
background-attachment: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ========================= */
|
||||
/* CARD DE LOGIN */
|
||||
/* ========================= */
|
||||
|
||||
/* Desktop grande (monitor) */
|
||||
.login-card {
|
||||
background-color: transparent;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #c91eb5;
|
||||
|
||||
max-width: 480px; /* antes 500px */
|
||||
width: 100%;
|
||||
min-height: 360px; /* antes 380px */
|
||||
padding: 26px 22px; /* antes 28px 24px */
|
||||
box-sizing: border-box;
|
||||
height: 100vh; /* Ocupa 100% da altura da viewport */
|
||||
min-height: 600px; /* Evita esmagamento em telas muito baixas */
|
||||
background: #fff;
|
||||
font-family: 'Inter', sans-serif;
|
||||
overflow: hidden; /* Garante que nada gere scroll indesejado na tela principal */
|
||||
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
|
||||
.mb-3,
|
||||
.mb-4 {
|
||||
margin-bottom: 0.8rem; /* antes 0.9rem */
|
||||
}
|
||||
}
|
||||
|
||||
/* NOTEB00KS (≤1440px) – deixa ainda mais compacto */
|
||||
@media (max-width: 1440px) {
|
||||
.login-card {
|
||||
max-width: 430px;
|
||||
min-height: 330px;
|
||||
padding: 22px 20px;
|
||||
}
|
||||
|
||||
.login-title h2 {
|
||||
font-size: 30px; /* levemente menor */
|
||||
}
|
||||
}
|
||||
|
||||
/* notebooks / tablets (≤992px) */
|
||||
/* Responsivo: Vira coluna no mobile/tablet vertical */
|
||||
@media (max-width: 992px) {
|
||||
.login-card {
|
||||
max-width: 400px;
|
||||
min-height: 310px;
|
||||
padding: 20px 18px;
|
||||
}
|
||||
|
||||
.login-title h2 {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
height: 36px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-btn-submit {
|
||||
font-size: 13px;
|
||||
padding: 7px 0;
|
||||
}
|
||||
|
||||
.mb-3,
|
||||
.mb-4 {
|
||||
margin-bottom: 0.7rem;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
overflow-y: auto; /* Permite scroll no mobile */
|
||||
}
|
||||
}
|
||||
|
||||
/* celulares (≤576px) – bem enxuto */
|
||||
@media (max-width: 576px) {
|
||||
.login-card {
|
||||
max-width: 340px;
|
||||
min-height: auto;
|
||||
padding: 18px 14px;
|
||||
/* ================================================= */
|
||||
/* LADO ESQUERDO (FORMULÁRIO) */
|
||||
/* ================================================= */
|
||||
.login-left {
|
||||
flex: 1; /* Ocupa o espaço necessário */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center; /* Centraliza verticalmente */
|
||||
padding: 0 60px;
|
||||
background: white;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
|
||||
/* Ajuste Notebook: Menos padding lateral */
|
||||
@media (max-width: 1366px) { padding: 0 40px; }
|
||||
|
||||
/* Ajuste Mobile */
|
||||
@media (max-width: 576px) { padding: 32px 24px; }
|
||||
}
|
||||
|
||||
.login-title h2 {
|
||||
font-size: 24px;
|
||||
/* Conteúdo Centralizado (Logo + Form) */
|
||||
.left-content {
|
||||
max-width: 380px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
height: 34px;
|
||||
font-size: 13px;
|
||||
}
|
||||
/* Logo no topo do form */
|
||||
.brand-header {
|
||||
margin-bottom: 32px;
|
||||
|
||||
.login-btn-submit {
|
||||
font-size: 12.5px;
|
||||
padding: 7px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ========================= */
|
||||
/* TIPOGRAFIA E FORM */
|
||||
/* ========================= */
|
||||
|
||||
/* Título centralizado rosa */
|
||||
.login-title {
|
||||
.brand-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.25rem !important;
|
||||
|
||||
h2 {
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
gap: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
|
||||
i { color: var(--brand-blue); font-size: 24px; }
|
||||
}
|
||||
}
|
||||
|
||||
/* Títulos */
|
||||
.welcome-title {
|
||||
font-size: 32px;
|
||||
color: #c91eb5;
|
||||
margin: 0;
|
||||
}
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.5px;
|
||||
line-height: 1.2;
|
||||
|
||||
@media (max-width: 1366px) { font-size: 28px; }
|
||||
}
|
||||
|
||||
/* Labels – mesmo tamanho do cadastro */
|
||||
.form-label {
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #000000;
|
||||
.welcome-subtitle {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 32px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Inputs iguais aos do cadastro (borda azul) */
|
||||
.form-control {
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #6066ff;
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #000000;
|
||||
/* INPUTS E LABELS */
|
||||
.form-group {
|
||||
margin-bottom: 18px;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/* Botão ENTRAR – rosa sólido, mesmo estilo do cadastrar */
|
||||
.login-btn-submit {
|
||||
border-radius: 40px;
|
||||
border: none;
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 48px; /* Altura confortável */
|
||||
padding: 0 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-input);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-main);
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
padding: 9px 0;
|
||||
background-color: #c91eb5;
|
||||
color: #ffffff;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.04);
|
||||
/* Ajuste Notebook: levemente menor */
|
||||
@media (max-width: 1366px) { height: 44px; }
|
||||
|
||||
&::placeholder { color: #cbd5e1; }
|
||||
|
||||
&:focus {
|
||||
background: #fff;
|
||||
border-color: var(--brand-blue);
|
||||
box-shadow: 0 0 0 3px rgba(3, 15, 170, 0.1);
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: #ef4444;
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Mensagens de erro */
|
||||
.text-danger.small {
|
||||
/* Wrapper para input de senha com ícone */
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
|
||||
input { padding-right: 40px; }
|
||||
|
||||
.toggle-pass {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
|
||||
&:hover { color: var(--text-main); }
|
||||
}
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
color: #ef4444;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* AÇÕES (Checkbox + Esqueci Senha) */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.custom-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
user-select: none;
|
||||
|
||||
input { display: none; }
|
||||
|
||||
.checkmark {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid var(--border-input);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
input:checked ~ .checkmark {
|
||||
background: var(--brand-blue);
|
||||
border-color: var(--brand-blue);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 4px; top: 1px;
|
||||
width: 5px; height: 9px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
color: var(--brand-blue);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
|
||||
&:hover { text-decoration: underline; }
|
||||
}
|
||||
|
||||
/* BOTÃO PRINCIPAL */
|
||||
.btn-primary-login {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: var(--brand-blue);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--brand-blue-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(3, 15, 170, 0.25);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #94a3b8;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
/* FOOTER INTERNO */
|
||||
.left-footer {
|
||||
margin-top: 40px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
a {
|
||||
color: var(--text-main);
|
||||
text-decoration: none;
|
||||
&:hover { text-decoration: underline; }
|
||||
}
|
||||
}
|
||||
|
||||
/* Alerta de erro da API */
|
||||
.api-alert {
|
||||
margin-top: 16px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #fef2f2;
|
||||
color: #ef4444;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
/* ================================================= */
|
||||
/* LADO DIREITO (VISUAL / MOCKUP) */
|
||||
/* ================================================= */
|
||||
.login-right {
|
||||
flex: 1.3; /* Ligeiramente maior que o form */
|
||||
padding: 16px; /* Borda branca ao redor do bloco azul */
|
||||
background: #fff;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
/* Some no tablet/mobile */
|
||||
@media (max-width: 992px) { display: none; }
|
||||
}
|
||||
|
||||
.right-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 24px;
|
||||
position: relative;
|
||||
overflow: hidden; /* Corta o mockup que sair da área */
|
||||
|
||||
/* Gradiente Azul LineGestão */
|
||||
background: radial-gradient(circle at 20% 20%, #2a39ff 0%, var(--brand-blue) 60%, var(--brand-blue-dark) 100%);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 56px;
|
||||
color: white;
|
||||
|
||||
/* Ajuste Notebook: Menos padding */
|
||||
@media (max-width: 1366px) { padding: 40px; }
|
||||
}
|
||||
|
||||
.text-area {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 16px;
|
||||
|
||||
@media (max-width: 1366px) { font-size: 28px; }
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
font-size: 16px;
|
||||
opacity: 0.85;
|
||||
max-width: 450px;
|
||||
line-height: 1.5;
|
||||
|
||||
@media (max-width: 1366px) { font-size: 14px; }
|
||||
}
|
||||
|
||||
/* ================================================= */
|
||||
/* MOCKUP SISTEMA (CSS PURO) */
|
||||
/* ================================================= */
|
||||
.dashboard-mockup {
|
||||
position: absolute;
|
||||
/* Posicionamento estratégico para não cortar feio */
|
||||
bottom: -30px;
|
||||
right: -30px;
|
||||
|
||||
width: 85%; /* Largura relativa */
|
||||
max-width: 700px; /* Trava máxima */
|
||||
aspect-ratio: 16/10; /* Mantém proporção de tela wide */
|
||||
|
||||
background: #F1F5F9; /* Cinza claro (fundo do sistema) */
|
||||
border-top-left-radius: 24px;
|
||||
box-shadow: -20px -20px 60px rgba(0,0,0,0.35); /* Sombra forte para dar profundidade */
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
z-index: 1;
|
||||
|
||||
/* 🔥 CORREÇÃO CRÍTICA PARA NOTEBOOKS 🔥 */
|
||||
/* Se a tela for baixa ou estreita, diminui o mockup automaticamente */
|
||||
@media (max-height: 800px), (max-width: 1366px) {
|
||||
width: 80%;
|
||||
right: -20px;
|
||||
bottom: -20px;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Barra do Topo (Mock) */
|
||||
.mock-top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.dots span {
|
||||
display: inline-block;
|
||||
width: 8px; height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #cbd5e1;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.mock-search {
|
||||
flex: 1; height: 32px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.mock-profile {
|
||||
width: 32px; height: 32px;
|
||||
background: #cbd5e1;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Grid Interno (Mock) */
|
||||
.mock-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr; /* Coluna esquerda levemente maior */
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Cards Genéricos (Mock) */
|
||||
.mock-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.03);
|
||||
|
||||
/* Card Destaque (Azul) */
|
||||
&.highlight {
|
||||
background: var(--brand-blue);
|
||||
color: white;
|
||||
|
||||
span { font-size: 11px; opacity: 0.8; margin-bottom: 4px; display: block; }
|
||||
h3 { font-size: 24px; margin: 0 0 8px 0; font-weight: 700; }
|
||||
.badge-mock {
|
||||
font-size: 10px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
width: fit-content;
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Card Gráfico */
|
||||
&.chart-card {
|
||||
grid-row: span 2; /* Ocupa altura toda */
|
||||
|
||||
span { font-size: 12px; font-weight: 600; color: #1e293b; margin-bottom: 16px; display: block;}
|
||||
|
||||
.fake-bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
height: 70%;
|
||||
padding: 0 8px;
|
||||
|
||||
.bar {
|
||||
width: 12px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 10px;
|
||||
|
||||
/* A barra rosa ativa */
|
||||
&.active {
|
||||
background: var(--brand-pink);
|
||||
box-shadow: 0 4px 10px rgba(227, 61, 207, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Card Tabela */
|
||||
&.table-card {
|
||||
gap: 10px;
|
||||
padding-top: 12px;
|
||||
|
||||
.table-row {
|
||||
display: flex; align-items: center; gap: 10px; height: 28px;
|
||||
|
||||
&.head { background: #f1f5f9; border-radius: 6px; height: 24px; width: 100%; margin-bottom: 4px; }
|
||||
|
||||
.avatar { width: 24px; height: 24px; border-radius: 50%; background: #e2e8f0; }
|
||||
.line { flex: 1; height: 8px; background: #f1f5f9; border-radius: 4px; }
|
||||
.status { width: 8px; height: 8px; border-radius: 50%; }
|
||||
.status.ok { background: #10b981; }
|
||||
.status.warn { background: #f59e0b; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Card Flutuante (Badges) */
|
||||
.float-card {
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
left: -40px;
|
||||
|
||||
background: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.12); /* Sombra suave */
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
z-index: 10;
|
||||
animation: float 5s ease-in-out infinite;
|
||||
|
||||
/* Ajuste no notebook */
|
||||
@media (max-width: 1366px) { left: -24px; padding: 10px 16px; }
|
||||
|
||||
.icon-circle {
|
||||
width: 40px; height: 40px;
|
||||
background: rgba(227, 61, 207, 0.1); /* Rosa suave */
|
||||
color: var(--brand-pink);
|
||||
border-radius: 10px;
|
||||
display: grid; place-items: center;
|
||||
font-size: 18px;
|
||||
|
||||
@media (max-width: 1366px) { width: 36px; height: 36px; font-size: 16px; }
|
||||
}
|
||||
|
||||
strong { display: block; font-size: 14px; color: #1e293b; font-weight: 700; }
|
||||
small { font-size: 11px; color: #64748b; }
|
||||
}
|
||||
|
||||
/* Animações Utilitárias */
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 20px; height: 20px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
|
@ -16,6 +16,9 @@ export class LoginComponent {
|
|||
isSubmitting = false;
|
||||
apiError = '';
|
||||
|
||||
// Variável necessária para o ícone de olho no HTML novo
|
||||
showPassword = false;
|
||||
|
||||
toastMessage = '';
|
||||
@ViewChild('successToast') successToast!: ElementRef;
|
||||
|
||||
|
|
@ -26,11 +29,18 @@ export class LoginComponent {
|
|||
@Inject(PLATFORM_ID) private platformId: object
|
||||
) {
|
||||
this.loginForm = this.fb.group({
|
||||
username: ['', [Validators.required]], // aqui é email
|
||||
password: ['', [Validators.required, Validators.minLength(6)]]
|
||||
username: ['', [Validators.required]],
|
||||
password: ['', [Validators.required, Validators.minLength(6)]],
|
||||
// Adicionado apenas para não dar erro no HTML novo, mas a lógica de envio ignora se quiser
|
||||
rememberMe: [false]
|
||||
});
|
||||
}
|
||||
|
||||
// Método necessário para o botão do "olhinho" funcionar
|
||||
togglePassword(): void {
|
||||
this.showPassword = !this.showPassword;
|
||||
}
|
||||
|
||||
private async showToast(message: string) {
|
||||
this.toastMessage = message;
|
||||
|
||||
|
|
@ -65,49 +75,66 @@ export class LoginComponent {
|
|||
|
||||
// evita token antigo conflitar
|
||||
localStorage.removeItem('token');
|
||||
|
||||
// Se quiser implementar a lógica de "Manter conectado", pode verificar o rememberMe aqui
|
||||
// mas mantive a lógica original simples:
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
console.log('🚀 Iniciando login...');
|
||||
this.apiError = '';
|
||||
|
||||
if (this.loginForm.invalid) {
|
||||
console.log('❌ Formulário inválido', this.loginForm.errors);
|
||||
this.loginForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSubmitting = true;
|
||||
|
||||
const v = this.loginForm.value;
|
||||
|
||||
const payload = {
|
||||
email: v.username,
|
||||
password: v.password
|
||||
};
|
||||
|
||||
this.authService.login(payload).subscribe({
|
||||
next: async (res) => {
|
||||
this.authService.login({ email: v.username, password: v.password }).subscribe({
|
||||
next: (res: any) => { // Use 'any' temporariamente para ver tudo que vem
|
||||
console.log('✅ Resposta da API:', res);
|
||||
this.isSubmitting = false;
|
||||
|
||||
const token = res?.token;
|
||||
// VERIFICAÇÃO 1: O token veio mesmo?
|
||||
// As vezes vem como res.accessToken, res.data.token, etc.
|
||||
const token = res?.token || res?.accessToken;
|
||||
|
||||
if (!token) {
|
||||
this.apiError = 'Login retornou sem token. Verifique a resposta da API.';
|
||||
console.error('❌ Token não encontrado na resposta!');
|
||||
this.apiError = 'Erro: A API não retornou o token.';
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ salva token para o Interceptor anexar nas próximas requisições
|
||||
console.log('🔑 Token encontrado. Salvando...');
|
||||
this.saveToken(token);
|
||||
|
||||
// VERIFICAÇÃO 2: Decodificação
|
||||
try {
|
||||
const nome = this.getNameFromToken(token);
|
||||
console.log('👤 Nome extraído:', nome);
|
||||
|
||||
// ✅ Vai para /geral já levando a mensagem do toast
|
||||
console.log('🔄 Tentando ir para /geral...');
|
||||
this.router.navigate(['/geral'], {
|
||||
state: { toastMessage: `Bem-vindo, ${nome}!` }
|
||||
}).then(sucesso => {
|
||||
if (sucesso) console.log('✅ Navegação funcionou!');
|
||||
else console.error('❌ Navegação falhou! A rota "/geral" existe?');
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error('❌ Erro ao processar token ou navegar:', e);
|
||||
// Força a ida mesmo se o nome falhar
|
||||
this.router.navigate(['/geral']);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('❌ Erro na requisição:', err);
|
||||
this.isSubmitting = false;
|
||||
this.apiError = err?.error ?? 'Erro ao fazer login.';
|
||||
this.apiError = err?.error?.message || 'Usuário ou senha incorretos.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -116,6 +143,6 @@ export class LoginComponent {
|
|||
const control = this.loginForm.get(field);
|
||||
if (!control) return false;
|
||||
if (error) return control.touched && control.hasError(error);
|
||||
return control.touched && control.invalid;
|
||||
return !!(control.touched && control.invalid);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,10 +24,12 @@
|
|||
<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
|
||||
|
|
@ -43,6 +45,7 @@
|
|||
<span *ngIf="!loading">{{ total || 0 }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="kpi">
|
||||
<span class="lbl">Registros</span>
|
||||
<span class="val">
|
||||
|
|
@ -50,6 +53,7 @@
|
|||
<span *ngIf="!loading">{{ groupLoadedRecords || 0 }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="kpi">
|
||||
<span class="lbl text-brand">Trocas</span>
|
||||
<span class="val text-brand">
|
||||
|
|
@ -57,6 +61,7 @@
|
|||
<span *ngIf="!loading">{{ groupTotalTrocas || 0 }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="kpi">
|
||||
<span class="lbl text-success">ICCID</span>
|
||||
<span class="val text-success">
|
||||
|
|
@ -68,15 +73,27 @@
|
|||
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
|
|
@ -86,23 +103,29 @@
|
|||
</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="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
|
||||
*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>
|
||||
|
|
@ -110,13 +133,18 @@
|
|||
<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 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>
|
||||
<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">
|
||||
|
|
@ -132,10 +160,14 @@
|
|||
<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>
|
||||
<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>
|
||||
|
|
@ -149,27 +181,42 @@
|
|||
</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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
|
@ -178,115 +225,245 @@
|
|||
|
||||
<div class="modal-backdrop-custom" *ngIf="editOpen || createOpen" (click)="closeEdit(); closeCreate()"></div>
|
||||
|
||||
<!-- ============================== -->
|
||||
<!-- EDIT MODAL -->
|
||||
<!-- ============================== -->
|
||||
<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-header">
|
||||
<span><i class="bi bi-card-text me-2"></i> Informações</span>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<div class="form-grid">
|
||||
|
||||
<!-- Cliente (select) -->
|
||||
<div class="form-field span-2">
|
||||
<label>Nome do Cliente</label>
|
||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.cliente" />
|
||||
<label>Cliente (GERAL)</label>
|
||||
<select
|
||||
class="form-control form-control-sm"
|
||||
[(ngModel)]="editModel.selectedClient"
|
||||
(change)="onEditClientChange()"
|
||||
>
|
||||
<option value="">Selecione...</option>
|
||||
<option *ngFor="let c of clientOptions" [value]="c">{{ c }}</option>
|
||||
</select>
|
||||
|
||||
<small class="text-muted fw-bold" *ngIf="editClientsLoading">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>Carregando clientes...
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Linha Antiga (select da Geral) -->
|
||||
<div class="form-field span-2">
|
||||
<label>Linha Antiga (GERAL)</label>
|
||||
<select
|
||||
class="form-control form-control-sm"
|
||||
[(ngModel)]="editModel.mobileLineId"
|
||||
(change)="onEditLineChange()"
|
||||
[disabled]="!editModel.selectedClient || editLinesLoading"
|
||||
>
|
||||
<option value="">Selecione a linha do cliente...</option>
|
||||
|
||||
<!-- ✅ ITEM • LINHA • USUÁRIO -->
|
||||
<option *ngFor="let l of lineOptionsEdit" [value]="l.id">
|
||||
{{ l.item }} • {{ l.linha || '-' }} • {{ l.usuario || 'SEM USUÁRIO' }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<small class="text-muted fw-bold" *ngIf="editLinesLoading">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>Carregando linhas...
|
||||
</small>
|
||||
</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>
|
||||
|
||||
<!-- LinhaAntiga (snapshot) - preenchido automaticamente -->
|
||||
<div class="form-field">
|
||||
<label>Linha Antiga</label>
|
||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.linhaAntiga" />
|
||||
<label>Linha Antiga (snapshot)</label>
|
||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.linhaAntiga" readonly />
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label>Linha Nova</label>
|
||||
<input class="form-control form-control-sm fw-bold text-blue" [(ngModel)]="editModel.linhaNova" />
|
||||
</div>
|
||||
|
||||
<!-- ICCID auto do GERAL -->
|
||||
<div class="form-field span-2">
|
||||
<label>ICCID</label>
|
||||
<input class="form-control form-control-sm font-monospace" [(ngModel)]="editModel.iccid" />
|
||||
<label>ICCID (auto)</label>
|
||||
<input class="form-control form-control-sm font-monospace" [(ngModel)]="editModel.iccid" readonly />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-3" *ngIf="editModel?.clienteInfo">
|
||||
<small class="text-muted fw-bold">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
{{ editModel.clienteInfo }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #editLoadingTpl><div class="p-5 text-center text-muted">Preparando edição...</div></ng-template>
|
||||
|
||||
<ng-template #editLoadingTpl>
|
||||
<div class="p-5 text-center text-muted">
|
||||
<span class="spinner-border me-2"></span> Preparando edição...
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================== -->
|
||||
<!-- CREATE MODAL -->
|
||||
<!-- ============================== -->
|
||||
<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-header">
|
||||
<span><i class="bi bi-pencil me-2"></i> Preencha os dados</span>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<div class="form-grid">
|
||||
|
||||
<!-- Cliente (select) -->
|
||||
<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" />
|
||||
<label>Cliente (GERAL) <span class="text-danger">*</span></label>
|
||||
<select
|
||||
class="form-control form-control-sm"
|
||||
[(ngModel)]="createModel.selectedClient"
|
||||
(change)="onCreateClientChange()"
|
||||
>
|
||||
<option value="">Selecione...</option>
|
||||
<option *ngFor="let c of clientOptions" [value]="c">{{ c }}</option>
|
||||
</select>
|
||||
|
||||
<small class="text-muted fw-bold" *ngIf="createClientsLoading">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>Carregando clientes...
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Linha Antiga (select Geral) -->
|
||||
<div class="form-field span-2">
|
||||
<label>Linha Antiga (GERAL) <span class="text-danger">*</span></label>
|
||||
<select
|
||||
class="form-control form-control-sm"
|
||||
[(ngModel)]="createModel.mobileLineId"
|
||||
(change)="onCreateLineChange()"
|
||||
[disabled]="!createModel.selectedClient || createLinesLoading"
|
||||
>
|
||||
<option value="">Selecione a linha do cliente...</option>
|
||||
|
||||
<!-- ✅ ITEM • LINHA • USUÁRIO -->
|
||||
<option *ngFor="let l of lineOptionsCreate" [value]="l.id">
|
||||
{{ l.item }} • {{ l.linha || '-' }} • {{ l.usuario || 'SEM USUÁRIO' }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<small class="text-muted fw-bold" *ngIf="createLinesLoading">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>Carregando linhas...
|
||||
</small>
|
||||
</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>
|
||||
|
||||
<!-- snapshot -->
|
||||
<div class="form-field">
|
||||
<label>Linha Antiga</label>
|
||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.linhaAntiga" />
|
||||
<label>Linha Antiga (snapshot)</label>
|
||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.linhaAntiga" readonly />
|
||||
</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>
|
||||
|
||||
<!-- ICCID auto do GERAL -->
|
||||
<div class="form-field span-2">
|
||||
<label>ICCID</label>
|
||||
<input class="form-control form-control-sm font-monospace" [(ngModel)]="createModel.iccid" />
|
||||
</div>
|
||||
</div>
|
||||
<label>ICCID (auto)</label>
|
||||
<input class="form-control form-control-sm font-monospace" [(ngModel)]="createModel.iccid" readonly />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-3" *ngIf="createModel?.clienteInfo">
|
||||
<small class="text-muted fw-bold">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
{{ createModel.clienteInfo }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -9,25 +9,11 @@ import {
|
|||
} from '@angular/core';
|
||||
import { isPlatformBrowser, CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
HttpClient,
|
||||
HttpClientModule,
|
||||
HttpParams
|
||||
} from '@angular/common/http';
|
||||
import { HttpClient, HttpClientModule, HttpParams } from '@angular/common/http';
|
||||
import { LinesService } from '../../services/lines.service';
|
||||
|
||||
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;
|
||||
|
|
@ -43,6 +29,48 @@ interface ClientGroup {
|
|||
semIccid: number;
|
||||
}
|
||||
|
||||
interface MuregRow {
|
||||
id: string;
|
||||
item: string;
|
||||
linhaAntiga: string;
|
||||
linhaNova: string;
|
||||
iccid: string;
|
||||
dataDaMureg: string;
|
||||
cliente: string;
|
||||
mobileLineId: string;
|
||||
raw: any;
|
||||
}
|
||||
|
||||
/** ✅ AGORA COM item/usuario/chip (igual Troca de Número) */
|
||||
interface LineOptionDto {
|
||||
id: string;
|
||||
item: number;
|
||||
linha: string | null;
|
||||
chip: string | null; // => ICCID
|
||||
usuario: string | null;
|
||||
cliente?: string | null;
|
||||
skil?: string | null;
|
||||
}
|
||||
|
||||
interface MuregDetailDto {
|
||||
id: string;
|
||||
item: number;
|
||||
linhaAntiga: string | null;
|
||||
linhaNova: string | null;
|
||||
iccid: string | null;
|
||||
dataDaMureg: string | null;
|
||||
mobileLineId: string;
|
||||
|
||||
cliente: string | null;
|
||||
usuario: string | null;
|
||||
skil: string | null;
|
||||
|
||||
linhaAtualNaGeral: string | null;
|
||||
chipNaGeral: string | null;
|
||||
contaNaGeral: string | null;
|
||||
statusNaGeral: string | null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, HttpClientModule],
|
||||
|
|
@ -58,7 +86,8 @@ export class Mureg implements AfterViewInit {
|
|||
constructor(
|
||||
@Inject(PLATFORM_ID) private platformId: object,
|
||||
private http: HttpClient,
|
||||
private cdr: ChangeDetectorRef
|
||||
private cdr: ChangeDetectorRef,
|
||||
private linesService: LinesService
|
||||
) {}
|
||||
|
||||
private readonly apiBase = 'https://localhost:7205/api/mureg';
|
||||
|
|
@ -68,7 +97,6 @@ export class Mureg implements AfterViewInit {
|
|||
pagedClientGroups: ClientGroup[] = [];
|
||||
expandedGroup: string | null = null;
|
||||
groupRows: MuregRow[] = [];
|
||||
|
||||
private rowsByClient = new Map<string, MuregRow[]>();
|
||||
|
||||
// KPIs
|
||||
|
|
@ -83,6 +111,19 @@ export class Mureg implements AfterViewInit {
|
|||
pageSize = 10;
|
||||
total = 0;
|
||||
|
||||
// ====== OPTIONS (GERAL) ======
|
||||
clientOptions: string[] = [];
|
||||
|
||||
// create options
|
||||
lineOptionsCreate: LineOptionDto[] = [];
|
||||
createClientsLoading = false;
|
||||
createLinesLoading = false;
|
||||
|
||||
// edit options
|
||||
lineOptionsEdit: LineOptionDto[] = [];
|
||||
editClientsLoading = false;
|
||||
editLinesLoading = false;
|
||||
|
||||
// ====== EDIT MODAL ======
|
||||
editOpen = false;
|
||||
editSaving = false;
|
||||
|
|
@ -92,18 +133,23 @@ export class Mureg implements AfterViewInit {
|
|||
createOpen = false;
|
||||
createSaving = false;
|
||||
createModel: any = {
|
||||
cliente: '',
|
||||
selectedClient: '',
|
||||
mobileLineId: '',
|
||||
item: '',
|
||||
linhaAntiga: '',
|
||||
linhaNova: '',
|
||||
iccid: '',
|
||||
dataDaMureg: ''
|
||||
dataDaMureg: '',
|
||||
clienteInfo: ''
|
||||
};
|
||||
|
||||
async ngAfterViewInit() {
|
||||
if (!isPlatformBrowser(this.platformId)) return;
|
||||
this.initAnimations();
|
||||
setTimeout(() => { this.refresh(); });
|
||||
setTimeout(() => {
|
||||
this.preloadClients(); // ✅ já deixa o select pronto
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
private initAnimations() {
|
||||
|
|
@ -148,6 +194,7 @@ export class Mureg implements AfterViewInit {
|
|||
}
|
||||
|
||||
get totalPages() { return Math.ceil((this.total || 0) / this.pageSize) || 1; }
|
||||
|
||||
get pageNumbers() {
|
||||
const total = this.totalPages;
|
||||
const current = this.page;
|
||||
|
|
@ -159,7 +206,9 @@ export class Mureg implements AfterViewInit {
|
|||
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);
|
||||
|
|
@ -168,7 +217,7 @@ export class Mureg implements AfterViewInit {
|
|||
trackById(_: number, row: MuregRow) { return row.id; }
|
||||
|
||||
// =======================================================================
|
||||
// LOAD LOGIC
|
||||
// LOAD LOGIC (lista e grupos)
|
||||
// =======================================================================
|
||||
private loadForGroups() {
|
||||
this.loading = true;
|
||||
|
|
@ -218,8 +267,10 @@ export class Mureg implements AfterViewInit {
|
|||
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 });
|
||||
});
|
||||
|
||||
|
|
@ -236,6 +287,7 @@ export class Mureg implements AfterViewInit {
|
|||
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 = [];
|
||||
|
|
@ -248,6 +300,7 @@ export class Mureg implements AfterViewInit {
|
|||
this.groupRows = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.expandedGroup = cliente;
|
||||
const rows = this.rowsByClient.get(cliente) ?? [];
|
||||
this.groupRows = [...rows].sort((a, b) => {
|
||||
|
|
@ -279,6 +332,7 @@ export class Mureg implements AfterViewInit {
|
|||
const iccid = pick(x, ['iccid', 'ICCID']);
|
||||
const dataDaMureg = pick(x, ['dataDaMureg', 'data_da_mureg', 'DATA DA MUREG']);
|
||||
const cliente = pick(x, ['cliente', 'CLIENTE']);
|
||||
const mobileLineId = String(pick(x, ['mobileLineId', 'MobileLineId', 'mobile_line_id']) ?? '');
|
||||
const id = String(pick(x, ['id', 'ID']) || `${idx}-${item}-${linhaNova}-${iccid}`);
|
||||
|
||||
return {
|
||||
|
|
@ -289,92 +343,171 @@ export class Mureg implements AfterViewInit {
|
|||
iccid: String(iccid ?? ''),
|
||||
dataDaMureg: String(dataDaMureg ?? ''),
|
||||
cliente: String(cliente ?? ''),
|
||||
mobileLineId,
|
||||
raw: x
|
||||
};
|
||||
}
|
||||
|
||||
// ====== MODAL EDIÇÃO ======
|
||||
// =======================================================================
|
||||
// CLIENTS / LINES OPTIONS (GERAL)
|
||||
// =======================================================================
|
||||
private preloadClients() {
|
||||
if (this.clientOptions.length > 0) return;
|
||||
|
||||
// 1. Abrir modal
|
||||
onEditar(r: MuregRow) {
|
||||
this.editOpen = true;
|
||||
this.editSaving = false;
|
||||
this.createClientsLoading = true;
|
||||
this.editClientsLoading = true;
|
||||
|
||||
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);
|
||||
this.linesService.getClients().subscribe({
|
||||
next: (list) => {
|
||||
this.clientOptions = (list ?? []).filter(x => !!String(x ?? '').trim());
|
||||
this.createClientsLoading = false;
|
||||
this.editClientsLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
},
|
||||
error: async () => {
|
||||
this.editSaving = false;
|
||||
await this.showToast('Erro ao salvar edição.');
|
||||
this.createClientsLoading = false;
|
||||
this.editClientsLoading = false;
|
||||
await this.showToast('Erro ao carregar clientes da GERAL.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====== MODAL CRIAÇÃO ======
|
||||
private loadLinesForClient(cliente: string, target: 'create' | 'edit') {
|
||||
const c = (cliente ?? '').trim();
|
||||
|
||||
if (!c) {
|
||||
if (target === 'create') this.lineOptionsCreate = [];
|
||||
else this.lineOptionsEdit = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (target === 'create') {
|
||||
this.createLinesLoading = true;
|
||||
this.lineOptionsCreate = [];
|
||||
} else {
|
||||
this.editLinesLoading = true;
|
||||
this.lineOptionsEdit = [];
|
||||
}
|
||||
|
||||
// ✅ aqui assumimos que o getLinesByClient retorna (id,item,linha,chip,usuario...)
|
||||
this.linesService.getLinesByClient(c).subscribe({
|
||||
next: (items: any[]) => {
|
||||
const mapped: LineOptionDto[] = (items ?? [])
|
||||
.filter(x => !!String(x?.id ?? '').trim())
|
||||
.map(x => ({
|
||||
id: String(x.id),
|
||||
item: Number(x.item ?? 0),
|
||||
linha: x.linha ?? null,
|
||||
chip: x.chip ?? null,
|
||||
usuario: x.usuario ?? null,
|
||||
cliente: x.cliente ?? null,
|
||||
skil: x.skil ?? null
|
||||
}))
|
||||
.filter(x => !!String(x.linha ?? '').trim());
|
||||
|
||||
if (target === 'create') {
|
||||
this.lineOptionsCreate = mapped;
|
||||
this.createLinesLoading = false;
|
||||
} else {
|
||||
this.lineOptionsEdit = mapped;
|
||||
this.editLinesLoading = false;
|
||||
}
|
||||
|
||||
this.cdr.detectChanges();
|
||||
},
|
||||
error: async () => {
|
||||
if (target === 'create') this.createLinesLoading = false;
|
||||
else this.editLinesLoading = false;
|
||||
|
||||
await this.showToast('Erro ao carregar linhas do cliente (GERAL).');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private applySelectedLineToModel(model: any, selectedId: string, options: LineOptionDto[]) {
|
||||
const id = String(selectedId ?? '').trim();
|
||||
const opt = options.find(x => x.id === id);
|
||||
|
||||
if (!opt) return;
|
||||
|
||||
// ✅ snapshot automático (linha antiga)
|
||||
model.linhaAntiga = opt.linha ?? '';
|
||||
|
||||
// ✅ ICCID automático (chip do GERAL)
|
||||
model.iccid = opt.chip ?? '';
|
||||
|
||||
// ✅ se item estiver vazio, preenche com item da GERAL (igual Troca)
|
||||
if (!String(model.item ?? '').trim() && opt.item) {
|
||||
model.item = String(opt.item);
|
||||
}
|
||||
|
||||
model.clienteInfo = `Vínculo (GERAL): ${model.selectedClient} • ${opt.item} • ${opt.linha ?? '-'} • ${opt.usuario ?? 'SEM USUÁRIO'}`;
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// CREATE MODAL
|
||||
// =======================================================================
|
||||
onCreate() {
|
||||
this.preloadClients();
|
||||
|
||||
this.createOpen = true;
|
||||
this.createSaving = false;
|
||||
|
||||
this.createModel = {
|
||||
cliente: '',
|
||||
selectedClient: '',
|
||||
mobileLineId: '',
|
||||
item: '',
|
||||
linhaAntiga: '',
|
||||
linhaNova: '',
|
||||
iccid: '',
|
||||
dataDaMureg: ''
|
||||
dataDaMureg: '',
|
||||
clienteInfo: ''
|
||||
};
|
||||
|
||||
this.lineOptionsCreate = [];
|
||||
}
|
||||
|
||||
closeCreate() {
|
||||
this.createOpen = false;
|
||||
}
|
||||
|
||||
onCreateClientChange() {
|
||||
const c = (this.createModel.selectedClient ?? '').trim();
|
||||
|
||||
this.createModel.mobileLineId = '';
|
||||
this.createModel.linhaAntiga = '';
|
||||
this.createModel.iccid = '';
|
||||
this.createModel.clienteInfo = c ? `Cliente selecionado: ${c}` : '';
|
||||
|
||||
this.loadLinesForClient(c, 'create');
|
||||
}
|
||||
|
||||
onCreateLineChange() {
|
||||
this.applySelectedLineToModel(this.createModel, this.createModel.mobileLineId, this.lineOptionsCreate);
|
||||
}
|
||||
|
||||
saveCreate() {
|
||||
if(!this.createModel.cliente || !this.createModel.linhaNova) {
|
||||
this.showToast('Preencha Cliente e Linha Nova.');
|
||||
const mobileLineId = String(this.createModel.mobileLineId ?? '').trim();
|
||||
const linhaNova = String(this.createModel.linhaNova ?? '').trim();
|
||||
|
||||
if (!mobileLineId || !linhaNova) {
|
||||
this.showToast('Selecione Cliente + Linha Antiga (GERAL) e preencha Linha Nova.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.createSaving = true;
|
||||
|
||||
const payload = {
|
||||
...this.createModel,
|
||||
const payload: any = {
|
||||
item: this.toIntOrZero(this.createModel.item),
|
||||
mobileLineId,
|
||||
linhaAntiga: (this.createModel.linhaAntiga ?? '') || null,
|
||||
linhaNova: (this.createModel.linhaNova ?? '') || null,
|
||||
iccid: (this.createModel.iccid ?? '') || null,
|
||||
dataDaMureg: this.dateInputToIso(this.createModel.dataDaMureg)
|
||||
};
|
||||
|
||||
if (!payload.item || payload.item <= 0) delete payload.item;
|
||||
|
||||
this.http.post(this.apiBase, payload).subscribe({
|
||||
next: async () => {
|
||||
this.createSaving = false;
|
||||
|
|
@ -382,14 +515,144 @@ export class Mureg implements AfterViewInit {
|
|||
this.closeCreate();
|
||||
this.loadForGroups();
|
||||
},
|
||||
error: async () => {
|
||||
error: async (err) => {
|
||||
this.createSaving = false;
|
||||
await this.showToast('Erro ao criar Mureg.');
|
||||
const msg = this.extractApiMessage(err) ?? 'Erro ao criar Mureg.';
|
||||
await this.showToast(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helpers de Data
|
||||
// =======================================================================
|
||||
// EDIT MODAL
|
||||
// =======================================================================
|
||||
onEditar(r: MuregRow) {
|
||||
this.preloadClients();
|
||||
|
||||
this.editOpen = true;
|
||||
this.editSaving = false;
|
||||
this.editModel = null;
|
||||
this.lineOptionsEdit = [];
|
||||
|
||||
this.http.get<MuregDetailDto>(`${this.apiBase}/${r.id}`).subscribe({
|
||||
next: (d) => {
|
||||
const selectedClient = String(d?.cliente ?? '').trim();
|
||||
|
||||
this.editModel = {
|
||||
id: d.id,
|
||||
item: String(d.item ?? ''),
|
||||
dataDaMureg: this.isoToDateInput(d.dataDaMureg),
|
||||
|
||||
// snapshot atual
|
||||
linhaAntiga: String(d.linhaAntiga ?? d.linhaAtualNaGeral ?? ''),
|
||||
linhaNova: String(d.linhaNova ?? ''),
|
||||
|
||||
// ✅ se não tiver iccid salvo, usa chip da geral
|
||||
iccid: String(d.iccid ?? d.chipNaGeral ?? ''),
|
||||
|
||||
mobileLineId: String(d.mobileLineId ?? ''),
|
||||
selectedClient,
|
||||
|
||||
clienteInfo: selectedClient
|
||||
? `GERAL: ${selectedClient} • Linha atual: ${d.linhaAtualNaGeral ?? '-'} • Usuário: ${d.usuario ?? '-'} • Chip: ${d.chipNaGeral ?? '-'}`
|
||||
: ''
|
||||
};
|
||||
|
||||
if (selectedClient) {
|
||||
this.loadLinesForClient(selectedClient, 'edit');
|
||||
}
|
||||
|
||||
this.cdr.detectChanges();
|
||||
},
|
||||
error: async () => {
|
||||
this.editOpen = false;
|
||||
await this.showToast('Erro ao abrir detalhes do registro.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeEdit() {
|
||||
this.editOpen = false;
|
||||
this.editModel = null;
|
||||
this.editSaving = false;
|
||||
}
|
||||
|
||||
onEditClientChange() {
|
||||
const c = (this.editModel?.selectedClient ?? '').trim();
|
||||
|
||||
this.editModel.mobileLineId = '';
|
||||
this.editModel.linhaAntiga = '';
|
||||
this.editModel.iccid = '';
|
||||
this.editModel.clienteInfo = c ? `Cliente selecionado: ${c}` : '';
|
||||
|
||||
this.loadLinesForClient(c, 'edit');
|
||||
}
|
||||
|
||||
onEditLineChange() {
|
||||
if (!this.editModel) return;
|
||||
this.applySelectedLineToModel(this.editModel, this.editModel.mobileLineId, this.lineOptionsEdit);
|
||||
}
|
||||
|
||||
saveEdit() {
|
||||
if (!this.editModel || !this.editModel.id) return;
|
||||
|
||||
const mobileLineId = String(this.editModel.mobileLineId ?? '').trim();
|
||||
if (!mobileLineId) {
|
||||
this.showToast('Selecione Cliente e Linha Antiga (GERAL).');
|
||||
return;
|
||||
}
|
||||
|
||||
this.editSaving = true;
|
||||
|
||||
const payload: any = {
|
||||
item: this.toIntOrNull(this.editModel.item),
|
||||
mobileLineId,
|
||||
linhaAntiga: (this.editModel.linhaAntiga ?? '') || null,
|
||||
linhaNova: (this.editModel.linhaNova ?? '') || null,
|
||||
iccid: (this.editModel.iccid ?? '') || null,
|
||||
dataDaMureg: this.dateInputToIso(this.editModel.dataDaMureg)
|
||||
};
|
||||
|
||||
if (payload.item == null) delete payload.item;
|
||||
|
||||
this.http.put(`${this.apiBase}/${this.editModel.id}`, payload).subscribe({
|
||||
next: async () => {
|
||||
this.editSaving = false;
|
||||
await this.showToast('Registro atualizado com sucesso!');
|
||||
const currentGroup = this.expandedGroup;
|
||||
this.closeEdit();
|
||||
this.loadForGroups();
|
||||
|
||||
if (currentGroup) {
|
||||
setTimeout(() => {
|
||||
this.expandedGroup = currentGroup;
|
||||
this.toggleGroup(currentGroup);
|
||||
}, 400);
|
||||
}
|
||||
},
|
||||
error: async (err) => {
|
||||
this.editSaving = false;
|
||||
const msg = this.extractApiMessage(err) ?? 'Erro ao salvar edição.';
|
||||
await this.showToast(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Helpers
|
||||
// =======================================================================
|
||||
private toIntOrZero(val: any): number {
|
||||
const n = parseInt(String(val ?? '').trim(), 10);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
private toIntOrNull(val: any): number | null {
|
||||
const s = String(val ?? '').trim();
|
||||
if (!s) return null;
|
||||
const n = parseInt(s, 10);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
private isoToDateInput(iso: string | null | undefined): string {
|
||||
if (!iso) return '';
|
||||
const dt = new Date(iso);
|
||||
|
|
@ -404,16 +667,28 @@ export class Mureg implements AfterViewInit {
|
|||
return dt.toISOString();
|
||||
}
|
||||
|
||||
private extractApiMessage(err: any): string | null {
|
||||
try {
|
||||
const m1 = err?.error?.message;
|
||||
if (m1) return String(m1);
|
||||
const m2 = err?.error?.title;
|
||||
if (m2) return String(m2);
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
if (!Number.isNaN(d.getTime())) return new Intl.DateTimeFormat('pt-BR').format(d);
|
||||
return s;
|
||||
}
|
||||
|
||||
return String(v);
|
||||
}
|
||||
|
||||
|
|
@ -422,9 +697,13 @@ export class Mureg implements AfterViewInit {
|
|||
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 });
|
||||
const toastInstance = bs.Toast.getOrCreateInstance(this.successToast.nativeElement, {
|
||||
autohide: true,
|
||||
delay: 3000
|
||||
});
|
||||
toastInstance.show();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,275 @@
|
|||
<section class="relatorios-page">
|
||||
<div class="wrap">
|
||||
<div class="container">
|
||||
<div class="page-head fade-in-up">
|
||||
<div class="title">
|
||||
<span class="badge">
|
||||
<i class="bi bi-bar-chart-fill"></i> Relatórios
|
||||
</span>
|
||||
<p class="subtitle">Resumo e indicadores do ambiente.</p>
|
||||
</div>
|
||||
|
||||
<div class="status" *ngIf="loading">
|
||||
<i class="bi bi-arrow-repeat spin"></i>
|
||||
<span>Carregando...</span>
|
||||
</div>
|
||||
|
||||
<div class="status warn" *ngIf="!loading && errorMsg">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<span>{{ errorMsg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPIs -->
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi-card lift" *ngFor="let k of kpis; let i = index" [style.animationDelay.ms]="i * 40">
|
||||
<div class="kpi-icon">
|
||||
<i [class]="k.icon"></i>
|
||||
</div>
|
||||
<div class="kpi-content">
|
||||
<div class="kpi-title">{{ k.title }}</div>
|
||||
<div class="kpi-value">{{ k.value }}</div>
|
||||
<div class="kpi-hint" *ngIf="k.hint">{{ k.hint }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status das linhas -->
|
||||
<div class="cardx fade-in-up">
|
||||
<div class="cardx-head">
|
||||
<div class="cardx-title">
|
||||
<i class="bi bi-pie-chart-fill"></i>
|
||||
Status das linhas
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-pie-grid">
|
||||
<div class="pie-wrap">
|
||||
<canvas #chartStatusPie></canvas>
|
||||
</div>
|
||||
|
||||
<div class="status-metrics">
|
||||
<div class="metric total">
|
||||
<span class="dot d1"></span>
|
||||
<div class="meta">
|
||||
<div class="k">Total linhas</div>
|
||||
<div class="v">{{ statusResumo.total | number:'1.0-0' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<span class="dot d2"></span>
|
||||
<div class="meta">
|
||||
<div class="k">Ativas</div>
|
||||
<div class="v">{{ statusResumo.ativos | number:'1.0-0' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<span class="dot d3"></span>
|
||||
<div class="meta">
|
||||
<div class="k">Bloqueadas (perda/roubo)</div>
|
||||
<div class="v">{{ statusResumo.perdaRoubo | number:'1.0-0' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<span class="dot d4"></span>
|
||||
<div class="meta">
|
||||
<div class="k">Bloqueadas (120 dias)</div>
|
||||
<div class="v">{{ statusResumo.bloq120 | number:'1.0-0' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<span class="dot d5"></span>
|
||||
<div class="meta">
|
||||
<div class="k">Reservas</div>
|
||||
<div class="v">{{ statusResumo.reservas | number:'1.0-0' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<span class="dot d6"></span>
|
||||
<div class="meta">
|
||||
<div class="k">Bloqueadas (outros)</div>
|
||||
<div class="v">{{ statusResumo.outras | number:'1.0-0' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ NOVO POSICIONAMENTO: VIGÊNCIA abaixo de Status e acima dos gráficos 12 meses -->
|
||||
<div class="charts-grid charts-vigencia">
|
||||
<div class="cardx fade-in-up lift">
|
||||
<div class="cardx-head">
|
||||
<div class="cardx-title">
|
||||
<i class="bi bi-calendar2-check"></i>
|
||||
Contratos a encerrar (próximos 12 meses)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-wrap">
|
||||
<canvas #chartVigenciaMesAno></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cardx fade-in-up lift">
|
||||
<div class="cardx-head">
|
||||
<div class="cardx-title">
|
||||
<i class="bi bi-shield-exclamation"></i>
|
||||
Vigência (supervisão)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-wrap">
|
||||
<canvas #chartVigenciaSupervisao></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts (12 meses) -->
|
||||
<div class="charts-grid">
|
||||
<div class="cardx fade-in-up lift">
|
||||
<div class="cardx-head">
|
||||
<div class="cardx-title">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
MUREG (últimos 12 meses)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-wrap">
|
||||
<canvas #chartMureg12></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cardx fade-in-up lift">
|
||||
<div class="cardx-head">
|
||||
<div class="cardx-title">
|
||||
<i class="bi bi-shuffle"></i>
|
||||
Troca de número (últimos 12 meses)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-wrap">
|
||||
<canvas #chartTroca12></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Clientes -->
|
||||
<div class="cardx fade-in-up">
|
||||
<div class="cardx-head">
|
||||
<div class="cardx-title">
|
||||
<i class="bi bi-trophy-fill"></i>
|
||||
Top clientes (por linhas)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="tablex">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Cliente</th>
|
||||
<th>Linhas</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngIf="!loading && (!topClientes || topClientes.length === 0)">
|
||||
<td colspan="3" class="muted">Nenhum dado encontrado.</td>
|
||||
</tr>
|
||||
|
||||
<tr *ngFor="let c of topClientes; let i = index">
|
||||
<td>{{ i + 1 }}</td>
|
||||
<td class="cell-strong">{{ c.cliente }}</td>
|
||||
<td>{{ c.linhas }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MUREGs recentes -->
|
||||
<div class="cardx fade-in-up">
|
||||
<div class="cardx-head">
|
||||
<div class="cardx-title">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
MUREGs recentes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="tablex">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Linha antiga</th>
|
||||
<th>Linha nova</th>
|
||||
<th>ICCID</th>
|
||||
<th>Cliente</th>
|
||||
<th>Data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngIf="!loading && (!muregsRecentes || muregsRecentes.length === 0)">
|
||||
<td colspan="6" class="muted">Nenhum registro recente.</td>
|
||||
</tr>
|
||||
|
||||
<tr *ngFor="let m of muregsRecentes">
|
||||
<td>{{ m.item }}</td>
|
||||
<td>{{ m.linhaAntiga || '-' }}</td>
|
||||
<td class="cell-strong">{{ m.linhaNova || '-' }}</td>
|
||||
<td>{{ m.iccid || '-' }}</td>
|
||||
<td>{{ m.cliente || '-' }}</td>
|
||||
<td>{{ m.dataDaMureg ? (m.dataDaMureg | date:'dd/MM/yyyy') : '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trocas recentes -->
|
||||
<div class="cardx fade-in-up">
|
||||
<div class="cardx-head">
|
||||
<div class="cardx-title">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
Trocas recentes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="tablex">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Linha antiga</th>
|
||||
<th>Linha nova</th>
|
||||
<th>ICCID</th>
|
||||
<th>Motivo</th>
|
||||
<th>Data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngIf="!loading && (!trocasRecentes || trocasRecentes.length === 0)">
|
||||
<td colspan="6" class="muted">Nenhum registro recente.</td>
|
||||
</tr>
|
||||
|
||||
<tr *ngFor="let t of trocasRecentes">
|
||||
<td>{{ t.item }}</td>
|
||||
<td>{{ t.linhaAntiga || '-' }}</td>
|
||||
<td class="cell-strong">{{ t.linhaNova || '-' }}</td>
|
||||
<td>{{ t.iccid || '-' }}</td>
|
||||
<td class="cell-clip" [title]="t.motivo || ''">{{ t.motivo || '-' }}</td>
|
||||
<td>{{ t.dataTroca ? (t.dataTroca | date:'dd/MM/yyyy') : '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- sem footer / sem foot-space -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ✅ remove footer nessa página */
|
||||
:host ::ng-deep footer,
|
||||
:host ::ng-deep .footer,
|
||||
:host ::ng-deep .portal-footer,
|
||||
:host ::ng-deep .app-footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.relatorios-page {
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ✅ SUBIR MAIS A PÁGINA (antes 44px) */
|
||||
.wrap {
|
||||
padding-top: 15px; /* ✅ mais perto do header */
|
||||
padding-bottom: 16px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
|
||||
margin-bottom: 10px; /* ✅ era 14px */
|
||||
|
||||
@media (max-width: 900px) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.title { min-width: 0; }
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 14px;
|
||||
font-weight: 900;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
color: rgba(17, 18, 20, 0.9);
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
i {
|
||||
color: var(--brand-primary);
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 8px 0 0; /* ✅ era 10px */
|
||||
color: rgba(17, 18, 20, 0.62);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
color: rgba(17, 18, 20, 0.78);
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
|
||||
i { color: var(--brand-primary); }
|
||||
&.warn i { color: #d97706; }
|
||||
}
|
||||
|
||||
.spin { animation: spin 0.9s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.fade-in-up { animation: fadeUp 420ms ease-out both; }
|
||||
@keyframes fadeUp {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.lift {
|
||||
transition: transform 180ms ease, box-shadow 180ms ease;
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 22px 50px rgba(0,0,0,0.10);
|
||||
}
|
||||
}
|
||||
|
||||
/* KPIs */
|
||||
.kpi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
|
||||
margin: 10px 0 12px; /* ✅ era 14px 0 16px */
|
||||
|
||||
@media (max-width: 1000px) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
@media (max-width: 520px) { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 14px;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kpi-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: linear-gradient(135deg, var(--brand-primary), #6a55ff);
|
||||
color: #fff;
|
||||
flex: 0 0 auto;
|
||||
|
||||
i { font-size: 18px; line-height: 1; }
|
||||
}
|
||||
|
||||
.kpi-content { min-width: 0; }
|
||||
|
||||
.kpi-title {
|
||||
font-weight: 900;
|
||||
font-size: 12px;
|
||||
color: rgba(17, 18, 20, 0.65);
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-weight: 900;
|
||||
font-size: 18px;
|
||||
color: rgba(17, 18, 20, 0.92);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.kpi-hint {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: rgba(17, 18, 20, 0.55);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.cardx {
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
backdrop-filter: blur(14px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.08);
|
||||
min-width: 0;
|
||||
|
||||
margin-top: 10px; /* ✅ era 12px */
|
||||
}
|
||||
|
||||
.cardx-head {
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
border-top-left-radius: 18px;
|
||||
border-top-right-radius: 18px;
|
||||
}
|
||||
|
||||
.cardx-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 900;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
color: rgba(17, 18, 20, 0.86);
|
||||
|
||||
i { color: var(--brand-primary); }
|
||||
}
|
||||
|
||||
/* Status grid */
|
||||
.status-pie-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
|
||||
@media (max-width: 900px) { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.pie-wrap {
|
||||
position: relative;
|
||||
height: 260px;
|
||||
padding: 6px;
|
||||
|
||||
canvas {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.status-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
align-content: start;
|
||||
|
||||
@media (max-width: 520px) { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 12px 12px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.70);
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* se quiser tirar o rosa do total, troque aqui */
|
||||
.metric.total .meta .v {
|
||||
color: #ff2d95;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 99px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* ✅ DOTS COM CORES "PADRÃO DE DASHBOARD" */
|
||||
.dot.d1 { background: #ff2d95; } /* total (mantém rosa no card de total, se você quiser) */
|
||||
.dot.d2 { background: #2E7D32; } /* Ativos - verde */
|
||||
.dot.d3 { background: #D32F2F; } /* Perda/Roubo - vermelho */
|
||||
.dot.d4 { background: #F57C00; } /* 120 dias - laranja */
|
||||
.dot.d5 { background: #1976D2; } /* Reservas - azul */
|
||||
.dot.d6 { background: #607D8B; } /* Outros - cinza */
|
||||
|
||||
.meta .k {
|
||||
font-weight: 900;
|
||||
font-size: 12px;
|
||||
color: rgba(17,18,20,0.65);
|
||||
}
|
||||
|
||||
.meta .v {
|
||||
font-weight: 900;
|
||||
font-size: 18px;
|
||||
color: rgba(17,18,20,0.92);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Charts */
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 10px; /* ✅ era 12px */
|
||||
|
||||
@media (max-width: 900px) { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.charts-vigencia {
|
||||
margin-top: 10px; /* ✅ era 12px */
|
||||
}
|
||||
|
||||
.chart-wrap {
|
||||
position: relative;
|
||||
height: 320px;
|
||||
padding: 12px 12px 16px;
|
||||
overflow: hidden;
|
||||
|
||||
canvas {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Table */
|
||||
.table-wrap {
|
||||
padding: 10px 12px 14px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tablex {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 720px;
|
||||
}
|
||||
|
||||
.tablex th,
|
||||
.tablex td {
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
font-weight: 800;
|
||||
color: rgba(17, 18, 20, 0.8);
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tablex th {
|
||||
color: rgba(17, 18, 20, 0.65);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: rgba(17, 18, 20, 0.55);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.cell-strong {
|
||||
font-weight: 900;
|
||||
color: rgba(17, 18, 20, 0.92);
|
||||
}
|
||||
|
||||
.cell-clip {
|
||||
max-width: 240px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
|
@ -0,0 +1,485 @@
|
|||
import {
|
||||
Component,
|
||||
AfterViewInit,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
Inject,
|
||||
} from '@angular/core';
|
||||
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { PLATFORM_ID } from '@angular/core';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { environment } from '../../../environments/environment';
|
||||
import Chart from 'chart.js/auto';
|
||||
|
||||
type KpiCard = {
|
||||
title: string;
|
||||
value: string;
|
||||
icon: string;
|
||||
hint?: string;
|
||||
};
|
||||
|
||||
type SerieMesDto = {
|
||||
mes: string;
|
||||
total: number;
|
||||
};
|
||||
|
||||
type TopClienteDto = {
|
||||
cliente: string;
|
||||
linhas: number;
|
||||
};
|
||||
|
||||
type MuregRecenteDto = {
|
||||
id: string;
|
||||
item: number;
|
||||
linhaAntiga?: string | null;
|
||||
linhaNova?: string | null;
|
||||
iccid?: string | null;
|
||||
dataDaMureg?: string | null;
|
||||
cliente?: string | null;
|
||||
mobileLineId: string;
|
||||
};
|
||||
|
||||
type TrocaRecenteDto = {
|
||||
id: string;
|
||||
item: number;
|
||||
linhaAntiga?: string | null;
|
||||
linhaNova?: string | null;
|
||||
iccid?: string | null;
|
||||
dataTroca?: string | null;
|
||||
motivo?: string | null;
|
||||
};
|
||||
|
||||
type VigenciaBucketsDto = {
|
||||
vencidos: number;
|
||||
aVencer0a30: number;
|
||||
aVencer31a60: number;
|
||||
aVencer61a90: number;
|
||||
acima90: number;
|
||||
};
|
||||
|
||||
type DashboardKpisDto = {
|
||||
totalLinhas: number;
|
||||
clientesUnicos: number;
|
||||
|
||||
ativos: number;
|
||||
bloqueados: number;
|
||||
reservas: number;
|
||||
|
||||
bloqueadosPerdaRoubo: number;
|
||||
bloqueados120Dias: number;
|
||||
bloqueadosOutros: number;
|
||||
|
||||
totalMuregs: number;
|
||||
muregsUltimos30Dias: number;
|
||||
|
||||
totalTrocas: number;
|
||||
trocasUltimos30Dias: number;
|
||||
|
||||
totalVigenciaLinhas: number;
|
||||
vigenciaVencidos: number;
|
||||
vigenciaAVencer30: number;
|
||||
|
||||
userDataRegistros: number;
|
||||
userDataComCpf: number;
|
||||
userDataComEmail: number;
|
||||
};
|
||||
|
||||
type RelatoriosDashboardDto = {
|
||||
kpis: DashboardKpisDto;
|
||||
|
||||
topClientes: TopClienteDto[];
|
||||
|
||||
serieMuregUltimos12Meses: SerieMesDto[];
|
||||
serieTrocaUltimos12Meses: SerieMesDto[];
|
||||
|
||||
muregsRecentes: MuregRecenteDto[];
|
||||
trocasRecentes: TrocaRecenteDto[];
|
||||
|
||||
// ✅ vigência
|
||||
serieVigenciaEncerramentosProx12Meses: SerieMesDto[];
|
||||
vigenciaBuckets: VigenciaBucketsDto;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-relatorios',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './relatorios.html',
|
||||
styleUrls: ['./relatorios.scss'],
|
||||
})
|
||||
export class Relatorios implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChild('chartMureg12') chartMureg12?: ElementRef<HTMLCanvasElement>;
|
||||
@ViewChild('chartTroca12') chartTroca12?: ElementRef<HTMLCanvasElement>;
|
||||
@ViewChild('chartStatusPie') chartStatusPie?: ElementRef<HTMLCanvasElement>;
|
||||
@ViewChild('chartVigenciaMesAno') chartVigenciaMesAno?: ElementRef<HTMLCanvasElement>;
|
||||
@ViewChild('chartVigenciaSupervisao') chartVigenciaSupervisao?: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
loading = true;
|
||||
errorMsg: string | null = null;
|
||||
|
||||
kpis: KpiCard[] = [];
|
||||
|
||||
muregLabels: string[] = [];
|
||||
muregValues: number[] = [];
|
||||
|
||||
trocaLabels: string[] = [];
|
||||
trocaValues: number[] = [];
|
||||
|
||||
vigenciaLabels: string[] = [];
|
||||
vigenciaValues: number[] = [];
|
||||
|
||||
vigBuckets: VigenciaBucketsDto = {
|
||||
vencidos: 0,
|
||||
aVencer0a30: 0,
|
||||
aVencer31a60: 0,
|
||||
aVencer61a90: 0,
|
||||
acima90: 0,
|
||||
};
|
||||
|
||||
topClientes: TopClienteDto[] = [];
|
||||
muregsRecentes: MuregRecenteDto[] = [];
|
||||
trocasRecentes: TrocaRecenteDto[] = [];
|
||||
|
||||
statusResumo = {
|
||||
total: 0,
|
||||
ativos: 0,
|
||||
perdaRoubo: 0,
|
||||
bloq120: 0,
|
||||
reservas: 0,
|
||||
outras: 0,
|
||||
};
|
||||
|
||||
private viewReady = false;
|
||||
private dataReady = false;
|
||||
|
||||
private chartMureg?: Chart;
|
||||
private chartTroca?: Chart;
|
||||
private chartPie?: Chart;
|
||||
private chartVigMesAno?: Chart;
|
||||
private chartVigSuper?: Chart;
|
||||
|
||||
private readonly baseApi: string;
|
||||
|
||||
// ✅ Paletas "padrão de dashboard" (fácil de entender)
|
||||
private readonly STATUS_COLORS = {
|
||||
ativos: '#2E7D32', // verde
|
||||
perdaRoubo: '#D32F2F', // vermelho
|
||||
bloq120: '#F57C00', // laranja
|
||||
reservas: '#1976D2', // azul
|
||||
outros: '#607D8B', // cinza
|
||||
};
|
||||
|
||||
private readonly VIG_COLORS = {
|
||||
vencidos: '#D32F2F', // vermelho
|
||||
d0a30: '#F57C00', // laranja
|
||||
d31a60: '#FBC02D', // amarelo
|
||||
d61a90: '#1976D2', // azul
|
||||
acima90: '#2E7D32', // verde
|
||||
};
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
@Inject(PLATFORM_ID) private platformId: object
|
||||
) {
|
||||
const raw = (environment.apiUrl || 'https://localhost:7205').replace(/\/+$/, '');
|
||||
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadDashboard();
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.viewReady = true;
|
||||
this.tryBuildCharts();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyCharts();
|
||||
}
|
||||
|
||||
private async loadDashboard() {
|
||||
this.loading = true;
|
||||
this.errorMsg = null;
|
||||
this.dataReady = false;
|
||||
|
||||
try {
|
||||
const dto = await this.fetchDashboardReal();
|
||||
this.applyDto(dto);
|
||||
|
||||
this.dataReady = true;
|
||||
this.loading = false;
|
||||
|
||||
this.tryBuildCharts();
|
||||
} catch {
|
||||
this.loading = false;
|
||||
this.errorMsg =
|
||||
'Falha ao carregar Relatórios. Verifique se a API está rodando e o endpoint /api/relatorios/dashboard está acessível.';
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchDashboardReal(): Promise<RelatoriosDashboardDto> {
|
||||
if (!isPlatformBrowser(this.platformId)) throw new Error('SSR não suportado para charts');
|
||||
const url = `${this.baseApi}/relatorios/dashboard`;
|
||||
return await firstValueFrom(this.http.get<RelatoriosDashboardDto>(url));
|
||||
}
|
||||
|
||||
private applyDto(dto: RelatoriosDashboardDto) {
|
||||
const k = dto.kpis;
|
||||
|
||||
this.kpis = [
|
||||
{ title: 'Linhas', value: this.formatInt(k.totalLinhas), icon: 'bi bi-sim', hint: 'Total cadastradas' },
|
||||
{ title: 'Clientes', value: this.formatInt(k.clientesUnicos), icon: 'bi bi-building', hint: 'Clientes únicos' },
|
||||
{ title: 'Ativos', value: this.formatInt(k.ativos), icon: 'bi bi-check2-circle', hint: 'Linhas ativas' },
|
||||
{ title: 'Bloqueados', value: this.formatInt(k.bloqueados), icon: 'bi bi-slash-circle', hint: 'Somatório de bloqueios' },
|
||||
{ title: 'Reservas', value: this.formatInt(k.reservas), icon: 'bi bi-inboxes', hint: 'Linhas em reserva' },
|
||||
{ title: 'MUREGs (30d)', value: this.formatInt(k.muregsUltimos30Dias), icon: 'bi bi-arrow-repeat', hint: 'Últimos 30 dias' },
|
||||
{ title: 'Trocas (30d)', value: this.formatInt(k.trocasUltimos30Dias), icon: 'bi bi-shuffle', hint: 'Últimos 30 dias' },
|
||||
{ title: 'Vencidos', value: this.formatInt(k.vigenciaVencidos), icon: 'bi bi-exclamation-triangle', hint: 'Vigência vencida' },
|
||||
{ title: 'A vencer (30d)', value: this.formatInt(k.vigenciaAVencer30), icon: 'bi bi-calendar2-week', hint: 'Vigência a vencer' },
|
||||
{ title: 'Registros', value: this.formatInt(k.userDataRegistros), icon: 'bi bi-person-vcard', hint: 'Cadastros de usuário' },
|
||||
];
|
||||
|
||||
this.muregLabels = (dto.serieMuregUltimos12Meses || []).map(x => x.mes);
|
||||
this.muregValues = (dto.serieMuregUltimos12Meses || []).map(x => x.total);
|
||||
|
||||
this.trocaLabels = (dto.serieTrocaUltimos12Meses || []).map(x => x.mes);
|
||||
this.trocaValues = (dto.serieTrocaUltimos12Meses || []).map(x => x.total);
|
||||
|
||||
this.vigenciaLabels = (dto.serieVigenciaEncerramentosProx12Meses || []).map(x => x.mes);
|
||||
this.vigenciaValues = (dto.serieVigenciaEncerramentosProx12Meses || []).map(x => x.total);
|
||||
|
||||
this.vigBuckets = dto.vigenciaBuckets || this.vigBuckets;
|
||||
|
||||
this.topClientes = dto.topClientes || [];
|
||||
this.muregsRecentes = dto.muregsRecentes || [];
|
||||
this.trocasRecentes = dto.trocasRecentes || [];
|
||||
|
||||
this.statusResumo = {
|
||||
total: k.totalLinhas ?? 0,
|
||||
ativos: k.ativos ?? 0,
|
||||
perdaRoubo: k.bloqueadosPerdaRoubo ?? 0,
|
||||
bloq120: k.bloqueados120Dias ?? 0,
|
||||
reservas: k.reservas ?? 0,
|
||||
outras: k.bloqueadosOutros ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
private tryBuildCharts() {
|
||||
if (!isPlatformBrowser(this.platformId)) return;
|
||||
if (!this.viewReady || !this.dataReady) return;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const canvases = [
|
||||
this.chartStatusPie?.nativeElement,
|
||||
this.chartVigenciaMesAno?.nativeElement,
|
||||
this.chartVigenciaSupervisao?.nativeElement,
|
||||
this.chartMureg12?.nativeElement,
|
||||
this.chartTroca12?.nativeElement,
|
||||
].filter(Boolean) as HTMLCanvasElement[];
|
||||
|
||||
if (canvases.length === 0) return;
|
||||
|
||||
// evita render quando canvas ainda não mediu (bug comum que "some")
|
||||
if (canvases.some(c => c.clientWidth === 0 || c.clientHeight === 0)) {
|
||||
requestAnimationFrame(() => this.tryBuildCharts());
|
||||
return;
|
||||
}
|
||||
|
||||
this.buildCharts();
|
||||
});
|
||||
}
|
||||
|
||||
private buildCharts() {
|
||||
if (!isPlatformBrowser(this.platformId)) return;
|
||||
|
||||
this.destroyCharts();
|
||||
|
||||
// ✅ Status das linhas (paleta padrão)
|
||||
const cP = this.chartStatusPie?.nativeElement;
|
||||
if (cP) {
|
||||
this.chartPie = new Chart(cP, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: [
|
||||
'Ativos',
|
||||
'Bloqueadas (perda/roubo)',
|
||||
'Bloqueadas (120 dias)',
|
||||
'Reservas',
|
||||
'Bloqueadas (outros)'
|
||||
],
|
||||
datasets: [{
|
||||
data: [
|
||||
this.statusResumo.ativos,
|
||||
this.statusResumo.perdaRoubo,
|
||||
this.statusResumo.bloq120,
|
||||
this.statusResumo.reservas,
|
||||
this.statusResumo.outras,
|
||||
],
|
||||
borderWidth: 1,
|
||||
backgroundColor: [
|
||||
this.STATUS_COLORS.ativos,
|
||||
this.STATUS_COLORS.perdaRoubo,
|
||||
this.STATUS_COLORS.bloq120,
|
||||
this.STATUS_COLORS.reservas,
|
||||
this.STATUS_COLORS.outros,
|
||||
],
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
cutout: '62%',
|
||||
plugins: {
|
||||
legend: { position: 'bottom' },
|
||||
tooltip: {
|
||||
callbacks: { label: (ctx) => ` ${ctx.label}: ${this.formatInt(Number(ctx.raw || 0))}` },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Contratos a encerrar (próximos 12 meses) - barra azul padrão
|
||||
const cV1 = this.chartVigenciaMesAno?.nativeElement;
|
||||
if (cV1) {
|
||||
this.chartVigMesAno = new Chart(cV1, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: this.vigenciaLabels,
|
||||
datasets: [{
|
||||
label: 'Encerramentos',
|
||||
data: this.vigenciaValues,
|
||||
borderWidth: 0,
|
||||
backgroundColor: '#1976D2', // azul padrão
|
||||
borderRadius: 10,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
y: { beginAtZero: true, border: { display: false } },
|
||||
x: { border: { display: false } },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Vigência (supervisão) - paleta por urgência
|
||||
const cV2 = this.chartVigenciaSupervisao?.nativeElement;
|
||||
if (cV2) {
|
||||
this.chartVigSuper = new Chart(cV2, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Vencidos', '0–30 dias', '31–60 dias', '61–90 dias', '> 90 dias'],
|
||||
datasets: [{
|
||||
data: [
|
||||
this.vigBuckets.vencidos,
|
||||
this.vigBuckets.aVencer0a30,
|
||||
this.vigBuckets.aVencer31a60,
|
||||
this.vigBuckets.aVencer61a90,
|
||||
this.vigBuckets.acima90,
|
||||
],
|
||||
borderWidth: 1,
|
||||
backgroundColor: [
|
||||
this.VIG_COLORS.vencidos,
|
||||
this.VIG_COLORS.d0a30,
|
||||
this.VIG_COLORS.d31a60,
|
||||
this.VIG_COLORS.d61a90,
|
||||
this.VIG_COLORS.acima90,
|
||||
],
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
cutout: '62%',
|
||||
plugins: {
|
||||
legend: { position: 'bottom' },
|
||||
tooltip: {
|
||||
callbacks: { label: (ctx) => ` ${ctx.label}: ${this.formatInt(Number(ctx.raw || 0))}` },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ MUREG 12 meses
|
||||
const cM = this.chartMureg12?.nativeElement;
|
||||
if (cM) {
|
||||
this.chartMureg = new Chart(cM, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: this.muregLabels,
|
||||
datasets: [{
|
||||
label: 'MUREG',
|
||||
data: this.muregValues,
|
||||
borderWidth: 0,
|
||||
backgroundColor: '#6A1B9A', // roxo (bem comum em dashboards)
|
||||
borderRadius: 10,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
y: { beginAtZero: true, border: { display: false } },
|
||||
x: { border: { display: false } },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Troca 12 meses
|
||||
const cT = this.chartTroca12?.nativeElement;
|
||||
if (cT) {
|
||||
this.chartTroca = new Chart(cT, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: this.trocaLabels,
|
||||
datasets: [{
|
||||
label: 'Troca',
|
||||
data: this.trocaValues,
|
||||
borderWidth: 0,
|
||||
backgroundColor: '#00897B', // teal (bem comum)
|
||||
borderRadius: 10,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
y: { beginAtZero: true, border: { display: false } },
|
||||
x: { border: { display: false } },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private destroyCharts() {
|
||||
try { this.chartMureg?.destroy(); } catch {}
|
||||
try { this.chartTroca?.destroy(); } catch {}
|
||||
try { this.chartPie?.destroy(); } catch {}
|
||||
try { this.chartVigMesAno?.destroy(); } catch {}
|
||||
try { this.chartVigSuper?.destroy(); } catch {}
|
||||
|
||||
this.chartMureg = undefined;
|
||||
this.chartTroca = undefined;
|
||||
this.chartPie = undefined;
|
||||
this.chartVigMesAno = undefined;
|
||||
this.chartVigSuper = undefined;
|
||||
}
|
||||
|
||||
private formatInt(v: number) {
|
||||
return (v || 0).toLocaleString('pt-BR');
|
||||
}
|
||||
}
|
||||
|
|
@ -278,7 +278,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CREATE MODAL -->
|
||||
<!-- CREATE MODAL (✅ BEBENDO DO GERAL) -->
|
||||
<div class="modal-custom" *ngIf="createOpen">
|
||||
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||
<div class="modal-header">
|
||||
|
|
@ -306,6 +306,7 @@
|
|||
|
||||
<div class="box-body">
|
||||
<div class="form-grid">
|
||||
|
||||
<div class="form-field">
|
||||
<label>Item</label>
|
||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.item" />
|
||||
|
|
@ -316,19 +317,60 @@
|
|||
<input class="form-control form-control-sm" type="date" [(ngModel)]="createModel.dataTroca" />
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label>Linha Antiga</label>
|
||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.linhaAntiga" />
|
||||
<!-- ✅ Cliente (GERAL) -->
|
||||
<div class="form-field span-2">
|
||||
<label>Cliente (GERAL)</label>
|
||||
<select class="form-control form-control-sm"
|
||||
[(ngModel)]="selectedCliente"
|
||||
(change)="onClienteChange()"
|
||||
[disabled]="loadingClients">
|
||||
<option value="">Selecione...</option>
|
||||
<option *ngFor="let c of clientsFromGeral" [value]="c">{{ c }}</option>
|
||||
</select>
|
||||
|
||||
<small class="hint" *ngIf="loadingClients">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span> Carregando clientes...
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Linha do Cliente (GERAL) -->
|
||||
<div class="form-field span-2">
|
||||
<label>Linha do Cliente (GERAL)</label>
|
||||
<select class="form-control form-control-sm"
|
||||
[(ngModel)]="selectedLineId"
|
||||
(change)="onLineChange()"
|
||||
[disabled]="!selectedCliente || loadingLines">
|
||||
<option value="">Selecione...</option>
|
||||
<option *ngFor="let l of linesFromClient" [value]="l.id">
|
||||
{{ l.item }} • {{ l.linha || '-' }} • {{ l.usuario || 'SEM USUÁRIO' }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<small class="hint" *ngIf="loadingLines">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span> Carregando linhas...
|
||||
</small>
|
||||
|
||||
<small class="hint warn" *ngIf="selectedCliente && !loadingLines && linesFromClient.length === 0">
|
||||
Nenhuma linha encontrada para este cliente no GERAL.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Linha Antiga (auto do GERAL) -->
|
||||
<div class="form-field">
|
||||
<label>Linha Antiga (auto)</label>
|
||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.linhaAntiga" readonly />
|
||||
</div>
|
||||
|
||||
<!-- Linha Nova -->
|
||||
<div class="form-field">
|
||||
<label>Linha Nova</label>
|
||||
<input class="form-control form-control-sm fw-bold text-blue" [(ngModel)]="createModel.linhaNova" />
|
||||
</div>
|
||||
|
||||
<!-- ✅ ICCID (auto do GERAL) -->
|
||||
<div class="form-field span-2">
|
||||
<label>ICCID</label>
|
||||
<input class="form-control form-control-sm font-monospace" [(ngModel)]="createModel.iccid" />
|
||||
<label>ICCID (auto)</label>
|
||||
<input class="form-control form-control-sm font-monospace" [(ngModel)]="createModel.iccid" readonly />
|
||||
</div>
|
||||
|
||||
<div class="form-field span-2">
|
||||
|
|
@ -340,6 +382,7 @@
|
|||
<label>Observação</label>
|
||||
<textarea class="form-control form-control-sm" rows="3" [(ngModel)]="createModel.observacao"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -588,7 +588,13 @@
|
|||
.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; }
|
||||
.box-body { padding: 16px; }
|
||||
|
||||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; @media (max-width: 600px) { grid-template-columns: 1fr; } }
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
|
||||
@media (max-width: 600px) { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
|
|
@ -603,7 +609,10 @@
|
|||
color: rgba(17,18,20,0.65);
|
||||
}
|
||||
|
||||
&.span-2 { grid-column: span 2; @media (max-width: 600px) { grid-column: span 1; } }
|
||||
&.span-2 {
|
||||
grid-column: span 2;
|
||||
@media (max-width: 600px) { grid-column: span 1; }
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
|
|
@ -612,3 +621,18 @@
|
|||
|
||||
&:focus { border-color: var(--brand); box-shadow: 0 0 0 2px rgba(227,61,207,0.15); outline: none; }
|
||||
}
|
||||
|
||||
/* ✅ HINTS do modal (carregando/avisos) */
|
||||
.hint {
|
||||
margin-top: 6px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 800;
|
||||
color: rgba(17, 18, 20, 0.55);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&.warn {
|
||||
color: #b58100;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,17 @@ interface GroupItem {
|
|||
semIccid: number;
|
||||
}
|
||||
|
||||
/** ✅ DTO da linha do GERAL (para selects) */
|
||||
interface LineOptionDto {
|
||||
id: string;
|
||||
item: number;
|
||||
linha: string | null;
|
||||
chip: string | null;
|
||||
cliente: string | null;
|
||||
usuario: string | null;
|
||||
skil: string | null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, HttpClientModule],
|
||||
|
|
@ -60,6 +71,9 @@ export class TrocaNumero implements AfterViewInit {
|
|||
|
||||
private readonly apiBase = 'https://localhost:7205/api/trocanumero';
|
||||
|
||||
/** ✅ base do GERAL (para buscar clientes/linhas no modal) */
|
||||
private readonly linesApiBase = 'https://localhost:7205/api/lines';
|
||||
|
||||
// ====== DATA ======
|
||||
groups: GroupItem[] = [];
|
||||
pagedGroups: GroupItem[] = [];
|
||||
|
|
@ -98,6 +112,14 @@ export class TrocaNumero implements AfterViewInit {
|
|||
observacao: ''
|
||||
};
|
||||
|
||||
/** ✅ selects do GERAL no modal */
|
||||
clientsFromGeral: string[] = [];
|
||||
linesFromClient: LineOptionDto[] = [];
|
||||
selectedCliente: string = '';
|
||||
selectedLineId: string = '';
|
||||
loadingClients = false;
|
||||
loadingLines = false;
|
||||
|
||||
async ngAfterViewInit() {
|
||||
if (!isPlatformBrowser(this.platformId)) return;
|
||||
this.initAnimations();
|
||||
|
|
@ -285,7 +307,7 @@ export class TrocaNumero implements AfterViewInit {
|
|||
const iccid = pick(x, ['iccid', 'ICCID']);
|
||||
const dataTroca = pick(x, ['dataTroca', 'data_troca', 'DATA TROCA', 'DATA DA TROCA']);
|
||||
const motivo = pick(x, ['motivo', 'MOTIVO']);
|
||||
const observacao = pick(x, ['observacao', 'OBSERVAÇÃO', 'OBSERVACAO', 'OBSERVACAO']);
|
||||
const observacao = pick(x, ['observacao', 'OBSERVAÇÃO', 'OBSERVACAO']);
|
||||
|
||||
const id = String(pick(x, ['id', 'ID']) || `${idx}-${item}-${linhaNova}-${iccid}`);
|
||||
|
||||
|
|
@ -302,6 +324,79 @@ export class TrocaNumero implements AfterViewInit {
|
|||
};
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// ✅ GERAL -> selects do modal (Clientes / Linhas do cliente)
|
||||
// =======================================================================
|
||||
private loadClientsFromGeral() {
|
||||
this.loadingClients = true;
|
||||
|
||||
this.http.get<string[]>(`${this.linesApiBase}/clients`).subscribe({
|
||||
next: (res) => {
|
||||
this.clientsFromGeral = (res ?? []).filter(x => !!String(x ?? '').trim());
|
||||
this.loadingClients = false;
|
||||
this.cdr.detectChanges();
|
||||
},
|
||||
error: async () => {
|
||||
this.loadingClients = false;
|
||||
await this.showToast('Erro ao carregar clientes do GERAL.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private loadLinesByClient(cliente: string) {
|
||||
const c = String(cliente ?? '').trim();
|
||||
if (!c) {
|
||||
this.linesFromClient = [];
|
||||
this.selectedLineId = '';
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingLines = true;
|
||||
|
||||
const params = new HttpParams().set('cliente', c);
|
||||
|
||||
this.http.get<LineOptionDto[]>(`${this.linesApiBase}/by-client`, { params }).subscribe({
|
||||
next: (res) => {
|
||||
this.linesFromClient = (res ?? []);
|
||||
this.loadingLines = false;
|
||||
this.cdr.detectChanges();
|
||||
},
|
||||
error: async () => {
|
||||
this.loadingLines = false;
|
||||
await this.showToast('Erro ao carregar linhas do cliente (GERAL).');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClienteChange() {
|
||||
// reset quando troca cliente
|
||||
this.selectedLineId = '';
|
||||
this.linesFromClient = [];
|
||||
|
||||
// limpa campos auto
|
||||
this.createModel.linhaAntiga = '';
|
||||
this.createModel.iccid = '';
|
||||
|
||||
this.loadLinesByClient(this.selectedCliente);
|
||||
}
|
||||
|
||||
onLineChange() {
|
||||
const id = String(this.selectedLineId ?? '').trim();
|
||||
const found = this.linesFromClient.find(x => x.id === id);
|
||||
|
||||
// preenche automaticamente a partir do GERAL
|
||||
this.createModel.linhaAntiga = found?.linha ?? '';
|
||||
this.createModel.iccid = found?.chip ?? ''; // Chip do GERAL => ICCID aqui
|
||||
|
||||
// se quiser, pode setar item automaticamente também:
|
||||
if (found?.item !== undefined && found?.item !== null) {
|
||||
// só seta se estiver vazio (pra não atrapalhar quem quiser digitar)
|
||||
if (!String(this.createModel.item ?? '').trim()) {
|
||||
this.createModel.item = String(found.item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====== MODAL EDIÇÃO ======
|
||||
onEditar(r: TrocaRow) {
|
||||
this.editOpen = true;
|
||||
|
|
@ -360,6 +455,7 @@ export class TrocaNumero implements AfterViewInit {
|
|||
this.createOpen = true;
|
||||
this.createSaving = false;
|
||||
|
||||
// reset do form
|
||||
this.createModel = {
|
||||
item: '',
|
||||
linhaAntiga: '',
|
||||
|
|
@ -369,6 +465,15 @@ export class TrocaNumero implements AfterViewInit {
|
|||
motivo: '',
|
||||
observacao: ''
|
||||
};
|
||||
|
||||
// reset dos selects
|
||||
this.selectedCliente = '';
|
||||
this.selectedLineId = '';
|
||||
this.clientsFromGeral = [];
|
||||
this.linesFromClient = [];
|
||||
|
||||
// carrega clientes do GERAL
|
||||
this.loadClientsFromGeral();
|
||||
}
|
||||
|
||||
closeCreate() {
|
||||
|
|
@ -376,13 +481,27 @@ export class TrocaNumero implements AfterViewInit {
|
|||
}
|
||||
|
||||
saveCreate() {
|
||||
// ✅ validações do "beber do GERAL"
|
||||
if (!String(this.selectedCliente ?? '').trim()) {
|
||||
this.showToast('Selecione um Cliente do GERAL.');
|
||||
return;
|
||||
}
|
||||
if (!String(this.selectedLineId ?? '').trim()) {
|
||||
this.showToast('Selecione uma Linha do Cliente (GERAL).');
|
||||
return;
|
||||
}
|
||||
if (!String(this.createModel.linhaNova ?? '').trim()) {
|
||||
this.showToast('Informe a Linha Nova.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.createSaving = true;
|
||||
|
||||
const payload = {
|
||||
item: this.toNumberOrNull(this.createModel.item),
|
||||
linhaAntiga: this.createModel.linhaAntiga,
|
||||
linhaAntiga: this.createModel.linhaAntiga, // auto do GERAL
|
||||
linhaNova: this.createModel.linhaNova,
|
||||
iccid: this.createModel.iccid,
|
||||
iccid: this.createModel.iccid, // auto do GERAL
|
||||
motivo: this.createModel.motivo,
|
||||
observacao: this.createModel.observacao,
|
||||
dataTroca: this.dateInputToIso(this.createModel.dataTroca)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,11 @@ export interface MobileLineDetail extends MobileLineList {
|
|||
dataEntregaCliente?: string | null;
|
||||
}
|
||||
|
||||
export interface LineOption {
|
||||
id: string;
|
||||
linha: string;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LinesService {
|
||||
// ✅ Mesma base do Swagger (evita redirect no preflight/CORS)
|
||||
|
|
@ -85,7 +90,6 @@ export class LinesService {
|
|||
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();
|
||||
|
|
@ -106,4 +110,11 @@ export class LinesService {
|
|||
|
||||
return this.http.get<PagedResult<any>>(`${this.baseUrl}/groups`, { params });
|
||||
}
|
||||
|
||||
// ✅ NOVO: usado no modal do MUREG
|
||||
// Precisa existir no backend: GET /api/lines/by-client?cliente=...
|
||||
getLinesByClient(cliente: string): Observable<LineOption[]> {
|
||||
let params = new HttpParams().set('cliente', (cliente ?? '').trim());
|
||||
return this.http.get<LineOption[]>(`${this.baseUrl}/by-client`, { params });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
import { AppComponent } from './app/app';
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
||||
|
||||
bootstrapApplication(App, appConfig)
|
||||
bootstrapApplication(AppComponent, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
|
|
|
|||
104
src/styles.scss
104
src/styles.scss
|
|
@ -1,5 +1,105 @@
|
|||
@import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
|
||||
body {
|
||||
background: #EFEFEF !important;
|
||||
/* Variáveis baseadas na sua marca, mas modernizadas */
|
||||
:root {
|
||||
--brand-primary: #E33DCF;
|
||||
--brand-hover: #c91eb5;
|
||||
--brand-soft: rgba(227, 61, 207, 0.08);
|
||||
--text-main: #0F172A; /* Slate 900 - Preto moderno */
|
||||
--text-muted: #64748B; /* Slate 500 - Cinza moderno */
|
||||
--bg-body: #F8FAFC;
|
||||
--glass-border: rgba(255, 255, 255, 0.6);
|
||||
--glass-bg: rgba(255, 255, 255, 0.7);
|
||||
--shadow-sm: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
||||
--shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.01);
|
||||
--radius-lg: 16px;
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-body);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Garante scroll da página em todo o app */
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
/* Utilitário de animação suave */
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Empurra o conteúdo pra baixo do header fixo */
|
||||
.app-main.has-header {
|
||||
padding-top: 84px; /* altura segura p/ header (mobile/desktop) */
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.app-main.has-header {
|
||||
padding-top: 96px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================== */
|
||||
/* 🚀 GLOBAL FIX: Proporção Horizontal e Vertical */
|
||||
/* ========================================================== */
|
||||
|
||||
/* 1. HORIZONTAL: Mantém a proporção mais "fechada" que você gostou */
|
||||
.container-geral,
|
||||
.container-fat,
|
||||
.container-mureg,
|
||||
.container-troca,
|
||||
.container-geral-responsive {
|
||||
max-width: 1100px !important; /* Largura controlada */
|
||||
width: 96% !important; /* Margem segura em telas menores */
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* 2. TABELAS: Remove limites de largura mínima desnecessários */
|
||||
.table-modern {
|
||||
width: 100% !important;
|
||||
min-width: unset !important;
|
||||
}
|
||||
|
||||
/* 3. VERTICAL: Libera o crescimento dos cards em todas as páginas */
|
||||
.geral-card,
|
||||
.fat-card,
|
||||
.mureg-card,
|
||||
.troca-card,
|
||||
.vigencia-page .geral-card {
|
||||
height: auto !important; /* Cresce conforme o conteúdo */
|
||||
max-height: none !important; /* Remove limites fixos */
|
||||
min-height: 80vh; /* Garante um tamanho mínimo bonito */
|
||||
overflow: visible !important; /* Remove scroll interno, usa o da janela */
|
||||
margin-bottom: 40px !important; /* Respiro no final */
|
||||
}
|
||||
|
||||
/* 4. LISTAS E WRAPPERS: Destrava o crescimento interno */
|
||||
.groups-container,
|
||||
.table-wrap,
|
||||
.table-wrap-tall,
|
||||
.inner-table-wrap {
|
||||
height: auto !important;
|
||||
max-height: none !important; /* CRÍTICO: Remove travas de pixels */
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* 5. PÁGINAS: Garante que o scroll do navegador funcione */
|
||||
.geral-page,
|
||||
.users-page,
|
||||
.fat-page,
|
||||
.mureg-page,
|
||||
.troca-page {
|
||||
overflow-y: auto !important;
|
||||
height: auto !important;
|
||||
display: block !important;
|
||||
}
|
||||
Loading…
Reference in New Issue