Ajustes front: geral consumindo API e import excel

This commit is contained in:
Eduardo 2025-12-17 18:05:35 -03:00
parent a4fb34146d
commit 2c05ac8311
10 changed files with 1098 additions and 466 deletions

View File

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

View File

@ -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;
z-index: 1100;
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.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;
}
.side-menu {
position: fixed;
top: 0;
left: 0;
.blob-1 {
top: -180px;
left: -180px;
}
height: 100vh;
width: min(340px, 88vw);
.blob-2 {
bottom: -220px;
right: -220px;
opacity: 0.12;
animation-duration: 11s;
}
}
z-index: 1150;
transform: translateX(-102%);
transition: transform 220ms ease;
/* =============================== */
/* HERO */
/* =============================== */
.hero {
position: relative;
overflow: hidden;
padding: clamp(48px, 5vw, 70px) 0 20px;
}
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
.hero-content {
text-align: center;
max-width: 980px;
margin: 0 auto;
}
/* =============================== */
/* KICKER */
/* =============================== */
.kicker {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.65);
border: 1px solid var(--border);
box-shadow: var(--shadow-soft);
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;
}
.side-menu.open { transform: translateX(0); }
.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); }
}
@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;
}
transition: background 180ms ease, transform 180ms ease;
}
@media (min-width: 820px) {
grid-template-columns: repeat(3, 1fr);
}
.side-item i { color: var(--brand); }
.side-item:hover {
background: rgba(227, 61, 207, 0.10);
transform: translateY(-1px);
}
/* =============================== */
/* FEATURES */
/* =============================== */
.section-head {
text-align: center;
margin: 0 auto 24px;
max-width: 820px;
.side-item:active {
transform: translateY(0) scale(0.99);
}
.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;
/* ========================================= */
/* ✅ OVERRIDE BOOTSTRAP: SEM TRANSPARÊNCIA */
/* ========================================= */
.btn.btn-cadastrar,
.btn.btn-login {
-webkit-tap-highlight-color: transparent;
}
.section-subtitle {
font-family: 'Poppins', sans-serif;
color: rgba(0, 0, 0, 0.64);
font-size: 15px;
margin: 0 auto;
.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-cards-row { margin-top: 18px; }
/* 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; }
.feature-item {
border-radius: 18px;
padding: 10px;
background: transparent;
transition: transform 0.25s ease, filter 0.25s ease;
}
.feature-item:hover {
transform: translateY(-6px);
filter: drop-shadow(0 14px 22px rgba(0, 0, 0, 0.10));
}
.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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'], {

View File

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