@@ -571,7 +571,7 @@
-
+
diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts
index b10e06c..3c6c305 100644
--- a/src/app/pages/geral/geral.ts
+++ b/src/app/pages/geral/geral.ts
@@ -341,6 +341,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
exporting = false;
isSysAdmin = false;
isGestor = false;
+ isFinanceiro = false;
isClientRestricted = false;
rows: LineRow[] = [];
@@ -644,7 +645,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
}
get hasGroupLineSelectionTools(): boolean {
- return !this.isClientRestricted && !!(this.expandedGroup ?? '').trim();
+ return this.canManageLines && !!(this.expandedGroup ?? '').trim();
+ }
+
+ get canManageLines(): boolean {
+ return this.isSysAdmin || this.isGestor;
}
get canMoveSelectedLinesToReserva(): boolean {
@@ -660,7 +665,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
}
get canOpenBatchStatusModal(): boolean {
- if (this.isClientRestricted) return false;
+ if (!this.canManageLines) return false;
if (this.loading || this.batchStatusSaving) return false;
return this.batchStatusSelectionCount > 0;
}
@@ -810,7 +815,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
if (!isPlatformBrowser(this.platformId)) return;
this.isSysAdmin = this.authService.hasRole('sysadmin');
this.isGestor = this.authService.hasRole('gestor');
- this.isClientRestricted = !(this.isSysAdmin || this.isGestor);
+ this.isFinanceiro = this.authService.hasRole('financeiro');
+ this.isClientRestricted = !(this.isSysAdmin || this.isGestor || this.isFinanceiro);
if (this.isClientRestricted) {
this.filterSkil = 'ALL';
@@ -2176,6 +2182,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
}
async onEditar(r: LineRow) {
+ if (this.isFinanceiro) {
+ await this.showToast('Perfil Financeiro possui acesso somente leitura.');
+ return;
+ }
+
this.editOpen = true;
this.editSaving = false;
this.editModel = null;
@@ -2255,6 +2266,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
}
async saveEdit() {
+ if (this.isFinanceiro) {
+ await this.showToast('Perfil Financeiro possui acesso somente leitura.');
+ return;
+ }
+
if (!this.editingId || !this.editModel) return;
this.editSaving = true;
@@ -2485,7 +2501,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
}
async onCadastrarLinha() {
- if (this.isClientRestricted) {
+ if (!this.canManageLines) {
await this.showToast('Você não tem permissão para cadastrar novos clientes.');
return;
}
@@ -2498,7 +2514,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
}
async onAddLineToGroup(clientName: string) {
- if (this.isClientRestricted) {
+ if (!this.canManageLines) {
await this.showToast('Você não tem permissão para adicionar linhas.');
return;
}
@@ -3481,7 +3497,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
}
async openBatchStatusModal(action: BatchStatusAction) {
- if (this.isClientRestricted) {
+ if (!this.canManageLines) {
await this.showToast('Você não tem permissão para bloquear/desbloquear em lote.');
return;
}
diff --git a/src/app/pages/historico-linhas/historico-linhas.html b/src/app/pages/historico-linhas/historico-linhas.html
new file mode 100644
index 0000000..eb67ad6
--- /dev/null
+++ b/src/app/pages/historico-linhas/historico-linhas.html
@@ -0,0 +1,278 @@
+
+
+
+
+ {{ toastMessage }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Informe a linha no filtro para carregar o histórico detalhado.
+
+
+
+
+
+
+
+ {{ errorMsg || 'Erro ao carregar histórico da linha.' }}
+
+
+
+
+ Nenhuma alteração encontrada para a linha informada.
+
+
+ 0">
+
+
+ | Data/Hora |
+ Usuário |
+ Origem |
+ Ação |
+ Resumo da alteração |
+ Detalhes |
+
+
+
+
+
+ | {{ formatDateTime(log.occurredAtUtc) }} |
+
+
+ {{ displayUserName(log) }}
+ {{ log.userEmail || '-' }}
+
+ |
+
+ {{ log.page || '-' }}
+ |
+
+ {{ formatAction(log.action) }}
+ |
+
+
+ {{ summary.title }}
+ {{ summary.description }}
+
+ {{ formatChangeValue(summary.before) }}
+
+ {{ formatChangeValue(summary.after) }}
+
+
+ DDD: {{ formatChangeValue(summary.beforeDdd) }} {{ formatChangeValue(summary.afterDdd) }}
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+ Mudanças de campos
+
+
+
+
+
+ {{ change.field }}
+
+ {{ changeTypeLabel(change.changeType) }}
+
+
+
+ {{ formatChangeValue(change.oldValue) }}
+
+ {{ formatChangeValue(change.newValue) }}
+
+
+
+
+
+ Sem mudanças detalhadas nesse evento.
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/pages/historico-linhas/historico-linhas.scss b/src/app/pages/historico-linhas/historico-linhas.scss
new file mode 100644
index 0000000..1fe3352
--- /dev/null
+++ b/src/app/pages/historico-linhas/historico-linhas.scss
@@ -0,0 +1,648 @@
+:host {
+ --brand: #e33dcf;
+ --brand-soft: rgba(227, 61, 207, 0.12);
+ --blue: #030faa;
+ --text: #111214;
+ --muted: rgba(17, 18, 20, 0.64);
+ --surface: rgba(255, 255, 255, 0.9);
+ --surface-strong: #ffffff;
+ --line: rgba(15, 23, 42, 0.11);
+ --radius-xl: 22px;
+ --radius-lg: 16px;
+ --shadow-card: 0 20px 44px rgba(17, 18, 20, 0.1);
+
+ display: block;
+ font-family: 'Inter', sans-serif;
+ color: var(--text);
+ box-sizing: border-box;
+}
+
+.historico-linhas-page {
+ min-height: 100vh;
+ padding: 0 12px;
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ position: relative;
+ overflow-y: auto;
+ 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%);
+
+ &::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background: rgba(255, 255, 255, 0.25);
+ }
+}
+
+.page-blob {
+ 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;
+
+ &.blob-1 { width: 420px; height: 420px; top: -140px; left: -140px; }
+ &.blob-2 { width: 520px; height: 520px; top: -220px; right: -240px; animation-duration: 12s; }
+ &.blob-3 { width: 360px; height: 360px; bottom: -180px; left: 25%; animation-duration: 14s; }
+ &.blob-4 { width: 520px; height: 520px; bottom: -260px; right: -260px; animation-duration: 16s; opacity: 0.45; }
+}
+
+@keyframes floaty {
+ 0% { transform: translate(0, 0) scale(1); }
+ 50% { transform: translate(18px, 10px) scale(1.03); }
+ 100% { transform: translate(0, 0) scale(1); }
+}
+
+.container-geral-responsive {
+ width: 98% !important;
+ max-width: 1500px !important;
+ position: relative;
+ z-index: 1;
+ margin-top: 40px;
+ margin-bottom: 200px;
+}
+
+.geral-card {
+ border-radius: var(--radius-xl);
+ overflow: hidden;
+ background: var(--surface);
+ border: 1px solid rgba(227, 61, 207, 0.16);
+ backdrop-filter: blur(12px);
+ box-shadow: var(--shadow-card);
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-height: 80vh;
+
+ &::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;
+ }
+}
+
+.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-row-top {
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ align-items: center;
+ gap: 12px;
+
+ @media (max-width: 900px) {
+ grid-template-columns: 1fr;
+ text-align: center;
+ gap: 14px;
+
+ .title-badge { justify-self: center; }
+ .header-actions { justify-self: center; }
+ }
+}
+
+.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.82);
+ border: 1px solid rgba(227, 61, 207, 0.22);
+ color: var(--text);
+ font-size: 13px;
+ font-weight: 800;
+
+ i { color: var(--brand); }
+}
+
+.header-title {
+ justify-self: center;
+ text-align: center;
+}
+
+.title {
+ font-size: 26px;
+ font-weight: 950;
+ letter-spacing: -0.3px;
+ color: var(--text);
+ margin-top: 10px;
+}
+
+.subtitle {
+ color: var(--muted);
+ font-weight: 700;
+}
+
+.header-actions {
+ justify-self: end;
+}
+
+.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;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 10px 20px rgba(227, 61, 207, 0.25);
+ filter: brightness(1.05);
+ }
+}
+
+.btn-glass {
+ background: rgba(255, 255, 255, 0.9);
+ border: 1px solid rgba(17, 18, 20, 0.16);
+ color: rgba(17, 18, 20, 0.85);
+ border-radius: 12px;
+ font-weight: 700;
+}
+
+.filters-card {
+ background: rgba(255, 255, 255, 0.92);
+ border: 1px solid rgba(17, 18, 20, 0.08);
+ border-radius: 16px;
+ padding: 16px;
+ display: grid;
+ gap: 14px;
+ box-shadow: 0 14px 28px rgba(17, 18, 20, 0.08);
+}
+
+.filters-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.filters-title {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 900;
+ font-size: 14px;
+ color: rgba(17, 18, 20, 0.82);
+}
+
+.filters-actions {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.filters-grid {
+ display: grid;
+ grid-template-columns: repeat(12, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.filter-field {
+ display: grid;
+ gap: 6px;
+ grid-column: span 2;
+ min-width: 0;
+
+ label {
+ font-size: 11px;
+ font-weight: 800;
+ color: rgba(17, 18, 20, 0.6);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ }
+
+ input {
+ width: 100%;
+ height: 40px;
+ border-radius: 10px;
+ border: 1px solid rgba(15, 23, 42, 0.15);
+ padding: 0 12px;
+ font-size: 14px;
+ background: #fff;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+ }
+
+ input:focus {
+ outline: none;
+ border-color: var(--brand);
+ box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.12);
+ }
+}
+
+.line-field {
+ grid-column: span 4;
+}
+
+.period-field {
+ grid-column: span 3;
+}
+
+.btn-primary,
+.btn-ghost {
+ height: 38px;
+ border-radius: 10px;
+ border: none;
+ font-weight: 700;
+ font-size: 12px;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 0 14px;
+}
+
+.btn-primary {
+ background: linear-gradient(135deg, var(--brand), #bc30ac);
+ color: #fff;
+ box-shadow: 0 8px 16px rgba(227, 61, 207, 0.24);
+}
+
+.btn-ghost {
+ background: rgba(15, 23, 42, 0.06);
+ color: rgba(15, 23, 42, 0.85);
+}
+
+.kpi-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.kpi-card {
+ background: var(--surface-strong);
+ border: 1px solid rgba(17, 18, 20, 0.08);
+ border-radius: 14px;
+ padding: 12px 14px;
+ display: grid;
+ gap: 6px;
+ box-shadow: 0 8px 16px rgba(17, 18, 20, 0.06);
+}
+
+.kpi-label {
+ font-size: 12px;
+ font-weight: 700;
+ color: rgba(17, 18, 20, 0.62);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.kpi-value {
+ font-size: 22px;
+ line-height: 1;
+ font-weight: 900;
+ color: var(--blue);
+}
+
+.geral-body {
+ padding: 18px 24px;
+ flex: 1;
+}
+
+.table-wrap {
+ width: 100%;
+ background: rgba(255, 255, 255, 0.84);
+ border: 1px solid rgba(15, 23, 42, 0.1);
+ border-radius: 14px;
+ overflow: hidden;
+}
+
+.table-modern {
+ margin: 0;
+ min-width: 980px;
+
+ thead th {
+ background: linear-gradient(180deg, rgba(3, 15, 170, 0.92), rgba(3, 15, 170, 0.82));
+ color: #fff;
+ font-size: 12px;
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ border: none;
+ padding: 12px;
+ white-space: nowrap;
+ }
+
+ tbody td {
+ border-top: 1px solid rgba(15, 23, 42, 0.08);
+ vertical-align: middle;
+ padding: 12px;
+ background: rgba(255, 255, 255, 0.92);
+ }
+
+ tbody tr.table-row-item:hover td {
+ background: rgba(227, 61, 207, 0.05);
+ }
+
+ tbody tr.table-row-item.expanded td {
+ background: rgba(227, 61, 207, 0.08);
+ }
+}
+
+.user-cell {
+ display: grid;
+ line-height: 1.2;
+}
+
+.user-name {
+ font-weight: 800;
+}
+
+.user-email {
+ color: rgba(17, 18, 20, 0.55);
+}
+
+.origin-pill {
+ display: inline-flex;
+ align-items: center;
+ border-radius: 999px;
+ padding: 4px 10px;
+ background: rgba(3, 15, 170, 0.1);
+ border: 1px solid rgba(3, 15, 170, 0.2);
+ color: rgba(3, 15, 170, 0.88);
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.badge-action {
+ display: inline-flex;
+ align-items: center;
+ border-radius: 999px;
+ padding: 5px 10px;
+ font-size: 12px;
+ font-weight: 800;
+ border: 1px solid transparent;
+
+ &.action-create {
+ color: #157347;
+ background: rgba(25, 135, 84, 0.12);
+ border-color: rgba(25, 135, 84, 0.24);
+ }
+
+ &.action-update {
+ color: #0a58ca;
+ background: rgba(13, 110, 253, 0.12);
+ border-color: rgba(13, 110, 253, 0.24);
+ }
+
+ &.action-delete {
+ color: #b02a37;
+ background: rgba(220, 53, 69, 0.12);
+ border-color: rgba(220, 53, 69, 0.24);
+ }
+
+ &.action-default {
+ color: #495057;
+ background: rgba(108, 117, 125, 0.12);
+ border-color: rgba(108, 117, 125, 0.24);
+ }
+}
+
+.summary-col {
+ min-width: 360px;
+}
+
+.summary-title {
+ font-size: 13px;
+ font-weight: 900;
+ margin-bottom: 2px;
+}
+
+.summary-description {
+ font-size: 12px;
+ color: rgba(17, 18, 20, 0.66);
+}
+
+.summary-diff {
+ margin-top: 6px;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+ border-radius: 10px;
+ background: rgba(15, 23, 42, 0.05);
+ padding: 4px 8px;
+
+ .old {
+ color: #b02a37;
+ font-weight: 700;
+ }
+
+ .new {
+ color: #157347;
+ font-weight: 700;
+ }
+}
+
+.summary-ddd {
+ margin-top: 5px;
+ font-size: 11px;
+ color: rgba(17, 18, 20, 0.62);
+ font-weight: 700;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.tone-mureg { color: #005f73; }
+.tone-troca { color: #6f42c1; }
+.tone-status { color: #0a58ca; }
+.tone-linha { color: #0d6efd; }
+.tone-chip { color: #198754; }
+.tone-generic { color: #495057; }
+
+.actions-col {
+ width: 84px;
+ text-align: center;
+}
+
+.expand-btn {
+ width: 34px;
+ height: 34px;
+ border-radius: 10px;
+ border: 1px solid rgba(15, 23, 42, 0.15);
+ background: #fff;
+ color: rgba(15, 23, 42, 0.85);
+}
+
+.details-row td {
+ background: rgba(255, 255, 255, 0.94);
+}
+
+.details-panel {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 12px;
+}
+
+.details-section {
+ border: 1px solid rgba(15, 23, 42, 0.1);
+ border-radius: 12px;
+ background: #fff;
+ padding: 10px 12px;
+}
+
+.section-title {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ font-weight: 800;
+ color: rgba(17, 18, 20, 0.84);
+ margin-bottom: 8px;
+}
+
+.changes-list {
+ display: grid;
+ gap: 8px;
+}
+
+.change-item {
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ border-radius: 10px;
+ padding: 8px;
+ background: rgba(248, 249, 250, 0.85);
+}
+
+.change-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 4px;
+}
+
+.change-field {
+ font-size: 12px;
+ font-weight: 800;
+ color: #111;
+}
+
+.change-type {
+ border-radius: 999px;
+ padding: 2px 8px;
+ font-size: 11px;
+ font-weight: 800;
+ border: 1px solid transparent;
+
+ &.change-added {
+ background: rgba(25, 135, 84, 0.12);
+ color: #157347;
+ border-color: rgba(25, 135, 84, 0.24);
+ }
+
+ &.change-removed {
+ background: rgba(220, 53, 69, 0.12);
+ color: #b02a37;
+ border-color: rgba(220, 53, 69, 0.24);
+ }
+
+ &.change-modified {
+ background: rgba(13, 110, 253, 0.12);
+ color: #0a58ca;
+ border-color: rgba(13, 110, 253, 0.24);
+ }
+}
+
+.change-values {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+
+ .old {
+ color: #b02a37;
+ font-weight: 700;
+ }
+
+ .new {
+ color: #157347;
+ font-weight: 700;
+ }
+}
+
+.empty-state {
+ font-size: 13px;
+ color: rgba(17, 18, 20, 0.62);
+ font-weight: 600;
+}
+
+.empty-group {
+ text-align: center;
+ padding: 28px;
+ color: rgba(17, 18, 20, 0.65);
+ font-weight: 700;
+}
+
+.empty-group.helper {
+ background: rgba(3, 15, 170, 0.05);
+ border-bottom: 1px solid rgba(3, 15, 170, 0.12);
+}
+
+.geral-footer {
+ display: none;
+}
+
+.footer-meta {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ flex-wrap: wrap;
+}
+
+.pagination-modern .page-link {
+ border-radius: 10px;
+ border: 1px solid rgba(15, 23, 42, 0.15);
+ color: rgba(15, 23, 42, 0.85);
+ margin: 0 2px;
+}
+
+.pagination-modern .page-item.active .page-link {
+ background: var(--brand);
+ border-color: var(--brand);
+ color: #fff;
+}
+
+@media (max-width: 1200px) {
+ .filters-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
+ .filter-field { grid-column: span 2; }
+ .line-field { grid-column: span 3; }
+ .period-field { grid-column: span 3; }
+ .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .details-panel { grid-template-columns: 1fr; }
+}
+
+@media (max-width: 768px) {
+ .geral-header,
+ .geral-body,
+ .geral-footer {
+ padding-left: 14px;
+ padding-right: 14px;
+ }
+
+ .filters-grid { grid-template-columns: repeat(1, minmax(0, 1fr)); }
+ .filter-field,
+ .line-field {
+ grid-column: span 1;
+ }
+
+ .kpi-grid {
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+ }
+}
diff --git a/src/app/pages/historico-linhas/historico-linhas.ts b/src/app/pages/historico-linhas/historico-linhas.ts
new file mode 100644
index 0000000..c9f4bf6
--- /dev/null
+++ b/src/app/pages/historico-linhas/historico-linhas.ts
@@ -0,0 +1,598 @@
+import { Component, OnInit, ElementRef, ViewChild, ChangeDetectorRef, Inject, PLATFORM_ID } from '@angular/core';
+import { CommonModule, isPlatformBrowser } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { HttpErrorResponse } from '@angular/common/http';
+import { firstValueFrom } from 'rxjs';
+
+import { CustomSelectComponent } from '../../components/custom-select/custom-select';
+import {
+ HistoricoService,
+ AuditLogDto,
+ AuditChangeType,
+ AuditFieldChangeDto,
+ LineHistoricoQuery
+} from '../../services/historico.service';
+import { TableExportService } from '../../services/table-export.service';
+
+interface SelectOption {
+ value: string;
+ label: string;
+}
+
+type EventTone = 'mureg' | 'troca' | 'status' | 'linha' | 'chip' | 'generic';
+
+interface EventSummary {
+ title: string;
+ description: string;
+ before?: string | null;
+ after?: string | null;
+ beforeDdd?: string | null;
+ afterDdd?: string | null;
+ tone: EventTone;
+}
+
+@Component({
+ selector: 'app-historico-linhas',
+ standalone: true,
+ imports: [CommonModule, FormsModule, CustomSelectComponent],
+ templateUrl: './historico-linhas.html',
+ styleUrls: ['./historico-linhas.scss'],
+})
+export class HistoricoLinhas implements OnInit {
+ @ViewChild('successToast', { static: false }) successToast!: ElementRef;
+
+ logs: AuditLogDto[] = [];
+ loading = false;
+ exporting = false;
+ error = false;
+ errorMsg = '';
+ toastMessage = '';
+
+ expandedLogId: string | null = null;
+
+ page = 1;
+ pageSize = 10;
+ pageSizeOptions = [10, 20, 50, 100];
+ total = 0;
+
+ filterLine = '';
+ filterPageName = '';
+ filterAction = '';
+ filterUser = '';
+ dateFrom = '';
+ dateTo = '';
+
+ readonly pageOptions: SelectOption[] = [
+ { value: '', label: 'Todas as origens' },
+ { value: 'Geral', label: 'Geral' },
+ { value: 'Mureg', label: 'Mureg' },
+ { value: 'Troca de número', label: 'Troca de número' },
+ { value: 'Vigência', label: 'Vigência' },
+ { value: 'Parcelamentos', label: 'Parcelamentos' },
+ ];
+
+ readonly actionOptions: SelectOption[] = [
+ { value: '', label: 'Todas as ações' },
+ { value: 'CREATE', label: 'Criação' },
+ { value: 'UPDATE', label: 'Atualização' },
+ { value: 'DELETE', label: 'Exclusão' },
+ ];
+
+ private readonly summaryCache = new Map();
+ private readonly idFieldExceptions = new Set(['iccid']);
+
+ constructor(
+ private readonly historicoService: HistoricoService,
+ private readonly cdr: ChangeDetectorRef,
+ @Inject(PLATFORM_ID) private readonly platformId: object,
+ private readonly tableExportService: TableExportService
+ ) {}
+
+ ngOnInit(): void {
+ // Tela inicia aguardando o usuário informar a linha.
+ }
+
+ applyFilters(): void {
+ this.page = 1;
+ this.fetch();
+ }
+
+ refresh(): void {
+ this.fetch();
+ }
+
+ clearFilters(): void {
+ this.filterLine = '';
+ this.filterPageName = '';
+ this.filterAction = '';
+ this.filterUser = '';
+ this.dateFrom = '';
+ this.dateTo = '';
+ this.page = 1;
+ this.logs = [];
+ this.total = 0;
+ this.error = false;
+ this.errorMsg = '';
+ this.summaryCache.clear();
+ }
+
+ onPageSizeChange(): void {
+ this.page = 1;
+ this.fetch();
+ }
+
+ goToPage(target: number): void {
+ this.page = Math.max(1, Math.min(this.totalPages, target));
+ this.fetch();
+ }
+
+ toggleDetails(log: AuditLogDto, event?: Event): void {
+ if (event) event.stopPropagation();
+ this.expandedLogId = this.expandedLogId === log.id ? null : log.id;
+ }
+
+ async onExport(): Promise {
+ if (this.exporting) return;
+
+ const lineTerm = this.normalizedLineTerm;
+ if (!lineTerm) {
+ await this.showToast('Informe a linha para exportar.');
+ return;
+ }
+
+ this.exporting = true;
+ try {
+ const allLogs = await this.fetchAllLogsForExport();
+ if (!allLogs.length) {
+ await this.showToast('Nenhum evento encontrado para exportar.');
+ return;
+ }
+
+ const timestamp = this.tableExportService.buildTimestamp();
+ await this.tableExportService.exportAsXlsx({
+ fileName: `historico_linhas_${timestamp}`,
+ sheetName: 'HistoricoLinhas',
+ rows: allLogs,
+ columns: [
+ { header: 'Data/Hora', type: 'datetime', value: (log) => log.occurredAtUtc ?? '' },
+ { header: 'Usuario', value: (log) => this.displayUserName(log) },
+ { header: 'E-mail', value: (log) => log.userEmail ?? '' },
+ { header: 'Origem', value: (log) => log.page ?? '' },
+ { header: 'Acao', value: (log) => this.formatAction(log.action) },
+ { header: 'Evento', value: (log) => this.summaryFor(log).title },
+ { header: 'Resumo', value: (log) => this.summaryFor(log).description },
+ { header: 'Valor Anterior', value: (log) => this.summaryFor(log).before ?? '' },
+ { header: 'Valor Novo', value: (log) => this.summaryFor(log).after ?? '' },
+ { header: 'DDD Anterior', value: (log) => this.summaryFor(log).beforeDdd ?? '' },
+ { header: 'DDD Novo', value: (log) => this.summaryFor(log).afterDdd ?? '' },
+ { header: 'Mudancas', value: (log) => this.formatChangesSummary(log) },
+ ],
+ });
+
+ await this.showToast(`Planilha exportada com ${allLogs.length} evento(s).`);
+ } catch {
+ await this.showToast('Erro ao exportar histórico de linhas.');
+ } finally {
+ this.exporting = false;
+ }
+ }
+
+ formatDateTime(value?: string | null): string {
+ if (!value) return '-';
+ const dt = new Date(value);
+ if (Number.isNaN(dt.getTime())) return '-';
+ return dt.toLocaleString('pt-BR');
+ }
+
+ displayUserName(log: AuditLogDto): string {
+ const name = (log.userName || '').trim();
+ return name ? name : 'SISTEMA';
+ }
+
+ formatAction(action?: string | null): string {
+ const value = (action || '').toUpperCase();
+ if (!value) return '-';
+ if (value === 'CREATE') return 'Criação';
+ if (value === 'UPDATE') return 'Atualização';
+ if (value === 'DELETE') return 'Exclusão';
+ return 'Outro';
+ }
+
+ actionClass(action?: string | null): string {
+ const value = (action || '').toUpperCase();
+ if (value === 'CREATE') return 'action-create';
+ if (value === 'UPDATE') return 'action-update';
+ if (value === 'DELETE') return 'action-delete';
+ return 'action-default';
+ }
+
+ changeTypeLabel(type?: AuditChangeType | string | null): string {
+ if (!type) return 'Alterado';
+ if (type === 'added') return 'Adicionado';
+ if (type === 'removed') return 'Removido';
+ return 'Alterado';
+ }
+
+ changeTypeClass(type?: AuditChangeType | string | null): string {
+ if (type === 'added') return 'change-added';
+ if (type === 'removed') return 'change-removed';
+ return 'change-modified';
+ }
+
+ formatChangeValue(value?: string | null): string {
+ if (value === undefined || value === null || value === '') return '-';
+ return String(value);
+ }
+
+ summaryFor(log: AuditLogDto): EventSummary {
+ const cached = this.summaryCache.get(log.id);
+ if (cached) return cached;
+ const summary = this.buildEventSummary(log);
+ this.summaryCache.set(log.id, summary);
+ return summary;
+ }
+
+ toneClass(tone: EventTone): string {
+ return `tone-${tone}`;
+ }
+
+ trackByLog(_: number, log: AuditLogDto): string {
+ return log.id;
+ }
+
+ trackByField(_: number, change: AuditFieldChangeDto): string {
+ return `${change.field}-${change.oldValue ?? ''}-${change.newValue ?? ''}`;
+ }
+
+ visibleChanges(log: AuditLogDto): AuditFieldChangeDto[] {
+ return this.publicChanges(log);
+ }
+
+ get normalizedLineTerm(): string {
+ return (this.filterLine || '').trim();
+ }
+
+ get hasLineFilter(): boolean {
+ return !!this.normalizedLineTerm;
+ }
+
+ get totalPages(): number {
+ return Math.ceil((this.total || 0) / this.pageSize) || 1;
+ }
+
+ get pageNumbers(): number[] {
+ 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: number[] = [];
+ for (let i = start; i <= end; i += 1) pages.push(i);
+ return pages;
+ }
+
+ get pageStart(): number {
+ return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1;
+ }
+
+ get pageEnd(): number {
+ if (this.total === 0) return 0;
+ return Math.min(this.page * this.pageSize, this.total);
+ }
+
+ get statusCountInPage(): number {
+ return this.logs.filter((log) => this.summaryFor(log).tone === 'status').length;
+ }
+
+ get trocaCountInPage(): number {
+ return this.logs.filter((log) => this.summaryFor(log).tone === 'troca').length;
+ }
+
+ get muregCountInPage(): number {
+ return this.logs.filter((log) => this.summaryFor(log).tone === 'mureg').length;
+ }
+
+ private fetch(): void {
+ const lineTerm = this.normalizedLineTerm;
+ if (!lineTerm) {
+ this.logs = [];
+ this.total = 0;
+ this.error = true;
+ this.errorMsg = 'Informe a linha para consultar o histórico.';
+ this.loading = false;
+ this.summaryCache.clear();
+ return;
+ }
+
+ this.loading = true;
+ this.error = false;
+ this.errorMsg = '';
+ this.expandedLogId = null;
+
+ const query: LineHistoricoQuery = {
+ ...this.buildBaseQuery(),
+ line: lineTerm,
+ page: this.page,
+ pageSize: this.pageSize,
+ };
+
+ this.historicoService.listByLine(query).subscribe({
+ next: (res) => {
+ this.logs = res.items || [];
+ this.total = res.total || 0;
+ this.page = res.page || this.page;
+ this.pageSize = res.pageSize || this.pageSize;
+ this.loading = false;
+ this.rebuildSummaryCache();
+ },
+ error: (err: HttpErrorResponse) => {
+ this.loading = false;
+ this.error = true;
+ this.logs = [];
+ this.total = 0;
+ this.summaryCache.clear();
+ if (err?.status === 400) {
+ this.errorMsg = err?.error?.message || 'Informe uma linha válida.';
+ return;
+ }
+ if (err?.status === 403) {
+ this.errorMsg = 'Acesso restrito.';
+ return;
+ }
+ this.errorMsg = 'Erro ao carregar histórico da linha. Tente novamente.';
+ }
+ });
+ }
+
+ private async fetchAllLogsForExport(): Promise {
+ const lineTerm = this.normalizedLineTerm;
+ if (!lineTerm) return [];
+
+ const pageSize = 500;
+ let page = 1;
+ let expectedTotal = 0;
+ const all: AuditLogDto[] = [];
+
+ while (page <= 500) {
+ const response = await firstValueFrom(
+ this.historicoService.listByLine({
+ ...this.buildBaseQuery(),
+ line: lineTerm,
+ page,
+ pageSize,
+ })
+ );
+
+ const items = response?.items ?? [];
+ expectedTotal = response?.total ?? 0;
+ all.push(...items);
+
+ if (items.length === 0) break;
+ if (items.length < pageSize) break;
+ if (expectedTotal > 0 && all.length >= expectedTotal) break;
+ page += 1;
+ }
+
+ return all;
+ }
+
+ private buildBaseQuery(): Omit {
+ return {
+ pageName: this.filterPageName || undefined,
+ action: this.filterAction || undefined,
+ user: this.filterUser?.trim() || undefined,
+ dateFrom: this.toIsoDate(this.dateFrom, false) || undefined,
+ dateTo: this.toIsoDate(this.dateTo, true) || undefined,
+ };
+ }
+
+ private rebuildSummaryCache(): void {
+ this.summaryCache.clear();
+ this.logs.forEach((log) => {
+ this.summaryCache.set(log.id, this.buildEventSummary(log));
+ });
+ }
+
+ private buildEventSummary(log: AuditLogDto): EventSummary {
+ const page = (log.page || '').toLowerCase();
+ const entity = (log.entityName || '').toLowerCase();
+
+ const linhaChange = this.findChange(log, 'linha');
+ const statusChange = this.findChange(log, 'status');
+ const chipChange = this.findChange(log, 'chip', 'iccid');
+ const linhaAntiga = this.findChange(log, 'linhaantiga');
+ const linhaNova = this.findChange(log, 'linhanova');
+
+ const muregLike = entity === 'muregline' || page.includes('mureg');
+ if (muregLike) {
+ const before = this.firstFilled(linhaAntiga?.newValue, linhaAntiga?.oldValue, linhaChange?.oldValue);
+ const after = this.firstFilled(linhaNova?.newValue, linhaNova?.oldValue, linhaChange?.newValue);
+ return {
+ title: 'Troca de Mureg',
+ description: 'Linha alterada no fluxo de Mureg.',
+ before,
+ after,
+ beforeDdd: this.extractDdd(before),
+ afterDdd: this.extractDdd(after),
+ tone: 'mureg',
+ };
+ }
+
+ const trocaLike = entity === 'trocanumeroline' || page.includes('troca');
+ if (trocaLike) {
+ const before = this.firstFilled(linhaAntiga?.newValue, linhaAntiga?.oldValue, linhaChange?.oldValue);
+ const after = this.firstFilled(linhaNova?.newValue, linhaNova?.oldValue, linhaChange?.newValue);
+ return {
+ title: 'Troca de Número',
+ description: 'Linha antiga substituída por uma nova.',
+ before,
+ after,
+ beforeDdd: this.extractDdd(before),
+ afterDdd: this.extractDdd(after),
+ tone: 'troca',
+ };
+ }
+
+ if (statusChange) {
+ const oldStatus = this.firstFilled(statusChange.oldValue);
+ const newStatus = this.firstFilled(statusChange.newValue);
+ const wasBlocked = this.isBlockedStatus(oldStatus);
+ const isBlocked = this.isBlockedStatus(newStatus);
+ let description = 'Status da linha atualizado.';
+ if (!wasBlocked && isBlocked) description = 'Linha foi bloqueada.';
+ if (wasBlocked && !isBlocked) description = 'Linha foi desbloqueada.';
+ return {
+ title: 'Status da Linha',
+ description,
+ before: oldStatus,
+ after: newStatus,
+ tone: 'status',
+ };
+ }
+
+ if (linhaChange) {
+ return {
+ title: 'Alteração da Linha',
+ description: 'Número da linha foi atualizado.',
+ before: this.firstFilled(linhaChange.oldValue),
+ after: this.firstFilled(linhaChange.newValue),
+ beforeDdd: this.extractDdd(linhaChange.oldValue),
+ afterDdd: this.extractDdd(linhaChange.newValue),
+ tone: 'linha',
+ };
+ }
+
+ if (chipChange) {
+ return {
+ title: 'Alteração de Chip',
+ description: 'ICCID/chip atualizado na linha.',
+ before: this.firstFilled(chipChange.oldValue),
+ after: this.firstFilled(chipChange.newValue),
+ tone: 'chip',
+ };
+ }
+
+ const first = this.publicChanges(log)[0];
+ if (first) {
+ return {
+ title: 'Outras alterações',
+ description: `Campo ${first.field} foi atualizado.`,
+ before: this.firstFilled(first.oldValue),
+ after: this.firstFilled(first.newValue),
+ tone: 'generic',
+ };
+ }
+
+ return {
+ title: 'Sem detalhes',
+ description: 'Não há mudanças detalhadas registradas para este evento.',
+ tone: 'generic',
+ };
+ }
+
+ private findChange(log: AuditLogDto, ...fields: string[]): AuditFieldChangeDto | null {
+ if (!fields.length) return null;
+ const normalizedTargets = new Set(fields.map((field) => this.normalizeField(field)));
+ return (log.changes || []).find((change) => normalizedTargets.has(this.normalizeField(change.field))) || null;
+ }
+
+ private normalizeField(value?: string | null): string {
+ return (value ?? '')
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .replace(/[^a-zA-Z0-9]/g, '')
+ .toLowerCase()
+ .trim();
+ }
+
+ private firstFilled(...values: Array): string | null {
+ for (const value of values) {
+ const normalized = (value ?? '').toString().trim();
+ if (normalized) return normalized;
+ }
+ return null;
+ }
+
+ private formatChangesSummary(log: AuditLogDto): string {
+ const changes = this.publicChanges(log);
+ if (!changes.length) return '';
+ return changes
+ .map((change) => {
+ const field = change?.field ?? 'campo';
+ const oldValue = this.formatChangeValue(change?.oldValue);
+ const newValue = this.formatChangeValue(change?.newValue);
+ return `${field}: ${oldValue} -> ${newValue}`;
+ })
+ .join(' | ');
+ }
+
+ private publicChanges(log: AuditLogDto): AuditFieldChangeDto[] {
+ return (log?.changes ?? []).filter((change) => !this.isHiddenIdField(change?.field));
+ }
+
+ private isHiddenIdField(field?: string | null): boolean {
+ const normalized = this.normalizeField(field);
+ if (!normalized) return false;
+ if (this.idFieldExceptions.has(normalized)) return false;
+ if (normalized === 'id') return true;
+ return normalized.endsWith('id');
+ }
+
+ private isBlockedStatus(status?: string | null): boolean {
+ const normalized = (status ?? '').toLowerCase().trim();
+ if (!normalized) return false;
+ return (
+ normalized.includes('bloque') ||
+ normalized.includes('perda') ||
+ normalized.includes('roubo') ||
+ normalized.includes('suspens')
+ );
+ }
+
+ private extractDdd(value?: string | null): string | null {
+ const digits = this.digitsOnly(value);
+ if (!digits) return null;
+
+ if (digits.startsWith('55') && digits.length >= 12) {
+ return digits.slice(2, 4);
+ }
+ if (digits.length >= 10) {
+ return digits.slice(0, 2);
+ }
+ if (digits.length >= 2) {
+ return digits.slice(0, 2);
+ }
+ return null;
+ }
+
+ private digitsOnly(value?: string | null): string {
+ return (value ?? '').replace(/\D/g, '');
+ }
+
+ private toIsoDate(value: string, endOfDay: boolean): string | null {
+ if (!value) return null;
+ const time = endOfDay ? '23:59:59' : '00:00:00';
+ const date = new Date(`${value}T${time}`);
+ if (isNaN(date.getTime())) return null;
+ return date.toISOString();
+ }
+
+ private async showToast(message: string): Promise {
+ 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);
+ }
+ }
+}
diff --git a/src/app/pages/mureg/mureg.html b/src/app/pages/mureg/mureg.html
index 9a4cfa7..c742410 100644
--- a/src/app/pages/mureg/mureg.html
+++ b/src/app/pages/mureg/mureg.html
@@ -35,7 +35,7 @@
Exportar
Exportando...
-
@@ -177,10 +177,10 @@
-
+
-
+
@@ -391,8 +391,8 @@
-
-
+
+
diff --git a/src/app/pages/mureg/mureg.ts b/src/app/pages/mureg/mureg.ts
index 86fdc7b..8ba13ea 100644
--- a/src/app/pages/mureg/mureg.ts
+++ b/src/app/pages/mureg/mureg.ts
@@ -11,6 +11,7 @@ import { isPlatformBrowser, CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpClient, HttpParams } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
+import { AuthService } from '../../services/auth.service';
import { LinesService } from '../../services/lines.service';
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
import { TableExportService } from '../../services/table-export.service';
@@ -105,6 +106,7 @@ export class Mureg implements AfterViewInit {
@Inject(PLATFORM_ID) private platformId: object,
private http: HttpClient,
private cdr: ChangeDetectorRef,
+ private authService: AuthService,
private linesService: LinesService,
private tableExportService: TableExportService
) {}
@@ -177,9 +179,20 @@ export class Mureg implements AfterViewInit {
clienteInfo: ''
};
+ isSysAdmin = false;
+ isGestor = false;
+ isFinanceiro = false;
+
+ get canManageRecords(): boolean {
+ return this.isSysAdmin || this.isGestor;
+ }
+
async ngAfterViewInit() {
if (!isPlatformBrowser(this.platformId)) return;
this.initAnimations();
+ this.isSysAdmin = this.authService.hasRole('sysadmin');
+ this.isGestor = this.authService.hasRole('gestor');
+ this.isFinanceiro = this.authService.hasRole('financeiro');
setTimeout(() => {
this.preloadClients(); // ✅ já deixa o select pronto
this.refresh();
@@ -624,6 +637,11 @@ export class Mureg implements AfterViewInit {
// CREATE MODAL
// =======================================================================
onCreate() {
+ if (!this.canManageRecords) {
+ this.showToast('Perfil Financeiro possui acesso somente leitura.');
+ return;
+ }
+
this.preloadClients();
this.createOpen = true;
@@ -636,7 +654,7 @@ export class Mureg implements AfterViewInit {
linhaAntiga: '',
linhaNova: '',
iccid: '',
- dataDaMureg: '',
+ dataDaMureg: this.nowDateInput(),
clienteInfo: ''
};
@@ -663,6 +681,11 @@ export class Mureg implements AfterViewInit {
}
saveCreate() {
+ if (!this.canManageRecords) {
+ this.showToast('Perfil Financeiro possui acesso somente leitura.');
+ return;
+ }
+
const mobileLineId = String(this.createModel.mobileLineId ?? '').trim();
const linhaNova = String(this.createModel.linhaNova ?? '').trim();
@@ -679,7 +702,7 @@ export class Mureg implements AfterViewInit {
linhaAntiga: (this.createModel.linhaAntiga ?? '') || null,
linhaNova: (this.createModel.linhaNova ?? '') || null,
iccid: (this.createModel.iccid ?? '') || null,
- dataDaMureg: this.dateInputToIso(this.createModel.dataDaMureg)
+ dataDaMureg: new Date().toISOString()
};
if (!payload.item || payload.item <= 0) delete payload.item;
@@ -703,6 +726,11 @@ export class Mureg implements AfterViewInit {
// EDIT MODAL
// =======================================================================
onEditar(r: MuregRow) {
+ if (!this.canManageRecords) {
+ this.showToast('Perfil Financeiro possui acesso somente leitura.');
+ return;
+ }
+
this.preloadClients();
this.editOpen = true;
@@ -770,6 +798,11 @@ export class Mureg implements AfterViewInit {
}
saveEdit() {
+ if (!this.canManageRecords) {
+ this.showToast('Perfil Financeiro possui acesso somente leitura.');
+ return;
+ }
+
if (!this.editModel || !this.editModel.id) return;
const mobileLineId = String(this.editModel.mobileLineId ?? '').trim();
@@ -844,6 +877,11 @@ export class Mureg implements AfterViewInit {
// DELETE MODAL
// =======================================================================
onDelete(row: MuregRow) {
+ if (!this.canManageRecords) {
+ this.showToast('Perfil Financeiro possui acesso somente leitura.');
+ return;
+ }
+
this.deleteTarget = row;
this.deleteOpen = true;
this.deleteSaving = false;
@@ -856,6 +894,11 @@ export class Mureg implements AfterViewInit {
}
async confirmDelete() {
+ if (!this.canManageRecords) {
+ await this.showToast('Perfil Financeiro possui acesso somente leitura.');
+ return;
+ }
+
if (!this.deleteTarget?.id) return;
if (!(await confirmDeletionWithTyping('esta Mureg'))) return;
@@ -914,6 +957,14 @@ export class Mureg implements AfterViewInit {
return dt.toISOString();
}
+ private nowDateInput(): string {
+ const now = new Date();
+ const year = now.getFullYear();
+ const month = String(now.getMonth() + 1).padStart(2, '0');
+ const day = String(now.getDate()).padStart(2, '0');
+ return `${year}-${month}-${day}`;
+ }
+
private extractApiMessage(err: any): string | null {
try {
const m1 = err?.error?.message;
diff --git a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html
index fb8c03c..3b3c311 100644
--- a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html
+++ b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html
@@ -100,6 +100,7 @@
diff --git a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts
index 45a2629..17861bd 100644
--- a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts
+++ b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts
@@ -26,7 +26,8 @@ export class ParcelamentosTableComponent {
@Input() items: ParcelamentoViewItem[] = [];
@Input() loading = false;
@Input() errorMessage = '';
- @Input() isSysAdmin = false;
+ @Input() canEdit = false;
+ @Input() canDelete = false;
@Input() segment: ParcelamentoSegment = 'todos';
@Input() segmentCounts: Record = {
diff --git a/src/app/pages/parcelamentos/parcelamentos.html b/src/app/pages/parcelamentos/parcelamentos.html
index 78ba26f..538debb 100644
--- a/src/app/pages/parcelamentos/parcelamentos.html
+++ b/src/app/pages/parcelamentos/parcelamentos.html
@@ -29,7 +29,7 @@
Exportar
Exportando...
-
+
Novo Parcelamento
@@ -79,7 +79,8 @@
[total]="total"
[pageSize]="pageSize"
[pageSizeOptions]="pageSizeOptions"
- [isSysAdmin]="isSysAdmin"
+ [canEdit]="canManageRecords"
+ [canDelete]="isSysAdmin"
(segmentChange)="setSegment($event)"
(detail)="openDetails($event)"
(edit)="openEdit($event)"
diff --git a/src/app/pages/parcelamentos/parcelamentos.ts b/src/app/pages/parcelamentos/parcelamentos.ts
index ef6ce4d..d60e17a 100644
--- a/src/app/pages/parcelamentos/parcelamentos.ts
+++ b/src/app/pages/parcelamentos/parcelamentos.ts
@@ -95,6 +95,12 @@ export class Parcelamentos implements OnInit, OnDestroy {
activeChips: FilterChip[] = [];
isSysAdmin = false;
+ isGestor = false;
+ isFinanceiro = false;
+
+ get canManageRecords(): boolean {
+ return this.isSysAdmin || this.isGestor;
+ }
detailOpen = false;
detailLoading = false;
@@ -168,6 +174,8 @@ export class Parcelamentos implements OnInit, OnDestroy {
private syncPermissions(): void {
this.isSysAdmin = this.authService.hasRole('sysadmin');
+ this.isGestor = this.authService.hasRole('gestor');
+ this.isFinanceiro = this.authService.hasRole('financeiro');
}
get totalPages(): number {
@@ -409,6 +417,11 @@ export class Parcelamentos implements OnInit, OnDestroy {
}
openCreateModal(): void {
+ if (!this.canManageRecords) {
+ this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger');
+ return;
+ }
+
this.createModel = this.buildCreateModel();
this.createError = '';
this.createOpen = true;
@@ -421,6 +434,11 @@ export class Parcelamentos implements OnInit, OnDestroy {
}
saveNewParcelamento(model: ParcelamentoCreateModel): void {
+ if (!this.canManageRecords) {
+ this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger');
+ return;
+ }
+
if (this.createSaving) return;
this.createSaving = true;
this.createError = '';
@@ -439,6 +457,11 @@ export class Parcelamentos implements OnInit, OnDestroy {
}
openEdit(item: ParcelamentoListItem): void {
+ if (!this.canManageRecords) {
+ this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger');
+ return;
+ }
+
const id = this.getItemId(item);
if (!id) return;
this.editOpen = true;
@@ -474,6 +497,11 @@ export class Parcelamentos implements OnInit, OnDestroy {
}
saveEditParcelamento(model: ParcelamentoCreateModel): void {
+ if (!this.canManageRecords) {
+ this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger');
+ return;
+ }
+
if (this.editSaving || !this.editModel || !this.editId) return;
this.editSaving = true;
this.editError = '';
diff --git a/src/app/pages/troca-numero/troca-numero.html b/src/app/pages/troca-numero/troca-numero.html
index 0edff36..0461aa6 100644
--- a/src/app/pages/troca-numero/troca-numero.html
+++ b/src/app/pages/troca-numero/troca-numero.html
@@ -35,7 +35,7 @@
Exportar
Exportando...
-
+
Nova Troca
@@ -156,7 +156,7 @@
{{ r.observacao || '-' }} |
-
+
diff --git a/src/app/pages/troca-numero/troca-numero.ts b/src/app/pages/troca-numero/troca-numero.ts
index 8eda5bc..673a9ce 100644
--- a/src/app/pages/troca-numero/troca-numero.ts
+++ b/src/app/pages/troca-numero/troca-numero.ts
@@ -11,6 +11,7 @@ import { isPlatformBrowser, CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpClient, HttpParams } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
+import { AuthService } from '../../services/auth.service';
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
import { TableExportService } from '../../services/table-export.service';
import { environment } from '../../../environments/environment';
@@ -73,6 +74,7 @@ export class TrocaNumero implements AfterViewInit {
@Inject(PLATFORM_ID) private platformId: object,
private http: HttpClient,
private cdr: ChangeDetectorRef,
+ private authService: AuthService,
private tableExportService: TableExportService
) {}
@@ -136,9 +138,20 @@ export class TrocaNumero implements AfterViewInit {
loadingClients = false;
loadingLines = false;
+ isSysAdmin = false;
+ isGestor = false;
+ isFinanceiro = false;
+
+ get canManageRecords(): boolean {
+ return this.isSysAdmin || this.isGestor;
+ }
+
async ngAfterViewInit() {
if (!isPlatformBrowser(this.platformId)) return;
this.initAnimations();
+ this.isSysAdmin = this.authService.hasRole('sysadmin');
+ this.isGestor = this.authService.hasRole('gestor');
+ this.isFinanceiro = this.authService.hasRole('financeiro');
setTimeout(() => this.refresh());
}
@@ -502,6 +515,11 @@ export class TrocaNumero implements AfterViewInit {
// ====== MODAL EDIÇÃO ======
onEditar(r: TrocaRow) {
+ if (!this.canManageRecords) {
+ this.showToast('Perfil Financeiro possui acesso somente leitura.');
+ return;
+ }
+
this.editOpen = true;
this.editSaving = false;
@@ -524,6 +542,11 @@ export class TrocaNumero implements AfterViewInit {
}
saveEdit() {
+ if (!this.canManageRecords) {
+ this.showToast('Perfil Financeiro possui acesso somente leitura.');
+ return;
+ }
+
if (!this.editModel || !this.editModel.id) return;
this.editSaving = true;
@@ -555,6 +578,11 @@ export class TrocaNumero implements AfterViewInit {
// ====== MODAL CRIAÇÃO ======
onCreate() {
+ if (!this.canManageRecords) {
+ this.showToast('Perfil Financeiro possui acesso somente leitura.');
+ return;
+ }
+
this.createOpen = true;
this.createSaving = false;
@@ -584,6 +612,11 @@ export class TrocaNumero implements AfterViewInit {
}
saveCreate() {
+ if (!this.canManageRecords) {
+ this.showToast('Perfil Financeiro possui acesso somente leitura.');
+ return;
+ }
+
// ✅ validações do "beber do GERAL"
if (!String(this.selectedCliente ?? '').trim()) {
this.showToast('Selecione um Cliente do GERAL.');
diff --git a/src/app/services/historico.service.ts b/src/app/services/historico.service.ts
index 2fb0c5c..f6ab9b3 100644
--- a/src/app/services/historico.service.ts
+++ b/src/app/services/historico.service.ts
@@ -50,6 +50,18 @@ export interface HistoricoQuery {
pageSize?: number;
}
+export interface LineHistoricoQuery {
+ line: string;
+ pageName?: string;
+ action?: AuditAction | string;
+ user?: string;
+ search?: string;
+ dateFrom?: string;
+ dateTo?: string;
+ page?: number;
+ pageSize?: number;
+}
+
@Injectable({ providedIn: 'root' })
export class HistoricoService {
private readonly baseApi: string;
@@ -74,4 +86,20 @@ export class HistoricoService {
return this.http.get>(`${this.baseApi}/historico`, { params: httpParams });
}
+
+ listByLine(params: LineHistoricoQuery): Observable> {
+ let httpParams = new HttpParams();
+ if (params.line) httpParams = httpParams.set('line', params.line);
+ if (params.pageName) httpParams = httpParams.set('pageName', params.pageName);
+ if (params.action) httpParams = httpParams.set('action', params.action);
+ if (params.user) httpParams = httpParams.set('user', params.user);
+ 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/linhas`, { params: httpParams });
+ }
}
diff --git a/src/app/services/users.service.ts b/src/app/services/users.service.ts
index 115f3ce..4d7bb8b 100644
--- a/src/app/services/users.service.ts
+++ b/src/app/services/users.service.ts
@@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
-export type UserPermission = 'sysadmin' | 'gestor' | 'cliente';
+export type UserPermission = 'sysadmin' | 'gestor' | 'financeiro' | 'cliente';
export type UserDto = {
id: string;
| |