From d9af2da0eb9a78e2f56ff2453ad15b677457742b Mon Sep 17 00:00:00 2001 From: Eduardo Date: Mon, 22 Dec 2025 17:43:36 -0300 Subject: [PATCH] Refactor: Ajuste na logica de busca por cliente, paginacao e layout do header --- src/app/pages/geral/geral.html | 148 ++++----- src/app/pages/geral/geral.scss | 443 +++++++++++++-------------- src/app/pages/geral/geral.ts | 530 ++++++++++++++++++++++++++------- 3 files changed, 691 insertions(+), 430 deletions(-) diff --git a/src/app/pages/geral/geral.html b/src/app/pages/geral/geral.html index 899d965..1a4168b 100644 --- a/src/app/pages/geral/geral.html +++ b/src/app/pages/geral/geral.html @@ -11,6 +11,7 @@
+ @@ -20,14 +21,19 @@
+
-
+ +
+ Gestão centralizada +
+
-
Gestão centralizada
Geral
Tabela de linhas e dados de telefonia
-
+ +
@@ -39,7 +45,6 @@
-
+
- -
-
-
+
- - + + + + +
+
- Itens por página + Itens:
@@ -433,7 +409,33 @@
-
+
Vivo +
+
+
+
+
+
+
+
+
+
+
+
Line Móvel +
+
+
+
+
+
+
+
Resultado +
+
+
+
+
+
Carregando...
diff --git a/src/app/pages/geral/geral.scss b/src/app/pages/geral/geral.scss index 31cbe3c..f336eda 100644 --- a/src/app/pages/geral/geral.scss +++ b/src/app/pages/geral/geral.scss @@ -1,29 +1,39 @@ +/* ========================================================== */ +/* 1. VARIÁVEIS E CONFIGURAÇÕES GERAIS */ +/* ========================================================== */ :host { - /* ================= PALETA DE CORES ================= */ - --brand: #E33DCF; - --blue: #030FAA; - --text: #111214; - --muted: rgba(17, 18, 20, 0.65); + /* Paleta de Cores */ + --brand: #E33DCF; /* Rosa da marca */ + --blue: #030FAA; /* Azul da marca */ + --text: #111214; /* Texto principal */ + --muted: rgba(17, 18, 20, 0.65); /* Texto secundário */ - /* Cores Específicas (Financeiro) */ + /* Cores Específicas (Financeiro/Status) */ --bg-vivo: #fbf5fc; --text-vivo: #8a2be2; --bg-line: #f5f6ff; --text-line: #030FAA; + --success-bg: rgba(25, 135, 84, 0.1); + --success-text: #198754; + --danger-bg: rgba(220, 53, 69, 0.1); + --danger-text: #dc3545; /* Dimensões & Bordas */ --radius-xl: 22px; --radius-lg: 16px; --radius-md: 12px; + --shadow-card: 0 22px 46px rgba(17, 18, 20, 0.10); + --glass-bg: rgba(255, 255, 255, 0.82); + --glass-border: 1px solid rgba(227, 61, 207, 0.16); display: block; font-family: 'Inter', sans-serif; color: var(--text); } -/* ========================================= */ -/* 1. LAYOUT DA PÁGINA & BACKGROUND */ -/* ========================================= */ +/* ========================================================== */ +/* 2. LAYOUT DA PÁGINA & BACKGROUND */ +/* ========================================================== */ .geral-page { min-height: 100vh; padding: 24px 12px 90px; @@ -32,25 +42,23 @@ position: relative; overflow: hidden; - /* Fundo com Gradientes Suaves */ + /* Fundo com Gradientes Suaves e modernos */ background: radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.14), transparent 60%), radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.08), transparent 60%), linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%); - /* Overlay sutil de ruído/textura (opcional) */ + /* Overlay sutil de ruído/textura (opcional, aqui usado como clareador) */ &::after { - content: ''; - position: absolute; - inset: 0; - pointer-events: none; + content: ''; position: absolute; inset: 0; pointer-events: none; background: rgba(255, 255, 255, 0.25); } } -/* Blobs Flutuantes (Animação de Fundo) */ +/* Blobs Flutuantes (Animação de fundo) */ .page-blob { - position: fixed; pointer-events: none; border-radius: 999px; filter: blur(34px); opacity: 0.55; z-index: 0; + position: fixed; pointer-events: none; border-radius: 999px; + filter: blur(34px); opacity: 0.55; z-index: 0; background: radial-gradient(circle at 30% 30%, rgba(227,61,207,0.55), rgba(227,61,207,0.06)); animation: floaty 10s ease-in-out infinite; @@ -67,44 +75,42 @@ } .container-geral { - width: 100%; - max-width: 1180px; - position: relative; - z-index: 1; + width: 100%; max-width: 1180px; position: relative; z-index: 1; } -/* Card Principal (Efeito de Vidro) */ +/* Card Principal (Container da Aplicação) */ .geral-card { border-radius: var(--radius-xl); - overflow: hidden; /* Importante para conter tabelas */ - background: rgba(255, 255, 255, 0.82); - border: 1px solid rgba(227, 61, 207, 0.16); + overflow: hidden; + background: var(--glass-bg); + border: var(--glass-border); backdrop-filter: blur(12px); - box-shadow: 0 22px 46px rgba(17, 18, 20, 0.10); + box-shadow: var(--shadow-card); position: relative; - display: flex; - flex-direction: column; - max-height: 85vh; /* Limita altura total do card na tela */ + display: flex; flex-direction: column; + max-height: 85vh; /* Limita altura para scroll interno */ + /* Borda interna brilhante */ &::before { content: ''; position: absolute; inset: 1px; border-radius: calc(var(--radius-xl) - 1px); pointer-events: none; border: 1px solid rgba(255, 255, 255, 0.65); opacity: 0.75; } } -/* ========================================= */ -/* 2. HEADER & AÇÕES */ -/* ========================================= */ +/* ========================================================== */ +/* 3. HEADER (GRID LAYOUT) */ +/* ========================================================== */ .geral-header { padding: 16px 24px; border-bottom: 1px solid rgba(17, 18, 20, 0.06); background: linear-gradient(180deg, rgba(227,61,207,0.06), rgba(255,255,255,0.2)); - flex-shrink: 0; /* Header não encolhe */ + flex-shrink: 0; } +/* GRID: Esquerda (1fr) | Centro (auto) | Direita (1fr) */ .header-row-top { display: grid; - grid-template-columns: 1fr auto 1fr; + grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px; @@ -112,14 +118,15 @@ grid-template-columns: 1fr; text-align: center; gap: 16px; + + .title-badge { justify-self: center; margin-bottom: 8px; } + .header-actions { justify-self: center; } } } -.header-title { - display: flex; flex-direction: column; align-items: center; text-align: center; -} - +/* 1. Badge à esquerda */ .title-badge { + justify-self: start; display: inline-flex; align-items: center; gap: 10px; padding: 6px 12px; border-radius: 999px; background: rgba(255, 255, 255, 0.78); border: 1px solid rgba(227, 61, 207, 0.22); backdrop-filter: blur(10px); @@ -127,10 +134,20 @@ i { color: var(--brand); } } +/* 2. Título Centralizado */ +.header-title { + justify-self: center; + display: flex; flex-direction: column; align-items: center; text-align: center; +} .title { font-size: 26px; font-weight: 950; letter-spacing: -0.3px; color: var(--text); margin-top: 10px; margin-bottom: 0; } .subtitle { color: rgba(17, 18, 20, 0.65); font-weight: 700; } -/* Botões de Ação Principais */ +/* 3. Ações à Direita */ +.header-actions { + justify-self: end; +} + +/* Botões do Header */ .btn-brand { background-color: var(--brand); border-color: var(--brand); color: #fff; font-weight: 900; border-radius: 12px; transition: transform 0.2s, box-shadow 0.2s; @@ -144,46 +161,31 @@ &:hover { transform: translateY(-2px); border-color: var(--brand); background: #fff; } } -/* ========================================= */ -/* 3. FILTROS (TABS E DROPDOWN) */ -/* ========================================= */ +/* ========================================================== */ +/* 4. FILTROS E DROPDOWN */ +/* ========================================================== */ .filters-row { - display: flex; - justify-content: center; - align-items: center; - gap: 12px; - flex-wrap: wrap; + display: flex; justify-content: center; align-items: center; gap: 12px; flex-wrap: wrap; } -/* Tabs (Todos / PF / PJ) */ .filter-tabs { display: flex; gap: 4px; padding: 4px; background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(17, 18, 20, 0.08); - border-radius: 12px; - backdrop-filter: blur(8px); + border-radius: 12px; backdrop-filter: blur(8px); } .filter-tab { border: none; background: transparent; padding: 8px 16px; border-radius: 8px; font-size: 0.85rem; font-weight: 700; color: var(--muted); - transition: all 0.2s ease; - display: flex; align-items: center; gap: 6px; - + transition: all 0.2s ease; display: flex; align-items: center; gap: 6px; &:hover { color: var(--text); background: rgba(255, 255, 255, 0.5); } - - &.active { - background: #fff; color: var(--brand); - box-shadow: 0 2px 8px rgba(227, 61, 207, 0.15); - } - + &.active { background: #fff; color: var(--brand); box-shadow: 0 2px 8px rgba(227, 61, 207, 0.15); } &:disabled { opacity: 0.5; cursor: not-allowed; } } /* Dropdown de Clientes */ -.client-filter-wrap { - position: relative; -} +.client-filter-wrap { position: relative; } .btn-client-filter { display: flex; align-items: center; gap: 8px; @@ -191,14 +193,9 @@ border: 1px solid rgba(17, 18, 20, 0.08); background: rgba(255, 255, 255, 0.6); color: var(--muted); font-weight: 700; font-size: 0.85rem; - backdrop-filter: blur(8px); transition: all 0.2s; - + backdrop-filter: blur(8px); transition: all 0.2s; white-space: nowrap; &:hover { background: #fff; border-color: var(--blue); color: var(--blue); } - - &.active { - background: var(--blue); color: #fff; border-color: var(--blue); - i { color: #fff; } - } + &.active { background: var(--blue); color: #fff; border-color: var(--blue); i { color: #fff; } } } .client-dropdown { @@ -207,184 +204,163 @@ background: #fff; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); border: 1px solid rgba(17,18,20,0.08); - z-index: 50; - display: flex; flex-direction: column; - overflow: hidden; + z-index: 50; display: flex; flex-direction: column; overflow: hidden; animation: fadeIn 0.2s ease; } +@keyframes fadeIn { from { opacity:0; transform:translateY(-10px); } to { opacity:1; transform:translateY(0); } } -@keyframes fadeIn { - from { opacity:0; transform:translateY(-10px); } - to { opacity:1; transform:translateY(0); } -} - -.dropdown-header-search { - padding: 8px; border-bottom: 1px solid rgba(0,0,0,0.05); background: #f9fafb; -} - -.dropdown-list { - overflow-y: auto; max-height: 300px; -} - +.dropdown-header-search { padding: 8px; border-bottom: 1px solid rgba(0,0,0,0.05); background: #f9fafb; } +.dropdown-list { overflow-y: auto; max-height: 300px; } .dropdown-item-custom { - padding: 10px 16px; font-size: 0.85rem; color: var(--text); - cursor: pointer; border-bottom: 1px solid rgba(0,0,0,0.03); - transition: background 0.1s; - + padding: 10px 16px; font-size: 0.85rem; color: var(--text); cursor: pointer; border-bottom: 1px solid rgba(0,0,0,0.03); transition: background 0.1s; &:hover { background: rgba(227,61,207,0.05); color: var(--brand); font-weight: 600; } &.selected { background: rgba(3,15,170,0.08); color: var(--blue); font-weight: 700; } } -/* Inputs de Busca e Paginação */ +/* ========================================================== */ +/* 5. CONTROLES (SEARCH BAR) */ +/* ========================================================== */ .controls { display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap; } +/* Estilo Moderno da Busca */ .search-group { - max-width: 340px; border-radius: 14px; overflow: hidden; - .input-group-text { background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(17, 18, 20, 0.1); color: var(--brand); } - .form-control { border: 1px solid rgba(17, 18, 20, 0.1); background: rgba(255, 255, 255, 0.6); &:focus { border-color: var(--brand); box-shadow: 0 0 0 2px rgba(227,61,207,0.1); } } - .btn-clear { background: rgba(255,255,255,0.6); border: 1px solid rgba(17,18,20,0.1); } -} - -.select-glass { - border-radius: 12px; background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(17, 18, 20, 0.1); width: auto; cursor: pointer; -} - -/* ========================================= */ -/* 4. ÁREA DE CONTEÚDO (BODY) */ -/* ========================================= */ -.geral-body { - padding: 0; - background: transparent; - flex: 1; /* Ocupa espaço restante do card */ - overflow: hidden; /* Scroll interno na tabela/grupos */ - display: flex; flex-direction: column; -} - -/* ========================================= */ -/* 5. MODO GRUPO (ACCORDION) */ -/* ========================================= */ -.groups-container { - padding: 16px; - overflow-y: auto; - height: 100%; -} - -.group-list { - display: flex; flex-direction: column; gap: 12px; -} - -.client-group-card { - background: #fff; - border-radius: 16px; - border: 1px solid rgba(17,18,20,0.08); + max-width: 225px; + border-radius: 12px; overflow: hidden; + display: flex; + align-items: stretch; + background: #fff; /* Fundo Branco Sólido */ + border: 1px solid rgba(17, 18, 20, 0.15); /* Borda Visível */ + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); + transition: all 0.2s ease; + + &:focus-within { + border-color: var(--brand); + box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15); + transform: translateY(-1px); + } + + .input-group-text { + background: transparent; + border: none; + color: var(--muted); + padding-left: 14px; + padding-right: 8px; + display: flex; align-items: center; + i { font-size: 1rem; } + } + + .form-control { + border: none; + background: transparent; + padding: 10px 0; + font-size: 0.9rem; + color: var(--text); + box-shadow: none; + &::placeholder { color: rgba(17, 18, 20, 0.4); font-weight: 500; } + &:focus { outline: none; } + } + + .btn-clear { + background: transparent; + border: none; + color: var(--muted); + padding: 0 12px; + display: flex; align-items: center; cursor: pointer; + transition: color 0.2s; + &:hover { color: #dc3545; } + i { font-size: 1rem; } + } +} + +/* Select de Paginação */ +.select-glass { + border-radius: 12px; + background: #fff; + border: 1px solid rgba(17, 18, 20, 0.15); + color: var(--text); + font-weight: 600; + box-shadow: 0 2px 6px rgba(0,0,0,0.04); + padding: 8px 32px 8px 12px; + cursor: pointer; + transition: all 0.2s; + width: auto; + + &:focus { + border-color: var(--brand); + box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15); + outline: none; + } +} + +/* ========================================================== */ +/* 6. CORPO, GRUPOS (ACCORDION) & TABELA */ +/* ========================================================== */ +.geral-body { + padding: 0; background: transparent; flex: 1; overflow: hidden; display: flex; flex-direction: column; +} + +.groups-container { padding: 16px; overflow-y: auto; height: 100%; } +.group-list { display: flex; flex-direction: column; gap: 12px; } + +/* Cards de Grupo */ +.client-group-card { + background: #fff; border-radius: 16px; + border: 1px solid rgba(17,18,20,0.08); overflow: hidden; transition: all 0.3s ease; - - &:hover { - border-color: var(--brand); - box-shadow: 0 4px 12px rgba(227,61,207,0.1); - } - - &.expanded { - border-color: var(--brand); - box-shadow: 0 8px 24px rgba(227,61,207,0.12); - } + &:hover { border-color: var(--brand); box-shadow: 0 4px 12px rgba(227,61,207,0.1); } + &.expanded { border-color: var(--brand); box-shadow: 0 8px 24px rgba(227,61,207,0.12); } } .group-header { - padding: 16px 24px; - display: flex; justify-content: space-between; align-items: center; - cursor: pointer; - background: linear-gradient(180deg, #fff, #fdfdfd); - + padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; + cursor: pointer; background: linear-gradient(180deg, #fff, #fdfdfd); &:hover .group-toggle-icon { color: var(--brand); } } -.group-info { - display: flex; flex-direction: column; gap: 6px; -} - -.group-badges { - display: flex; gap: 8px; flex-wrap: wrap; -} +.group-info { display: flex; flex-direction: column; gap: 6px; } +.group-badges { display: flex; gap: 8px; flex-wrap: wrap; } .badge-pill { font-size: 0.7rem; padding: 4px 10px; border-radius: 99px; font-weight: 800; text-transform: uppercase; &.total { background: rgba(3,15,170,0.1); color: var(--blue); } - &.active { background: rgba(25,135,84,0.1); color: #198754; } - &.blocked { background: rgba(220,53,69,0.1); color: #dc3545; } + &.active { background: var(--success-bg); color: var(--success-text); } + &.blocked { background: var(--danger-bg); color: var(--danger-text); } } -.group-toggle-icon { - font-size: 1.2rem; color: var(--muted); transition: transform 0.3s ease; -} - -.client-group-card.expanded .group-toggle-icon { - transform: rotate(180deg); color: var(--brand); -} +.group-toggle-icon { font-size: 1.2rem; color: var(--muted); transition: transform 0.3s ease; } +.client-group-card.expanded .group-toggle-icon { transform: rotate(180deg); color: var(--brand); } .group-body { - border-top: 1px solid rgba(17,18,20,0.06); - background: #fbfbfc; + border-top: 1px solid rgba(17,18,20,0.06); background: #fbfbfc; animation: slideDown 0.3s cubic-bezier(0.16, 1, 0.3, 1); } +@keyframes slideDown { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } -@keyframes slideDown { - from { opacity: 0; transform: translateY(-10px); } - to { opacity: 1; transform: translateY(0); } -} - -.inner-table-wrap { - max-height: 450px; - overflow-y: auto; -} - -/* ========================================= */ -/* 6. MODO TABELA (CENTRALIZADA) */ -/* ========================================= */ -.table-wrap { - overflow-x: auto; - overflow-y: auto; - height: 100%; -} +.inner-table-wrap { max-height: 450px; overflow-y: auto; } +/* Tabela */ +.table-wrap { overflow-x: auto; overflow-y: auto; height: 100%; } .table-modern { width: 100%; min-width: 1000px; border-collapse: separate; border-spacing: 0; - thead th { - position: sticky; top: 0; z-index: 10; - background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(8px); - border-bottom: 2px solid rgba(227, 61, 207, 0.15); - padding: 12px; + position: sticky; top: 0; z-index: 10; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(8px); + border-bottom: 2px solid rgba(227, 61, 207, 0.15); padding: 12px; color: rgba(17, 18, 20, 0.7); font-size: 0.8rem; font-weight: 950; letter-spacing: 0.05em; text-transform: uppercase; - white-space: nowrap; cursor: pointer; transition: color 0.2s; - - /* Centralização Forçada */ - text-align: center !important; - + white-space: nowrap; cursor: pointer; transition: color 0.2s; text-align: center !important; &:hover { color: var(--brand); } } - tbody tr { - transition: background-color 0.2s; - border-bottom: 1px solid rgba(17,18,20,0.05); + transition: background-color 0.2s; border-bottom: 1px solid rgba(17,18,20,0.05); &:hover { background-color: rgba(227, 61, 207, 0.05); } td { border-bottom: 1px solid rgba(17,18,20,0.04); } } - - td { - padding: 12px; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - font-size: 0.875rem; color: var(--text); - - /* Centralização Forçada */ - text-align: center !important; - } + td { padding: 12px; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.875rem; color: var(--text); text-align: center !important; } } .th-content { display: flex; align-items: center; justify-content: center; gap: 4px; } - .sort-caret { display: inline-block; width: 14px; text-align: center; opacity: 0.3; &.active { opacity: 1; color: var(--brand); } } .text-brand { color: var(--brand) !important; } .text-blue { color: var(--blue) !important; } @@ -392,24 +368,34 @@ .td-clip { overflow: hidden; text-overflow: ellipsis; max-width: 250px; } .empty-state { background: rgba(255,255,255,0.4); } -/* Ações */ +.status-pill { + display: inline-block; vertical-align: middle; max-width: 100%; padding: 6px 14px; + border-radius: 999px; font-weight: 950; font-size: 0.75rem; letter-spacing: 0.3px; text-align: center; text-transform: uppercase; + border: 1px solid rgba(17,18,20,0.1); background: rgba(17,18,20,0.05); color: rgba(17,18,20,0.7); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + &.is-active { background: rgba(25, 135, 84, 0.15); border-color: rgba(25, 135, 84, 0.25); color: #157347; } + &.is-blocked { background: rgba(220, 53, 69, 0.15); border-color: rgba(220, 53, 69, 0.25); color: #b02a37; } +} + +/* Botões de Ação na Tabela */ .action-group { display: flex; justify-content: center; gap: 6px; } .btn-icon { width: 32px; height: 32px; border: none; background: transparent; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: rgba(17,18,20,0.5); transition: all 0.2s; &:hover { background: rgba(17,18,20,0.05); color: var(--text); transform: translateY(-1px); } - &.success:hover { color: #198754; background: rgba(25,135,84,0.1); } + &.success:hover { color: var(--success-text); background: var(--success-bg); } &.primary:hover { color: var(--blue); background: rgba(3,15,170,0.1); } - &.danger:hover { color: #dc3545; background: rgba(220,53,69,0.1); } + &.danger:hover { color: var(--danger-text); background: var(--danger-bg); } } -/* Footer */ +/* ========================================================== */ +/* 7. FOOTER (PAGINAÇÃO) */ +/* ========================================================== */ .geral-footer { padding: 14px 24px; border-top: 1px solid rgba(17, 18, 20, 0.06); display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; flex-shrink: 0; @media (max-width: 768px) { justify-content: center; text-align: center; } } - .pagination-modern .page-link { color: var(--blue); font-weight: 900; border-radius: 10px; border: 1px solid rgba(17,18,20,0.1); background: rgba(255,255,255,0.6); margin: 0 2px; @@ -417,9 +403,9 @@ } .pagination-modern .page-item.active .page-link { background-color: var(--blue); border-color: var(--blue); color: #fff; } -/* ========================================= */ -/* 7. MODAIS (Geral) */ -/* ========================================= */ +/* ========================================================== */ +/* 8. MODAIS (Z-INDEX ALTO) */ +/* ========================================================== */ .modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); } .modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; } @@ -427,41 +413,26 @@ background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); - - /* Tamanho Padrão */ - width: min(850px, 100%); - max-height: 90vh; + width: min(850px, 100%); max-height: 90vh; } - @keyframes modalPop { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } } .modal-header { padding: 16px 24px; border-bottom: 1px solid rgba(0,0,0,0.06); background: #fff; display: flex; justify-content: space-between; align-items: center; .modal-title { font-size: 1.1rem; font-weight: 800; color: var(--text); display: flex; align-items: center; gap: 12px; } - .icon-bg { width: 32px; height: 32px; border-radius: 10px; background: rgba(3, 15, 170, 0.1); color: var(--blue); display: flex; align-items: center; justify-content: center; font-size: 16px; &.success { background: rgba(25, 135, 84, 0.1); color: #198754; } &.primary-soft { background: rgba(3, 15, 170, 0.1); color: var(--blue); } } + .icon-bg { width: 32px; height: 32px; border-radius: 10px; background: rgba(3, 15, 170, 0.1); color: var(--blue); display: flex; align-items: center; justify-content: center; font-size: 16px; &.success { background: var(--success-bg); color: var(--success-text); } &.primary-soft { background: rgba(3, 15, 170, 0.1); color: var(--blue); } } .btn-icon { color: var(--muted); background: transparent; font-size: 1.2rem; &:hover { color: var(--brand); transform: rotate(90deg); } } } - .modal-body { padding: 24px; overflow-y: auto; &.bg-light-gray { background-color: #f8f9fa; } } -/* ========================================= */ -/* 8. MODAL DETALHES (Responsivo Inteligente) */ -/* ========================================= */ - -/* Regra apenas para telas GRANDES (>1440px) */ +/* Estilos de conteúdo dos modais (Detalhes, Financeiro, Edit) - Mantidos para garantir consistência */ @media (min-width: 1441px) { - .modal-card.modal-responsive { - width: min(1200px, 95%); - height: 85vh; - max-height: none; - } - + .modal-card.modal-responsive { width: min(1200px, 95%); height: 85vh; max-height: none; } .detail-box .box-header { padding: 8px 16px; } .detail-box .box-body.compact { padding: 10px 14px; } .box-body.compact .row-item { margin-bottom: 4px; font-size: 0.85rem; } .details-dashboard { gap: 16px; } } - .details-dashboard { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; @media(max-width: 700px) { grid-template-columns: 1fr; } } .detail-box { background: #fff; border-radius: 12px; border: 1px solid rgba(0,0,0,0.06); overflow: hidden; box-shadow: 0 2px 5px rgba(0,0,0,0.02); height: 100%; .box-header { padding: 10px 16px; font-weight: 800; font-size: 0.85rem; background: #fdfdfd; border-bottom: 1px solid rgba(0,0,0,0.04); color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; i { color: var(--blue); } } .box-body { padding: 16px; } .box-body.compact { padding: 12px 14px; } } .row-item { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px; font-size: 0.9rem; color: var(--muted); .lbl { font-size: 0.75rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.3px; } .val { color: var(--text); font-weight: 700; text-align: right; } } @@ -471,28 +442,16 @@ .divider { height: 1px; background: rgba(0,0,0,0.06); margin: 12px 0; } .divider.small { margin: 8px 0; } -/* ========================================= */ -/* 9. MODAL FINANCEIRO */ -/* ========================================= */ .finance-dashboard { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; @media(max-width: 700px) { grid-template-columns: 1fr; } } .finance-card { background: #fff; border-radius: 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.04); border: 1px solid rgba(0,0,0,0.04); overflow: hidden; &.vivo-card { border-top: 4px solid var(--brand); .card-header-f { color: var(--brand); background: var(--bg-vivo); } } &.line-card { border-top: 4px solid var(--blue); .card-header-f { color: var(--blue); background: var(--bg-line); } } } .card-header-f { padding: 12px 16px; font-weight: 800; font-size: 0.95rem; display: flex; align-items: center; } .card-body-f { padding: 16px; .row-item { display: flex; justify-content: space-between; margin-bottom: 10px; font-size: 0.85rem; color: var(--muted); strong { color: var(--text); font-weight: 700; } &.total { font-size: 1rem; color: var(--text); margin-top: 8px; margin-bottom: 0; strong { color: var(--text); font-weight: 900; } } } .divider { height: 1px; background: rgba(0,0,0,0.06); margin: 12px 0; } } .finance-summary { background: #fff; border-radius: 16px; padding: 16px 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.03); display: flex; align-items: center; justify-content: space-around; .summary-item { display: flex; flex-direction: column; align-items: center; .lbl { font-size: 0.75rem; text-transform: uppercase; font-weight: 800; color: var(--muted); } .val { font-size: 1.25rem; font-weight: 900; } } .vertical-line { width: 1px; height: 40px; background: rgba(0,0,0,0.08); } } -.text-success { color: #198754 !important; } +.text-success { color: var(--success-text) !important; } -/* ========================================= */ -/* 10. MODAL EDITAR */ -/* ========================================= */ .modal-card.modal-edit { width: min(1100px, 100%); } .edit-sections { display: grid; gap: 12px; } -.edit-section { - background: #fff; border: 1px solid rgba(0,0,0,0.06); border-radius: 16px; overflow: hidden; box-shadow: 0 2px 10px rgba(0,0,0,0.03); - summary { - cursor: pointer; user-select: none; padding: 12px 16px; font-weight: 950; color: rgba(17,18,20,0.75); display: flex; align-items: center; background: linear-gradient(180deg, rgba(227,61,207,0.06), rgba(255,255,255,0.6)); border-bottom: 1px solid rgba(0,0,0,0.06); - i { color: var(--blue); } - } -} +.edit-section { background: #fff; border: 1px solid rgba(0,0,0,0.06); border-radius: 16px; overflow: hidden; box-shadow: 0 2px 10px rgba(0,0,0,0.03); summary { cursor: pointer; user-select: none; padding: 12px 16px; font-weight: 950; color: rgba(17,18,20,0.75); display: flex; align-items: center; background: linear-gradient(180deg, rgba(227,61,207,0.06), rgba(255,255,255,0.6)); border-bottom: 1px solid rgba(0,0,0,0.06); i { color: var(--blue); } } } .edit-grid { padding: 14px 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; @media (max-width: 700px) { grid-template-columns: 1fr; } } .form-field { display: flex; flex-direction: column; gap: 6px; label { font-size: 0.75rem; font-weight: 900; letter-spacing: 0.04em; text-transform: uppercase; color: rgba(17,18,20,0.65); } } .form-field.span-2 { grid-column: span 2; @media (max-width: 700px) { grid-column: span 1; } } diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts index 1fe6675..525ece6 100644 --- a/src/app/pages/geral/geral.ts +++ b/src/app/pages/geral/geral.ts @@ -3,36 +3,81 @@ import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient, HttpClientModule, HttpParams, HttpErrorResponse } from '@angular/common/http'; +// Tipos e Interfaces type SortDir = 'asc' | 'desc'; -// ✅ CORREÇÃO: Adicionado 'usuario' aqui -type LineRow = { +interface LineRow { id: string; item: string; linha: string; cliente: string; - usuario: string; // <--- NOVO CAMPO + usuario: string; status: string; skil: string; contrato: string; -}; +} -type ApiPagedResult = { page: number; pageSize: number; total: number; items: T[]; }; -// ✅ CORREÇÃO: Adicionado 'usuario' no DTO de listagem também, caso não tivesse -type ApiLineList = { - id: string; - item: number; - linha: string | null; - cliente: string | null; - usuario: string | null; // <--- NOVO CAMPO - vencConta: string | null; - status?: string | null; - skil?: string | null; -}; +interface ApiPagedResult { + page: number; + pageSize: number; + total: number; + items: T[]; +} + +interface ApiLineList { + id: string; + item: number; + linha: string | null; + cliente: string | null; + usuario: string | null; + vencConta: string | null; + status?: string | null; + skil?: string | null; +} + +// Interface completa para Edição/Detalhes +interface ApiLineDetail { + 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; + dataBloqueio?: string | null; + cedente?: string | null; + solicitante?: string | null; + dataEntregaOpera?: string | null; + dataEntregaCliente?: string | null; + vencConta?: string | null; + 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; +} -type ApiLineDetail = { /* ... campos detalhe (mantenha os mesmos) ... */ 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; dataBloqueio?: string | null; cedente?: string | null; solicitante?: string | null; dataEntregaOpera?: string | null; dataEntregaCliente?: string | null; vencConta?: string | null; 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; }; type UpdateMobileLineRequest = Omit; -type ClientGroupDto = { cliente: string; totalLinhas: number; ativos: number; bloqueados: number; }; + +interface ClientGroupDto { + cliente: string; + totalLinhas: number; + ativos: number; + bloqueados: number; +} @Component({ standalone: true, @@ -41,57 +86,86 @@ type ClientGroupDto = { cliente: string; totalLinhas: number; ativos: number; bl styleUrls: ['./geral.scss'] }) export class Geral implements AfterViewInit { + // Toast & Upload toastMessage = ''; @ViewChild('successToast', { static: false }) successToast!: ElementRef; @ViewChild('excelInput') excelInput!: ElementRef; - constructor(@Inject(PLATFORM_ID) private platformId: object, private http: HttpClient, private cdr: ChangeDetectorRef) {} + constructor( + @Inject(PLATFORM_ID) private platformId: object, + private http: HttpClient, + private cdr: ChangeDetectorRef + ) {} private readonly apiBase = 'https://localhost:7205/api/lines'; loading = false; - + + // Dados rows: LineRow[] = []; - clientGroups: ClientGroupDto[] = []; - groupLines: LineRow[] = []; + clientGroups: ClientGroupDto[] = []; // Cards de Clientes + groupLines: LineRow[] = []; // Linhas internas do Card (Accordion) + // Controle de Estado expandedGroup: string | null = null; loadingLines = false; - + + // Filtros searchTerm = ''; - filterSkil: 'ALL' | 'PF' | 'PJ' = 'ALL'; + filterSkil: 'ALL' | 'PF' | 'PJ' | 'RESERVA' = 'ALL'; + + // Dropdown de Clientes clientsList: string[] = []; selectedClient: string | null = null; showClientMenu = false; clientSearchTerm = ''; + // Modo de Visualização: 'GROUPS' (Cards) ou 'TABLE' (Linhas) + viewMode: 'GROUPS' | 'TABLE' = 'GROUPS'; + + // Paginação e Ordenação sortKey: keyof LineRow = 'item'; sortDir: SortDir = 'asc'; page = 1; pageSize = 10; total = 0; - detailOpen = false; financeOpen = false; editOpen = false; editSaving = false; - detailData: any = null; financeData: any = null; editModel: any = null; + // Modais + detailOpen = false; + financeOpen = false; + editOpen = false; + editSaving = false; + + detailData: any = null; + financeData: any = null; + editModel: any = null; + private editingId: string | null = null; private searchTimer: any = null; + // Getter auxiliar para o HTML saber se exibe os grupos get isGroupMode(): boolean { - return this.filterSkil === 'ALL' && !this.selectedClient && !this.searchTerm; + return this.viewMode === 'GROUPS'; } async ngAfterViewInit() { if (!isPlatformBrowser(this.platformId)) return; - this.initAnimations(); - this.refreshData(); - this.loadClients(); - const state = history.state; - if (state && state.toastMessage) { - const msg = String(state.toastMessage); - const newState = { ...state }; delete newState.toastMessage; - history.replaceState(newState, '', location.href); - setTimeout(() => this.showToast(msg), 500); - } + this.initAnimations(); + + setTimeout(() => { + this.refreshData(); + this.loadClients(); + + // Verifica se houve redirecionamento com mensagem de toast + const state = history.state; + if (state && state.toastMessage) { + const msg = String(state.toastMessage); + const newState = { ...state }; + delete newState.toastMessage; + history.replaceState(newState, '', location.href); + this.showToast(msg); + } + }); } private initAnimations() { @@ -102,19 +176,86 @@ export class Geral implements AfterViewInit { }, 100); } + // ========================================================== + // LÓGICA PRINCIPAL DE DADOS + // ========================================================== + refreshData() { - if (this.isGroupMode) { + // Se um cliente específico foi selecionado no dropdown, força modo TABELA + if (this.selectedClient) { + this.viewMode = 'TABLE'; + this.loadFromApi(); + return; + } + + // Se não, respeita o modo definido pela busca ou padrão + if (this.viewMode === 'GROUPS') { this.loadGroups(); } else { this.loadFromApi(); } } + /** + * Lógica de Pesquisa Inteligente: + * - Se contém números -> Busca Linha/Chip -> Modo TABELA + * - Se é texto -> Busca Nome Cliente -> Modo GRUPOS (Cards) + */ + onSearch() { + if (this.searchTimer) clearTimeout(this.searchTimer); + + this.searchTimer = setTimeout(() => { + this.page = 1; // Reseta paginação ao pesquisar + const s = this.searchTerm.trim(); + + if (!s) { + // Busca vazia -> Volta para o padrão (Grupos) + this.viewMode = 'GROUPS'; + this.loadGroups(); + return; + } + + // Verifica se tem números (busca específica) + if (/\d/.test(s)) { + this.viewMode = 'TABLE'; + this.loadFromApi(); + } else { + // Texto -> Busca por cliente nos Grupos + this.viewMode = 'GROUPS'; + this.loadGroups(); + } + }, 300); // Debounce de 300ms + } + + // Carrega os Cards (Grupos) private loadGroups() { this.loading = true; - this.http.get(`${this.apiBase}/groups`).subscribe({ - next: (data) => { - this.clientGroups = data || []; + + let params = new HttpParams() + .set('page', String(this.page)) + .set('pageSize', String(this.pageSize)); + + // Filtros de Aba + if (this.filterSkil === 'PF') params = params.set('skil', 'PESSOA FÍSICA'); + else if (this.filterSkil === 'PJ') params = params.set('skil', 'PESSOA JURÍDICA'); + else if (this.filterSkil === 'RESERVA') params = params.set('skil', 'RESERVA'); + + // Filtro de Busca Textual (Nome do Cliente) + if (this.searchTerm) params = params.set('search', this.searchTerm); + + this.http.get>(`${this.apiBase}/groups`, { params }).subscribe({ + next: (res) => { + // Fallback: Se buscou texto, mas não retornou nenhum grupo de cliente, + // pode ser que o usuário esteja buscando o nome de um "Usuário da Linha". + // Nesse caso, tentamos buscar na tabela. + if (this.searchTerm && res.total === 0 && !/\d/.test(this.searchTerm)) { + this.viewMode = 'TABLE'; + this.loadFromApi(); + return; + } + + this.clientGroups = res.items || []; + this.total = res.total; // Total de Grupos para paginação this.loading = false; }, error: () => { @@ -124,33 +265,39 @@ export class Geral implements AfterViewInit { }); } + // Expandir o Card do Cliente (Accordion) toggleGroup(clientName: string) { if (this.expandedGroup === clientName) { - this.expandedGroup = null; + this.expandedGroup = null; // Fecha se já estiver aberto return; } this.expandedGroup = clientName; this.groupLines = []; this.loadingLines = true; - const params = new HttpParams() - .set('client', clientName) - .set('page', '1') - .set('pageSize', '500') - .set('sortBy', 'item') - .set('sortDir', 'asc'); + // Busca as linhas desse cliente específico + let params = new HttpParams() + .set('client', clientName) + .set('page', '1') + .set('pageSize', '500') // Carrega até 500 linhas na expansão + .set('sortBy', 'item') + .set('sortDir', 'asc'); + + // ✅ IMPORTANTE: Passa o filtro Skil ativo (Ex: Reserva) + if (this.filterSkil === 'PF') params = params.set('skil', 'PESSOA FÍSICA'); + else if (this.filterSkil === 'PJ') params = params.set('skil', 'PESSOA JURÍDICA'); + else if (this.filterSkil === 'RESERVA') params = params.set('skil', 'RESERVA'); this.http.get>(this.apiBase, { params }).subscribe({ next: (res) => { - // ✅ CORREÇÃO: Mapeando 'usuario' corretamente aqui this.groupLines = (res.items ?? []).map(x => ({ - id: x.id, - item: String(x.item ?? ''), - linha: x.linha ?? '', - cliente: x.cliente ?? '', - usuario: x.usuario ?? '', // <--- Mapeado - status: x.status ?? '', - skil: x.skil ?? '', + id: x.id, + item: String(x.item ?? ''), + linha: x.linha ?? '', + cliente: x.cliente ?? '', + usuario: x.usuario ?? '', + status: x.status ?? '', + skil: x.skil ?? '', contrato: x.vencConta ?? '' })); this.loadingLines = false; @@ -162,32 +309,33 @@ export class Geral implements AfterViewInit { }); } + // Carrega a Tabela Plana (Modo Tabela) private loadFromApi() { this.loading = true; let 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); - + .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); + if (this.filterSkil === 'PF') params = params.set('skil', 'PESSOA FÍSICA'); else if (this.filterSkil === 'PJ') params = params.set('skil', 'PESSOA JURÍDICA'); + else if (this.filterSkil === 'RESERVA') params = params.set('skil', 'RESERVA'); if (this.selectedClient) params = params.set('client', this.selectedClient); this.http.get>(this.apiBase, { params }).subscribe({ next: (res) => { - this.total = res.total ?? 0; - // ✅ CORREÇÃO: Mapeando 'usuario' corretamente aqui também + this.total = res.total ?? 0; // Total de Linhas para paginação this.rows = (res.items ?? []).map(x => ({ - id: x.id, - item: String(x.item ?? ''), - linha: x.linha ?? '', - cliente: x.cliente ?? '', - usuario: x.usuario ?? '', // <--- Mapeado - status: x.status ?? '', - skil: x.skil ?? '', + id: x.id, + item: String(x.item ?? ''), + linha: x.linha ?? '', + cliente: x.cliente ?? '', + usuario: x.usuario ?? '', + status: x.status ?? '', + skil: x.skil ?? '', contrato: x.vencConta ?? '' })); this.loading = false; @@ -199,73 +347,205 @@ export class Geral implements AfterViewInit { }); } - setFilter(type: 'ALL' | 'PF' | 'PJ') { + // Troca de Abas (Todos, PF, PJ, Reserva) + setFilter(type: 'ALL' | 'PF' | 'PJ' | 'RESERVA') { if (this.filterSkil === type) return; this.filterSkil = type; this.page = 1; this.refreshData(); } - onSearch() { - if (this.searchTimer) clearTimeout(this.searchTimer); - this.searchTimer = setTimeout(() => { - this.page = 1; - this.refreshData(); - }, 300); + // ========================================================== + // UX & FILTROS DE CLIENTE + // ========================================================== + + clearSearch() { + this.searchTerm = ''; + this.page = 1; + this.refreshData(); } - clearSearch() { - this.searchTerm = ''; - this.page = 1; - this.refreshData(); + private loadClients() { + this.http.get(`${this.apiBase}/clients`).subscribe({ + next: (data) => this.clientsList = data || [] + }); } - - private loadClients() { this.http.get(`${this.apiBase}/clients`).subscribe({ next: (data) => this.clientsList = data || [] }); } - toggleClientMenu() { this.showClientMenu = !this.showClientMenu; this.clientSearchTerm = ''; } - closeClientDropdown() { this.showClientMenu = false; } + + toggleClientMenu() { + this.showClientMenu = !this.showClientMenu; + this.clientSearchTerm = ''; + } + + closeClientDropdown() { + this.showClientMenu = false; + } + selectClient(client: string | null) { this.selectedClient = client; this.showClientMenu = false; this.page = 1; this.refreshData(); } + get filteredClientsList(): string[] { if (!this.clientSearchTerm) return this.clientsList; const s = this.clientSearchTerm.toLowerCase(); return this.clientsList.filter(c => c.toLowerCase().includes(s)); } + // ========================================================== + // PAGINAÇÃO E ORDENAÇÃO + // ========================================================== + setSort(key: keyof LineRow) { - if (this.sortKey === key) this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc'; - else { this.sortKey = key; this.sortDir = 'asc'; } - this.page = 1; this.loadFromApi(); + if (this.sortKey === key) { + this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc'; + } else { + this.sortKey = key; + this.sortDir = 'asc'; + } + this.page = 1; + this.loadFromApi(); // Ordenação só faz sentido visualmente na tabela } - onPageSizeChange() { this.page = 1; this.loadFromApi(); } - goToPage(p: number) { this.page = Math.max(1, Math.min(this.totalPages, p)); this.loadFromApi(); } + + onPageSizeChange() { + this.page = 1; + this.refreshData(); + } + + goToPage(p: number) { + this.page = Math.max(1, Math.min(this.totalPages, p)); + this.refreshData(); + } + trackById(_: number, row: LineRow) { return row.id; } - get pagedRows() { return this.rows; } + + get pagedRows() { return this.rows; } // Dados já vêm paginados do back get totalPages() { return Math.ceil((this.total || 0) / this.pageSize) || 1; } get filteredCount() { return this.total || 0; } - get pageStart() { return this.filteredCount === 0 ? 0 : (this.page - 1) * this.pageSize + 1; } - get pageEnd() { return this.filteredCount === 0 ? 0 : Math.min((this.page - 1) * this.pageSize + this.rows.length, this.filteredCount); } - get pageNumbers() { const total = this.totalPages; const current = this.page; const max = 5; let start = Math.max(1, current - 2); let end = Math.min(total, start + (max - 1)); start = Math.max(1, end - (max - 1)); const pages = []; for (let i = start; i <= end; i++) pages.push(i); return pages; } - private getById(id: string, cb: (d: any) => void) { this.http.get(`${this.apiBase}/${id}`).subscribe({ next: cb }); } - onDetalhes(r: LineRow) { this.detailOpen = true; this.detailData = null; this.getById(r.id, d => this.detailData = d); } - onFinanceiro(r: LineRow) { this.financeOpen = true; this.financeData = null; this.getById(r.id, d => this.financeData = d); } + get pageStart() { return this.filteredCount === 0 ? 0 : (this.page - 1) * this.pageSize + 1; } + + get pageEnd() { + return this.filteredCount === 0 ? 0 : Math.min((this.page - 1) * this.pageSize + (this.isGroupMode ? this.clientGroups.length : this.rows.length), this.filteredCount); + } + + get pageNumbers() { + const total = this.totalPages; + const current = this.page; + const max = 5; + let start = Math.max(1, current - 2); + let end = Math.min(total, start + (max - 1)); + start = Math.max(1, end - (max - 1)); + + const pages = []; + for (let i = start; i <= end; i++) pages.push(i); + return pages; + } + + // ========================================================== + // IMPORTAÇÃO / AÇÕES + // ========================================================== + + async onImportExcel() { + if (!this.excelInput?.nativeElement) return; + this.excelInput.nativeElement.value = ''; + this.excelInput.nativeElement.click(); + } + + onExcelSelected(ev: Event) { + const file = (ev.target as HTMLInputElement).files?.[0]; + if (!file) return; + + const form = new FormData(); + form.append('file', file); + + this.loading = true; + this.http.post<{ imported: number }>(`${this.apiBase}/import-excel`, form).subscribe({ + next: async (r) => { + await this.showToast(`Sucesso! ${r?.imported ?? 0} registros importados.`); + this.page = 1; + this.refreshData(); + }, + error: async () => { + this.loading = false; + await this.showToast('Falha ao importar planilha.'); + } + }); + } + + async onCadastrarLinha() { + await this.showToast('Em breve.'); + } + + // ========================================================== + // MODAIS E HELPERS + // ========================================================== + + private async showToast(message: string) { + if (!isPlatformBrowser(this.platformId)) return; + this.toastMessage = message; + this.cdr.detectChanges(); + if (!this.successToast?.nativeElement) return; + + try { + const bs = await import('bootstrap'); + const toastInstance = bs.Toast.getOrCreateInstance(this.successToast.nativeElement, { autohide: true, delay: 3000 }); + toastInstance.show(); + } catch (error) { + console.error(error); + } + } + + private getById(id: string, cb: (d: any) => void) { + this.http.get(`${this.apiBase}/${id}`).subscribe({ + next: cb, + error: () => this.showToast('Erro ao carregar detalhes.') + }); + } + + onDetalhes(r: LineRow) { + this.detailOpen = true; + this.detailData = null; + this.getById(r.id, d => this.detailData = d); + } + + onFinanceiro(r: LineRow) { + this.financeOpen = true; + this.financeData = null; + this.getById(r.id, d => this.financeData = d); + } + closeDetail() { this.detailOpen = false; this.detailData = null; } closeFinance() { this.financeOpen = false; this.financeData = null; } - + + // Edição async onEditar(r: LineRow) { - this.editOpen = true; this.editSaving = false; this.editModel = null; this.editingId = r.id; - this.http.get(`${this.apiBase}/${r.id}`).subscribe({ next: (d) => this.editModel = this.toEditModel(d) }); + this.editOpen = true; + this.editSaving = false; + this.editModel = null; + this.editingId = r.id; + + this.http.get(`${this.apiBase}/${r.id}`).subscribe({ + next: (d) => { this.editModel = this.toEditModel(d); }, + error: async () => { + this.editOpen = false; + await this.showToast('Erro ao carregar dados para edição.'); + } + }); } - closeEdit() { this.editOpen = false; this.editSaving = false; this.editModel = null; this.editingId = null; } - + + closeEdit() { + this.editOpen = false; + this.editSaving = false; + this.editModel = null; + this.editingId = null; + } + async saveEdit() { if (!this.editingId || !this.editModel) return; this.editSaving = true; - + const payload: UpdateMobileLineRequest = { item: this.toInt(this.editModel.item), conta: (this.editModel.conta ?? '').toString(), @@ -296,12 +576,25 @@ export class Geral implements AfterViewInit { locacaoAp: this.toNullableNumber(this.editModel.locacaoAp), valorContratoLine: this.toNullableNumber(this.editModel.valorContratoLine), desconto: this.toNullableNumber(this.editModel.desconto), - lucro: this.toNullableNumber(this.editModel.lucro), + lucro: this.toNullableNumber(this.editModel.lucro) }; this.http.put(`${this.apiBase}/${this.editingId}`, payload).subscribe({ - next: async () => { this.editSaving = false; this.closeEdit(); await this.showToast('Registro atualizado!'); if(this.isGroupMode && this.expandedGroup) { this.toggleGroup(this.expandedGroup); } else { this.refreshData(); } }, - error: async () => { this.editSaving = false; await this.showToast('Erro ao salvar.'); } + next: async () => { + this.editSaving = false; + this.closeEdit(); + await this.showToast('Registro atualizado!'); + // Se estiver em grupo e ele estiver aberto, recarrega o grupo + if(this.isGroupMode && this.expandedGroup) { + this.toggleGroup(this.expandedGroup); + } else { + this.refreshData(); + } + }, + error: async (err: HttpErrorResponse) => { + this.editSaving = false; + await this.showToast('Erro ao salvar.'); + } }); } @@ -309,27 +602,34 @@ export class Geral implements AfterViewInit { if (!confirm(`Remover linha ${r.linha}?`)) return; this.loading = true; this.http.delete(`${this.apiBase}/${r.id}`).subscribe({ - next: async () => { await this.showToast('Removido com sucesso.'); if(fromGroup && this.expandedGroup) { this.toggleGroup(this.expandedGroup); } else { this.refreshData(); } }, - error: async () => { this.loading = false; await this.showToast('Erro ao remover.'); } + next: async () => { + await this.showToast('Removido com sucesso.'); + if(fromGroup && this.expandedGroup) { + this.toggleGroup(this.expandedGroup); + } else { + this.refreshData(); + } + }, + error: async () => { + this.loading = false; + await this.showToast('Erro ao remover.'); + } }); } - async onCadastrarLinha() { await this.showToast('Em breve.'); } - - private async showToast(message: string) { if (!isPlatformBrowser(this.platformId)) return; this.toastMessage = message; this.cdr.detectChanges(); if (!this.successToast?.nativeElement) return; try { const bs = await import('bootstrap'); const toastInstance = bs.Toast.getOrCreateInstance(this.successToast.nativeElement, { autohide: true, delay: 3000 }); toastInstance.show(); } catch (error) { console.error(error); } } + // Formatters formatMoney(v: any): string { if (v == null || Number.isNaN(v)) return '-'; return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(v); } formatNumber(v: any): string { if (v == null || Number.isNaN(v)) return '-'; return new Intl.NumberFormat('pt-BR').format(v); } formatFranquia(v: any): string { if (v == null || Number.isNaN(v)) return '-'; return `${new Intl.NumberFormat('pt-BR').format(v)} GB`; } formatDateBr(iso: any): string { if (!iso) return '-'; const d = new Date(iso); return Number.isNaN(d.getTime()) ? '-' : new Intl.DateTimeFormat('pt-BR').format(d); } statusClass(s: any): string { const n = (s??'').toLowerCase(); if(n.includes('bloq')||n.includes('perda')) return 'is-blocked'; if(n.includes('ativo')) return 'is-active'; return ''; } statusLabel(s: any): string { return s || '-'; } + + // Mappers private mapSortKeyToApi(sortKey: keyof LineRow): string { const map: Record = { item: 'item', linha: 'linha', cliente: 'cliente', status: 'status', skil: 'skil', contrato: 'vencConta' }; return map[String(sortKey)] ?? 'item'; } private isoToDateInput(iso: string | null | undefined): string { if (!iso) return ''; const dt = new Date(iso); return Number.isNaN(dt.getTime()) ? '' : dt.toISOString().slice(0, 10); } private dateInputToIso(dateStr: string | null | undefined): string | null { const s = (dateStr ?? '').trim(); return s ? new Date(`${s}T00:00:00.000Z`).toISOString() : null; } private toNullableNumber(v: any): number | null { if (v === null || v === undefined || v === '') return null; const n = Number(String(v).trim().replace(',', '.')); return Number.isFinite(n) ? n : null; } private toInt(v: any): number { const n = parseInt(String(v ?? '0'), 10); return Number.isFinite(n) ? n : 0; } private toEditModel(d: ApiLineDetail) { return { ...d, dataBloqueio: this.isoToDateInput(d.dataBloqueio), dataEntregaOpera: this.isoToDateInput(d.dataEntregaOpera), dataEntregaCliente: this.isoToDateInput(d.dataEntregaCliente) }; } - - async onImportExcel() { if (!this.excelInput?.nativeElement) return; this.excelInput.nativeElement.value = ''; this.excelInput.nativeElement.click(); } - onExcelSelected(ev: Event) { const file = (ev.target as HTMLInputElement).files?.[0]; if (!file) return; const form = new FormData(); form.append('file', file); this.loading = true; this.http.post<{ imported: number }>(`${this.apiBase}/import-excel`, form).subscribe({ next: async (r) => { await this.showToast(`Sucesso! ${r?.imported ?? 0} registros importados.`); this.page = 1; this.loadFromApi(); }, error: async () => { this.loading = false; await this.showToast('Falha ao importar planilha.'); } }); } } \ No newline at end of file