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>
|
||||
</button>
|
||||
|
||||
<!-- ✅ Logo some apenas quando menu estiver aberto (no /geral) -->
|
||||
<a class="logo-area" routerLink="/" *ngIf="!menuOpen">
|
||||
<!-- ✅ 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>
|
||||
|
|
@ -44,20 +44,30 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAIXA AZUL – SÓ NA HOME -->
|
||||
<div class="header-bar" *ngIf="isHome">
|
||||
<!-- ✅ FAIXA (SÓ NA HOME) com degradê igual footer -->
|
||||
<div class="header-bar footer-gradient" *ngIf="isHome">
|
||||
<span class="header-bar-text">
|
||||
Somos a escolha certa para estar sempre conectado!
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- OVERLAY -->
|
||||
<div class="menu-overlay" *ngIf="menuOpen" (click)="closeMenu()"></div>
|
||||
<!-- ✅ OVERLAY (só no /geral) -->
|
||||
<div
|
||||
class="menu-overlay"
|
||||
*ngIf="isLoggedHeader && menuOpen"
|
||||
(click)="closeMenu()"
|
||||
></div>
|
||||
|
||||
<!-- MENU LATERAL -->
|
||||
<aside class="side-menu" [class.open]="menuOpen" (click)="$event.stopPropagation()">
|
||||
<!-- ✅ MENU LATERAL (só no /geral) -->
|
||||
<aside
|
||||
*ngIf="isLoggedHeader"
|
||||
class="side-menu"
|
||||
[class.open]="menuOpen"
|
||||
(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">
|
||||
|
|
@ -76,7 +86,6 @@
|
|||
</div>
|
||||
|
||||
<div class="side-menu-body">
|
||||
<!-- ✅ Opções do Geral (mantém) -->
|
||||
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
|
||||
<i class="bi bi-clipboard-data me-2"></i> Controle de Contratos
|
||||
</a>
|
||||
|
|
@ -95,7 +104,6 @@
|
|||
|
||||
<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()">
|
||||
<i class="bi bi-info-circle me-2"></i> O que é a Line Móvel?
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,420 +1,484 @@
|
|||
:host {
|
||||
--brand: #E33DCF;
|
||||
--brand-2: rgba(227, 61, 207, 0.18);
|
||||
--brand-3: rgba(227, 61, 207, 0.10);
|
||||
--blue: #030FAA;
|
||||
|
||||
--text: #0b0b0f;
|
||||
--muted: rgba(0, 0, 0, 0.66);
|
||||
--border: rgba(0, 0, 0, 0.10);
|
||||
|
||||
--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.08);
|
||||
--shadow-soft: 0 10px 26px rgba(0, 0, 0, 0.10);
|
||||
}
|
||||
|
||||
/* =============================== */
|
||||
/* PÁGINA / FUNDO GLOBAL */
|
||||
/* =============================== */
|
||||
.home-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background: #efefef;
|
||||
/* ===================== */
|
||||
/* HEADER PRINCIPAL */
|
||||
/* ===================== */
|
||||
.header-container {
|
||||
width: 100%;
|
||||
font-family: 'Inter', sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
/* 🔑 evita o background “atrapalhar” o conteúdo */
|
||||
isolation: isolate;
|
||||
position: sticky;
|
||||
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) */
|
||||
.page-bg {
|
||||
.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 {
|
||||
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;
|
||||
inset: 0;
|
||||
z-index: -1; /* 🔑 sempre atrás */
|
||||
pointer-events: none;
|
||||
|
||||
.grid {
|
||||
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;
|
||||
}
|
||||
z-index: 1100;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* =============================== */
|
||||
/* HERO */
|
||||
/* =============================== */
|
||||
.hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: clamp(48px, 5vw, 70px) 0 20px;
|
||||
}
|
||||
.side-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
.hero-content {
|
||||
text-align: center;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
height: 100vh;
|
||||
width: min(340px, 88vw);
|
||||
|
||||
/* =============================== */
|
||||
/* KICKER */
|
||||
/* =============================== */
|
||||
.kicker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
z-index: 1150;
|
||||
transform: translateX(-102%);
|
||||
transition: transform 220ms ease;
|
||||
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow-soft);
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
|
||||
color: var(--muted);
|
||||
font-family: 'Poppins', sans-serif;
|
||||
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;
|
||||
border-right: 1px solid rgba(227, 61, 207, 0.18);
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.14);
|
||||
|
||||
display: flex;
|
||||
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 */
|
||||
@media (min-width: 1200px) {
|
||||
.main-title .first-line { transform: translateX(-70px); }
|
||||
.main-title .second-line { transform: translateX(100px) translateY(-6px); }
|
||||
}
|
||||
.side-menu.open { transform: translateX(0); }
|
||||
|
||||
@media (max-width: 1199.98px) {
|
||||
.main-title .first-line,
|
||||
.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;
|
||||
.side-menu-header {
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.secondary-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
color: rgba(0, 0, 0, 0.72);
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
|
||||
padding: 12px 14px;
|
||||
.close-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
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;
|
||||
}
|
||||
|
||||
.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%);
|
||||
border: 1px solid rgba(0, 0, 0, 0.10);
|
||||
background: rgba(255, 255, 255, 0.60);
|
||||
|
||||
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;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
padding: 12px 12px;
|
||||
border-radius: 14px;
|
||||
|
||||
background: rgba(255, 255, 255, 0.58);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow-soft);
|
||||
|
||||
i {
|
||||
color: var(--brand);
|
||||
font-size: 18px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
span {
|
||||
text-decoration: none;
|
||||
color: rgba(0, 0, 0, 0.80);
|
||||
font-weight: 800;
|
||||
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) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
transition: background 180ms ease, transform 180ms ease;
|
||||
}
|
||||
|
||||
/* =============================== */
|
||||
/* FEATURES */
|
||||
/* =============================== */
|
||||
.section-head {
|
||||
text-align: center;
|
||||
margin: 0 auto 24px;
|
||||
max-width: 820px;
|
||||
.side-item i { color: var(--brand); }
|
||||
|
||||
.side-item:hover {
|
||||
background: rgba(227, 61, 207, 0.10);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: clamp(20px, 2.4vw, 30px);
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.02em;
|
||||
.side-item:active {
|
||||
transform: translateY(0) scale(0.99);
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
color: rgba(0, 0, 0, 0.64);
|
||||
font-size: 15px;
|
||||
margin: 0 auto;
|
||||
/* ========================================= */
|
||||
/* ✅ OVERRIDE BOOTSTRAP: SEM TRANSPARÊNCIA */
|
||||
/* ========================================= */
|
||||
.btn.btn-cadastrar,
|
||||
.btn.btn-login {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.feature-cards-row { margin-top: 18px; }
|
||||
|
||||
.feature-item {
|
||||
border-radius: 18px;
|
||||
padding: 10px;
|
||||
background: transparent;
|
||||
transition: transform 0.25s ease, filter 0.25s ease;
|
||||
.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;
|
||||
}
|
||||
|
||||
.feature-item:hover {
|
||||
transform: translateY(-6px);
|
||||
filter: drop-shadow(0 14px 22px rgba(0, 0, 0, 0.10));
|
||||
}
|
||||
/* mantém as cores originais até quando clica */
|
||||
.btn.btn-cadastrar:active { background: #E1E1E1 !important; }
|
||||
.btn.btn-login:active { background: var(--brand) !important; border-color: var(--brand) !important; color: #fff !important; }
|
||||
|
||||
.button-section {
|
||||
margin-top: 18px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
/* =============================== */
|
||||
/* 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;
|
||||
}
|
||||
/* remove “inset” do bootstrap */
|
||||
.btn.btn-cadastrar:active,
|
||||
.btn.btn-login:active {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.10) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
|
||||
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);
|
||||
|
||||
const authReq = req.clone({
|
||||
return next(
|
||||
req.clone({
|
||||
setHeaders: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
return next(authReq);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -49,16 +49,27 @@
|
|||
class="btn btn-outline-primary btn-sm btn-glass"
|
||||
(click)="onImportExcel()"
|
||||
title="Importar dados de planilha"
|
||||
[disabled]="loading"
|
||||
>
|
||||
<i class="bi bi-file-earmark-excel me-1"></i>
|
||||
Importar Excel
|
||||
</button>
|
||||
|
||||
<!-- input escondido para upload -->
|
||||
<input
|
||||
#excelInput
|
||||
type="file"
|
||||
class="d-none"
|
||||
accept=".xlsx"
|
||||
(change)="onExcelSelected($event)"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm btn-brand"
|
||||
(click)="onCadastrarLinha()"
|
||||
title="Cadastrar uma nova linha"
|
||||
[disabled]="loading"
|
||||
>
|
||||
<i class="bi bi-plus-circle me-1"></i>
|
||||
Cadastrar Linha
|
||||
|
|
@ -79,6 +90,7 @@
|
|||
placeholder="Pesquisar..."
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="onSearch()"
|
||||
[disabled]="loading"
|
||||
/>
|
||||
|
||||
<button
|
||||
|
|
@ -87,6 +99,7 @@
|
|||
(click)="clearSearch()"
|
||||
*ngIf="searchTerm"
|
||||
title="Limpar busca"
|
||||
[disabled]="loading"
|
||||
>
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
|
|
@ -98,6 +111,7 @@
|
|||
class="form-select form-select-sm w-auto select-glass"
|
||||
[(ngModel)]="pageSize"
|
||||
(change)="onPageSizeChange()"
|
||||
[disabled]="loading"
|
||||
>
|
||||
<option [ngValue]="10">10</option>
|
||||
<option [ngValue]="20">20</option>
|
||||
|
|
@ -160,14 +174,14 @@
|
|||
</th>
|
||||
|
||||
<th class="sortable" (click)="setSort('plano')">
|
||||
PLANO
|
||||
PLANO CONTRATO
|
||||
<span class="sort-caret">
|
||||
{{ sortKey==='plano' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
|
||||
</span>
|
||||
</th>
|
||||
|
||||
<th class="sortable" (click)="setSort('contrato')">
|
||||
CONTRATO
|
||||
VENC. DA CONTA
|
||||
<span class="sort-caret">
|
||||
{{ sortKey==='contrato' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
|
||||
</span>
|
||||
|
|
@ -178,6 +192,13 @@
|
|||
</thead>
|
||||
|
||||
<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>
|
||||
<td class="text-muted">{{ r.item }}</td>
|
||||
<td>{{ r.conta }}</td>
|
||||
|
|
@ -194,6 +215,7 @@
|
|||
class="btn btn-sm btn-outline-secondary btn-icon"
|
||||
(click)="onDetalhes(r)"
|
||||
title="Detalhes"
|
||||
[disabled]="loading"
|
||||
>
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
|
|
@ -203,6 +225,7 @@
|
|||
class="btn btn-sm btn-outline-success btn-icon ms-1"
|
||||
(click)="onFinanceiro(r)"
|
||||
title="Financeiro"
|
||||
[disabled]="loading"
|
||||
>
|
||||
<i class="bi bi-cash-coin"></i>
|
||||
</button>
|
||||
|
|
@ -212,13 +235,14 @@
|
|||
class="btn btn-sm btn-outline-danger btn-icon ms-1"
|
||||
(click)="onRemover(r)"
|
||||
title="Remover"
|
||||
[disabled]="loading"
|
||||
>
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr *ngIf="pagedRows.length === 0">
|
||||
<tr *ngIf="!loading && pagedRows.length === 0">
|
||||
<td colspan="9" class="text-center py-5 text-muted empty-state">
|
||||
<i class="bi bi-inbox me-2"></i>
|
||||
Nenhum registro encontrado.
|
||||
|
|
@ -239,18 +263,18 @@
|
|||
|
||||
<nav aria-label="Paginação">
|
||||
<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)">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
Anterior
|
||||
</button>
|
||||
</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>
|
||||
</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)">
|
||||
Próxima
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
|
|
@ -263,3 +287,115 @@
|
|||
</div>
|
||||
</div>
|
||||
</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); }
|
||||
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 { isPlatformBrowser, CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClient, HttpClientModule, HttpParams } from '@angular/common/http';
|
||||
|
||||
type SortDir = 'asc' | 'desc';
|
||||
|
||||
type LineRow = {
|
||||
id: number;
|
||||
id: string; // GUID
|
||||
item: string;
|
||||
conta: string;
|
||||
linha: string;
|
||||
chip: string;
|
||||
cliente: string;
|
||||
usuario: string;
|
||||
plano: string;
|
||||
contrato: string;
|
||||
plano: string; // PLANO CONTRATO
|
||||
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({
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
imports: [CommonModule, FormsModule, HttpClientModule],
|
||||
templateUrl: './geral.html',
|
||||
styleUrls: ['./geral.scss']
|
||||
})
|
||||
export class Geral implements AfterViewInit {
|
||||
toastMessage = '';
|
||||
@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() {
|
||||
if (!isPlatformBrowser(this.platformId)) return;
|
||||
|
||||
// ✅ animações (igual Home)
|
||||
this.initAnimations();
|
||||
this.loadFromApi();
|
||||
|
||||
const msg = (history.state && history.state.toastMessage) ? String(history.state.toastMessage) : '';
|
||||
if (!msg) return;
|
||||
|
|
@ -76,85 +158,120 @@ export class Geral implements AfterViewInit {
|
|||
|
||||
private async showToast(message: string) {
|
||||
this.toastMessage = message;
|
||||
|
||||
// ✅ segurança (caso algum dia seja chamado fora do browser)
|
||||
if (!isPlatformBrowser(this.platformId)) return;
|
||||
|
||||
const bs = await import('bootstrap');
|
||||
const toast = new bs.Toast(this.successToast.nativeElement, { autohide: true, delay: 1500 });
|
||||
toast.show();
|
||||
}
|
||||
|
||||
// ========= TABELA (mock por enquanto) =========
|
||||
rows: LineRow[] = [
|
||||
{ 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' },
|
||||
{ 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' },
|
||||
{ id: 3, item: '003', conta: 'Empresa A', linha: '71999990003', chip: 'ICCID-0003', cliente: 'Cliente A', usuario: 'Carlos', plano: 'Controle 20GB', contrato: 'CT-2025-003' },
|
||||
];
|
||||
// ========= API LOAD =========
|
||||
private mapSortKeyToApi(sortKey: keyof LineRow): string {
|
||||
const map: Record<string, string> = {
|
||||
item: 'item',
|
||||
conta: 'conta',
|
||||
linha: 'linha',
|
||||
chip: 'chip',
|
||||
cliente: 'cliente',
|
||||
usuario: 'usuario',
|
||||
plano: 'planoContrato',
|
||||
contrato: 'vencConta',
|
||||
};
|
||||
return map[String(sortKey)] ?? 'item';
|
||||
}
|
||||
|
||||
searchTerm = '';
|
||||
sortKey: keyof LineRow = 'item';
|
||||
sortDir: SortDir = 'asc';
|
||||
private loadFromApi() {
|
||||
this.loading = true;
|
||||
|
||||
page = 1;
|
||||
pageSize = 10;
|
||||
const params = new HttpParams()
|
||||
.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; }
|
||||
clearSearch() { this.searchTerm = ''; this.page = 1; }
|
||||
this.http.get<ApiPagedResult<ApiLineList>>(this.apiBase, { params }).subscribe({
|
||||
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) {
|
||||
if (this.sortKey === key) {
|
||||
this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.sortKey = key;
|
||||
this.sortDir = 'asc';
|
||||
}
|
||||
|
||||
onPageSizeChange() { this.page = 1; }
|
||||
this.page = 1;
|
||||
this.loadFromApi();
|
||||
}
|
||||
|
||||
onPageSizeChange() {
|
||||
this.page = 1;
|
||||
this.loadFromApi();
|
||||
}
|
||||
|
||||
goToPage(p: number) {
|
||||
const target = Math.max(1, Math.min(this.totalPages, p));
|
||||
this.page = target;
|
||||
this.loadFromApi();
|
||||
}
|
||||
|
||||
trackById(_: number, row: LineRow) { return row.id; }
|
||||
|
||||
get filteredRows(): LineRow[] {
|
||||
const term = this.searchTerm.trim().toLowerCase();
|
||||
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 pagedRows(): LineRow[] {
|
||||
return this.rows;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
get pagedRows(): LineRow[] {
|
||||
const start = (this.page - 1) * this.pageSize;
|
||||
const end = start + this.pageSize;
|
||||
return this.sortedRows.slice(start, end);
|
||||
get filteredCount(): number {
|
||||
return this.total || 0;
|
||||
}
|
||||
|
||||
get filteredCount(): number { return this.sortedRows.length; }
|
||||
|
||||
get pageStart(): number {
|
||||
if (this.filteredCount === 0) return 0;
|
||||
return (this.page - 1) * this.pageSize + 1;
|
||||
|
|
@ -162,7 +279,7 @@ export class Geral implements AfterViewInit {
|
|||
|
||||
get pageEnd(): number {
|
||||
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[] {
|
||||
|
|
@ -179,10 +296,97 @@ export class Geral implements AfterViewInit {
|
|||
return pages;
|
||||
}
|
||||
|
||||
// ========= BOTÕES (placeholders) =========
|
||||
async onImportExcel() { await this.showToast('Importação via Excel será implementada.'); }
|
||||
async onCadastrarLinha() { await this.showToast('Cadastro de linha será implementado.'); }
|
||||
async onDetalhes(row: LineRow) { await this.showToast(`Detalhes: linha ${row.linha}`); }
|
||||
async onFinanceiro(row: LineRow) { await this.showToast(`Financeiro: linha ${row.linha} (em implementação)`); }
|
||||
async onRemover(row: LineRow) { await this.showToast(`Remover: linha ${row.linha} (em implementação)`); }
|
||||
// ========= IMPORTAR EXCEL =========
|
||||
async onImportExcel() {
|
||||
if (!this.excelInput?.nativeElement) return;
|
||||
this.excelInput.nativeElement.value = '';
|
||||
this.excelInput.nativeElement.click();
|
||||
}
|
||||
|
||||
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 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">
|
||||
<i class="bi bi-check2-circle"></i>
|
||||
|
|
@ -195,6 +186,17 @@
|
|||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@
|
|||
|
||||
/* faixa de valores */
|
||||
.value-strip {
|
||||
margin-top: 26px;
|
||||
margin-top: 50px;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius-xl);
|
||||
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 {
|
||||
this.apiError = '';
|
||||
|
||||
|
|
@ -77,10 +86,19 @@ export class LoginComponent {
|
|||
};
|
||||
|
||||
this.authService.login(payload).subscribe({
|
||||
next: (res) => {
|
||||
next: async (res) => {
|
||||
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
|
||||
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