Ajustes front: geral consumindo API e import excel
This commit is contained in:
parent
a4fb34146d
commit
2c05ac8311
|
|
@ -15,8 +15,8 @@
|
||||||
<i class="bi bi-list"></i>
|
<i class="bi bi-list"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- ✅ Logo some apenas quando menu estiver aberto (no /geral) -->
|
<!-- ✅ Logo SEMPRE aparece no header -->
|
||||||
<a class="logo-area" routerLink="/" *ngIf="!menuOpen">
|
<a class="logo-area" routerLink="/">
|
||||||
<img src="logo.png" alt="Logo" class="logo" />
|
<img src="logo.png" alt="Logo" class="logo" />
|
||||||
<div class="logo-text ms-2">
|
<div class="logo-text ms-2">
|
||||||
<span class="line">Line</span><span class="gestao">Gestão</span>
|
<span class="line">Line</span><span class="gestao">Gestão</span>
|
||||||
|
|
@ -44,20 +44,30 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- FAIXA AZUL – SÓ NA HOME -->
|
<!-- ✅ FAIXA (SÓ NA HOME) com degradê igual footer -->
|
||||||
<div class="header-bar" *ngIf="isHome">
|
<div class="header-bar footer-gradient" *ngIf="isHome">
|
||||||
<span class="header-bar-text">
|
<span class="header-bar-text">
|
||||||
Somos a escolha certa para estar sempre conectado!
|
Somos a escolha certa para estar sempre conectado!
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- OVERLAY -->
|
<!-- ✅ OVERLAY (só no /geral) -->
|
||||||
<div class="menu-overlay" *ngIf="menuOpen" (click)="closeMenu()"></div>
|
<div
|
||||||
|
class="menu-overlay"
|
||||||
|
*ngIf="isLoggedHeader && menuOpen"
|
||||||
|
(click)="closeMenu()"
|
||||||
|
></div>
|
||||||
|
|
||||||
<!-- MENU LATERAL -->
|
<!-- ✅ MENU LATERAL (só no /geral) -->
|
||||||
<aside class="side-menu" [class.open]="menuOpen" (click)="$event.stopPropagation()">
|
<aside
|
||||||
|
*ngIf="isLoggedHeader"
|
||||||
|
class="side-menu"
|
||||||
|
[class.open]="menuOpen"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
<div class="side-menu-header">
|
<div class="side-menu-header">
|
||||||
|
<!-- ✅ Logo DENTRO do menu lateral -->
|
||||||
<a class="logo-area" routerLink="/" (click)="closeMenu()">
|
<a class="logo-area" routerLink="/" (click)="closeMenu()">
|
||||||
<img src="logo.png" alt="Logo" class="logo" />
|
<img src="logo.png" alt="Logo" class="logo" />
|
||||||
<div class="logo-text ms-2">
|
<div class="logo-text ms-2">
|
||||||
|
|
@ -76,7 +86,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="side-menu-body">
|
<div class="side-menu-body">
|
||||||
<!-- ✅ Opções do Geral (mantém) -->
|
|
||||||
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-clipboard-data me-2"></i> Controle de Contratos
|
<i class="bi bi-clipboard-data me-2"></i> Controle de Contratos
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -95,7 +104,6 @@
|
||||||
|
|
||||||
<hr class="my-2" />
|
<hr class="my-2" />
|
||||||
|
|
||||||
<!-- ✅ Links da home só aqui dentro (no /geral) -->
|
|
||||||
<a href="https://www.linemovel.com.br/sobrenos" target="_blank" class="side-item" (click)="closeMenu()">
|
<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?
|
<i class="bi bi-info-circle me-2"></i> O que é a Line Móvel?
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -1,420 +1,484 @@
|
||||||
:host {
|
:host {
|
||||||
--brand: #E33DCF;
|
--brand: #E33DCF;
|
||||||
--brand-2: rgba(227, 61, 207, 0.18);
|
--blue: #030FAA;
|
||||||
--brand-3: rgba(227, 61, 207, 0.10);
|
|
||||||
|
|
||||||
--text: #0b0b0f;
|
--border: rgba(0, 0, 0, 0.10);
|
||||||
--muted: rgba(0, 0, 0, 0.66);
|
|
||||||
|
|
||||||
--border: rgba(0, 0, 0, 0.08);
|
/* ✅ glass */
|
||||||
|
--glass: rgba(255, 255, 255, 0.35);
|
||||||
|
--glass-strong: rgba(255, 255, 255, 0.48);
|
||||||
|
|
||||||
--shadow: 0 18px 45px rgba(0, 0, 0, 0.10);
|
--shadow-soft: 0 10px 26px rgba(0, 0, 0, 0.10);
|
||||||
--shadow-soft: 0 10px 26px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =============================== */
|
/* ===================== */
|
||||||
/* PÁGINA / FUNDO GLOBAL */
|
/* HEADER PRINCIPAL */
|
||||||
/* =============================== */
|
/* ===================== */
|
||||||
.home-page {
|
.header-container {
|
||||||
position: relative;
|
width: 100%;
|
||||||
min-height: 100vh;
|
font-family: 'Inter', sans-serif;
|
||||||
background: #efefef;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
/* 🔑 evita o background “atrapalhar” o conteúdo */
|
position: sticky;
|
||||||
isolation: isolate;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* BACKGROUND GLOBAL (viewport inteiro) */
|
.header-container.header-scrolled {
|
||||||
.page-bg {
|
background: var(--glass-strong);
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
border-color: rgba(227, 61, 207, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== */
|
||||||
|
/* TOP AREA */
|
||||||
|
/* ===================== */
|
||||||
|
.header-top {
|
||||||
|
width: 100%;
|
||||||
|
height: 72px;
|
||||||
|
padding: 0 32px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 22px;
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
padding: 0 22px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
/* como agora é grid, não usamos space-between */
|
||||||
|
justify-content: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center; /* ✅ centraliza os links no centro */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-area {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== */
|
||||||
|
/* HAMBURGUER (GERAL) */
|
||||||
|
/* ===================== */
|
||||||
|
.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-area {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/* ✅ nunca quebrar linha */
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
/* ✅ não empurrar botões */
|
||||||
|
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; }
|
||||||
|
|
||||||
|
/* telas menores: some o menu */
|
||||||
|
@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;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
width: 150px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
width: 46vw;
|
||||||
|
max-width: 190px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cadastrar {
|
||||||
|
background: #E1E1E1; /* ✅ sólido (sem transparência) */
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login {
|
||||||
|
background: var(--brand);
|
||||||
|
border-color: var(--brand);
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cadastrar:hover,
|
||||||
|
.btn-login:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login:hover {
|
||||||
|
filter: brightness(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== */
|
||||||
|
/* FAIXA (HOME) */
|
||||||
|
/* ===================== */
|
||||||
|
.header-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* degradê igual ao footer */
|
||||||
|
.footer-gradient {
|
||||||
|
background: linear-gradient(90deg, #0B2BD6 0%, #6A55FF 40%, #E33DCF 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-bar-text {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
|
||||||
|
@media (max-width: 480px) { font-size: 13px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================================== */
|
||||||
|
/* MENU LATERAL (GERAL) */
|
||||||
|
/* ===================================================== */
|
||||||
|
|
||||||
|
.menu-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: -1; /* 🔑 sempre atrás */
|
z-index: 1100;
|
||||||
pointer-events: none;
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
.grid {
|
-webkit-backdrop-filter: blur(4px);
|
||||||
position: absolute;
|
|
||||||
inset: -40px;
|
|
||||||
opacity: 0.30;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 25% 25%, var(--brand-2), transparent 45%),
|
|
||||||
radial-gradient(circle at 80% 30%, var(--brand-3), transparent 55%),
|
|
||||||
radial-gradient(circle at 45% 90%, var(--brand-2), transparent 50%),
|
|
||||||
repeating-linear-gradient(
|
|
||||||
90deg,
|
|
||||||
rgba(0, 0, 0, 0.05) 0px,
|
|
||||||
rgba(0, 0, 0, 0.05) 1px,
|
|
||||||
transparent 1px,
|
|
||||||
transparent 46px
|
|
||||||
),
|
|
||||||
repeating-linear-gradient(
|
|
||||||
0deg,
|
|
||||||
rgba(0, 0, 0, 0.04) 0px,
|
|
||||||
rgba(0, 0, 0, 0.04) 1px,
|
|
||||||
transparent 1px,
|
|
||||||
transparent 46px
|
|
||||||
);
|
|
||||||
/* ❌ SEM blur */
|
|
||||||
transform: translateZ(0);
|
|
||||||
animation: gridMove 12s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blob {
|
|
||||||
position: absolute;
|
|
||||||
width: 520px;
|
|
||||||
height: 520px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: radial-gradient(circle at 30% 30%, var(--brand), transparent 62%);
|
|
||||||
opacity: 0.16; /* um pouco mais leve sem blur */
|
|
||||||
/* ❌ SEM blur */
|
|
||||||
transform: translateZ(0);
|
|
||||||
animation: floaty 9s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blob-1 {
|
|
||||||
top: -180px;
|
|
||||||
left: -180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blob-2 {
|
|
||||||
bottom: -220px;
|
|
||||||
right: -220px;
|
|
||||||
opacity: 0.12;
|
|
||||||
animation-duration: 11s;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =============================== */
|
.side-menu {
|
||||||
/* HERO */
|
position: fixed;
|
||||||
/* =============================== */
|
top: 0;
|
||||||
.hero {
|
left: 0;
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: clamp(48px, 5vw, 70px) 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content {
|
height: 100vh;
|
||||||
text-align: center;
|
width: min(340px, 88vw);
|
||||||
max-width: 980px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================== */
|
z-index: 1150;
|
||||||
/* KICKER */
|
transform: translateX(-102%);
|
||||||
/* =============================== */
|
transition: transform 220ms ease;
|
||||||
.kicker {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
padding: 10px 14px;
|
background: rgba(255, 255, 255, 0.82);
|
||||||
border-radius: 999px;
|
backdrop-filter: blur(16px);
|
||||||
background: rgba(255, 255, 255, 0.65);
|
-webkit-backdrop-filter: blur(16px);
|
||||||
border: 1px solid var(--border);
|
|
||||||
box-shadow: var(--shadow-soft);
|
|
||||||
|
|
||||||
color: var(--muted);
|
border-right: 1px solid rgba(227, 61, 207, 0.18);
|
||||||
font-family: 'Poppins', sans-serif;
|
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.14);
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
i {
|
|
||||||
color: var(--brand);
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================== */
|
|
||||||
/* TÍTULO PRINCIPAL */
|
|
||||||
/* =============================== */
|
|
||||||
.main-title {
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
font-size: clamp(28px, 4.6vw, 54px);
|
|
||||||
line-height: 1.08;
|
|
||||||
margin: 22px auto 22px;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
width: fit-content;
|
|
||||||
|
|
||||||
.first-line,
|
|
||||||
.second-line {
|
|
||||||
color: var(--brand);
|
|
||||||
font-weight: 600;
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.second-line strong {
|
|
||||||
background: linear-gradient(90deg, var(--brand), rgba(227, 61, 207, 0.55));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.second-line strong::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: -6px;
|
|
||||||
right: -6px;
|
|
||||||
bottom: -6px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(227, 61, 207, 0.18);
|
|
||||||
z-index: -1;
|
|
||||||
transform: scaleX(0);
|
|
||||||
transform-origin: left;
|
|
||||||
animation: underlineGrow 900ms ease forwards;
|
|
||||||
animation-delay: 700ms;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* deslocamento em telas grandes */
|
.side-menu.open { transform: translateX(0); }
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.main-title .first-line { transform: translateX(-70px); }
|
|
||||||
.main-title .second-line { transform: translateX(100px) translateY(-6px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1199.98px) {
|
.side-menu-header {
|
||||||
.main-title .first-line,
|
padding: 14px;
|
||||||
.main-title .second-line { transform: none; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================== */
|
|
||||||
/* PARÁGRAFO PRINCIPAL */
|
|
||||||
/* =============================== */
|
|
||||||
.main-paragraph {
|
|
||||||
width: min(980px, 92%);
|
|
||||||
margin: 0 auto 26px;
|
|
||||||
|
|
||||||
font-family: 'Poppins', sans-serif;
|
|
||||||
font-size: clamp(15px, 1.55vw, 20px);
|
|
||||||
color: var(--text);
|
|
||||||
line-height: 1.45;
|
|
||||||
font-weight: 400;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
strong { font-weight: 700; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-name { color: var(--brand); }
|
|
||||||
|
|
||||||
.highlight {
|
|
||||||
color: var(--text);
|
|
||||||
position: relative;
|
|
||||||
padding: 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: -2px;
|
|
||||||
right: -2px;
|
|
||||||
bottom: 2px;
|
|
||||||
height: 10px;
|
|
||||||
background: rgba(227, 61, 207, 0.12);
|
|
||||||
border-radius: 999px;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================== */
|
|
||||||
/* AÇÕES / LINKS */
|
|
||||||
/* =============================== */
|
|
||||||
.hero-actions {
|
|
||||||
margin-top: 10px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
gap: 14px;
|
gap: 12px;
|
||||||
flex-wrap: wrap;
|
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-link {
|
.close-btn {
|
||||||
display: inline-flex;
|
width: 44px;
|
||||||
align-items: center;
|
height: 44px;
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
color: rgba(0, 0, 0, 0.72);
|
|
||||||
font-family: 'Poppins', sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
padding: 12px 14px;
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: rgba(255, 255, 255, 0.55);
|
|
||||||
box-shadow: var(--shadow-soft);
|
|
||||||
|
|
||||||
transition: transform 0.22s ease, box-shadow 0.22s ease;
|
border: 1px solid rgba(0, 0, 0, 0.10);
|
||||||
}
|
background: rgba(255, 255, 255, 0.60);
|
||||||
|
|
||||||
.secondary-link:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-badges {
|
|
||||||
margin-top: 18px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-pill {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
padding: 9px 12px;
|
|
||||||
border-radius: 999px;
|
|
||||||
|
|
||||||
background: rgba(255, 255, 255, 0.70);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
box-shadow: var(--shadow-soft);
|
|
||||||
|
|
||||||
font-family: 'Poppins', sans-serif;
|
|
||||||
font-size: 13px;
|
|
||||||
color: rgba(0, 0, 0, 0.75);
|
|
||||||
|
|
||||||
i { color: var(--brand); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================== */
|
|
||||||
/* BENEFÍCIOS */
|
|
||||||
/* =============================== */
|
|
||||||
.hero-benefits {
|
|
||||||
margin: 20px auto 0;
|
|
||||||
width: min(820px, 96%);
|
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
place-items: center;
|
||||||
|
|
||||||
.benefit {
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu-body {
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
padding: 12px 14px;
|
width: 100%;
|
||||||
border-radius: 16px;
|
padding: 12px 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
|
||||||
background: rgba(255, 255, 255, 0.58);
|
text-decoration: none;
|
||||||
border: 1px solid var(--border);
|
color: rgba(0, 0, 0, 0.80);
|
||||||
box-shadow: var(--shadow-soft);
|
font-weight: 800;
|
||||||
|
|
||||||
i {
|
|
||||||
color: var(--brand);
|
|
||||||
font-size: 18px;
|
|
||||||
margin-top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-family: 'Poppins', sans-serif;
|
font-family: 'Poppins', sans-serif;
|
||||||
color: rgba(0, 0, 0, 0.78);
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.35;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 820px) {
|
transition: background 180ms ease, transform 180ms ease;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =============================== */
|
.side-item i { color: var(--brand); }
|
||||||
/* FEATURES */
|
|
||||||
/* =============================== */
|
.side-item:hover {
|
||||||
.section-head {
|
background: rgba(227, 61, 207, 0.10);
|
||||||
text-align: center;
|
transform: translateY(-1px);
|
||||||
margin: 0 auto 24px;
|
|
||||||
max-width: 820px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.side-item:active {
|
||||||
font-family: 'Inter', sans-serif;
|
transform: translateY(0) scale(0.99);
|
||||||
font-size: clamp(20px, 2.4vw, 30px);
|
|
||||||
color: rgba(0, 0, 0, 0.88);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-subtitle {
|
/* ========================================= */
|
||||||
font-family: 'Poppins', sans-serif;
|
/* ✅ OVERRIDE BOOTSTRAP: SEM TRANSPARÊNCIA */
|
||||||
color: rgba(0, 0, 0, 0.64);
|
/* ========================================= */
|
||||||
font-size: 15px;
|
.btn.btn-cadastrar,
|
||||||
margin: 0 auto;
|
.btn.btn-login {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-cards-row { margin-top: 18px; }
|
.btn.btn-cadastrar:active,
|
||||||
|
.btn.btn-login:active,
|
||||||
.feature-item {
|
.btn.btn-cadastrar:active:focus,
|
||||||
border-radius: 18px;
|
.btn.btn-login:active:focus,
|
||||||
padding: 10px;
|
.btn.btn-cadastrar:focus,
|
||||||
background: transparent;
|
.btn.btn-login:focus,
|
||||||
transition: transform 0.25s ease, filter 0.25s ease;
|
.btn.btn-cadastrar:focus-visible,
|
||||||
|
.btn.btn-login:focus-visible {
|
||||||
|
opacity: 1 !important;
|
||||||
|
filter: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-item:hover {
|
/* mantém as cores originais até quando clica */
|
||||||
transform: translateY(-6px);
|
.btn.btn-cadastrar:active { background: #E1E1E1 !important; }
|
||||||
filter: drop-shadow(0 14px 22px rgba(0, 0, 0, 0.10));
|
.btn.btn-login:active { background: var(--brand) !important; border-color: var(--brand) !important; color: #fff !important; }
|
||||||
}
|
|
||||||
|
|
||||||
.button-section {
|
/* remove “inset” do bootstrap */
|
||||||
margin-top: 18px;
|
.btn.btn-cadastrar:active,
|
||||||
padding-bottom: 30px;
|
.btn.btn-login:active {
|
||||||
}
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.10) !important;
|
||||||
|
|
||||||
/* =============================== */
|
|
||||||
/* REVEAL (scroll) */
|
|
||||||
/* =============================== */
|
|
||||||
.reveal {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(18px) scale(0.98);
|
|
||||||
transition:
|
|
||||||
opacity 650ms ease,
|
|
||||||
transform 650ms ease;
|
|
||||||
transition-delay: var(--delay, 0ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal.is-visible {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================== */
|
|
||||||
/* ANIMAÇÕES */
|
|
||||||
/* =============================== */
|
|
||||||
@keyframes floaty {
|
|
||||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
||||||
50% { transform: translate(14px, 18px) scale(1.03); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gridMove {
|
|
||||||
0%, 100% { transform: translate(0, 0); }
|
|
||||||
50% { transform: translate(-10px, 10px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes underlineGrow {
|
|
||||||
from { transform: scaleX(0); }
|
|
||||||
to { transform: scaleX(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================== */
|
|
||||||
/* Acessibilidade */
|
|
||||||
/* =============================== */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.reveal,
|
|
||||||
.page-bg .grid,
|
|
||||||
.page-bg .blob,
|
|
||||||
.main-title .second-line strong::after {
|
|
||||||
animation: none !important;
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal {
|
|
||||||
opacity: 1;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { HttpInterceptorFn } from '@angular/common/http';
|
import { HttpInterceptorFn } from '@angular/common/http';
|
||||||
|
|
||||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
const token = localStorage.getItem('token');
|
// ✅ SSR-safe
|
||||||
|
if (typeof window === 'undefined') return next(req);
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
if (!token) return next(req);
|
if (!token) return next(req);
|
||||||
|
|
||||||
const authReq = req.clone({
|
return next(
|
||||||
|
req.clone({
|
||||||
setHeaders: { Authorization: `Bearer ${token}` }
|
setHeaders: { Authorization: `Bearer ${token}` }
|
||||||
});
|
})
|
||||||
|
);
|
||||||
return next(authReq);
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -49,16 +49,27 @@
|
||||||
class="btn btn-outline-primary btn-sm btn-glass"
|
class="btn btn-outline-primary btn-sm btn-glass"
|
||||||
(click)="onImportExcel()"
|
(click)="onImportExcel()"
|
||||||
title="Importar dados de planilha"
|
title="Importar dados de planilha"
|
||||||
|
[disabled]="loading"
|
||||||
>
|
>
|
||||||
<i class="bi bi-file-earmark-excel me-1"></i>
|
<i class="bi bi-file-earmark-excel me-1"></i>
|
||||||
Importar Excel
|
Importar Excel
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- input escondido para upload -->
|
||||||
|
<input
|
||||||
|
#excelInput
|
||||||
|
type="file"
|
||||||
|
class="d-none"
|
||||||
|
accept=".xlsx"
|
||||||
|
(change)="onExcelSelected($event)"
|
||||||
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm btn-brand"
|
class="btn btn-primary btn-sm btn-brand"
|
||||||
(click)="onCadastrarLinha()"
|
(click)="onCadastrarLinha()"
|
||||||
title="Cadastrar uma nova linha"
|
title="Cadastrar uma nova linha"
|
||||||
|
[disabled]="loading"
|
||||||
>
|
>
|
||||||
<i class="bi bi-plus-circle me-1"></i>
|
<i class="bi bi-plus-circle me-1"></i>
|
||||||
Cadastrar Linha
|
Cadastrar Linha
|
||||||
|
|
@ -79,6 +90,7 @@
|
||||||
placeholder="Pesquisar..."
|
placeholder="Pesquisar..."
|
||||||
[(ngModel)]="searchTerm"
|
[(ngModel)]="searchTerm"
|
||||||
(ngModelChange)="onSearch()"
|
(ngModelChange)="onSearch()"
|
||||||
|
[disabled]="loading"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -87,6 +99,7 @@
|
||||||
(click)="clearSearch()"
|
(click)="clearSearch()"
|
||||||
*ngIf="searchTerm"
|
*ngIf="searchTerm"
|
||||||
title="Limpar busca"
|
title="Limpar busca"
|
||||||
|
[disabled]="loading"
|
||||||
>
|
>
|
||||||
<i class="bi bi-x-lg"></i>
|
<i class="bi bi-x-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -98,6 +111,7 @@
|
||||||
class="form-select form-select-sm w-auto select-glass"
|
class="form-select form-select-sm w-auto select-glass"
|
||||||
[(ngModel)]="pageSize"
|
[(ngModel)]="pageSize"
|
||||||
(change)="onPageSizeChange()"
|
(change)="onPageSizeChange()"
|
||||||
|
[disabled]="loading"
|
||||||
>
|
>
|
||||||
<option [ngValue]="10">10</option>
|
<option [ngValue]="10">10</option>
|
||||||
<option [ngValue]="20">20</option>
|
<option [ngValue]="20">20</option>
|
||||||
|
|
@ -160,14 +174,14 @@
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th class="sortable" (click)="setSort('plano')">
|
<th class="sortable" (click)="setSort('plano')">
|
||||||
PLANO
|
PLANO CONTRATO
|
||||||
<span class="sort-caret">
|
<span class="sort-caret">
|
||||||
{{ sortKey==='plano' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
|
{{ sortKey==='plano' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th class="sortable" (click)="setSort('contrato')">
|
<th class="sortable" (click)="setSort('contrato')">
|
||||||
CONTRATO
|
VENC. DA CONTA
|
||||||
<span class="sort-caret">
|
<span class="sort-caret">
|
||||||
{{ sortKey==='contrato' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
|
{{ sortKey==='contrato' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -178,6 +192,13 @@
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr *ngIf="loading">
|
||||||
|
<td colspan="9" class="text-center py-5 text-muted empty-state">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
|
Carregando registros...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr *ngFor="let r of pagedRows; trackBy: trackById" data-row>
|
<tr *ngFor="let r of pagedRows; trackBy: trackById" data-row>
|
||||||
<td class="text-muted">{{ r.item }}</td>
|
<td class="text-muted">{{ r.item }}</td>
|
||||||
<td>{{ r.conta }}</td>
|
<td>{{ r.conta }}</td>
|
||||||
|
|
@ -194,6 +215,7 @@
|
||||||
class="btn btn-sm btn-outline-secondary btn-icon"
|
class="btn btn-sm btn-outline-secondary btn-icon"
|
||||||
(click)="onDetalhes(r)"
|
(click)="onDetalhes(r)"
|
||||||
title="Detalhes"
|
title="Detalhes"
|
||||||
|
[disabled]="loading"
|
||||||
>
|
>
|
||||||
<i class="bi bi-eye"></i>
|
<i class="bi bi-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -203,6 +225,7 @@
|
||||||
class="btn btn-sm btn-outline-success btn-icon ms-1"
|
class="btn btn-sm btn-outline-success btn-icon ms-1"
|
||||||
(click)="onFinanceiro(r)"
|
(click)="onFinanceiro(r)"
|
||||||
title="Financeiro"
|
title="Financeiro"
|
||||||
|
[disabled]="loading"
|
||||||
>
|
>
|
||||||
<i class="bi bi-cash-coin"></i>
|
<i class="bi bi-cash-coin"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -212,13 +235,14 @@
|
||||||
class="btn btn-sm btn-outline-danger btn-icon ms-1"
|
class="btn btn-sm btn-outline-danger btn-icon ms-1"
|
||||||
(click)="onRemover(r)"
|
(click)="onRemover(r)"
|
||||||
title="Remover"
|
title="Remover"
|
||||||
|
[disabled]="loading"
|
||||||
>
|
>
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr *ngIf="pagedRows.length === 0">
|
<tr *ngIf="!loading && pagedRows.length === 0">
|
||||||
<td colspan="9" class="text-center py-5 text-muted empty-state">
|
<td colspan="9" class="text-center py-5 text-muted empty-state">
|
||||||
<i class="bi bi-inbox me-2"></i>
|
<i class="bi bi-inbox me-2"></i>
|
||||||
Nenhum registro encontrado.
|
Nenhum registro encontrado.
|
||||||
|
|
@ -239,18 +263,18 @@
|
||||||
|
|
||||||
<nav aria-label="Paginação">
|
<nav aria-label="Paginação">
|
||||||
<ul class="pagination pagination-sm mb-0 pagination-modern">
|
<ul class="pagination pagination-sm mb-0 pagination-modern">
|
||||||
<li class="page-item" [class.disabled]="page === 1">
|
<li class="page-item" [class.disabled]="page === 1 || loading">
|
||||||
<button class="page-link" (click)="goToPage(page - 1)">
|
<button class="page-link" (click)="goToPage(page - 1)">
|
||||||
<i class="bi bi-chevron-left"></i>
|
<i class="bi bi-chevron-left"></i>
|
||||||
Anterior
|
Anterior
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="page-item" *ngFor="let p of pageNumbers" [class.active]="p === page">
|
<li class="page-item" *ngFor="let p of pageNumbers" [class.active]="p === page" [class.disabled]="loading">
|
||||||
<button class="page-link" (click)="goToPage(p)">{{ p }}</button>
|
<button class="page-link" (click)="goToPage(p)">{{ p }}</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="page-item" [class.disabled]="page === totalPages">
|
<li class="page-item" [class.disabled]="page === totalPages || loading">
|
||||||
<button class="page-link" (click)="goToPage(page + 1)">
|
<button class="page-link" (click)="goToPage(page + 1)">
|
||||||
Próxima
|
Próxima
|
||||||
<i class="bi bi-chevron-right"></i>
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
|
@ -263,3 +287,115 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ===================== -->
|
||||||
|
<!-- MODAL DETALHES -->
|
||||||
|
<!-- ===================== -->
|
||||||
|
<div class="modal-backdrop-custom" *ngIf="detailOpen" (click)="closeDetail()"></div>
|
||||||
|
<div class="modal-custom" *ngIf="detailOpen">
|
||||||
|
<div class="modal-card" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
Detalhes da Linha
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary btn-icon" (click)="closeDetail()">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body" *ngIf="detailData; else detailLoading">
|
||||||
|
<div class="detail-grid">
|
||||||
|
<div><span class="k">ITÉM</span><span class="v">{{ detailData.item }}</span></div>
|
||||||
|
<div><span class="k">CONTA</span><span class="v">{{ detailData.conta }}</span></div>
|
||||||
|
<div><span class="k">LINHA</span><span class="v">{{ detailData.linha }}</span></div>
|
||||||
|
<div><span class="k">CHIP</span><span class="v">{{ detailData.chip }}</span></div>
|
||||||
|
|
||||||
|
<div><span class="k">CLIENTE</span><span class="v">{{ detailData.cliente }}</span></div>
|
||||||
|
<div><span class="k">USUÁRIO</span><span class="v">{{ detailData.usuario }}</span></div>
|
||||||
|
<div class="span-2"><span class="k">PLANO CONTRATO</span><span class="v">{{ detailData.planoContrato }}</span></div>
|
||||||
|
|
||||||
|
<div><span class="k">STATUS</span><span class="v">{{ detailData.status }}</span></div>
|
||||||
|
<div><span class="k">DATA DO BLOQUEIO</span><span class="v">{{ detailData.dataBloqueio }}</span></div>
|
||||||
|
|
||||||
|
<div><span class="k">SKIL</span><span class="v">{{ detailData.skil }}</span></div>
|
||||||
|
<div><span class="k">MODALIDADE</span><span class="v">{{ detailData.modalidade }}</span></div>
|
||||||
|
|
||||||
|
<div><span class="k">CEDENTE</span><span class="v">{{ detailData.cedente }}</span></div>
|
||||||
|
<div><span class="k">SOLICITANTE</span><span class="v">{{ detailData.solicitante }}</span></div>
|
||||||
|
|
||||||
|
<div><span class="k">DATA ENTREGA OPERA.</span><span class="v">{{ detailData.dataEntregaOpera }}</span></div>
|
||||||
|
<div><span class="k">DATA ENTREGA CLIENTE</span><span class="v">{{ detailData.dataEntregaCliente }}</span></div>
|
||||||
|
|
||||||
|
<div><span class="k">VENC. DA CONTA</span><span class="v">{{ detailData.vencConta }}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #detailLoading>
|
||||||
|
<div class="modal-body text-muted text-center py-4">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
Carregando detalhes...
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== -->
|
||||||
|
<!-- MODAL FINANCEIRO -->
|
||||||
|
<!-- ===================== -->
|
||||||
|
<div class="modal-backdrop-custom" *ngIf="financeOpen" (click)="closeFinance()"></div>
|
||||||
|
<div class="modal-custom" *ngIf="financeOpen">
|
||||||
|
<div class="modal-card" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<i class="bi bi-cash-coin me-2"></i>
|
||||||
|
Financeiro
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary btn-icon" (click)="closeFinance()">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body" *ngIf="financeData; else financeLoading">
|
||||||
|
|
||||||
|
<div class="finance-box vivo">
|
||||||
|
<div class="box-title">Vivo</div>
|
||||||
|
<div class="box-grid">
|
||||||
|
<div><span class="k">FRAQUIA</span><span class="v">{{ financeData.franquiaVivo }}</span></div>
|
||||||
|
<div><span class="k">VALOR DO PLANO R$</span><span class="v">{{ financeData.valorPlanoVivo }}</span></div>
|
||||||
|
<div><span class="k">GESTÃO VOZ E DADOS R$</span><span class="v">{{ financeData.gestaoVozDados }}</span></div>
|
||||||
|
<div><span class="k">SKEELO</span><span class="v">{{ financeData.skeelo }}</span></div>
|
||||||
|
<div><span class="k">VIVO NEWS PLUS</span><span class="v">{{ financeData.vivoNewsPlus }}</span></div>
|
||||||
|
<div><span class="k">VIVO TRAVEL MUNDO</span><span class="v">{{ financeData.vivoTravelMundo }}</span></div>
|
||||||
|
<div><span class="k">VIVO GESTÃO DISPOSITIVO</span><span class="v">{{ financeData.vivoGestaoDispositivo }}</span></div>
|
||||||
|
<div><span class="k">VALOR CONTRATO VIVO</span><span class="v">{{ financeData.valorContratoVivo }}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="finance-box line">
|
||||||
|
<div class="box-title">Line Móvel</div>
|
||||||
|
<div class="box-grid">
|
||||||
|
<div><span class="k">FRANQUIA LINE</span><span class="v">{{ financeData.franquiaLine }}</span></div>
|
||||||
|
<div><span class="k">FRANQUIA GESTÃO</span><span class="v">{{ financeData.franquiaGestao }}</span></div>
|
||||||
|
<div><span class="k">LOCAÇÃO AP.</span><span class="v">{{ financeData.locacaoAp }}</span></div>
|
||||||
|
<div><span class="k">VALOR CONTRATO LINE</span><span class="v">{{ financeData.valorContratoLine }}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="finance-footer">
|
||||||
|
<div><span class="k">DESCONTO</span><span class="v">{{ financeData.desconto }}</span></div>
|
||||||
|
<div><span class="k">LUCRO</span><span class="v">{{ financeData.lucro }}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #financeLoading>
|
||||||
|
<div class="modal-body text-muted text-center py-4">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
Carregando financeiro...
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -398,3 +398,115 @@
|
||||||
50% { transform: translate(18px, 10px) scale(1.03); }
|
50% { transform: translate(18px, 10px) scale(1.03); }
|
||||||
100% { transform: translate(0, 0) scale(1); }
|
100% { transform: translate(0, 0) scale(1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =============================== */
|
||||||
|
/* ✅ MODAIS (Detalhes / Financeiro) */
|
||||||
|
/* =============================== */
|
||||||
|
|
||||||
|
.modal-backdrop-custom{
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,.35);
|
||||||
|
z-index: 2500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-custom{
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2501;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card{
|
||||||
|
width: min(980px, 100%);
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border: 1px solid rgba(227, 61, 207, 0.16);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 22px 46px rgba(17, 18, 20, 0.18);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header{
|
||||||
|
padding: 12px 14px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(180deg, rgba(227,61,207,0.08), rgba(255,255,255,0.10));
|
||||||
|
border-bottom: 1px solid rgba(17, 18, 20, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title{
|
||||||
|
font-weight: 950;
|
||||||
|
color: var(--text);
|
||||||
|
letter-spacing: -0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body{
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-grid{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px 14px;
|
||||||
|
|
||||||
|
.k { display:block; font-size: 12px; color: rgba(17, 18, 20, 0.65); font-weight: 800; }
|
||||||
|
.v { display:block; font-size: 14px; color: var(--text); font-weight: 950; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-grid .span-2{
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-box{
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid rgba(17, 18, 20, 0.08);
|
||||||
|
background: rgba(255,255,255,0.78);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.box-title{
|
||||||
|
font-weight: 950;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-grid{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px 14px;
|
||||||
|
|
||||||
|
.k { display:block; font-size: 12px; opacity: .8; font-weight: 800; }
|
||||||
|
.v { display:block; font-weight: 950; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-box.vivo{
|
||||||
|
border-color: rgba(227, 61, 207, 0.25);
|
||||||
|
.box-title, .v { color: var(--brand); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-box.line{
|
||||||
|
border-color: rgba(3, 15, 170, 0.20);
|
||||||
|
.box-title, .v { color: var(--blue); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-footer{
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 16px;
|
||||||
|
padding-top: 4px;
|
||||||
|
|
||||||
|
.k { display:block; font-size: 12px; color: rgba(17, 18, 20, 0.65); font-weight: 800; }
|
||||||
|
.v { display:block; font-weight: 950; color: var(--text); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px){
|
||||||
|
.detail-grid,
|
||||||
|
.finance-box .box-grid{
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,120 @@
|
||||||
import { Component, ElementRef, ViewChild, Inject, PLATFORM_ID, AfterViewInit } from '@angular/core';
|
import { Component, ElementRef, ViewChild, Inject, PLATFORM_ID, AfterViewInit } from '@angular/core';
|
||||||
import { isPlatformBrowser, CommonModule } from '@angular/common';
|
import { isPlatformBrowser, CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { HttpClient, HttpClientModule, HttpParams } from '@angular/common/http';
|
||||||
|
|
||||||
type SortDir = 'asc' | 'desc';
|
type SortDir = 'asc' | 'desc';
|
||||||
|
|
||||||
type LineRow = {
|
type LineRow = {
|
||||||
id: number;
|
id: string; // GUID
|
||||||
item: string;
|
item: string;
|
||||||
conta: string;
|
conta: string;
|
||||||
linha: string;
|
linha: string;
|
||||||
chip: string;
|
chip: string;
|
||||||
cliente: string;
|
cliente: string;
|
||||||
usuario: string;
|
usuario: string;
|
||||||
plano: string;
|
plano: string; // PLANO CONTRATO
|
||||||
contrato: string;
|
contrato: string; // VENC. DA CONTA
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ seu backend está retornando camelCase (confirmado no Network)
|
||||||
|
type ApiPagedResult<T> = {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
items: T[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApiLineList = {
|
||||||
|
id: string;
|
||||||
|
item: number;
|
||||||
|
conta: string | null;
|
||||||
|
linha: string | null;
|
||||||
|
chip: string | null;
|
||||||
|
cliente: string | null;
|
||||||
|
usuario: string | null;
|
||||||
|
planoContrato: string | null;
|
||||||
|
vencConta: string | null;
|
||||||
|
|
||||||
|
// se vier no list também (como no seu Response)
|
||||||
|
status?: string | null;
|
||||||
|
skil?: string | null;
|
||||||
|
modalidade?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApiLineDetail = ApiLineList & {
|
||||||
|
dataBloqueio?: string | null;
|
||||||
|
cedente?: string | null;
|
||||||
|
solicitante?: string | null;
|
||||||
|
dataEntregaOpera?: string | null;
|
||||||
|
dataEntregaCliente?: string | null;
|
||||||
|
|
||||||
|
// Vivo
|
||||||
|
franquiaVivo?: number | null;
|
||||||
|
valorPlanoVivo?: number | null;
|
||||||
|
gestaoVozDados?: number | null;
|
||||||
|
skeelo?: number | null;
|
||||||
|
vivoNewsPlus?: number | null;
|
||||||
|
vivoTravelMundo?: number | null;
|
||||||
|
vivoGestaoDispositivo?: number | null;
|
||||||
|
valorContratoVivo?: number | null;
|
||||||
|
|
||||||
|
// Line
|
||||||
|
franquiaLine?: number | null;
|
||||||
|
franquiaGestao?: number | null;
|
||||||
|
locacaoAp?: number | null;
|
||||||
|
valorContratoLine?: number | null;
|
||||||
|
|
||||||
|
desconto?: number | null;
|
||||||
|
lucro?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
imports: [CommonModule, FormsModule, HttpClientModule],
|
||||||
templateUrl: './geral.html',
|
templateUrl: './geral.html',
|
||||||
styleUrls: ['./geral.scss']
|
styleUrls: ['./geral.scss']
|
||||||
})
|
})
|
||||||
export class Geral implements AfterViewInit {
|
export class Geral implements AfterViewInit {
|
||||||
toastMessage = '';
|
toastMessage = '';
|
||||||
@ViewChild('successToast') successToast!: ElementRef;
|
@ViewChild('successToast') successToast!: ElementRef;
|
||||||
|
@ViewChild('excelInput') excelInput!: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
constructor(@Inject(PLATFORM_ID) private platformId: object) {}
|
constructor(
|
||||||
|
@Inject(PLATFORM_ID) private platformId: object,
|
||||||
|
private http: HttpClient
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// ========= API =========
|
||||||
|
private readonly apiBase = 'https://localhost:7205/api/lines';
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
// ========= TABELA (agora vem da API) =========
|
||||||
|
rows: LineRow[] = [];
|
||||||
|
|
||||||
|
searchTerm = '';
|
||||||
|
sortKey: keyof LineRow = 'item';
|
||||||
|
sortDir: SortDir = 'asc';
|
||||||
|
|
||||||
|
page = 1;
|
||||||
|
pageSize = 10;
|
||||||
|
|
||||||
|
total = 0; // total do banco
|
||||||
|
|
||||||
|
// modais
|
||||||
|
detailOpen = false;
|
||||||
|
financeOpen = false;
|
||||||
|
detailData: ApiLineDetail | null = null;
|
||||||
|
financeData: ApiLineDetail | null = null;
|
||||||
|
|
||||||
|
private searchTimer: any = null;
|
||||||
|
|
||||||
async ngAfterViewInit() {
|
async ngAfterViewInit() {
|
||||||
if (!isPlatformBrowser(this.platformId)) return;
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
|
||||||
// ✅ animações (igual Home)
|
|
||||||
this.initAnimations();
|
this.initAnimations();
|
||||||
|
this.loadFromApi();
|
||||||
|
|
||||||
const msg = (history.state && history.state.toastMessage) ? String(history.state.toastMessage) : '';
|
const msg = (history.state && history.state.toastMessage) ? String(history.state.toastMessage) : '';
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
|
@ -76,85 +158,120 @@ export class Geral implements AfterViewInit {
|
||||||
|
|
||||||
private async showToast(message: string) {
|
private async showToast(message: string) {
|
||||||
this.toastMessage = message;
|
this.toastMessage = message;
|
||||||
|
|
||||||
|
// ✅ segurança (caso algum dia seja chamado fora do browser)
|
||||||
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
|
||||||
const bs = await import('bootstrap');
|
const bs = await import('bootstrap');
|
||||||
const toast = new bs.Toast(this.successToast.nativeElement, { autohide: true, delay: 1500 });
|
const toast = new bs.Toast(this.successToast.nativeElement, { autohide: true, delay: 1500 });
|
||||||
toast.show();
|
toast.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========= TABELA (mock por enquanto) =========
|
// ========= API LOAD =========
|
||||||
rows: LineRow[] = [
|
private mapSortKeyToApi(sortKey: keyof LineRow): string {
|
||||||
{ id: 1, item: '001', conta: 'Empresa A', linha: '71999990001', chip: 'ICCID-0001', cliente: 'Cliente A', usuario: 'João', plano: 'Pós 30GB', contrato: 'CT-2025-001' },
|
const map: Record<string, string> = {
|
||||||
{ id: 2, item: '002', conta: 'Empresa B', linha: '71999990002', chip: 'ICCID-0002', cliente: 'Cliente B', usuario: 'Maria', plano: 'Pós Ilimitado', contrato: 'CT-2025-002' },
|
item: 'item',
|
||||||
{ id: 3, item: '003', conta: 'Empresa A', linha: '71999990003', chip: 'ICCID-0003', cliente: 'Cliente A', usuario: 'Carlos', plano: 'Controle 20GB', contrato: 'CT-2025-003' },
|
conta: 'conta',
|
||||||
];
|
linha: 'linha',
|
||||||
|
chip: 'chip',
|
||||||
|
cliente: 'cliente',
|
||||||
|
usuario: 'usuario',
|
||||||
|
plano: 'planoContrato',
|
||||||
|
contrato: 'vencConta',
|
||||||
|
};
|
||||||
|
return map[String(sortKey)] ?? 'item';
|
||||||
|
}
|
||||||
|
|
||||||
searchTerm = '';
|
private loadFromApi() {
|
||||||
sortKey: keyof LineRow = 'item';
|
this.loading = true;
|
||||||
sortDir: SortDir = 'asc';
|
|
||||||
|
|
||||||
page = 1;
|
const params = new HttpParams()
|
||||||
pageSize = 10;
|
.set('page', String(this.page))
|
||||||
|
.set('pageSize', String(this.pageSize))
|
||||||
|
.set('search', (this.searchTerm ?? '').trim())
|
||||||
|
.set('sortBy', this.mapSortKeyToApi(this.sortKey))
|
||||||
|
.set('sortDir', this.sortDir);
|
||||||
|
|
||||||
onSearch() { this.page = 1; }
|
this.http.get<ApiPagedResult<ApiLineList>>(this.apiBase, { params }).subscribe({
|
||||||
clearSearch() { this.searchTerm = ''; this.page = 1; }
|
next: (res) => {
|
||||||
|
// ✅ camelCase (igual seu Response)
|
||||||
|
this.total = res.total ?? 0;
|
||||||
|
|
||||||
|
this.rows = (res.items ?? []).map((x) => ({
|
||||||
|
id: x.id,
|
||||||
|
item: String(x.item ?? ''),
|
||||||
|
conta: x.conta ?? '',
|
||||||
|
linha: x.linha ?? '',
|
||||||
|
chip: x.chip ?? '',
|
||||||
|
cliente: x.cliente ?? '',
|
||||||
|
usuario: x.usuario ?? '',
|
||||||
|
plano: x.planoContrato ?? '',
|
||||||
|
contrato: x.vencConta ?? '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: async () => {
|
||||||
|
this.loading = false;
|
||||||
|
await this.showToast('Erro ao carregar dados da API.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= BUSCA / ORDENAÇÃO / PAGINAÇÃO =========
|
||||||
|
onSearch() {
|
||||||
|
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||||
|
|
||||||
|
this.searchTimer = setTimeout(() => {
|
||||||
|
this.page = 1;
|
||||||
|
this.loadFromApi();
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSearch() {
|
||||||
|
this.searchTerm = '';
|
||||||
|
this.page = 1;
|
||||||
|
this.loadFromApi();
|
||||||
|
}
|
||||||
|
|
||||||
setSort(key: keyof LineRow) {
|
setSort(key: keyof LineRow) {
|
||||||
if (this.sortKey === key) {
|
if (this.sortKey === key) {
|
||||||
this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
|
this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
this.sortKey = key;
|
this.sortKey = key;
|
||||||
this.sortDir = 'asc';
|
this.sortDir = 'asc';
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageSizeChange() { this.page = 1; }
|
this.page = 1;
|
||||||
|
this.loadFromApi();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageSizeChange() {
|
||||||
|
this.page = 1;
|
||||||
|
this.loadFromApi();
|
||||||
|
}
|
||||||
|
|
||||||
goToPage(p: number) {
|
goToPage(p: number) {
|
||||||
const target = Math.max(1, Math.min(this.totalPages, p));
|
const target = Math.max(1, Math.min(this.totalPages, p));
|
||||||
this.page = target;
|
this.page = target;
|
||||||
|
this.loadFromApi();
|
||||||
}
|
}
|
||||||
|
|
||||||
trackById(_: number, row: LineRow) { return row.id; }
|
trackById(_: number, row: LineRow) { return row.id; }
|
||||||
|
|
||||||
get filteredRows(): LineRow[] {
|
get pagedRows(): LineRow[] {
|
||||||
const term = this.searchTerm.trim().toLowerCase();
|
return this.rows;
|
||||||
if (!term) return this.rows;
|
|
||||||
|
|
||||||
return this.rows.filter(r => {
|
|
||||||
const blob = [r.item, r.conta, r.linha, r.chip, r.cliente, r.usuario, r.plano, r.contrato].join(' ').toLowerCase();
|
|
||||||
return blob.includes(term);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get sortedRows(): LineRow[] {
|
|
||||||
const copy = [...this.filteredRows];
|
|
||||||
const key = this.sortKey;
|
|
||||||
const dir = this.sortDir;
|
|
||||||
|
|
||||||
copy.sort((a, b) => {
|
|
||||||
const av = String(a[key] ?? '').toLowerCase();
|
|
||||||
const bv = String(b[key] ?? '').toLowerCase();
|
|
||||||
if (av < bv) return dir === 'asc' ? -1 : 1;
|
|
||||||
if (av > bv) return dir === 'asc' ? 1 : -1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return copy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalPages(): number {
|
get totalPages(): number {
|
||||||
const total = Math.ceil(this.sortedRows.length / this.pageSize);
|
const total = Math.ceil((this.total || 0) / this.pageSize);
|
||||||
return total > 0 ? total : 1;
|
return total > 0 ? total : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
get pagedRows(): LineRow[] {
|
get filteredCount(): number {
|
||||||
const start = (this.page - 1) * this.pageSize;
|
return this.total || 0;
|
||||||
const end = start + this.pageSize;
|
|
||||||
return this.sortedRows.slice(start, end);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get filteredCount(): number { return this.sortedRows.length; }
|
|
||||||
|
|
||||||
get pageStart(): number {
|
get pageStart(): number {
|
||||||
if (this.filteredCount === 0) return 0;
|
if (this.filteredCount === 0) return 0;
|
||||||
return (this.page - 1) * this.pageSize + 1;
|
return (this.page - 1) * this.pageSize + 1;
|
||||||
|
|
@ -162,7 +279,7 @@ export class Geral implements AfterViewInit {
|
||||||
|
|
||||||
get pageEnd(): number {
|
get pageEnd(): number {
|
||||||
if (this.filteredCount === 0) return 0;
|
if (this.filteredCount === 0) return 0;
|
||||||
return Math.min(this.page * this.pageSize, this.filteredCount);
|
return Math.min((this.page - 1) * this.pageSize + this.rows.length, this.filteredCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageNumbers(): number[] {
|
get pageNumbers(): number[] {
|
||||||
|
|
@ -179,10 +296,97 @@ export class Geral implements AfterViewInit {
|
||||||
return pages;
|
return pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========= BOTÕES (placeholders) =========
|
// ========= IMPORTAR EXCEL =========
|
||||||
async onImportExcel() { await this.showToast('Importação via Excel será implementada.'); }
|
async onImportExcel() {
|
||||||
async onCadastrarLinha() { await this.showToast('Cadastro de linha será implementado.'); }
|
if (!this.excelInput?.nativeElement) return;
|
||||||
async onDetalhes(row: LineRow) { await this.showToast(`Detalhes: linha ${row.linha}`); }
|
this.excelInput.nativeElement.value = '';
|
||||||
async onFinanceiro(row: LineRow) { await this.showToast(`Financeiro: linha ${row.linha} (em implementação)`); }
|
this.excelInput.nativeElement.click();
|
||||||
async onRemover(row: LineRow) { await this.showToast(`Remover: linha ${row.linha} (em implementação)`); }
|
}
|
||||||
|
|
||||||
|
onExcelSelected(ev: Event) {
|
||||||
|
const input = ev.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const form = new FormData();
|
||||||
|
|
||||||
|
// ✅ mantém 'file' porque já funcionou no seu import
|
||||||
|
form.append('file', file);
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
this.http.post<{ imported: number }>(`${this.apiBase}/import-excel`, form).subscribe({
|
||||||
|
next: async (r) => {
|
||||||
|
await this.showToast(`Importação concluída: ${r?.imported ?? 0} linhas.`);
|
||||||
|
this.page = 1;
|
||||||
|
this.loadFromApi();
|
||||||
|
},
|
||||||
|
error: async () => {
|
||||||
|
this.loading = false;
|
||||||
|
await this.showToast('Falha ao importar Excel. Verifique a aba "GERAL" e o formato .xlsx.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= DETALHES / FINANCEIRO =========
|
||||||
|
private getById(id: string, cb: (d: ApiLineDetail) => void) {
|
||||||
|
this.http.get<ApiLineDetail>(`${this.apiBase}/${id}`).subscribe({
|
||||||
|
next: (d) => cb(d),
|
||||||
|
error: async () => {
|
||||||
|
await this.showToast('Erro ao carregar dados da linha.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onDetalhes(row: LineRow) {
|
||||||
|
this.detailOpen = true;
|
||||||
|
this.detailData = null;
|
||||||
|
|
||||||
|
this.getById(row.id, (d) => {
|
||||||
|
this.detailData = d;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onFinanceiro(row: LineRow) {
|
||||||
|
this.financeOpen = true;
|
||||||
|
this.financeData = null;
|
||||||
|
|
||||||
|
this.getById(row.id, (d) => {
|
||||||
|
this.financeData = d;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDetail() {
|
||||||
|
this.detailOpen = false;
|
||||||
|
this.detailData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeFinance() {
|
||||||
|
this.financeOpen = false;
|
||||||
|
this.financeData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= REMOVER =========
|
||||||
|
async onRemover(row: LineRow) {
|
||||||
|
if (!confirm(`Deseja remover a linha ${row.linha}?`)) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
this.http.delete<void>(`${this.apiBase}/${row.id}`).subscribe({
|
||||||
|
next: async () => {
|
||||||
|
await this.showToast('Linha removida com sucesso.');
|
||||||
|
if (this.rows.length === 1 && this.page > 1) this.page -= 1;
|
||||||
|
this.loadFromApi();
|
||||||
|
},
|
||||||
|
error: async () => {
|
||||||
|
this.loading = false;
|
||||||
|
await this.showToast('Erro ao remover a linha.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= BOTÕES (mantidos) =========
|
||||||
|
async onCadastrarLinha() {
|
||||||
|
await this.showToast('Cadastro de linha será implementado.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -171,15 +171,6 @@
|
||||||
|
|
||||||
</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 class="value-strip" data-animate>
|
<div class="value-strip" data-animate>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<i class="bi bi-check2-circle"></i>
|
<i class="bi bi-check2-circle"></i>
|
||||||
|
|
@ -195,6 +186,17 @@
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -467,7 +467,7 @@
|
||||||
|
|
||||||
/* faixa de valores */
|
/* faixa de valores */
|
||||||
.value-strip {
|
.value-strip {
|
||||||
margin-top: 26px;
|
margin-top: 50px;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius-xl);
|
||||||
background: rgba(255, 255, 255, 0.80);
|
background: rgba(255, 255, 255, 0.80);
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,15 @@ export class LoginComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private saveToken(token: string) {
|
||||||
|
// ✅ SSR-safe
|
||||||
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
|
||||||
|
// evita token antigo conflitar
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
}
|
||||||
|
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
this.apiError = '';
|
this.apiError = '';
|
||||||
|
|
||||||
|
|
@ -77,10 +86,19 @@ export class LoginComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.authService.login(payload).subscribe({
|
this.authService.login(payload).subscribe({
|
||||||
next: (res) => {
|
next: async (res) => {
|
||||||
this.isSubmitting = false;
|
this.isSubmitting = false;
|
||||||
|
|
||||||
const nome = this.getNameFromToken(res.token);
|
const token = res?.token;
|
||||||
|
if (!token) {
|
||||||
|
this.apiError = 'Login retornou sem token. Verifique a resposta da API.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ salva token para o Interceptor anexar nas próximas requisições
|
||||||
|
this.saveToken(token);
|
||||||
|
|
||||||
|
const nome = this.getNameFromToken(token);
|
||||||
|
|
||||||
// ✅ Vai para /geral já levando a mensagem do toast
|
// ✅ Vai para /geral já levando a mensagem do toast
|
||||||
this.router.navigate(['/geral'], {
|
this.router.navigate(['/geral'], {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
export interface PagedResult<T> {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
items: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MobileLineList {
|
||||||
|
id: string;
|
||||||
|
item: number;
|
||||||
|
conta: string | null;
|
||||||
|
linha: string | null;
|
||||||
|
chip: string | null;
|
||||||
|
cliente: string | null;
|
||||||
|
usuario: string | null;
|
||||||
|
planoContrato: string | null;
|
||||||
|
status: string | null;
|
||||||
|
skil: string | null;
|
||||||
|
modalidade: string | null;
|
||||||
|
vencConta: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MobileLineDetail extends MobileLineList {
|
||||||
|
franquiaVivo?: number | null;
|
||||||
|
valorPlanoVivo?: number | null;
|
||||||
|
gestaoVozDados?: number | null;
|
||||||
|
skeelo?: number | null;
|
||||||
|
vivoNewsPlus?: number | null;
|
||||||
|
vivoTravelMundo?: number | null;
|
||||||
|
vivoGestaoDispositivo?: number | null;
|
||||||
|
valorContratoVivo?: number | null;
|
||||||
|
|
||||||
|
franquiaLine?: number | null;
|
||||||
|
franquiaGestao?: number | null;
|
||||||
|
locacaoAp?: number | null;
|
||||||
|
valorContratoLine?: number | null;
|
||||||
|
|
||||||
|
desconto?: number | null;
|
||||||
|
lucro?: number | null;
|
||||||
|
|
||||||
|
dataBloqueio?: string | null;
|
||||||
|
cedente?: string | null;
|
||||||
|
solicitante?: string | null;
|
||||||
|
dataEntregaOpera?: string | null;
|
||||||
|
dataEntregaCliente?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class LinesService {
|
||||||
|
// ajuste aqui conforme sua API (mesmo host do auth)
|
||||||
|
private baseUrl = 'http://localhost:5000/api/lines';
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
getLines(page: number, pageSize: number, search: string): Observable<PagedResult<MobileLineList>> {
|
||||||
|
let params = new HttpParams()
|
||||||
|
.set('page', page)
|
||||||
|
.set('pageSize', pageSize);
|
||||||
|
|
||||||
|
if (search?.trim()) params = params.set('search', search.trim());
|
||||||
|
|
||||||
|
return this.http.get<PagedResult<MobileLineList>>(this.baseUrl, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
getById(id: string): Observable<MobileLineDetail> {
|
||||||
|
return this.http.get<MobileLineDetail>(`${this.baseUrl}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: string, payload: any): Observable<void> {
|
||||||
|
return this.http.put<void>(`${this.baseUrl}/${id}`, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(id: string): Observable<void> {
|
||||||
|
return this.http.delete<void>(`${this.baseUrl}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
importExcel(file: File): Observable<{ imported: number }> {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('file', file);
|
||||||
|
return this.http.post<{ imported: number }>(`${this.baseUrl}/import-excel`, form);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue