-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Confirma remover o registro {{ deleteTarget?.linha }}?
+
+
+
+
diff --git a/src/app/pages/vigencia/vigencia.scss b/src/app/pages/vigencia/vigencia.scss
index 1a9a7f4..e382393 100644
--- a/src/app/pages/vigencia/vigencia.scss
+++ b/src/app/pages/vigencia/vigencia.scss
@@ -6,6 +6,9 @@
--blue: #030FAA;
--text: #111214;
--muted: rgba(17, 18, 20, 0.65);
+ --surface-soft: rgba(255, 255, 255, 0.7);
+ --surface-hover: rgba(255, 255, 255, 0.94);
+ --focus-ring: 0 0 0 3px rgba(227, 61, 207, 0.16);
--success-bg: rgba(25, 135, 84, 0.1);
--success-text: #198754;
@@ -75,42 +78,107 @@
.title-badge {
display: inline-flex; align-items: center; gap: 10px; padding: 6px 12px;
- border-radius: 999px; background: rgba(255, 255, 255, 0.78);
+ border-radius: 999px;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0.7));
border: 1px solid rgba(227, 61, 207, 0.22); font-size: 13px; font-weight: 800;
+ box-shadow: 0 8px 20px rgba(17, 18, 20, 0.06);
i { color: var(--brand); }
}
.header-title { text-align: center; }
.title { font-size: 1.5rem; font-weight: 950; margin: 0; letter-spacing: -0.5px; }
.subtitle { color: var(--muted); font-weight: 700; }
+.header-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 8px;
+ flex-wrap: wrap;
+
+ .btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.45rem;
+ white-space: nowrap;
+ min-height: 38px;
+ border-width: 1px;
+ }
+}
/* KPIs */
.mureg-kpis {
- display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(158px, 205px));
+ justify-content: center;
+ gap: 8px;
.kpi {
background: rgba(255,255,255,0.7); border: 1px solid rgba(17,18,20,0.08);
- border-radius: 16px; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center;
+ border-radius: 14px;
+ padding: 8px 10px;
+ min-height: 58px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
transition: transform 0.2s;
&:hover { transform: translateY(-2px); border-color: var(--brand); background: #fff; }
- .lbl { font-size: 0.72rem; font-weight: 900; text-transform: uppercase; color: var(--muted); }
- .val { font-size: 1.25rem; font-weight: 950; color: var(--text); }
+ .lbl { font-size: 0.64rem; font-weight: 900; text-transform: uppercase; color: var(--muted); }
+ .val { font-size: 1.02rem; font-weight: 950; color: var(--text); }
.text-brand { color: var(--brand) !important; }
}
-
- .kpi.kpi-stack {
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 6px;
- text-align: center;
- }
}
/* Controls */
.search-group {
- border-radius: 12px; background: #fff; border: 1px solid rgba(17,18,20,0.15); display: flex; align-items: center;
- &:focus-within { border-color: var(--brand); box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15); }
- .form-control { border: none; background: transparent; padding: 10px 0; font-size: 0.9rem; &:focus { outline: none; } }
+ max-width: 270px;
+ border-radius: 12px;
+ overflow: hidden;
+ display: flex;
+ align-items: stretch;
+ background: #fff;
+ border: 1px solid rgba(17,18,20,0.15);
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
+
+ &: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;
+ }
+
+ .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; }
+ }
}
.select-glass {
@@ -118,6 +186,75 @@
color: var(--blue); font-weight: 800;
}
+.btn-brand,
+.btn-glass,
+.btn-primary,
+.btn-danger {
+ border-radius: 12px;
+ font-weight: 900;
+ transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s, filter 0.2s;
+
+ &:hover {
+ transform: translateY(-1px);
+ }
+
+ &:focus-visible {
+ outline: none;
+ box-shadow: var(--focus-ring);
+ }
+
+ &:disabled {
+ opacity: 0.72;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: none;
+ }
+}
+
+.btn-brand {
+ background-color: var(--brand);
+ border-color: var(--brand);
+ color: #fff;
+ box-shadow: 0 10px 22px rgba(227, 61, 207, 0.22);
+
+ &:hover {
+ box-shadow: 0 12px 24px rgba(227, 61, 207, 0.28);
+ filter: brightness(1.05);
+ }
+}
+
+.btn-glass {
+ background: var(--surface-soft);
+ border: 1px solid rgba(3, 15, 170, 0.24);
+ color: var(--blue);
+ box-shadow: 0 6px 16px rgba(3, 15, 170, 0.1);
+
+ &:hover {
+ background: var(--surface-hover);
+ border-color: var(--brand);
+ color: var(--brand);
+ box-shadow: 0 8px 18px rgba(227, 61, 207, 0.16);
+ }
+}
+
+.btn-primary {
+ background: linear-gradient(135deg, #1543ff, #030faa);
+ border-color: #030faa;
+ color: #fff;
+ box-shadow: 0 10px 22px rgba(3, 15, 170, 0.28);
+
+ &:hover {
+ box-shadow: 0 12px 24px rgba(3, 15, 170, 0.3);
+ filter: brightness(1.05);
+ }
+}
+
+.btn-danger {
+ background: linear-gradient(135deg, #ef4444, #dc2626);
+ border-color: #dc2626;
+ box-shadow: 0 10px 22px rgba(220, 38, 38, 0.26);
+}
+
/* BODY E GRUPOS */
.geral-body { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
.groups-container { padding: 16px; overflow-y: auto; height: 100%; }
@@ -166,10 +303,23 @@
.text-blue { color: var(--blue) !important; }
.td-clip { max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.actions-col { min-width: 152px; }
+
+.action-group {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ flex-wrap: nowrap;
+ white-space: nowrap;
+}
+
.btn-icon {
- width: 32px; height: 32px; border: none; background: transparent; border-radius: 8px; color: rgba(17,18,20,0.5);
+ width: 32px; height: 32px; border: none; background: rgba(17,18,20,0.04); border-radius: 8px; color: rgba(17,18,20,0.6);
display: flex; align-items: center; justify-content: center; transition: all 0.2s;
- &:hover { background: rgba(3,15,170,0.1); color: var(--blue); }
+ &:hover { background: rgba(17,18,20,0.08); color: var(--text); transform: translateY(-1px); }
+ &.primary:hover { background: rgba(3,15,170,0.1); color: var(--blue); }
+ &.danger:hover { background: rgba(220, 53, 69, 0.12); color: #dc3545; }
}
/* FOOTER */
@@ -177,10 +327,309 @@
padding: 14px 24px; border-top: 1px solid rgba(17, 18, 20, 0.06); display: flex; justify-content: space-between; align-items: center;
}
.pagination-modern .page-link { color: var(--blue); font-weight: 900; border-radius: 10px; border: 1px solid rgba(17,18,20,0.1); background: #fff; margin: 0 2px; }
+.pagination-modern .page-link:hover { border-color: var(--brand); color: var(--brand); background: rgba(255, 255, 255, 0.98); }
.pagination-modern .page-item.active .page-link { background-color: var(--blue); border-color: var(--blue); color: #fff; }
/* MODAL */
-.lg-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); }
+.lg-backdrop {
+ position: fixed;
+ inset: 0;
+ background: radial-gradient(circle at 20% 0%, rgba(227, 61, 207, 0.18), rgba(0, 0, 0, 0.55) 45%);
+ z-index: 9990;
+ backdrop-filter: blur(5px);
+}
.lg-modal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; }
-.lg-modal-card { background: #ffffff; border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); width: 600px; overflow: hidden; animation: popUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
+.lg-modal-card {
+ background: #ffffff;
+ border: 1px solid rgba(255,255,255,0.86);
+ border-radius: 20px;
+ box-shadow: 0 30px 60px -18px rgba(0, 0, 0, 0.4);
+ width: min(860px, 96vw);
+ max-height: 90vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ animation: popUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+.lg-modal-card.modal-lg { width: min(760px, 94vw); }
+.lg-modal-card.modal-xl { width: min(1040px, 95vw); max-height: 86vh; }
+
+.lg-modal-card .modal-header {
+ padding: 16px 24px;
+ border-bottom: 1px solid rgba(0,0,0,0.06);
+ background: linear-gradient(180deg, rgba(227, 61, 207, 0.09), rgba(255, 255, 255, 0.95) 70%);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.lg-modal-card .modal-title {
+ font-size: 1.08rem;
+ font-weight: 900;
+ color: var(--text);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.lg-modal-card .icon-bg {
+ width: 32px;
+ height: 32px;
+ border-radius: 10px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ background: rgba(3, 15, 170, 0.1);
+ color: var(--blue);
+}
+
+.lg-modal-card .icon-bg.primary-soft { background: rgba(3, 15, 170, 0.1); color: var(--blue); }
+.lg-modal-card .icon-bg.danger-soft { background: rgba(220, 53, 69, 0.12); color: #dc3545; }
+
+.lg-modal-card .modal-body { flex: 1; min-height: 0; overflow-y: auto; }
+.lg-modal-card .modal-footer {
+ flex-shrink: 0;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 10px;
+ background: linear-gradient(180deg, #ffffff, rgba(248, 249, 251, 0.96));
+}
+.lg-modal-card.create-modal { width: min(1080px, 95vw); max-height: 86vh; }
+.lg-modal-card.create-modal .modal-header { background: linear-gradient(180deg, rgba(227, 61, 207, 0.08), #ffffff 70%); }
+.lg-modal-card.create-modal .modal-body { background: linear-gradient(180deg, rgba(248, 249, 250, 0.96), rgba(255, 255, 255, 0.98)); }
+.lg-modal-card.create-modal .edit-sections { gap: 14px; }
+.lg-modal-card.create-modal .detail-box { border: 1px solid rgba(227, 61, 207, 0.14); box-shadow: 0 10px 24px rgba(17, 18, 20, 0.06); }
+.lg-modal-card.create-modal .box-header { color: var(--brand); background: linear-gradient(135deg, rgba(227, 61, 207, 0.1), rgba(3, 15, 170, 0.07)); }
+.lg-modal-card.create-modal .box-body { background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(250, 250, 252, 0.96)); }
+.lg-modal-card.create-modal .form-field label { color: rgba(17, 18, 20, 0.68); }
+.lg-modal-card.create-modal .form-control,
+.lg-modal-card.create-modal .form-select { min-height: 40px; }
+.lg-modal-card.create-modal .modal-footer {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 10px;
+ padding: 14px 20px !important;
+ background: linear-gradient(180deg, #ffffff, rgba(248, 249, 251, 0.95));
+}
+.lg-modal-card.create-modal .modal-footer .btn {
+ border-radius: 12px;
+ font-weight: 900;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 120px;
+}
+.lg-modal-card.create-modal .modal-footer .btn.me-2 { margin-right: 0 !important; }
+.bg-light-gray { background-color: #f8f9fa; }
+
+.lg-modal-card .btn-icon {
+ width: 32px;
+ height: 32px;
+ border: none;
+ border-radius: 8px;
+ background: rgba(17, 18, 20, 0.04);
+ color: var(--muted);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: rgba(17, 18, 20, 0.08);
+ color: var(--brand);
+ transform: translateY(-1px);
+ }
+}
+
@keyframes popUp { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } }
+
+.details-dashboard { display: grid; grid-template-columns: 1fr; gap: 16px; }
+.detail-box { background: #fff; border-radius: 16px; border: 1px solid rgba(0,0,0,0.06); box-shadow: 0 2px 10px rgba(0,0,0,0.03); overflow: hidden; }
+
+.box-header {
+ padding: 10px 16px;
+ font-size: 0.76rem;
+ font-weight: 900;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--muted);
+ border-bottom: 1px solid rgba(0,0,0,0.05);
+ background: #fdfdfd;
+ display: flex;
+ align-items: center;
+}
+
+.box-header.justify-content-center {
+ justify-content: center;
+ text-align: center;
+ color: var(--brand);
+ background: linear-gradient(135deg, rgba(227, 61, 207, 0.08), rgba(59, 130, 246, 0.08));
+}
+
+.box-body { padding: 16px; }
+
+.info-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 8px;
+}
+
+.info-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ padding: 8px;
+ background: rgba(245, 245, 247, 0.55);
+ border-radius: 12px;
+ border: 1px solid rgba(0,0,0,0.04);
+
+ &.span-2 { grid-column: span 2; }
+
+ .lbl {
+ font-size: 0.64rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ font-weight: 800;
+ color: var(--muted);
+ margin-bottom: 2px;
+ }
+
+ .val {
+ font-size: 0.92rem;
+ font-weight: 700;
+ color: var(--text);
+ word-break: break-word;
+ line-height: 1.25;
+ }
+}
+
+.status-pill {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 84px;
+ padding: 5px 12px;
+ border-radius: 999px;
+ font-size: 0.72rem;
+ font-weight: 900;
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+ background: rgba(25, 135, 84, 0.12);
+ color: #157347;
+ border: 1px solid rgba(25, 135, 84, 0.24);
+}
+
+.status-pill.is-danger {
+ background: rgba(220, 53, 69, 0.12);
+ color: #b02a37;
+ border-color: rgba(220, 53, 69, 0.24);
+}
+
+.edit-sections { display: grid; gap: 12px; }
+.edit-sections .detail-box { border: 1px solid rgba(17, 18, 20, 0.08); box-shadow: 0 8px 22px rgba(17, 18, 20, 0.06); }
+
+summary.box-header {
+ cursor: pointer;
+ list-style: none;
+ user-select: none;
+
+ i:not(.transition-icon) { color: var(--brand); margin-right: 6px; }
+ &::-webkit-details-marker { display: none; }
+}
+
+.transition-icon { color: var(--muted); transition: transform 0.25s ease, color 0.25s ease; }
+details[open] .transition-icon { transform: rotate(180deg); color: var(--brand); }
+
+.form-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.form-field {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+
+ &.span-2 { grid-column: span 2; }
+
+ label {
+ font-size: 0.72rem;
+ font-weight: 900;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: rgba(17, 18, 20, 0.64);
+ }
+}
+
+.form-control,
+.form-select {
+ border-radius: 10px;
+ border: 1px solid rgba(17,18,20,0.15);
+ background: #fff;
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: var(--text);
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
+
+ &:hover { border-color: rgba(17, 18, 20, 0.36); }
+ &:focus {
+ border-color: var(--brand);
+ box-shadow: 0 0 0 3px rgba(227,61,207,0.15);
+ outline: none;
+ transform: translateY(-1px);
+ }
+}
+
+.confirm-delete {
+ border: 1px solid rgba(220, 53, 69, 0.16);
+ background: #fff;
+ border-radius: 14px;
+ padding: 18px 16px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ p { font-weight: 700; color: rgba(17, 18, 20, 0.85); }
+}
+
+.confirm-icon {
+ width: 36px;
+ height: 36px;
+ border-radius: 10px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(220, 53, 69, 0.12);
+ color: #dc3545;
+ flex-shrink: 0;
+}
+
+@media (max-width: 992px) {
+ .mureg-kpis {
+ grid-template-columns: repeat(2, minmax(150px, 198px));
+ }
+}
+
+@media (max-width: 700px) {
+ .mureg-kpis {
+ grid-template-columns: minmax(0, 1fr);
+ justify-content: stretch;
+ }
+ .lg-modal-card { border-radius: 16px; }
+ .lg-modal-card .modal-header { padding: 12px 16px; }
+ .lg-modal-card .modal-body { padding: 16px !important; }
+ .lg-modal-card.create-modal .modal-footer { flex-direction: column-reverse; }
+ .lg-modal-card.create-modal .modal-footer .btn { width: 100%; min-width: 0; }
+ .form-grid,
+ .info-grid { grid-template-columns: 1fr; }
+ .info-item.span-2,
+ .form-field.span-2 { grid-column: span 1; }
+}
diff --git a/src/app/pages/vigencia/vigencia.ts b/src/app/pages/vigencia/vigencia.ts
index d5dd528..bda08c7 100644
--- a/src/app/pages/vigencia/vigencia.ts
+++ b/src/app/pages/vigencia/vigencia.ts
@@ -1,14 +1,25 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnDestroy, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpErrorResponse } from '@angular/common/http';
-import { VigenciaService, VigenciaRow, VigenciaClientGroup, PagedResult } from '../../services/vigencia.service';
+import { VigenciaService, VigenciaRow, VigenciaClientGroup, PagedResult, UpdateVigenciaRequest } from '../../services/vigencia.service';
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
+import { AuthService } from '../../services/auth.service';
+import { LinesService, MobileLineDetail } from '../../services/lines.service';
+import { PlanAutoFillService } from '../../services/plan-autofill.service';
type SortDir = 'asc' | 'desc';
type ToastType = 'success' | 'danger';
type ViewMode = 'lines' | 'groups';
+interface LineOptionDto {
+ id: string;
+ item: number;
+ linha: string | null;
+ usuario: string | null;
+ label?: string;
+}
+
@Component({
selector: 'app-vigencia',
standalone: true,
@@ -16,7 +27,7 @@ type ViewMode = 'lines' | 'groups';
templateUrl: './vigencia.html',
styleUrls: ['./vigencia.scss'],
})
-export class VigenciaComponent implements OnInit {
+export class VigenciaComponent implements OnInit, OnDestroy {
loading = false;
errorMsg = '';
@@ -46,7 +57,6 @@ export class VigenciaComponent implements OnInit {
kpiTotalClientes = 0;
kpiTotalLinhas = 0;
kpiTotalVencidos = 0;
- kpiValorTotal = 0;
// === ACORDEÃO ===
expandedGroup: string | null = null;
@@ -56,18 +66,63 @@ export class VigenciaComponent implements OnInit {
// UI
detailsOpen = false;
selectedRow: VigenciaRow | null = null;
+ editOpen = false;
+ editSaving = false;
+ editModel: VigenciaRow | null = null;
+ editEfetivacao = '';
+ editTermino = '';
+ editingId: string | null = null;
+ deleteOpen = false;
+ deleteTarget: VigenciaRow | null = null;
+
+ createOpen = false;
+ createSaving = false;
+ createModel: any = {
+ selectedClient: '',
+ mobileLineId: '',
+ item: '',
+ conta: '',
+ linha: '',
+ cliente: '',
+ usuario: '',
+ planoContrato: '',
+ total: null
+ };
+ createEfetivacao = '';
+ createTermino = '';
+
+ lineOptionsCreate: LineOptionDto[] = [];
+ createClientsLoading = false;
+ createLinesLoading = false;
+ clientsFromGeral: string[] = [];
+ planOptions: string[] = [];
+
+ isAdmin = false;
toastOpen = false;
toastMessage = '';
toastType: ToastType = 'success';
private toastTimer: any = null;
+ private searchTimer: any = null;
- constructor(private vigenciaService: VigenciaService) {}
+ constructor(
+ private vigenciaService: VigenciaService,
+ private authService: AuthService,
+ private linesService: LinesService,
+ private planAutoFill: PlanAutoFillService
+ ) {}
ngOnInit(): void {
+ this.isAdmin = this.authService.hasRole('admin');
this.loadClients();
+ this.loadPlanRules();
this.fetch(1);
}
+ ngOnDestroy(): void {
+ if (this.searchTimer) clearTimeout(this.searchTimer);
+ if (this.toastTimer) clearTimeout(this.toastTimer);
+ }
+
setView(mode: ViewMode): void {
if (this.viewMode === mode) return;
this.viewMode = mode;
@@ -85,6 +140,15 @@ export class VigenciaComponent implements OnInit {
});
}
+ private async loadPlanRules() {
+ try {
+ await this.planAutoFill.load();
+ this.planOptions = this.planAutoFill.getPlanOptions();
+ } catch {
+ this.planOptions = [];
+ }
+ }
+
get totalPages(): number {
return Math.max(1, Math.ceil((this.total || 0) / (this.pageSize || 10)));
}
@@ -120,7 +184,6 @@ export class VigenciaComponent implements OnInit {
this.kpiTotalClientes = res.kpis.totalClientes;
this.kpiTotalLinhas = res.kpis.totalLinhas;
this.kpiTotalVencidos = res.kpis.totalVencidos;
- this.kpiValorTotal = res.kpis.valorTotal;
this.loading = false;
},
@@ -199,10 +262,298 @@ export class VigenciaComponent implements OnInit {
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
}
- clearFilters() { this.search = ''; this.fetch(1); }
+ onSearchChange() {
+ if (this.searchTimer) clearTimeout(this.searchTimer);
+ this.searchTimer = setTimeout(() => this.fetch(1), 300);
+ }
+
+ clearFilters() {
+ this.search = '';
+ if (this.searchTimer) clearTimeout(this.searchTimer);
+ this.fetch(1);
+ }
openDetails(r: VigenciaRow) { this.selectedRow = r; this.detailsOpen = true; }
closeDetails() { this.detailsOpen = false; }
+ openEdit(r: VigenciaRow) {
+ if (!this.isAdmin) return;
+ this.editingId = r.id;
+ this.editModel = { ...r };
+ this.editEfetivacao = this.toDateInput(r.dtEfetivacaoServico);
+ this.editTermino = this.toDateInput(r.dtTerminoFidelizacao);
+ this.editOpen = true;
+ }
+
+ closeEdit() {
+ this.editOpen = false;
+ this.editSaving = false;
+ this.editModel = null;
+ this.editEfetivacao = '';
+ this.editTermino = '';
+ this.editingId = null;
+ }
+
+ saveEdit() {
+ if (!this.editModel || !this.editingId) return;
+ this.editSaving = true;
+
+ const payload: UpdateVigenciaRequest = {
+ item: this.toNullableNumber(this.editModel.item),
+ conta: this.editModel.conta,
+ linha: this.editModel.linha,
+ cliente: this.editModel.cliente,
+ usuario: this.editModel.usuario,
+ planoContrato: this.editModel.planoContrato,
+ dtEfetivacaoServico: this.dateInputToIso(this.editEfetivacao),
+ dtTerminoFidelizacao: this.dateInputToIso(this.editTermino),
+ total: this.toNullableNumber(this.editModel.total)
+ };
+
+ this.vigenciaService.update(this.editingId, payload).subscribe({
+ next: () => {
+ this.editSaving = false;
+ this.closeEdit();
+ this.fetch();
+ this.showToast('Registro atualizado!', 'success');
+ },
+ error: () => {
+ this.editSaving = false;
+ this.showToast('Erro ao salvar.', 'danger');
+ }
+ });
+ }
+
+ // ==========================
+ // CREATE
+ // ==========================
+ openCreate() {
+ if (!this.isAdmin) return;
+ this.resetCreateModel();
+ this.createOpen = true;
+ this.preloadGeralClients();
+ }
+
+ closeCreate() {
+ this.createOpen = false;
+ this.createSaving = false;
+ this.createModel = null;
+ }
+
+ private resetCreateModel() {
+ this.createModel = {
+ selectedClient: '',
+ mobileLineId: '',
+ item: '',
+ conta: '',
+ linha: '',
+ cliente: '',
+ usuario: '',
+ planoContrato: '',
+ total: null
+ };
+ this.createEfetivacao = '';
+ this.createTermino = '';
+ this.lineOptionsCreate = [];
+ this.createLinesLoading = false;
+ this.createClientsLoading = false;
+ }
+
+ private preloadGeralClients() {
+ this.createClientsLoading = true;
+ this.linesService.getClients().subscribe({
+ next: (list) => {
+ this.clientsFromGeral = list ?? [];
+ this.createClientsLoading = false;
+ },
+ error: () => {
+ this.clientsFromGeral = [];
+ this.createClientsLoading = false;
+ }
+ });
+ }
+
+ onCreateClientChange() {
+ const c = (this.createModel.selectedClient ?? '').trim();
+ this.createModel.mobileLineId = '';
+ this.createModel.linha = '';
+ this.createModel.conta = '';
+ this.createModel.usuario = '';
+ this.createModel.planoContrato = '';
+ this.createModel.total = null;
+ this.createModel.cliente = c;
+ this.lineOptionsCreate = [];
+
+ if (c) this.loadLinesForClient(c);
+ }
+
+ private loadLinesForClient(cliente: string) {
+ const c = (cliente ?? '').trim();
+ if (!c) return;
+
+ this.createLinesLoading = true;
+ this.linesService.getLinesByClient(c).subscribe({
+ next: (items: any[]) => {
+ const mapped: LineOptionDto[] = (items ?? [])
+ .filter(x => !!String(x?.id ?? '').trim())
+ .map(x => ({
+ id: String(x.id),
+ item: Number(x.item ?? 0),
+ linha: x.linha ?? null,
+ usuario: x.usuario ?? null,
+ label: `${x.item ?? ''} • ${x.linha ?? '-'} • ${x.usuario ?? 'SEM USUÁRIO'}`
+ }))
+ .filter(x => !!String(x.linha ?? '').trim());
+
+ this.lineOptionsCreate = mapped;
+ this.createLinesLoading = false;
+ },
+ error: () => {
+ this.lineOptionsCreate = [];
+ this.createLinesLoading = false;
+ this.showToast('Erro ao carregar linhas da GERAL.', 'danger');
+ }
+ });
+ }
+
+ onCreateLineChange() {
+ const id = String(this.createModel.mobileLineId ?? '').trim();
+ if (!id) return;
+
+ this.linesService.getById(id).subscribe({
+ next: (d: MobileLineDetail) => this.applyLineDetailToCreate(d),
+ error: () => this.showToast('Erro ao carregar dados da linha.', 'danger')
+ });
+ }
+
+ private applyLineDetailToCreate(d: MobileLineDetail) {
+ this.createModel.linha = d.linha ?? '';
+ this.createModel.conta = d.conta ?? '';
+ this.createModel.cliente = d.cliente ?? this.createModel.cliente ?? '';
+ this.createModel.usuario = d.usuario ?? '';
+ this.createModel.planoContrato = d.planoContrato ?? '';
+ this.createEfetivacao = this.toDateInput(d.dtEfetivacaoServico ?? null);
+ this.createTermino = this.toDateInput(d.dtTerminoFidelizacao ?? null);
+
+ this.ensurePlanOption(this.createModel.planoContrato);
+
+ if (!String(this.createModel.item ?? '').trim() && d.item) {
+ this.createModel.item = String(d.item);
+ }
+
+ this.onCreatePlanChange();
+ }
+
+ onCreatePlanChange() {
+ this.ensurePlanOption(this.createModel?.planoContrato);
+ this.applyPlanSuggestion(this.createModel);
+ }
+
+ onEditPlanChange() {
+ if (!this.editModel) return;
+ this.ensurePlanOption(this.editModel?.planoContrato);
+ this.applyPlanSuggestion(this.editModel);
+ }
+
+ private applyPlanSuggestion(model: any) {
+ const plan = (model?.planoContrato ?? '').toString().trim();
+ if (!plan) return;
+
+ const suggestion = this.planAutoFill.suggest(plan);
+ if (!suggestion) return;
+
+ if (suggestion.valorPlano != null) {
+ model.total = suggestion.valorPlano;
+ }
+ }
+
+ private ensurePlanOption(plan: any) {
+ const p = (plan ?? '').toString().trim();
+ if (!p) return;
+ if (!this.planOptions.includes(p)) {
+ this.planOptions = [p, ...this.planOptions];
+ }
+ }
+
+ saveCreate() {
+ if (!this.createModel) return;
+ this.applyPlanSuggestion(this.createModel);
+
+ const payload = {
+ item: this.toNullableNumber(this.createModel.item),
+ conta: this.createModel.conta,
+ linha: this.createModel.linha,
+ cliente: this.createModel.cliente,
+ usuario: this.createModel.usuario,
+ planoContrato: this.createModel.planoContrato,
+ dtEfetivacaoServico: this.dateInputToIso(this.createEfetivacao),
+ dtTerminoFidelizacao: this.dateInputToIso(this.createTermino),
+ total: this.toNullableNumber(this.createModel.total)
+ };
+
+ this.createSaving = true;
+ this.vigenciaService.create(payload).subscribe({
+ next: () => {
+ this.createSaving = false;
+ this.closeCreate();
+ this.fetch();
+ this.showToast('Vigência criada com sucesso!', 'success');
+ },
+ error: () => {
+ this.createSaving = false;
+ this.showToast('Erro ao criar vigência.', 'danger');
+ }
+ });
+ }
+
+ openDelete(r: VigenciaRow) {
+ if (!this.isAdmin) return;
+ this.deleteTarget = r;
+ this.deleteOpen = true;
+ }
+
+ cancelDelete() {
+ this.deleteOpen = false;
+ this.deleteTarget = null;
+ }
+
+ confirmDelete() {
+ if (!this.deleteTarget) return;
+ const id = this.deleteTarget.id;
+ this.vigenciaService.remove(id).subscribe({
+ next: () => {
+ this.deleteOpen = false;
+ this.deleteTarget = null;
+ this.fetch();
+ this.showToast('Registro removido.', 'success');
+ },
+ error: () => {
+ this.deleteOpen = false;
+ this.deleteTarget = null;
+ this.showToast('Erro ao remover.', 'danger');
+ }
+ });
+ }
+
+ private toDateInput(value: string | null): string {
+ if (!value) return '';
+ const d = new Date(value);
+ if (isNaN(d.getTime())) return '';
+ return d.toISOString().slice(0, 10);
+ }
+
+ private dateInputToIso(value: string): string | null {
+ if (!value) return null;
+ const d = new Date(`${value}T00:00:00`);
+ if (isNaN(d.getTime())) return null;
+ return d.toISOString();
+ }
+
+ private toNullableNumber(value: any): number | null {
+ if (value === undefined || value === null || value === '') return null;
+ const n = Number(value);
+ return Number.isNaN(n) ? null : n;
+ }
+
handleError(err: HttpErrorResponse, msg: string) {
this.loading = false;
this.expandedLoading = false;
diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts
index dbeda6a..89a9b5a 100644
--- a/src/app/services/auth.service.ts
+++ b/src/app/services/auth.service.ts
@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
+import { BehaviorSubject } from 'rxjs';
import { tap } from 'rxjs/operators';
export interface RegisterPayload {
@@ -16,35 +17,112 @@ export interface LoginPayload {
password: string;
}
+export interface LoginOptions {
+ rememberMe?: boolean;
+}
+
+export interface LoginResponse {
+ token?: string;
+ accessToken?: string;
+}
+
+export interface AuthUserProfile {
+ id: string;
+ nome: string;
+ email: string;
+ tenantId: string;
+ roles: string[];
+}
+
@Injectable({ providedIn: 'root' })
export class AuthService {
private baseUrl = `${environment.apiUrl}/auth`;
+ private userProfileSubject = new BehaviorSubject
(null);
+ readonly userProfile$ = this.userProfileSubject.asObservable();
+ private readonly tokenStorageKey = 'token';
+ private readonly tokenExpiresAtKey = 'tokenExpiresAt';
+ private readonly rememberMeHours = 6;
- constructor(private http: HttpClient) {}
+ constructor(private http: HttpClient) {
+ this.syncUserProfileFromToken();
+ }
register(payload: RegisterPayload) {
return this.http.post<{ token: string }>(`${this.baseUrl}/register`, payload)
- .pipe(tap(r => localStorage.setItem('token', r.token)));
+ .pipe(tap(r => this.setToken(r.token)));
}
- login(payload: LoginPayload) {
- return this.http.post<{ token: string }>(`${this.baseUrl}/login`, payload)
- .pipe(tap(r => localStorage.setItem('token', r.token)));
+ login(payload: LoginPayload, options?: LoginOptions) {
+ return this.http.post(`${this.baseUrl}/login`, payload)
+ .pipe(
+ tap((r) => {
+ const token = this.resolveLoginToken(r);
+ if (!token) return;
+ this.setToken(token, options?.rememberMe ?? false);
+ })
+ );
}
logout() {
- localStorage.removeItem('token');
+ if (typeof window === 'undefined') {
+ this.userProfileSubject.next(null);
+ return;
+ }
+
+ this.clearTokenStorage(localStorage);
+ this.clearTokenStorage(sessionStorage);
+ this.userProfileSubject.next(null);
+ }
+
+ setToken(token: string, rememberMe = false) {
+ if (typeof window === 'undefined') return;
+ this.clearTokenStorage(localStorage);
+ this.clearTokenStorage(sessionStorage);
+
+ if (rememberMe) {
+ const expiresAt = Date.now() + this.rememberMeHours * 60 * 60 * 1000;
+ localStorage.setItem(this.tokenStorageKey, token);
+ localStorage.setItem(this.tokenExpiresAtKey, String(expiresAt));
+ } else {
+ sessionStorage.setItem(this.tokenStorageKey, token);
+ }
+
+ this.syncUserProfileFromToken();
}
get token(): string | null {
if (typeof window === 'undefined') return null;
- return localStorage.getItem('token');
+ this.cleanupExpiredRememberSession();
+
+ const sessionToken = sessionStorage.getItem(this.tokenStorageKey);
+ if (sessionToken) return sessionToken;
+
+ return localStorage.getItem(this.tokenStorageKey);
}
isLoggedIn(): boolean {
return !!this.token;
}
+ get currentUserProfile(): AuthUserProfile | null {
+ return this.userProfileSubject.value;
+ }
+
+ syncUserProfileFromToken() {
+ this.userProfileSubject.next(this.buildProfileFromToken());
+ }
+
+ updateUserProfile(profile: Pick) {
+ const current = this.userProfileSubject.value;
+ if (!current) return;
+
+ this.userProfileSubject.next({
+ ...current,
+ nome: profile.nome.trim(),
+ email: profile.email.trim().toLowerCase(),
+ });
+ }
+
getTokenPayload(): Record | null {
const token = this.token;
if (!token) return null;
@@ -66,6 +144,10 @@ export class AuthService {
getRoles(): string[] {
const payload = this.getTokenPayload();
if (!payload) return [];
+ return this.extractRoles(payload);
+ }
+
+ private extractRoles(payload: Record): string[] {
const possibleKeys = [
'role',
'roles',
@@ -81,9 +163,74 @@ export class AuthService {
return roles.map(r => r.toLowerCase());
}
+ private buildProfileFromToken(): AuthUserProfile | null {
+ const payload = this.getTokenPayload();
+ if (!payload) return null;
+
+ const id = String(
+ payload['sub'] ??
+ payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'] ??
+ ''
+ ).trim();
+ const nome = String(payload['name'] ?? '').trim();
+ const email = String(
+ payload['email'] ??
+ payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] ??
+ ''
+ ).trim().toLowerCase();
+ const tenantId = String(
+ payload['tenantId'] ??
+ payload['tenant'] ??
+ payload['TenantId'] ??
+ ''
+ ).trim();
+
+ if (!id || !tenantId) return null;
+
+ return {
+ id,
+ nome,
+ email,
+ tenantId,
+ roles: this.extractRoles(payload),
+ };
+ }
+
hasRole(role: string): boolean {
const target = (role || '').toLowerCase();
if (!target) return false;
return this.getRoles().includes(target);
}
+
+ private cleanupExpiredRememberSession() {
+ const token = localStorage.getItem(this.tokenStorageKey);
+ if (!token) return;
+
+ const expiresAtRaw = localStorage.getItem(this.tokenExpiresAtKey);
+ if (!expiresAtRaw) {
+ this.clearTokenStorage(localStorage);
+ return;
+ }
+
+ const expiresAt = Number(expiresAtRaw);
+ if (!Number.isFinite(expiresAt)) {
+ this.clearTokenStorage(localStorage);
+ return;
+ }
+
+ if (Date.now() > expiresAt) {
+ this.clearTokenStorage(localStorage);
+ }
+ }
+
+ private clearTokenStorage(storage: Storage) {
+ storage.removeItem(this.tokenStorageKey);
+ storage.removeItem(this.tokenExpiresAtKey);
+ }
+
+ private resolveLoginToken(response: LoginResponse | null | undefined): string | null {
+ const raw = response?.token ?? response?.accessToken ?? null;
+ const token = (raw ?? '').toString().trim();
+ return token || null;
+ }
}
diff --git a/src/app/services/billing.ts b/src/app/services/billing.ts
index 1638710..adcd886 100644
--- a/src/app/services/billing.ts
+++ b/src/app/services/billing.ts
@@ -34,6 +34,22 @@ export interface BillingItem {
aparelho?: string | null;
formaPagamento?: string | null;
+ createdAt?: string | null;
+ updatedAt?: string | null;
+}
+
+export interface BillingUpdateRequest {
+ tipo?: string;
+ item?: number | null;
+ cliente?: string | null;
+ qtdLinhas?: number | null;
+ franquiaVivo?: number | null;
+ valorContratoVivo?: number | null;
+ franquiaLine?: number | null;
+ valorContratoLine?: number | null;
+ lucro?: number | null;
+ aparelho?: string | null;
+ formaPagamento?: string | null;
}
export interface BillingQuery {
@@ -84,4 +100,16 @@ export class BillingService {
return this.getPaged(q).pipe(map((res) => res.items ?? []));
}
+
+ getById(id: string): Observable {
+ return this.http.get(`${this.baseUrl}/${id}`);
+ }
+
+ update(id: string, payload: BillingUpdateRequest): Observable {
+ return this.http.put(`${this.baseUrl}/${id}`, payload);
+ }
+
+ remove(id: string): Observable {
+ return this.http.delete(`${this.baseUrl}/${id}`);
+ }
}
diff --git a/src/app/services/chips-controle.service.ts b/src/app/services/chips-controle.service.ts
index 87374b7..d21be34 100644
--- a/src/app/services/chips-controle.service.ts
+++ b/src/app/services/chips-controle.service.ts
@@ -17,8 +17,18 @@ export interface ChipVirgemListDto {
item: number;
numeroDoChip: string | null;
observacoes: string | null;
+ createdAt?: string | null;
+ updatedAt?: string | null;
}
+export interface UpdateChipVirgemRequest {
+ item?: number | null;
+ numeroDoChip?: string | null;
+ observacoes?: string | null;
+}
+
+export interface CreateChipVirgemRequest extends UpdateChipVirgemRequest {}
+
export interface ControleRecebidoListDto {
id: string;
ano: number | null;
@@ -34,8 +44,28 @@ export interface ControleRecebidoListDto {
dataDoRecebimento: string | null;
quantidade: number | null;
isResumo: boolean | null;
+ createdAt?: string | null;
+ updatedAt?: string | null;
}
+export interface UpdateControleRecebidoRequest {
+ ano?: number | null;
+ item?: number | null;
+ notaFiscal?: string | null;
+ chip?: string | null;
+ serial?: string | null;
+ conteudoDaNf?: string | null;
+ numeroDaLinha?: string | null;
+ valorUnit?: number | null;
+ valorDaNf?: number | null;
+ dataDaNf?: string | null;
+ dataDoRecebimento?: string | null;
+ quantidade?: number | null;
+ isResumo?: boolean | null;
+}
+
+export interface CreateControleRecebidoRequest extends UpdateControleRecebidoRequest {}
+
@Injectable({ providedIn: 'root' })
export class ChipsControleService {
private readonly baseApi: string;
@@ -67,6 +97,18 @@ export class ChipsControleService {
return this.http.get(`${this.baseApi}/chips-virgens/${id}`);
}
+ updateChipVirgem(id: string, payload: UpdateChipVirgemRequest): Observable {
+ return this.http.put(`${this.baseApi}/chips-virgens/${id}`, payload);
+ }
+
+ createChipVirgem(payload: CreateChipVirgemRequest): Observable {
+ return this.http.post(`${this.baseApi}/chips-virgens`, payload);
+ }
+
+ removeChipVirgem(id: string): Observable {
+ return this.http.delete(`${this.baseApi}/chips-virgens/${id}`);
+ }
+
getControleRecebidos(opts: {
ano?: number | string | null;
isResumo?: boolean | string | null;
@@ -95,4 +137,16 @@ export class ChipsControleService {
getControleRecebidoById(id: string): Observable {
return this.http.get(`${this.baseApi}/controle-recebidos/${id}`);
}
+
+ updateControleRecebido(id: string, payload: UpdateControleRecebidoRequest): Observable {
+ return this.http.put(`${this.baseApi}/controle-recebidos/${id}`, payload);
+ }
+
+ createControleRecebido(payload: CreateControleRecebidoRequest): Observable {
+ return this.http.post(`${this.baseApi}/controle-recebidos`, payload);
+ }
+
+ removeControleRecebido(id: string): Observable {
+ return this.http.delete(`${this.baseApi}/controle-recebidos/${id}`);
+ }
}
diff --git a/src/app/services/dados-usuarios.service.ts b/src/app/services/dados-usuarios.service.ts
index 88e69d6..d39a627 100644
--- a/src/app/services/dados-usuarios.service.ts
+++ b/src/app/services/dados-usuarios.service.ts
@@ -17,6 +17,10 @@ export interface UserDataRow {
item: number;
linha: string | null;
cliente: string | null;
+ tipoPessoa?: string | null;
+ nome?: string | null;
+ razaoSocial?: string | null;
+ cnpj?: string | null;
cpf: string | null;
email: string | null;
celular: string | null;
@@ -26,10 +30,30 @@ export interface UserDataRow {
dataNascimento: string | null;
}
+export interface UpdateUserDataRequest {
+ item?: number | null;
+ linha?: string | null;
+ cliente?: string | null;
+ tipoPessoa?: string | null;
+ nome?: string | null;
+ razaoSocial?: string | null;
+ cnpj?: string | null;
+ cpf?: string | null;
+ rg?: string | null;
+ dataNascimento?: string | null;
+ email?: string | null;
+ endereco?: string | null;
+ celular?: string | null;
+ telefoneFixo?: string | null;
+}
+
+export interface CreateUserDataRequest extends UpdateUserDataRequest {}
+
export interface UserDataClientGroup {
cliente: string;
totalRegistros: number;
comCpf: number;
+ comCnpj: number;
comEmail: number;
}
@@ -37,6 +61,7 @@ export interface UserDataKpis {
totalRegistros: number;
clientesUnicos: number;
comCpf: number;
+ comCnpj: number;
comEmail: number;
}
@@ -56,6 +81,7 @@ export class DadosUsuariosService {
getGroups(opts: {
search?: string;
+ tipo?: string;
page?: number;
pageSize?: number;
sortBy?: string;
@@ -63,6 +89,7 @@ export class DadosUsuariosService {
}): Observable {
let params = new HttpParams();
if (opts.search) params = params.set('search', opts.search);
+ if (opts.tipo) params = params.set('tipo', opts.tipo);
params = params.set('page', String(opts.page || 1));
params = params.set('pageSize', String(opts.pageSize || 10));
@@ -75,6 +102,7 @@ export class DadosUsuariosService {
getRows(opts: {
search?: string;
client?: string;
+ tipo?: string;
page?: number;
pageSize?: number;
sortBy?: string;
@@ -83,6 +111,7 @@ export class DadosUsuariosService {
let params = new HttpParams();
if (opts.search) params = params.set('search', opts.search);
if (opts.client) params = params.set('client', opts.client);
+ if (opts.tipo) params = params.set('tipo', opts.tipo);
params = params.set('page', String(opts.page || 1));
params = params.set('pageSize', String(opts.pageSize || 20));
@@ -92,11 +121,25 @@ export class DadosUsuariosService {
return this.http.get>(`${this.baseApi}/user-data`, { params });
}
- getClients(): Observable {
- return this.http.get(`${this.baseApi}/user-data/clients`);
+ getClients(tipo?: string): Observable {
+ let params = new HttpParams();
+ if (tipo) params = params.set('tipo', tipo);
+ return this.http.get(`${this.baseApi}/user-data/clients`, { params });
}
getById(id: string): Observable {
return this.http.get(`${this.baseApi}/user-data/${id}`);
}
-}
\ No newline at end of file
+
+ update(id: string, payload: UpdateUserDataRequest): Observable {
+ return this.http.put(`${this.baseApi}/user-data/${id}`, payload);
+ }
+
+ create(payload: CreateUserDataRequest): Observable {
+ return this.http.post(`${this.baseApi}/user-data`, payload);
+ }
+
+ remove(id: string): Observable {
+ return this.http.delete(`${this.baseApi}/user-data/${id}`);
+ }
+}
diff --git a/src/app/services/historico.service.ts b/src/app/services/historico.service.ts
new file mode 100644
index 0000000..69e9636
--- /dev/null
+++ b/src/app/services/historico.service.ts
@@ -0,0 +1,77 @@
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Observable } from 'rxjs';
+
+import { environment } from '../../environments/environment';
+
+export type AuditAction = 'CREATE' | 'UPDATE' | 'DELETE';
+export type AuditChangeType = 'added' | 'modified' | 'removed';
+
+export interface AuditFieldChangeDto {
+ field: string;
+ changeType: AuditChangeType;
+ oldValue?: string | null;
+ newValue?: string | null;
+}
+
+export interface AuditLogDto {
+ id: string;
+ occurredAtUtc: string;
+ action: AuditAction | string;
+ page: string;
+ entityName: string;
+ entityId?: string | null;
+ entityLabel?: string | null;
+ userId?: string | null;
+ userName?: string | null;
+ userEmail?: string | null;
+ requestPath?: string | null;
+ requestMethod?: string | null;
+ ipAddress?: string | null;
+ changes: AuditFieldChangeDto[];
+}
+
+export interface PagedResult {
+ page: number;
+ pageSize: number;
+ total: number;
+ items: T[];
+}
+
+export interface HistoricoQuery {
+ pageName?: string;
+ action?: AuditAction | string;
+ entity?: string;
+ userId?: string;
+ search?: string;
+ dateFrom?: string;
+ dateTo?: string;
+ page?: number;
+ pageSize?: number;
+}
+
+@Injectable({ providedIn: 'root' })
+export class HistoricoService {
+ private readonly baseApi: string;
+
+ constructor(private http: HttpClient) {
+ const raw = (environment.apiUrl || '').replace(/\/+$/, '');
+ this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
+ }
+
+ list(params: HistoricoQuery): Observable> {
+ let httpParams = new HttpParams();
+ if (params.pageName) httpParams = httpParams.set('pageName', params.pageName);
+ if (params.action) httpParams = httpParams.set('action', params.action);
+ if (params.entity) httpParams = httpParams.set('entity', params.entity);
+ if (params.userId) httpParams = httpParams.set('userId', params.userId);
+ if (params.search) httpParams = httpParams.set('search', params.search);
+ if (params.dateFrom) httpParams = httpParams.set('dateFrom', params.dateFrom);
+ if (params.dateTo) httpParams = httpParams.set('dateTo', params.dateTo);
+
+ httpParams = httpParams.set('page', String(params.page || 1));
+ httpParams = httpParams.set('pageSize', String(params.pageSize || 10));
+
+ return this.http.get>(`${this.baseApi}/historico`, { params: httpParams });
+ }
+}
diff --git a/src/app/services/lines.service.ts b/src/app/services/lines.service.ts
index 3483a2e..d65f616 100644
--- a/src/app/services/lines.service.ts
+++ b/src/app/services/lines.service.ts
@@ -47,6 +47,8 @@ export interface MobileLineDetail extends MobileLineList {
solicitante?: string | null;
dataEntregaOpera?: string | null;
dataEntregaCliente?: string | null;
+ dtEfetivacaoServico?: string | null;
+ dtTerminoFidelizacao?: string | null;
}
export interface LineOption {
diff --git a/src/app/services/notifications.service.ts b/src/app/services/notifications.service.ts
index 07e16e8..174fbc9 100644
--- a/src/app/services/notifications.service.ts
+++ b/src/app/services/notifications.service.ts
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
+import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
@@ -20,6 +20,10 @@ export type NotificationDto = {
cliente?: string | null;
linha?: string | null;
usuario?: string | null;
+ conta?: string | null;
+ planoContrato?: string | null;
+ dtEfetivacaoServico?: string | null;
+ dtTerminoFidelizacao?: string | null;
};
@Injectable({ providedIn: 'root' })
@@ -38,4 +42,29 @@ export class NotificationsService {
markAsRead(id: string): Observable {
return this.http.patch(`${this.baseApi}/notifications/${id}/read`, {});
}
+
+ markAllAsRead(filter?: string, notificationIds?: string[]): Observable {
+ let params = new HttpParams();
+ if (filter) params = params.set('filter', filter);
+ const body = notificationIds && notificationIds.length ? { notificationIds } : {};
+ return this.http.patch(`${this.baseApi}/notifications/read-all`, body, { params });
+ }
+
+ export(filter?: string, notificationIds?: string[]): Observable> {
+ let params = new HttpParams();
+ if (filter) params = params.set('filter', filter);
+ if (notificationIds && notificationIds.length) {
+ return this.http.post(`${this.baseApi}/notifications/export`, { notificationIds }, {
+ params,
+ observe: 'response',
+ responseType: 'blob'
+ });
+ }
+
+ return this.http.get(`${this.baseApi}/notifications/export`, {
+ params,
+ observe: 'response',
+ responseType: 'blob'
+ });
+ }
}
diff --git a/src/app/services/parcelamentos.service.ts b/src/app/services/parcelamentos.service.ts
new file mode 100644
index 0000000..1a3a9b2
--- /dev/null
+++ b/src/app/services/parcelamentos.service.ts
@@ -0,0 +1,119 @@
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { environment } from '../../environments/environment';
+
+export interface PagedResult {
+ page: number;
+ pageSize: number;
+ total: number;
+ items: T[];
+}
+
+export interface ParcelamentoListItem {
+ id: string;
+ anoRef?: number | null;
+ item?: number | null;
+ linha?: string | null;
+ cliente?: string | null;
+ qtParcelas?: string | null;
+ parcelaAtual?: number | null;
+ totalParcelas?: number | null;
+ valorCheio?: number | string | null;
+ desconto?: number | string | null;
+ valorComDesconto?: number | string | null;
+}
+
+export interface ParcelamentoParcela {
+ competencia: string;
+ valor?: number | string | null;
+}
+
+export interface ParcelamentoAnnualMonth {
+ month: number;
+ valor?: number | string | null;
+}
+
+export interface ParcelamentoAnnualRow {
+ year: number;
+ total?: number | string | null;
+ months?: ParcelamentoAnnualMonth[];
+}
+
+export interface ParcelamentoMonthInput {
+ competencia: string;
+ valor?: number | string | null;
+}
+
+export interface ParcelamentoUpsertRequest {
+ anoRef?: number | null;
+ item?: number | null;
+ linha?: string | null;
+ cliente?: string | null;
+ qtParcelas?: string | null;
+ parcelaAtual?: number | null;
+ totalParcelas?: number | null;
+ valorCheio?: number | string | null;
+ desconto?: number | string | null;
+ valorComDesconto?: number | string | null;
+ monthValues?: ParcelamentoMonthInput[] | null;
+}
+
+export interface ParcelamentoDetail extends ParcelamentoListItem {
+ parcelasMensais?: ParcelamentoParcela[];
+ annualRows?: ParcelamentoAnnualRow[];
+}
+
+export interface ParcelamentoDetailResponse extends ParcelamentoListItem {
+ parcelasMensais?: ParcelamentoParcela[];
+ parcelas?: ParcelamentoParcela[];
+ monthValues?: ParcelamentoParcela[];
+ annualRows?: ParcelamentoAnnualRow[];
+}
+
+@Injectable({ providedIn: 'root' })
+export class ParcelamentosService {
+ private readonly baseApi: string;
+
+ constructor(private http: HttpClient) {
+ const raw = (environment.apiUrl || '').replace(/\/+$/, '');
+ this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
+ }
+
+ list(filters: {
+ anoRef?: number;
+ linha?: string;
+ cliente?: string;
+ competenciaAno?: number;
+ competenciaMes?: number;
+ page?: number;
+ pageSize?: number;
+ }): Observable> {
+ let params = new HttpParams();
+ if (filters.anoRef !== undefined) params = params.set('anoRef', String(filters.anoRef));
+ if (filters.linha && filters.linha.trim()) params = params.set('linha', filters.linha.trim());
+ if (filters.cliente && filters.cliente.trim()) params = params.set('cliente', filters.cliente.trim());
+ if (filters.competenciaAno !== undefined) params = params.set('competenciaAno', String(filters.competenciaAno));
+ if (filters.competenciaMes !== undefined) params = params.set('competenciaMes', String(filters.competenciaMes));
+ params = params.set('page', String(filters.page ?? 1));
+ params = params.set('pageSize', String(filters.pageSize ?? 10));
+
+ return this.http.get>(`${this.baseApi}/parcelamentos`, { params });
+ }
+
+ getById(id: string): Observable {
+ return this.http.get(`${this.baseApi}/parcelamentos/${id}`);
+ }
+
+ create(payload: ParcelamentoUpsertRequest): Observable {
+ return this.http.post(`${this.baseApi}/parcelamentos`, payload);
+ }
+
+ update(id: string, payload: ParcelamentoUpsertRequest): Observable {
+ return this.http.put(`${this.baseApi}/parcelamentos/${id}`, payload);
+ }
+
+ delete(id: string): Observable {
+ return this.http.delete(`${this.baseApi}/parcelamentos/${id}`);
+ }
+}
diff --git a/src/app/services/plan-autofill.service.ts b/src/app/services/plan-autofill.service.ts
new file mode 100644
index 0000000..2955b32
--- /dev/null
+++ b/src/app/services/plan-autofill.service.ts
@@ -0,0 +1,116 @@
+import { Injectable } from '@angular/core';
+import { firstValueFrom } from 'rxjs';
+import { ResumoService, PlanoContratoResumo, MacrophonyPlan } from './resumo.service';
+
+export type PlanSuggestion = {
+ franquiaGb?: number | null;
+ valorPlano?: number | null;
+};
+
+@Injectable({ providedIn: 'root' })
+export class PlanAutoFillService {
+ private loaded = false;
+ private loadingPromise: Promise | null = null;
+ private planMap = new Map();
+ private planOptions: string[] = [];
+
+ constructor(private resumoService: ResumoService) {}
+
+ async load(): Promise {
+ if (this.loaded) return;
+ if (this.loadingPromise) return this.loadingPromise;
+
+ this.loadingPromise = firstValueFrom(this.resumoService.getResumo())
+ .then((res) => {
+ const items: Array = [
+ ...(res?.planoContratoResumos ?? []),
+ ...(res?.macrophonyPlans ?? [])
+ ];
+
+ items.forEach((row) => this.addPlanRule(row));
+ this.planOptions = Array.from(new Set(this.planOptions)).sort((a, b) =>
+ a.localeCompare(b, 'pt-BR', { sensitivity: 'base' })
+ );
+
+ this.loaded = true;
+ })
+ .catch(() => {
+ this.loaded = true;
+ })
+ .finally(() => {
+ this.loadingPromise = null;
+ });
+
+ return this.loadingPromise;
+ }
+
+ getPlanOptions(): string[] {
+ return [...this.planOptions];
+ }
+
+ suggest(planName: string | null | undefined): PlanSuggestion | null {
+ const plan = (planName ?? '').trim();
+ if (!plan) return null;
+
+ const key = this.normalizePlan(plan);
+ const fromMap = this.planMap.get(key);
+
+ const franquia = fromMap?.franquiaGb ?? this.parseFranquiaFromPlan(plan);
+ const valorPlano = fromMap?.valorPlano ?? null;
+
+ if (franquia == null && valorPlano == null) return null;
+ return { franquiaGb: franquia ?? null, valorPlano };
+ }
+
+ private addPlanRule(row: PlanoContratoResumo | MacrophonyPlan) {
+ const plano = (row?.planoContrato ?? '').toString().trim();
+ if (!plano) return;
+
+ const key = this.normalizePlan(plano);
+ const current = this.planMap.get(key) || {};
+
+ const franquia = this.toNumber((row as any).franquiaGb ?? (row as any).gb);
+ const valorPlano = this.toNumber((row as any).valorIndividualComSvas);
+
+ const next: PlanSuggestion = {
+ franquiaGb: current.franquiaGb ?? franquia ?? null,
+ valorPlano: current.valorPlano ?? valorPlano ?? null
+ };
+
+ this.planMap.set(key, next);
+ this.planOptions.push(plano);
+ }
+
+ private normalizePlan(plan: string): string {
+ return plan.trim().replace(/\s+/g, ' ').toUpperCase();
+ }
+
+ private toNumber(value: any): number | null {
+ if (value === null || value === undefined || value === '') return null;
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
+
+ const raw = String(value).trim();
+ if (!raw) return null;
+
+ const cleaned = raw.replace(/[^0-9,.-]/g, '').replace(',', '.');
+ const n = parseFloat(cleaned);
+ return Number.isFinite(n) ? n : null;
+ }
+
+ private parseFranquiaFromPlan(plan: string): number | null {
+ const match = plan.match(/(\d+(?:[.,]\d+)?)\s*(GB|MB)/i);
+ if (!match) return null;
+
+ const raw = match[1].replace(',', '.');
+ const unit = match[2].toUpperCase();
+
+ const value = parseFloat(raw);
+ if (!Number.isFinite(value)) return null;
+
+ if (unit === 'MB') {
+ return Number((value / 1000).toFixed(4));
+ }
+
+ return value;
+ }
+}
diff --git a/src/app/services/profile.service.ts b/src/app/services/profile.service.ts
new file mode 100644
index 0000000..3879844
--- /dev/null
+++ b/src/app/services/profile.service.ts
@@ -0,0 +1,44 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable } from 'rxjs';
+
+import { environment } from '../../environments/environment';
+
+export type ProfileMeDto = {
+ id: string;
+ nome: string;
+ email: string;
+};
+
+export type UpdateProfilePayload = {
+ nome: string;
+ email: string;
+};
+
+export type ChangePasswordPayload = {
+ credencialAtual: string;
+ novaCredencial: string;
+ confirmarNovaCredencial: string;
+};
+
+@Injectable({ providedIn: 'root' })
+export class ProfileService {
+ private readonly baseApi: string;
+
+ constructor(private http: HttpClient) {
+ const raw = (environment.apiUrl || '').replace(/\/+$/, '');
+ this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
+ }
+
+ getMe(): Observable {
+ return this.http.get(`${this.baseApi}/profile/me`);
+ }
+
+ updateProfile(payload: UpdateProfilePayload): Observable {
+ return this.http.patch(`${this.baseApi}/profile`, payload);
+ }
+
+ changePassword(payload: ChangePasswordPayload): Observable {
+ return this.http.post(`${this.baseApi}/profile/change-password`, payload);
+ }
+}
diff --git a/src/app/services/resumo.service.ts b/src/app/services/resumo.service.ts
new file mode 100644
index 0000000..16be4b3
--- /dev/null
+++ b/src/app/services/resumo.service.ts
@@ -0,0 +1,129 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { environment } from '../../environments/environment';
+
+export interface MacrophonyPlan {
+ planoContrato?: string | null;
+ gb?: string | number | null;
+ valorIndividualComSvas?: string | number | null;
+ franquiaGb?: string | number | null;
+ totalLinhas?: number | string | null;
+ valorTotal?: string | number | null;
+ vivoTravel?: boolean | string | number | null;
+}
+
+export interface MacrophonyTotals {
+ franquiaGbTotal?: string | number | null;
+ totalLinhasTotal?: number | string | null;
+ valorTotal?: string | number | null;
+}
+
+export interface VivoLineResumo {
+ skil?: string | null;
+ cliente?: string | null;
+ qtdLinhas?: number | string | null;
+ franquiaTotal?: string | number | null;
+ valorContratoVivo?: string | number | null;
+ franquiaLine?: string | number | null;
+ valorContratoLine?: string | number | null;
+ lucro?: string | number | null;
+}
+
+export interface VivoLineTotals {
+ qtdLinhasTotal?: number | string | null;
+ franquiaTotal?: string | number | null;
+ valorContratoVivo?: string | number | null;
+ franquiaLine?: string | number | null;
+ valorContratoLine?: string | number | null;
+ lucro?: string | number | null;
+}
+
+export interface ClienteEspecial {
+ nome?: string | null;
+ valor?: string | number | null;
+}
+
+export interface PlanoContratoResumo {
+ planoContrato?: string | null;
+ gb?: string | number | null;
+ valorIndividualComSvas?: string | number | null;
+ franquiaGb?: string | number | null;
+ totalLinhas?: number | string | null;
+ valorTotal?: string | number | null;
+}
+
+export interface PlanoContratoTotal {
+ valorTotal?: string | number | null;
+}
+
+export interface LineTotal {
+ tipo?: string | null;
+ valorTotalLine?: string | number | null;
+ lucroTotalLine?: string | number | null;
+ qtdLinhas?: number | string | null;
+}
+
+export interface GbDistribuicao {
+ gb?: string | number | null;
+ qtd?: number | string | null;
+ soma?: string | number | null;
+}
+
+export interface GbDistribuicaoTotal {
+ totalLinhas?: number | string | null;
+ somaTotal?: string | number | null;
+}
+
+export interface ReservaLine {
+ ddd?: string | number | null;
+ franquiaGb?: string | number | null;
+ qtdLinhas?: number | string | null;
+ total?: string | number | null;
+}
+
+export interface ReservaPorFranquia {
+ franquiaGb?: string | number | null;
+ totalLinhas?: number | string | null;
+}
+
+export interface ReservaPorDdd {
+ ddd?: string | number | null;
+ totalLinhas?: number | string | null;
+ porFranquia?: ReservaPorFranquia[];
+}
+
+export interface ReservaTotal {
+ qtdLinhasTotal?: number | string | null;
+ total?: string | number | null;
+}
+
+export interface ResumoResponse {
+ macrophonyPlans?: MacrophonyPlan[];
+ macrophonyTotals?: MacrophonyTotals;
+ vivoLineResumos?: VivoLineResumo[];
+ vivoLineTotals?: VivoLineTotals;
+ clienteEspeciais?: ClienteEspecial[];
+ planoContratoResumos?: PlanoContratoResumo[];
+ planoContratoTotal?: PlanoContratoTotal;
+ lineTotais?: LineTotal[];
+ gbDistribuicao?: GbDistribuicao[];
+ gbDistribuicaoTotal?: GbDistribuicaoTotal;
+ reservaLines?: ReservaLine[];
+ reservaPorDdd?: ReservaPorDdd[];
+ totalGeralLinhasReserva?: number | string | null;
+ reservaTotal?: ReservaTotal;
+}
+
+@Injectable({ providedIn: 'root' })
+export class ResumoService {
+ private readonly apiBase: string;
+
+ constructor(private http: HttpClient) {
+ const raw = (environment.apiUrl || 'https://localhost:7205').replace(/\/+$/, '');
+ this.apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
+ }
+
+ getResumo() {
+ return this.http.get(`${this.apiBase}/resumo`);
+ }
+}
diff --git a/src/app/services/session-notice.service.ts b/src/app/services/session-notice.service.ts
new file mode 100644
index 0000000..5f9f9df
--- /dev/null
+++ b/src/app/services/session-notice.service.ts
@@ -0,0 +1,110 @@
+import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
+import { isPlatformBrowser } from '@angular/common';
+import { Router } from '@angular/router';
+import { AuthService } from './auth.service';
+
+@Injectable({ providedIn: 'root' })
+export class SessionNoticeService {
+ private toastEl: HTMLElement | null = null;
+ private toastBodyEl: HTMLElement | null = null;
+ private toastHeaderEl: HTMLElement | null = null;
+ private handling401 = false;
+ private last401At = 0;
+
+ constructor(
+ private authService: AuthService,
+ private router: Router,
+ @Inject(PLATFORM_ID) private platformId: object
+ ) {}
+
+ async handleUnauthorized(): Promise {
+ if (!isPlatformBrowser(this.platformId)) return;
+
+ const now = Date.now();
+ if (this.handling401 && now - this.last401At < 3000) return;
+ this.handling401 = true;
+ this.last401At = now;
+
+ await this.showToast('Sua sessão expirou. Faça login novamente.', 'danger');
+ this.authService.logout();
+ this.router.navigateByUrl('/login');
+
+ setTimeout(() => {
+ this.handling401 = false;
+ }, 3000);
+ }
+
+ async handleForbidden(): Promise {
+ if (!isPlatformBrowser(this.platformId)) return;
+ await this.showToast('Acesso restrito.', 'warning');
+ }
+
+ private ensureToast(): void {
+ if (!isPlatformBrowser(this.platformId)) return;
+ if (this.toastEl && this.toastBodyEl && this.toastHeaderEl) return;
+
+ const doc = document;
+ let container = doc.getElementById('lg-global-toast-container');
+ if (!container) {
+ container = doc.createElement('div');
+ container.id = 'lg-global-toast-container';
+ container.className = 'toast-container position-fixed top-0 end-0 p-3';
+ container.style.zIndex = '10000';
+ doc.body.appendChild(container);
+ }
+
+ const toast = doc.createElement('div');
+ toast.className = 'toast text-bg-danger border-0 shadow';
+ toast.setAttribute('role', 'alert');
+ toast.setAttribute('aria-live', 'assertive');
+ toast.setAttribute('aria-atomic', 'true');
+
+ const header = doc.createElement('div');
+ header.className = 'toast-header border-bottom-0';
+
+ const title = doc.createElement('strong');
+ title.className = 'me-auto text-primary';
+ title.textContent = 'LineGestão';
+
+ const closeBtn = doc.createElement('button');
+ closeBtn.type = 'button';
+ closeBtn.className = 'btn-close';
+ closeBtn.setAttribute('data-bs-dismiss', 'toast');
+ closeBtn.setAttribute('aria-label', 'Fechar');
+
+ header.appendChild(title);
+ header.appendChild(closeBtn);
+
+ const body = doc.createElement('div');
+ body.className = 'toast-body bg-white rounded-bottom text-dark';
+
+ toast.appendChild(header);
+ toast.appendChild(body);
+ container.appendChild(toast);
+
+ this.toastEl = toast;
+ this.toastBodyEl = body;
+ this.toastHeaderEl = header;
+ }
+
+ private async showToast(message: string, variant: 'danger' | 'warning'): Promise {
+ if (!isPlatformBrowser(this.platformId)) return;
+ this.ensureToast();
+ if (!this.toastEl || !this.toastBodyEl || !this.toastHeaderEl) return;
+
+ this.toastBodyEl.textContent = message;
+ this.toastEl.classList.remove('text-bg-danger', 'text-bg-warning');
+ this.toastEl.classList.add(variant === 'warning' ? 'text-bg-warning' : 'text-bg-danger');
+
+ try {
+ const bs = await import('bootstrap');
+ const toastInstance = bs.Toast.getOrCreateInstance(this.toastEl, {
+ autohide: true,
+ delay: 3000
+ });
+ toastInstance.show();
+ } catch (error) {
+ console.error(error);
+ }
+ }
+}
diff --git a/src/app/services/vigencia.service.ts b/src/app/services/vigencia.service.ts
index b062c74..8840fcf 100644
--- a/src/app/services/vigencia.service.ts
+++ b/src/app/services/vigencia.service.ts
@@ -23,8 +23,24 @@ export interface VigenciaRow {
dtEfetivacaoServico: string | null;
dtTerminoFidelizacao: string | null;
total: number | null;
+ createdAt?: string | null;
+ updatedAt?: string | null;
}
+export interface UpdateVigenciaRequest {
+ item?: number | null;
+ conta?: string | null;
+ linha?: string | null;
+ cliente?: string | null;
+ usuario?: string | null;
+ planoContrato?: string | null;
+ dtEfetivacaoServico?: string | null;
+ dtTerminoFidelizacao?: string | null;
+ total?: number | null;
+}
+
+export interface CreateVigenciaRequest extends UpdateVigenciaRequest {}
+
export interface VigenciaClientGroup {
cliente: string;
linhas: number;
@@ -86,4 +102,20 @@ export class VigenciaService {
getClients(): Observable {
return this.http.get(`${this.baseApi}/lines/vigencia/clients`);
}
-}
\ No newline at end of file
+
+ getById(id: string): Observable {
+ return this.http.get(`${this.baseApi}/lines/vigencia/${id}`);
+ }
+
+ update(id: string, payload: UpdateVigenciaRequest): Observable {
+ return this.http.put(`${this.baseApi}/lines/vigencia/${id}`, payload);
+ }
+
+ create(payload: CreateVigenciaRequest): Observable {
+ return this.http.post(`${this.baseApi}/lines/vigencia`, payload);
+ }
+
+ remove(id: string): Observable {
+ return this.http.delete(`${this.baseApi}/lines/vigencia/${id}`);
+ }
+}
diff --git a/src/index.html b/src/index.html
index f2bb223..cc6e397 100644
--- a/src/index.html
+++ b/src/index.html
@@ -1,11 +1,11 @@
-
+
- LineGestaoFrontend
+ LineGestão
-
+
diff --git a/src/main.server.ts b/src/main.server.ts
index 723e001..4a25c51 100644
--- a/src/main.server.ts
+++ b/src/main.server.ts
@@ -1,7 +1,11 @@
import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser';
+import { registerLocaleData } from '@angular/common';
+import localePt from '@angular/common/locales/pt';
import { App } from './app/app';
import { config } from './app/app.config.server';
+registerLocaleData(localePt, 'pt-BR');
+
const bootstrap = (context: BootstrapContext) =>
bootstrapApplication(App, config, context);
diff --git a/src/main.ts b/src/main.ts
index f4c0acf..2a8b0e7 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,7 +1,11 @@
import { bootstrapApplication } from '@angular/platform-browser';
+import { registerLocaleData } from '@angular/common';
+import localePt from '@angular/common/locales/pt';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app';
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
+registerLocaleData(localePt, 'pt-BR');
+
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
diff --git a/src/styles.scss b/src/styles.scss
index 8cc0a47..f4c8f3e 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -83,7 +83,12 @@ select.form-control-sm {
/* Empurra o conteúdo pra baixo do header fixo */
.app-main.has-header {
+ position: relative;
padding-top: 84px; /* altura segura p/ header (mobile/desktop) */
+ background:
+ radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.1), transparent 60%),
+ radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.06), transparent 60%),
+ linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%);
}
@media (max-width: 600px) {
@@ -92,6 +97,21 @@ select.form-control-sm {
}
}
+/* Ajuste para monitores grandes: elimina o "vão" visual entre header e corpo. */
+@media (min-width: 1400px) {
+ .app-main.has-header {
+ padding-top: 72px;
+ }
+
+ .container-geral,
+ .container-geral-responsive,
+ .container-fat,
+ .container-mureg,
+ .container-troca {
+ margin-top: 14px !important;
+ }
+}
+
/* ========================================================== */
/* 🚀 GLOBAL FIX: Proporção Horizontal e Vertical */
/* ========================================================== */
@@ -143,7 +163,14 @@ select.form-control-sm {
.users-page,
.fat-page,
.mureg-page,
-.troca-page {
+.troca-page,
+.historico-page,
+.perfil-page,
+.dashboard-page,
+.chips-page,
+.parcelamentos-page,
+.resumo-page,
+.create-user-page {
overflow-y: auto !important;
height: auto !important;
display: block !important;
@@ -280,3 +307,23 @@ app-header .modal-card .btn-secondary:hover {
}
}
+/* Remove separators inside search inputs (icon / text / clear button). */
+.input-group.search-group {
+ > .input-group-text,
+ > .form-control,
+ > .btn,
+ > .btn-clear {
+ border: 0 !important;
+ box-shadow: none !important;
+ background: transparent !important;
+ }
+
+ > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.invalid-tooltip) {
+ margin-left: 0 !important;
+ }
+
+ > .form-control:focus {
+ box-shadow: none !important;
+ }
+}
+